Solidity Call と delegatecall について

基本的な考え方

通話の利用

callは Solidity のコントラクト間の対話のための最下位の呼び出しですが、このメソッドはイーサリアムを送信する場合にのみ使用することが公式に推奨されています。コントラクトでは、転送を受け入れるために受信または支払い可能なフォールバックを定義する必要があります (存在しないメソッドを呼び出す場合)コントラクトでは、このメソッドはデフォルトの呼び出しです)、コントラクト内に存在するメソッドを呼び出すために call を使用することはお勧めできません。受信とフォールバックの違いについては、以下のサンプル コントラクト Caller の RemoteCall メソッドを参照してください。詳細な手順については、こちら を参照してください。

電話の例

次に、イーサリアムの送信とコントラクトメソッドの呼び出しをデモンストレーションします。注: この例で使用されているハードハットの console.sol コントラクトを呼び出してログ情報を出力する便宜のためです。

サンプル契約書

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract Callee {
    
    
    fallback() external payable {
    
    
        console.log("In fallback Ether", msg.sender, msg.value);
    }

    receive() external payable {
    
    
        console.log("In Receive Ether", msg.sender, msg.value);
    }

    function foo(string memory _message, uint256 _x)
        public
        view
        returns (uint256)
    {
    
    
        console.log("foo invoking", msg.sender, _message);
        return _x + 1;
    }
}

contract Caller {
    
    
    constructor() payable{
    
    

    }
    function remoteCall(address instance) public payable {
    
    
        //触发fallback调用
        (bool sucess, ) = instance.call{
    
    value: 200}(abi.encodeWithSignature('nonExistingFunction()'));
        require(sucess, "call error");

        // //触发receive调用
        (bool sucess2, ) = instance.call{
    
    value: 200}('');
        require(sucess2, "call error");

        //调用foo
        (bool sucess3, ) = instance.call(abi.encodeWithSignature('foo(string,uint256)', 'hello foo', 100));
        require(sucess3, "call error");
    }
}

契約間の通話

  • JS スクリプト呼び出しを使用してコントラクト呼び出しをトリガーする
const hre = require('hardhat');
async function main () {
    
    

  let Callee = await hre.ethers.getContractFactory("Callee");
  let Caller = await hre.ethers.getContractFactory("Caller");
  let callee = await Callee.deploy();
  //由于remoteCall方法向其它合约转以太坊,因此该合约部署时需要转入
  let caller = await Caller.deploy({
    
    value:10000});
  await caller.remoteCall(callee.address);
}

main()
  .then(() => process.exit(0))
  .catch(error => {
    
    
    console.error(error);
    process.exit(1);
  });
  • 実行結果に関する注意ここに画像の説明を挿入します
    :ハードハット フレームワークを使用するとnpx hardhat run <script>、ハードハット フレームワークはデフォルトでハードハット環境変数を JS スクリプトに導入するため、スクリプト内で hre 変数を定義する必要はありません。ここに示すものでは、スクリプトを通常の JS スクリプト ( ) のように実行できるように、ハードハット環境変数 (const hre = require('hardhat');) を導入していますnode <script>

ヘルメットの公式説明は次のとおりです。

ここでは Hardhat ランタイム環境を明示的に要求します。これはオプションですが、 を介してスタンドアロン方式でスクリプトを実行する場合に便利ですnode <script>
を使用してスクリプトを実行することもできますnpx hardhat run <script>これを行うと、Hardhat はコントラクトをコンパイルし、Hardhat ランタイム環境のメンバーをグローバル スコープに追加して、スクリプトを実行します。

リミックスコール

  • 300wei をコントラクトに転送し、calldata は空ではありません (フォールバックが呼び出されます)。
    ここに画像の説明を挿入します
  • 300wei をコントラクトに転送し、calldata は空です (receive が呼び出されます)。
    ここに画像の説明を挿入します
  • メソッド呼び出し
    ここに画像の説明を挿入します

電話をかけるときの注意点

他のコントラクトのメソッドをcallする場合は、上記例のCallerコントラクトのremoteCallのcallメソッドなど、必ず実行結果を確認してください。

//触发fallback调用
(bool sucess, ) = instance.call{
    
    value: 200}(abi.encodeWithSignature('nonExistingFunction()'));
require(sucess, "call error");

call は状況によっては便利なツールですが、他のコントラクト内の既存の関数を呼び出す場合の使用は一般的に推奨されません。その理由は次のとおりです。

  • Revert は呼び出しスタックをバブルアップできません

call を使用して関数を呼び出す場合、呼び出された関数内で発生する Revert は呼び出し元のコントラクトに反映されません。これは、呼び出し元のコントラクトが Revert の発生を認識せず、誤って実行され続ける可能性があることを意味します。

  • 型チェックは無視される

