📚

[Bunzz Decipher] 『Multicall2』コントラクトを理解しよう!

2023/09/06に公開

はじめに

初めまして。
CryptoGamesというブロックチェーンゲーム企業でエンジニアをしている cardene(かるでね) です!
スマートコントラクトを書いたり、フロントエンド・バックエンド・インフラと幅広く触れています。

https://cryptogames.co.jp/

代表的なゲームはクリプトスペルズというブロックチェーンゲームです。

https://cryptospells.jp/

今回はBunzzの新機能『DeCipher』を使用して、「Multicall2」コントラクトを見てみようと思います。

DeCipher』はAIを使用してコントラクトのドキュメントを自動生成してくれるサービスです。

https://www.bunzz.dev/decipher

詳しい使い方に関しては以下の記事を参考にしてください!

https://zenn.dev/heku/articles/33266f0c19d523

今回使用する『DeCipher』のリンクは以下になります。

https://app.bunzz.dev/decipher/chains/1/addresses/0x5BA1e12693Dc8F9c48aAD8770482f4739bEeD696

Etherscanのリンクは以下になります。

https://etherscan.io/address/0x5BA1e12693Dc8F9c48aAD8770482f4739bEeD696

概要

Multicall2コントラクトは、MakerDAOが開発した便利なツールで、Ethereumブロックチェーン上で複数の関数呼び出しを効率的に実行します。
元々のMulticallコントラクトの機能を広げ、バージョン2としてさらに使いやすくしたものです。

目的

Multicall2コントラクトの目的は、複数の関数呼び出しの結果を集めることです。
これは、Ethereumブロックチェーン上で複数のコントラクトや関数からデータを取得する必要がある場合に便利です。
通常、1つずつ関数を呼び出すよりも、Multicall2コントラクトを使ってまとめて実行するほうが時間とコストを節約できます。

責務

まとめて関数呼び出し

コントラクト内の「aggregate」という関数は、複数の関数呼び出しを受け取ることができます。
各呼び出しの対象アドレスとデータがCall構造体に格納され、一度に実行されます。
エラーが発生した場合はリバートします。

ブロック情報の取得

コントラクトは、現在のブロックに関する情報を取得するための関数を提供しています。
ブロック番号やタイムスタンプなどの情報を取得できます。

ETH残高の取得

指定されたアドレスのEther残高を取得するための関数も提供されています。

トライ関数呼び出し

tryAggregate」という関数は、複数の関数呼び出しをまとめて実行しますが、必要に応じてエラーハンドリングの方法を選ぶことができます。

ブロックと関数呼び出しのトライ

tryBlockAndAggregate」という関数は、関数呼び出しとブロック情報の取得を組み合わせたもので、ブロック番号やタイムスタンプなどを取得できます。

使い方

Multicall2コントラクトは、複数関数呼び出しの結果を1つにまとめるためのものです。
1つのトランザクションで複数のコントラクトや関数からデータを取得することができ、ネットワークリクエストの数を減らし、効率的にデータを取得できます。

以下は、Multicall2コントラクトを使い時の手順です。

1. 呼び出しの定義

最初に、まとめて実行する呼び出しデータを定義します。
それぞれの呼び出しはCall構造体で表され、対象アドレスとcallDataバイト列を含みます。callDataには、呼び出す関数のシグネチャとパラメータがエンコードされています。

2. aggregate関数の呼び出し

定義した呼び出しを配列として、aggregate関数に渡します。
この関数は、各呼び出しを順番に実行し、戻りデータを配列に格納します。
どれかの呼び出しが失敗すると、トランザクション全体が元に戻ります。

3. 戻りデータの処理

aggregate関数は、実行時のブロック番号と、各呼び出しの戻りデータをバイト列の配列で返します。
このデータを、呼び出した関数の戻り型に合わせてデコードする必要があります。

4. tryAggregate関数の利用(オプション)

呼び出しを実行したいが、1つの呼び出しが失敗してもトランザクション全体を失敗させたくない場合は、tryAggregate関数を使います。
この関数はaggregateと似ていますが、失敗してもリバートされず、呼び出し毎にResult構造体を返します。
この構造体には、成功フラグと戻りデータが含まれます。

5. tryBlockAndAggregate関数の利用(オプション)

