MultiCall
存在三个版本:
常用于以下两个使用场景:
- 将多个合约读取的结果聚合到一个单独的 JSON-RPC 请求中
- 在单笔交易中执行多个合约状态更改
Multicall3
被部署在多条链上,地址都是 0xcA11bde05977b3631167028862bE2a173976CA11
。其他版本的地址见这个 仓库
Multicall3 合约源码
参数
调用该合约方法携带的参数数据结构。存在以下三种:
struct Call {
address target; // 目标合约地址
bytes callData; // 调用目标合约地址的数据
}
struct Call3 {
address target; // 目标合约地址
bool allowFailure; // 是否允许失败,如果值为false, 则调用失败时会 revert
bytes callData; // 调用目标合约地址的数据
}
struct Call3Value {
address target; // 目标合约地址
bool allowFailure; // 是否允许失败,如果值为false, 则调用失败时会 revert
uint256 value; // 调用需要携带的资金
bytes callData;// 调用目标合约地址的数据
}
返回结果
struct Result {
bool success; // 合约调用是否失败
bytes returnData; // 合约调用的返回值
}
合约方法
aggregate
: 批量调用,且不允许单次调用失败。返回区块高度和调用结果数组tryAggregate
: 批量调用,是否允许失败由参数requireSuccess
控制。返回调用结果数组tryBlockAndAggregate
: 同tryAggregate
,返回结果新增区块高度和区块哈希blockAndAggregate
: 同aggregate
, 返回结果新增区块高度和区块哈希aggregate3
: 批量调用,可指定单次调用是否允许失败。如果某次调用失败,且allowFailure
被指定为false
, 则整个交易revert
aggregate3Value
: 同aggregate3
,但可以在交易中携带资金
aggregate
批量调用,且不允许单次调用失败。返回区块高度和调用结果数组
function aggregate(Call[] calldata calls) public payable returns (uint256 blockNumber, bytes[] memory returnData) {
blockNumber = block.number; // 区块高度
uint256 length = calls.length; // 调用次数
returnData = new bytes[](length); // 返回结果变量定义
Call calldata call;
// 遍历需要执行的合约调用
for (uint256 i = 0; i < length;) {
bool success;
call = calls[i];
// 合约调用
(success, returnData[i]) = call.target.call(call.callData);
// 要求本次调用是成功的,如果失败,则整个交易失败
require(success, "Multicall3: call failed");
unchecked { ++i; }
}
}
tryAggregate
批量调用,是否允许失败由参数 requireSuccess
控制。返回调用结果数组
function tryAggregate(bool requireSuccess, Call[] calldata calls) public payable returns (Result[] memory returnData) {
uint256 length = calls.length;
returnData = new Result[](length);
Call calldata call;
// 遍历需要执行的合约调用
for (uint256 i = 0; i < length;) {
Result memory result = returnData[i];
call = calls[i];
// 合约调用
(result.success, result.returnData) = call.target.call(call.callData);
// requireSuccess为 true, 且调用失败,则整个交易失败
if (requireSuccess) require(result.success, "Multicall3: call failed");
unchecked { ++i; }
}
}
tryBlockAndAggregate
同 tryAggregate
,返回结果新增区块高度和区块哈希
function tryBlockAndAggregate(bool requireSuccess, Call[] calldata calls) public payable returns (uint256 blockNumber, bytes32 blockHash, Result[] memory returnData) {
blockNumber = block.number;
blockHash = blockhash(block.number);
returnData = tryAggregate(requireSuccess, calls);
}
blockAndAggregate
同 aggregate
, 返回结果新增区块高度和区块哈希
function blockAndAggregate(Call[] calldata calls) public payable returns (uint256 blockNumber, bytes32 blockHash, Result[] memory returnData) {
(blockNumber, blockHash, returnData) = tryBlockAndAggregate(true, calls);
}
aggregate3
批量调用,可指定单次调用是否允许失败。如果某次调用失败,且 allowFailure
被指定为false
, 则整个交易 revert
function aggregate3(Call3[] calldata calls) public payable returns (Result[] memory returnData) {
uint256 length = calls.length;
returnData = new Result[](length);
Call3 calldata calli;
// 遍历需要执行的合约调用
for (uint256 i = 0; i < length;) {
Result memory result = returnData[i];
calli = calls[i];
// 合约调用
(result.success, result.returnData) = calli.target.call(calli.callData);
assembly {
// 如果调用失败, 且 calli.allowFailure 是 false, 则整个交易失败
// `allowFailure := calldataload(add(calli, 0x20))` and `success := mload(result)`
if iszero(or(calldataload(add(calli, 0x20)), mload(result))) {
// set "Error(string)" signature: bytes32(bytes4(keccak256("Error(string)")))
mstore(0x00, 0x08c379a000000000000000000000000000000000000000000000000000000000)
// set data offset
mstore(0x04, 0x0000000000000000000000000000000000000000000000000000000000000020)
// set length of revert string
mstore(0x24, 0x0000000000000000000000000000000000000000000000000000000000000017)
// set revert string: bytes32(abi.encodePacked("Multicall3: call failed"))
mstore(0x44, 0x4d756c746963616c6c333a2063616c6c206661696c6564000000000000000000)
revert(0x00, 0x64)
}
}
unchecked { ++i; }
}
}
aggregate3Value
同 aggregate3
,但可以在交易中携带资金
function aggregate3Value(Call3Value[] calldata calls) public payable returns (Result[] memory returnData) {
uint256 valAccumulator; // 所有调用的总资金量
uint256 length = calls.length;
returnData = new Result[](length);
Call3Value calldata calli;
// 遍历需要执行的合约调用
for (uint256 i = 0; i < length;) {
Result memory result = returnData[i];
calli = calls[i];
uint256 val = calli.value;
// 将本次调用的资金加到总资金量中
unchecked { valAccumulator += val; }
// 合约调用
(result.success, result.returnData) = calli.target.call{value: val}(calli.callData);
assembly {
// 如果调用失败, 且 calli.allowFailure 是 false, 则整个交易失败
// `allowFailure := calldataload(add(calli, 0x20))` and `success := mload(result)`
if iszero(or(calldataload(add(calli, 0x20)), mload(result))) {
// set "Error(string)" signature: bytes32(bytes4(keccak256("Error(string)")))
mstore(0x00, 0x08c379a000000000000000000000000000000000000000000000000000000000)
// set data offset
mstore(0x04, 0x0000000000000000000000000000000000000000000000000000000000000020)
// set length of revert string
mstore(0x24, 0x0000000000000000000000000000000000000000000000000000000000000017)
// set revert string: bytes32(abi.encodePacked("Multicall3: call failed"))
mstore(0x44, 0x4d756c746963616c6c333a2063616c6c206661696c6564000000000000000000)
revert(0x00, 0x84)
}
}
unchecked { ++i; }
}
// 最后要求每次调用的总资金量总和等于交易携带的资金量。否则整个交易失败
require(msg.value == valAccumulator, "Multicall3: value mismatch");
}
实践
读取账户中 token 余额
import { Address, multicall, erc20ABI } from '@wagmi/core'
const getTokenBalance = async () => {
const USDTContract = {
address: '0xdac17f958d2ee523a2206206994597c13d831ec7' as Address,
abi: erc20ABI
}
const SHIBContract = {
address: '0x95ad61b0a150d79219dcf64e1e6cc01f0b64c4ce' as Address,
abi: erc20ABI
}
const address = '0x.....'
const data = await multicall({
contracts: [
{
...USDTContract,
functionName: 'balanceOf',
args: [address]
},
{
...SHIBContract,
functionName: 'balanceOf',
args: [address]
}
]
})
console.log(data)
// [{result: 0n, status: "success"}, {result: 0n, status: "success"}]
}