Solidity は、データの整合性とセキュリティを保証する型システムを提供します。ただし、call を使用する場合、関数パラメータの型チェックはバイパスされます。これにより、入力タイプが正しく処理されない場合、潜在的な脆弱性が発生する可能性があります。

  • 関数の存在のチェックがありません

call を使用して関数を呼び出すことで、Solidity によって実行される自動存在チェックをバイパスできます。関数が存在しないか名前が変更されている場合、呼び出しによってフォールバック関数がトリガーされ、予期しない動作が発生する可能性があります。

コールとデリゲートコール

call と delegatecall はどちらもコントラクト間の対話に使用される低レベル関数です。違いは 2 つの実行コンテキストにあります。前者のコンテキストは呼び出されるコントラクトであり、後者のコンテキストは呼び出しを開始したコントラクトです。 。少し抽象的に聞こえるので、例を挙げてみましょう。

説明例

実行コンテキストの呼び出し

実行コンテキストの呼び出し図に示すように、コントラクト A がコントラクト B 内のメソッドを呼び出しの形式で呼び出すと、呼び出されたメソッド (B 内のメソッド) の実行コンテキストはコントラクト B にあります。画像のアドレス(この)コントラクトアドレスの値を参照してください。

  • テスト コントラクトのソース コードは次のとおりです。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.2 <0.9.0;
import "hardhat/console.sol";
contract A {
    
    
    constructor() {
    
    
        console.log("Contract A's address:", address(this));
    }

    function remoteCall(address instance) public {
    
    
        (bool sucess, ) = instance.call(abi.encodeWithSignature("myAddress()"));
        require(sucess, "call error");
    }
}

contract B {
    
    
    constructor() {
    
    
        console.log("Contract B's address:", address(this));
    }

    function myAddress() public view {
    
    
        console.log("In contract B's myAddress", address(this));
    }
}

この例では、コントラクト A はコントラクト B の myAddress メソッドを呼び出し、このメソッドは実行コンテキスト コントラクトのアドレスを単に出力します。A は呼び出しの形式で B のメソッドを呼び出すため、前述したとおり、myAddress メソッドの実行コンテキストはコントラクト B であるため、このメソッドの出力はコントラクト B のアドレスになるはずです。コントラクト A と B をデプロイします。 Remix でそれらを順番に呼び出します。

  • Remix での実行結果:

ここに画像の説明を挿入します

delegatecall呼び出しの実行コンテキスト

デリゲートコール

例の呼び出しを delegatecall に変更するだけです。delegatecall で呼び出された場合、前述したように myAddress メソッドの実行コンテキストはコントラクト A であるため、コントラクト A のアドレスはメソッド myAddress に出力される必要があります。

  • 契約 A コードは次のように変更されます。
function remoteCall(address instance) public {
    
    
        (bool sucess, ) = instance.delegatecall(abi.encodeWithSignature("myAddress()"));
        require(sucess, "call error");
    }
  • Remixでの実行結果は以下の通りです。

ここに画像の説明を挿入します

デリゲートコールの脆弱性

堅牢性の特徴

delegatecall 呼び出しが脆弱性になりやすい理由は、堅牢性の次の 2 つの特性に関連しています。

  1. コントラクトが呼び出されるとき、実行コンテキストは呼び出しを開始したコントラクト内にあります。
  2. 呼び出し元と呼び出し先の状態変数のレイアウトは一貫している必要があります。

脆弱性の例

例を通してさらに詳しく見てみましょう。

  • 脆弱な契約コード
contract Vulnerable {
    
    
    address public owner;
    Lib public lib;

    constructor(Lib _lib) {
    
    
        owner = msg.sender;
        lib = Lib(_lib);
    }

    fallback() external payable {
    
    
        address(lib).delegatecall(msg.data);
    }
}

contract Lib {
    
    
    address public owner;

    function setowner() public {
    
    
        owner = msg.sender;
    }
}

上記のコントラクトは比較的理解しやすいです. Vulnerable コントラクトをデプロイするときは、デプロイされた Lib コントラクトのアドレスを渡す必要があります. Vulnerable コントラクトのフォールバックは、delegatecall の形式でコントラクト Lib 内のメソッドを呼び出します (呼び出しメソッドは指定されています)呼び出し元が msg.data を通じて送信します)。ここの Lib コントラクトにはメソッド setOwner が 1 つだけあり、これは所有者の値を変更するために使用されます。

  • 攻撃コード
    次に、コードを使用して、上記の脆弱なコントラクトの所有者を変更します。