実行時のブロック番号とブロックハッシュも取得したい場合は、tryBlockAndAggregate関数を使います。
この関数はtryAggregateと同様に実行時のブロック番号とブロックハッシュも返します。

パラメーター

なし。

コントラクト

Multicall2

Call

Call
struct Call {
    address target;
    bytes callData;
}

概要
関数呼び出しの対象となるコントラクトアドレスと関数呼び出しのデータを保持する構造体。

パラメーター

  • target
    • 関数を呼び出す対象のコントラクトアドレス。
  • callData
    • 呼び出す関数のシグネチャと引数をエンコードしたバイトデータ。
    • 関数呼び出しの詳細な情報が含まれています。

Result

Result
struct Result {
    bool success;
    bytes returnData;
}

概要
関数呼び出しの結果を示す情報を保持する構造体。
成功したかどうかと戻りデータを格納します。

パラメーター

  • success
    • 関数呼び出しが成功したかどうかを示す真偽値。
    • 成功した場合はtrue、失敗した場合はfalseが格納されます。
  • returnData
    • 関数呼び出しの結果として返されたデータを格納するバイトデータ。
    • 関数の戻り値が含まれます。

aggregate

aggregate
function aggregate(Call[] memory calls) public returns (uint256 blockNumber, bytes[] memory returnData) {
    blockNumber = block.number;
    returnData = new bytes[](calls.length);
    for(uint256 i = 0; i < calls.length; i++) {
        (bool success, bytes memory ret) = calls[i].target.call(calls[i].callData);
        require(success, "Multicall aggregate: call failed");
        returnData[i] = ret;
    }
}

概要
複数の外部呼び出しをまとめて行い結果を返す関数。

詳細

  • callsという配列に含まれる複数のCallオブジェクトを受け取ります。
  • 現在のブロック番号をblockNumberに保存します。
  • returnDataの初期化を行います。
    • callsの数だけ要素を持つバイト列の配列です。
  • Callオブジェクトを順番に実行し、その結果をreturnDataに格納します。
    • 呼び出しに失敗した場合は例外が発生します。

引数

  • calls
    • 外部呼び出しを行うためのCallオブジェクトの配列。

戻り値

  • blockNumber
    • 現在のブロック番号。
  • returnData
    • ≥各外部呼び出しの結果を格納したバイト列の配列。

blockAndAggregate

blockAndAggregate
function blockAndAggregate(Call[] memory calls) public returns (uint256 blockNumber, bytes32 blockHash, Result[] memory returnData) {
    (blockNumber, blockHash, returnData) = tryBlockAndAggregate(true, calls);
}

概要
tryBlockAndAggregate関数を呼び出して、ブロック情報と外部呼び出しの結果をまとめて取得する関数。

詳細

  • tryBlockAndAggregate関数を呼び出して、ブロック情報と外部呼び出しの結果を取得します。
  • tryBlockAndAggregate関数の呼び出し結果がblockNumberblockHash、およびreturnDataに代入されます。

引数

  • calls
    • 外部呼び出しを行うためのCallオブジェクトの配列。

戻り値

  • blockNumber
    • 現在のブロック番号。
  • blockHash
    • 現在のブロックのハッシュ値。
  • returnData
    • 各外部呼び出しの結果を格納したResultオブジェクトの配列。

getBlockHash

getBlockHash
function getBlockHash(uint256 blockNumber) public view returns (bytes32 blockHash) {
    blockHash = blockhash(blockNumber);
}

概要
指定されたブロック番号のブロックハッシュ値を取得する関数。

詳細

  • 指定されたblockNumberのブロックハッシュ値をblockHashに代入して返します。

引数

  • blockNumber
    • 取得したいブロックの番号。

戻り値

  • blockHash
    • 指定されたブロックのハッシュ値。

getBlockNumber

getBlockNumber
function getBlockNumber() public view returns (uint256 blockNumber) {
    blockNumber = block.number;
}

概要
現在のブロック番号を取得する関数。

詳細

  • 現在のブロック番号をblockNumberに代入して返します。

戻り値

  • blockNumber
    • 現在のブロック番号。

getCurrentBlockCoinbase

getCurrentBlockCoinbase
function getCurrentBlockCoinbase() public view returns (address coinbase) {
    coinbase = block.coinbase;
}

