Multicall を使って効率的に contract を呼び出す
はじめに
今回は、効率的に Contract を呼び出すための Multicall と、その利用方法を紹介します。
Multicall aggregates results from multiple contract constant function calls.
簡単にどう言ったものかを紹介すると、Multicall 自体は1つのコントラクトなのですが、Multicall に対して、call したいものに関する情報を渡すことで、contract 内で、指定した address, function に対して呼び出しを行い、その結果をまとめて返却してくれます。
これを利用することで、コントラクトだけでなく、フロントエンドや Bot からの呼び出しを効率的に行うことができます。早速使い方から紹介していきたいと思います。
使ってみる
今回は JavaScript ベースの環境を利用し、主に以下のツールを使用します。
- ethers.js -> ethereum blockchain と連携するライブラリ
- Hardhat -> ethereum 開発環境
- ※ JS を環境できる実行であればなんでも大丈夫なのですが、network 接続など楽なので、今回は hardhat の task を利用しています。
- (typescript)
イメージ
multicall (および Contract そのもの) と連携し呼び出すことができるコードのイメージを簡単に持ってもらうために、まずはコードのイメージで流れの説明をしたいと思います。
// 1. Multicall Contract と連携するための Contract Instance を生成
const multicall = new ethers.Contract(
"address", // multicall contract の address
ABI, // Application Binary Interface
ethers.provider // 実際の ethereum network と通信するための Provider
)
// 2. Multicall#aggregate を呼び出すための引数を作成
const inputs = [ ... ]
// 3. Multicall#aggregate を呼び出し、実行結果を受け取る
const result = await multicall.callStatic.aggregate(inputs);
// 4. 3 で取得したデータから指定した function の実行結果を出力
console.log(result[1][N])
ロジックの中心部分は上記のようなイメージになります。
ここに Multicall#aggregate
で渡す引数の生成 が考慮されることで、ぐっと複雑になります。
今回は Curve.fi の pool である 3pool を利用して、実際に Multicall を使ってみたいと思います。
この 3pool には色んなユーザーが stable coin である、USDC/USDT/DAI が deposit
していて、実際に 3Pool の Contract がこれらの token を保有しています。
この 3pool を user とし、それぞれの token の保有量を multicall を利用し確認する、ということを試みてみます。
Multicall を使わない場合
Multicall を利用せずに呼び出す場合には、各トークンコントラクトの #balanceOf(address)
を call する必要があります。
イメージとしては以下の通りで、3回 ethereum network を呼び出すことになります。
const ERC20_ABI = jsonfile.readFileSync("./abis/ERC20.json")
const TOKENS = {
USDC: {
address: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
decimals: 6
},
USDT: {
address: "0xdac17f958d2ee523a2206206994597c13d831ec7",
decimals: 6
},
DAI: {
address: "0x6b175474e89094c44da98b954eedeac495271d0f",
decimals: 18
}
}
const USER = "0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7" // Curve.fi: DAI/USDC/USDT Pool
task("call:direct", "call:direct").setAction(async ({}, hre: HardhatRuntimeEnvironment) => {
const { ethers } = hre
const usdc = new ethers.Contract(
TOKENS.USDC.address,
new ethers.utils.Interface(ERC20_ABI),
ethers.provider
)
const usdt = new ethers.Contract(
TOKENS.USDT.address,
new ethers.utils.Interface(ERC20_ABI),
ethers.provider
)
const dai = new ethers.Contract(
TOKENS.DAI.address,
new ethers.utils.Interface(ERC20_ABI),
ethers.provider
)
const results = await Promise.all([
usdc.balanceOf(USER), // 1回
usdt.balanceOf(USER), // 2回
dai.balanceOf(USER), // 3回
])
console.log(...)
})
同じようなコードで Contract Instance を3つ生成することはいいのですが(よくあるので)、
Promise.all
を利用して、並列実行で呼び出すようにしていますが、ethereum network に対しては3回 RPC request を行っています。
利用する側も呼び出される側も非効率なので、multicall を利用して効率化してみましょう。
Multicall を使ってみる
いきなりですが、完成版のコードは以下になります、個別コメントで説明を加えているので参考にしてください。
import keccak256 from "keccak256"
import { task } from "hardhat/config"
import { HardhatRuntimeEnvironment } from "hardhat/types"
import jsonfile from "jsonfile"
import { BigNumber } from "ethers"
import { ERC20__factory, Multicall__factory } from "../../libs/contracts/__generated__"
const multicall_abi = jsonfile.readFileSync("./abis/Multicall.json") // Multicall の Contract のABI (etherscan などで取得可能です)
const MULTICALL_ADDRESS = "0xeefba1e63905ef1d7acba5a8513c70307c1ce441" // Multicall Contract の address
// 今回利用する token (USDC/USDT/DAI) の address, decimals
const TOKENS = {
USDC: {
address: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
decimals: 6
},
USDT: {
address: "0xdac17f958d2ee523a2206206994597c13d831ec7",
decimals: 6
},
DAI: {
address: "0x6b175474e89094c44da98b954eedeac495271d0f",
decimals: 18
}
}
// Curve.fi: DAI/USDC/USDT Pool の address
const ADDRESS = "0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7"
/**
* use multicall by only ethersjs
* - support only mainnet
*/
task("multicall", "multicall").setAction(async ({}, hre: HardhatRuntimeEnvironment) => {
const { ethers } = hre
const multicall = new ethers.Contract(
MULTICALL_ADDRESS,
new ethers.utils.Interface(multicall_abi),
ethers.provider
)
// Multicall#aggregate に渡す callData の作成
const selector = keccak256("balanceOf(address)").toString('hex').substr(0, 8) // #balanceOf(address) の method id 作成
const param = ADDRESS.substring(2).padStart(64, "0") // 呼び出す function の引数生成: input を zero padding し 32byte のデータに加工する
const inputs = [
{
target: TOKENS.USDC.address, // USDC
callData: `0x${selector}${param}`
},
{
target: TOKENS.USDT.address, // USDT
callData: `0x${selector}${param}`
},
{
target: TOKENS.DAI.address, // DAI
callData: `0x${selector}${param}`
},
]
const result = await multicall.callStatic.aggregate(inputs);
for (const [index, key] of Object.keys(TOKENS).entries()) {
const balanceOf = ethers.utils.formatUnits( // 表示向けに、token の decimals 分 shift させる
BigNumber.from(result[1][index]), // Multicall#aggregate 実行結果から、arguments order を考慮して、該当するデータを取得する
TOKENS[key as keyof typeof TOKENS].decimals
)
console.log(`${key} ... ${balanceOf}`)
}
})
上記のコードを実行すると以下の実行結果が得られ、それぞれの token の保有量が確認できています。
$ npx hardhat multicall --network mainnet
USDC ... 1085091186.161086
USDT ... 822667786.055421
DAI ... 1252359115.58683885475104325
Self Q&A
Q. callData の作成って何やってるの?
- Multicall は実態として、#call を利用しています
- call の引数には、以下を組み合わせた
0x${selector}${param}
が必要- selector ... 呼び出すべき function を指定
- function の signature を hash 化した value の先頭の 4byte
- param ... 呼び出す function に必要な引数部分
- input を zero padding し 32byte のデータに加工する
- selector ... 呼び出すべき function を指定
参考
スマートコントラクトを使った入金システムについて全力で理解してみた
Q. Multicall はどこでも使えるの?
- repository にあるように、testnet も deploy がされていて利用可能です
- また Parachain についても有志によって deploy されており、利用できるところが多いです
- deploy されたことが PR で報告されていますが、マージされておらず README では確認できません、PR をみていただくとアドレスがわかるかと思います
- こういったことができるので、自分で活動したいチェーンに Multicall がなければ自分で deploy して利用することも可能です
Q. そもそも Multicall をサクッと扱えるライブラリないの?
-
Multicall.js
というのがあるようです- Multicall の repository にリンクがあり、MakerDao から公式に提供されているようです
- (自分自身は使ったことなく、これ以上紹介できないです、すみません...)
Multicall を使ってみる with Typechain
既に Multicall を利用する方法については説明しましたが、おまけとして typechain を利用するともっと簡単に呼び出せるよということを紹介して締めようかと思います。
typechain とは、ABI から Contract と連携するための、TypeScript コードを自動生成してくれる integration tool になります。
hardhat plugin
, truffle plugin
などもあり、自分の ethereum 開発環境に組み込むことで、test, deploy script などを記述しやすくしてくれるため、個人的に必須アイテムです。
Typechain による自動生成コードを利用すると以下のようになります。
task("multicall-with-typechain", "multicall-with-typechain").setAction(async ({}, hre: HardhatRuntimeEnvironment) => {
const { ethers } = hre
const multicall = Multicall__factory.connect(
MULTICALL_ADDRESS,
ethers.provider
)
const _interface = ERC20__factory.createInterface()
const callData = [
{
target: TOKENS.USDC.address, // USDC
callData: _interface.encodeFunctionData("balanceOf", [ADDRESS])
},
{
target: TOKENS.USDT.address, // USDT
callData: _interface.encodeFunctionData("balanceOf", [ADDRESS])
},
{
target: TOKENS.DAI.address, // DAI
callData: _interface.encodeFunctionData("balanceOf", [ADDRESS])
},
]
const result = await multicall.callStatic.aggregate(callData)
for (const [index, key] of Object.keys(TOKENS).entries()) {
console.log(
ethers.utils.formatUnits(
_interface.decodeFunctionResult(
"balanceOf",
result.returnData[index]
)[0],
TOKENS[key as keyof typeof TOKENS].decimals)
)
}
})
Typechain を利用していないパターンと比較すると、
- Contract Instance (今回で言うと Multicall Contract) の生成
- Contract function の呼び出し
- Contract function 呼び出しに必要な引数の生成
あたりが、非常に簡潔に、かつ、他ライブラリを必要とせずにできているのがわかると思います。
特に Multicall の場合は、keccak256
を利用したり、指定 byte 数での切り出しをおこなったりしているので、そういった部分の実装を省略できているので、よりフットワーク軽く扱えるようになっています。
おわりに
ピンポイントな topic ではありますが、Typechain 含め Dapps 開発において非常に生産性を上げるものなので今回紹介させていただきました。
実際に動作可能なコードを、なるべく最低限必要な情報のみで紹介したので、興味ある人は実際に触ってみてもらえると嬉しいです。
(前回更新からだいぶ日が空いてしまいましたが、)技術知見共有として記事を上げられてよかったです。今後も"web3"に関して興味を持っている方々に対し何かしら貢献していけたらと思います。
最後までお読みいただきありがとうございました!🙇
参考
Discussion