contract AttackVulnerable {
    
    
    address public vulnerable;

    constructor(address _vulnerable) {
    
    
        vulnerable = _vulnerable;
    }

    function attack() public {
    
    
        vulnerable.call(abi.encodeWithSignature("setowner()"));
    }
}

攻撃者は、 AttackVulnerable コントラクトをデプロイするときに Vulnerable コントラクトのアドレスを渡し、デプロイメントの完了後に独自の攻撃メソッドを呼び出して、Vulnerable コントラクトの所有者の値を変更します。

  • 攻撃のプロセスと原理
  1. AttackVulnerable コントラクトをデプロイするときは、Vulnerable アドレスを渡し、その状態変数を Vulnerable コントラクトのアドレスに設定します。
  2. AttackVulnerable の攻撃メソッドを呼び出し、Vulnerable コントラクトの setowner メソッドを呼び出します。このメソッドは Vulnerable コントラクトには存在しないため、フォールバックがトリガーされます。
  3. フォールバック メソッドでは、msg.dataコントラクト Lib 内のメソッドが delegatecall の形式で呼び出され、ここでの msg.data の値は であるabi.encodeWithSignature("setowner()")ため、Lib 内の setowner メソッドが呼び出されます。
  4. コントラクト Lib の setowner メソッドは、コントラクト内の所有者を msg.sender の値に変更するために使用されます。msg.sender は攻撃者のアドレスです。
  5. 脆弱性コントラクト内のフォールバックは delegatecall 呼び出しを実行するため、ステップ 4 の setowner の実行コンテキストは脆弱性コントラクトであるため、変更されるのは脆弱性コントラクト内の所有者の値です。この時点で、脆弱性コントラクトのすべての所有者が攻撃者になった。

注:ステップ 5 での Vulnerable の所有者の変更は、Solidity の特性によって決定されます (呼び出し元と呼び出し先の状態変数のレイアウトは一貫している必要があります)。

  • 実行結果
    ここに画像の説明を挿入します
    注: ここでの Vulnerable の所有者は、攻撃コントラクト AttackVulnerable のコントラクト アドレスに設定されています。これは、テストが Remix で実行され、呼び出しが AttackVulnerable コントラクトによって直接開始されるためです。AttackVulnerable コントラクトの攻撃メソッドをスクリプトの形式で呼び出して、脆弱性の所有者を攻撃者が指定した任意のアドレスに設定できます。

拡大する

以下の契約書の抜け穴は比較的隠されており、上記の議論方法に従って読者自身が分析することができます。

contract Lib {
    
    
    uint public num;

    function performOperation(uint _num) public {
    
    
        num = _num;
    }
}

contract Vulnerable {
    
    
    address public lib;
    address public owner;
    uint public num;

    constructor(address _lib) {
    
    
        lib = _lib;
        owner = msg.sender;
    }

    function performOperation(uint _num) public {
    
    
        lib.delegatecall(abi.encodeWithSignature("performOperation(uint256)", _num));
    }
}

//攻击者
contract AttackVulnerable {
    
    

    address public lib;
    address public owner;
    uint public num;

    Vulnerable public vulnerable;

    constructor(Vulnerable _vulnerable) {
    
    
        vulnerable = Vulnerable(_vulnerable);
    }

    function attack() public {
    
    
        vulnerable.performOperation(uint(address(this)));
        vulnerable.performOperation(9);
    }

    // function signature must match Vulnerable.performOperation()
    function performOperation(uint _num) public {
    
    
        owner = msg.sender;
    }
}

結論

契約上のセキュリティ上の注意事項

上で、delegatecall の脆弱性が堅牢性の 2 つの機能に関連していると述べました。より安全なコントラクトを作成するために、Solidity は Library キーワードを提供します。Library ワードで定義されたコントラクトはステートレスでなければなりません (コントラクト内に状態変数が存在することはできません)。これにより、外部コントラクト内の状態変数の変更が回避されます。私たちの実践では、共有関数コントラクトを作成するときにライブラリとして定義するようにしています。

コールとデリゲートコールの概要

call と delegatecall の違いは微妙であり、効果的かつ安全に使用するにはそれらを理解することが重要です。実行コンテキストと呼び出しとデリゲート呼び出しの違いを理解することで、Solidity でより効率的でモジュール式の安全なスマート コントラクトを作成できます。

参考:
https://medium.com/0xmantle/solidity-series-part-3-call-vs-delegatecall-8113b3c76855
https://celo.academy/t/preventing-vulnerabilities-in-solidity-delegate-call/38
https://docs.soliditylang.org/en/v0.6.2/contracts.html#receive-ether-function

おすすめ

転載: blog.csdn.net/haifeng_zhang_it/article/details/135273703