跳到主要内容

MultiCall 源码解析

· 阅读需 7 分钟

源码

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"}]
}