概要
現在のブロックマイニング報酬を受け取るアドレスを取得する関数。

詳細

  • block.coinbaseは、現在のブロックをマイニングしたマイナーのアドレス、つまりコインベースアドレスを表します。
  • 現在のブロックのコインベースアドレスをcoinbaseに代入して返します。

戻り値

  • coinbase
    • 現在のブロックのコインベースアドレス。

getCurrentBlockDifficulty

getCurrentBlockDifficulty
function getCurrentBlockDifficulty() public view returns (uint256 difficulty) {
    difficulty = block.difficulty;
}

概要
現在のブロックの難易度を取得する関数。

詳細

  • 現在のブロックのマイニング難易度をdifficultyに代入して返します。

戻り値

  • difficulty
    • 現在のブロックの難易度。

getCurrentBlockGasLimit

getCurrentBlockGasLimit
function getCurrentBlockGasLimit() public view returns (uint256 gaslimit) {
    gaslimit = block.gaslimit;
}

概要
現在のブロックのガスリミットを取得する関数。

詳細

  • 現在のブロックのガスリミットをgaslimitに代入して返します。

戻り値

  • gaslimit
    • 現在のブロックのガスリミット。

getCurrentBlockTimestamp

getCurrentBlockTimestamp
function getCurrentBlockTimestamp() public view returns (uint256 timestamp) {
    timestamp = block.timestamp;
}

概要
現在のブロックのタイムスタンプを取得する関数。

詳細

  • 現在のブロックのタイムスタンプをtimestampに代入して返します。

戻り値

  • timestamp
    • 現在のブロックのタイムスタンプ。

getEthBalance

getEthBalance
function getEthBalance(address addr) public view returns (uint256 balance) {
    balance = addr.balance;
}

概要
指定されたアドレスのETH残高を取得する関数。

詳細

  • 指定されたアドレスのETH残高をbalanceに代入して返します。

引数

  • addr
    • ETH残高を取得したいアドレス。

戻り値

  • balance
    • 指定されたアドレスのETH残高。

getLastBlockHash

getLastBlockHash
function getLastBlockHash() public view returns (bytes32 blockHash) {
    blockHash = blockhash(block.number - 1);
}

概要
直前のブロックのハッシュ値を取得する関数。

詳細

  • 直前のブロックのハッシュ値をblockHashに代入して返します。

戻り値

  • blockHash
    • 直前のブロックのハッシュ値。

tryAggregate

tryAggregate
function tryAggregate(bool requireSuccess, Call[] memory calls) public returns (Result[] memory returnData) {
    returnData = new Result[](calls.length);
    for(uint256 i = 0; i < calls.length; i++) {
        (bool success, bytes memory ret) = calls[i].target.call(calls[i].callData);

        if (requireSuccess) {
            require(success, "Multicall2 aggregate: call failed");
        }

        returnData[i] = Result(success, ret);
    }
}

概要
指定された外部呼び出しを順番に実行し、結果を収集する関数。
必要に応じて呼び出しの成功を確認し、結果をResultオブジェクトの配列として返します。

詳細

  • requireSuccesstrueの場合、各呼び出しの成功を確認し、失敗した場合に例外が発生します。
  • callsという配列に含まれる複数のCallオブジェクトを受け取ります。
  • Callオブジェクトを順番に実行し、その結果と成功フラグをResultオブジェクトに格納します。

引数

  • requireSuccess
    • 呼び出しの成功を確認するかどうかのフラグ。
  • calls
    • 外部呼び出しを行うためのCallオブジェクトの配列。

戻り値

  • returnData
    • 各外部呼び出しの結果と成功フラグを格納したResultオブジェクトの配列。

tryBlockAndAggregate

tryBlockAndAggregate
function tryBlockAndAggregate(bool requireSuccess, Call[] memory calls) public returns (uint256 blockNumber, bytes32 blockHash, Result[] memory returnData) {
    blockNumber = block.number;
    blockHash = blockhash(block.number);
    returnData = tryAggregate(requireSuccess, calls);
}

概要
指定された外部呼び出しとブロック情報を組み合わせて実行し、結果を収集する関数。
必要に応じて呼び出しの成功を確認し、結果をResultオブジェクトの配列として返します。

