avatar
原创2023/09/29

MultiCall 源码解析

AI 总结

深度解析 MultiCall 合约三个版本(Multicall、Multicall2、Multicall3)的源码实现,逐一拆解批量调用、允许失败调用、区块信息聚合查询等核心函数的工作原理,并结合 wagmi/viem 的实际使用示例,说明如何利用 MultiCall 大幅减少 RPC 请求次数、提升 DApp 性能。

源码

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"}]
}
点击播放