詳細

  • requireSuccesstrueの場合、各呼び出しの成功を確認し、失敗した場合に例外が発生します。
  • 現在のブロック番号をblockNumberに保存します。
  • 現在のブロックのハッシュ値をblockHashに保存します。
  • 外部呼び出しを行い、その結果と成功フラグをResultオブジェクトに格納します。

引数

  • requireSuccess
    • 呼び出しの成功を確認するかどうかのフラグ。
  • calls
    • 外部呼び出しを行うためのCallオブジェクトの配列。

戻り値

  • blockNumber
    • 現在のブロック番号。
  • blockHash
    • 現在のブロックのハッシュ値。
  • returnData
    • 各外部呼び出しの結果と成功フラグを格納したResultオブジェクトの配列。

コード

Multicall2.sol

Multicall2.sol
/**
 *Submitted for verification at Etherscan.io on 2021-03-23
*/

pragma solidity >=0.5.0;
pragma experimental ABIEncoderV2;

/// @title Multicall2 - Aggregate results from multiple read-only function calls
/// @author Michael Elliot <mike@makerdao.com>
/// @author Joshua Levine <joshua@makerdao.com>
/// @author Nick Johnson <arachnid@notdot.net>

contract Multicall2 {
    struct Call {
        address target;
        bytes callData;
    }
    struct Result {
        bool success;
        bytes returnData;
    }

    function aggregate(Call[] memory calls) public returns (uint256 blockNumber, bytes[] memory returnData) {
        blockNumber = block.number;
        returnData = new bytes[](calls.length);
        for(uint256 i = 0; i < calls.length; i++) {
            (bool success, bytes memory ret) = calls[i].target.call(calls[i].callData);
            require(success, "Multicall aggregate: call failed");
            returnData[i] = ret;
        }
    }
    function blockAndAggregate(Call[] memory calls) public returns (uint256 blockNumber, bytes32 blockHash, Result[] memory returnData) {
        (blockNumber, blockHash, returnData) = tryBlockAndAggregate(true, calls);
    }
    function getBlockHash(uint256 blockNumber) public view returns (bytes32 blockHash) {
        blockHash = blockhash(blockNumber);
    }
    function getBlockNumber() public view returns (uint256 blockNumber) {
        blockNumber = block.number;
    }
    function getCurrentBlockCoinbase() public view returns (address coinbase) {
        coinbase = block.coinbase;
    }
    function getCurrentBlockDifficulty() public view returns (uint256 difficulty) {
        difficulty = block.difficulty;
    }
    function getCurrentBlockGasLimit() public view returns (uint256 gaslimit) {
        gaslimit = block.gaslimit;
    }
    function getCurrentBlockTimestamp() public view returns (uint256 timestamp) {
        timestamp = block.timestamp;
    }
    function getEthBalance(address addr) public view returns (uint256 balance) {
        balance = addr.balance;
    }
    function getLastBlockHash() public view returns (bytes32 blockHash) {
        blockHash = blockhash(block.number - 1);
    }
    function tryAggregate(bool requireSuccess, Call[] memory calls) public returns (Result[] memory returnData) {
        returnData = new Result[](calls.length);
        for(uint256 i = 0; i < calls.length; i++) {
            (bool success, bytes memory ret) = calls[i].target.call(calls[i].callData);

            if (requireSuccess) {
                require(success, "Multicall2 aggregate: call failed");
            }

            returnData[i] = Result(success, ret);
        }
    }
    function tryBlockAndAggregate(bool requireSuccess, Call[] memory calls) public returns (uint256 blockNumber, bytes32 blockHash, Result[] memory returnData) {
        blockNumber = block.number;
        blockHash = blockhash(block.number);
        returnData = tryAggregate(requireSuccess, calls);
    }
}

最後に

今回の記事では、Bunzzの新機能『DeCipher』を使用して、「Multicall2」コントラクトを見てきました。
いかがだったでしょうか?
今後も特定のNFTやコントラクトをピックアップしてまとめて行きたいと思います。

普段はブログやQiitaでブロックチェーンやAIに関する記事を挙げているので、よければ見ていってください!

https://chaldene.net/

https://qiita.com/cardene

CryptoGames

Discussion