← 返回
Web3与WASM 2026.03.09

動態調用與Fallback機制

動態調用與Fallback機制

學習目標

  • 掌握 call 動態調用的語法與使用場景
  • 理解 calldata 數據結構(selector + 參數編碼)
  • 掌握 fallback / receive 函數的觸發機制
  • 理解 tx、msg、block 三種上下文變量的區別

call 動態調用

基本語法

(bool success, bytes memory data) = <address>.call(bytes calldata);
  • calladdress 類型的方法,用於在運行時動態調用目標合約的函數
  • 返回兩個值:success 表示調用是否成功,data 是返回的字節數據
  • 必須檢查返回值 success,忽視返回值會造成嚴重的安全問題——調用失敗時程序不會自動 revert,而是繼續執行後續邏輯

calldata 數據結構

calldata 是傳遞給合約函數的二進制編碼數據,由兩部分組成:

  1. 前 4 字節:函數選擇器(selector)
  2. selector = bytes4(keccak256("函數簽名"))
  3. 例如 bytes4(keccak256("setX(uint256)")) 得到 setX 函數的選擇器
  4. 剩餘字節:ABI 編碼的參數

生成與解碼

  • 生成 calldata:abi.encodeWithSignature(sig, params...)
  • 解碼返回值:abi.decode(bytes, (types...))

ABI 編碼/解碼示例

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

contract AbiDecode {
    struct MyStruct {
        string name;
        uint[2] nums;
    }

    function encode(
        uint x,
        address addr,
        uint[] calldata arr,
        MyStruct calldata myStruct
    ) external pure returns (bytes memory) {
        return abi.encode(x, addr, arr, myStruct);
    }

    function decode(bytes calldata data) external pure returns (
        uint x, address addr, uint[] memory arr, MyStruct memory myStruct
    ) {
        (x, addr, arr, myStruct) = abi.decode(data, (uint, address, uint[], MyStruct));
    }
}

call 調用示例

以下示例展示如何通過 call 動態調用另一個合約的函數:

// SPDX-License-Identifier: MIT
pragma solidity >=0.8.2 <0.9.0;

contract Callee {
    uint256 public x;

    function setX(uint _x) public returns(uint) {
        x = _x;
        return x;
    }
}

contract Caller {
    uint public xx;
    address calleeAddress;

    constructor(address _callee) {
        calleeAddress = _callee;
    }

    function setCalleeX(uint _x) public {
        // 動態生成 calldata
        bytes memory cd = abi.encodeWithSignature("setX(uint256)", _x);
        (bool succ, bytes memory result) = calleeAddress.call(cd);
        if (!succ) {
            revert("call failed");
        }
        // 解碼返回值
        (uint x) = abi.decode(result, (uint));
        xx = x;
    }
}

要點: - abi.encodeWithSignature 自動計算 selector 並編碼參數 - 調用後必須檢查 succ,否則失敗時程序會繼續執行,導致狀態不一致 - 返回的 result 是 bytes 類型,需要用 abi.decode 還原爲具體類型


fallback 函數

fallback 是一個特殊的”備胎”函數,當調用的函數在目標合約中不存在時自動觸發。

核心用途

  • 代理模式(Proxy Pattern):可升級合約的核心機制,所有調用通過 fallback 轉發給邏輯合約
  • 轉賬功能:在接收 Ether 時扮演重要角色(詳見下一章)

示例

// SPDX-License-Identifier: MIT
pragma solidity >=0.8.2 <0.9.0;

contract Callee {
    uint256 public x;

    function setX(uint _x) public returns(uint) {
        x = _x;
        return x;
    }

    // 當調用不存在的函數時觸發
    fallback() external {
        x = 100000000;
    }
}

注意:如果在 Caller 中把函數簽名寫錯了(比如把 "setX(uint256)" 寫成 "setY(uint256)"),調用依然會”成功”——不過觸發的是 fallback,x 會被設置成 1 億。fallback 不是用來處理手誤的,而是用於特意安排的應用場景(如代理模式)。


上下文變量:tx、msg、block

Transaction 和 Block 的關係

  • 任何合約函數調用,最終都由一個 EOA(外部賬戶)發送 transaction 觸發
  • 合約之間的調用形成調用鏈條(A 調 B,B 調 C…)
  • 整個調用鏈共享同一個 transaction,打包在同一個 block 中

三種上下文

上下文作用域關鍵屬性
tx全局,整個調用鏈共享tx.origin(發起交易的 EOA 地址)
msg局部,每次跨合約調用產生新的 msgmsg.sender(直接調用者)、msg.value(附帶的 ETH)、msg.data(calldata)
block當前區塊信息block.numberblock.timestamp

Transaction與Block關係

Message 上下文的變化規則

  1. 合約間調用產生新的 message(msg.sender 變爲調用方合約地址)
  2. 直接被 EOA 調用時,message 是 transaction 的拷貝(msg.sender == tx.origin)
  3. 合約內部調用(非 external),message 不變
  4. external 和 this 會使內部調用變成合約間調用(產生新 message)

示例:觀察 msg.sender 和 tx.origin 的變化

// SPDX-License-Identifier: MIT
pragma solidity >=0.8.2 <0.9.0;

contract Callee {
    uint256 public x;
    address public caller;      // msg.sender
    address public eoaaddress;  // tx.origin

    function setX(uint _x) public {
        caller = msg.sender;        // 直接調用者
        eoaaddress = tx.origin;     // 最初的 EOA
        x = _x;
    }
}

contract Caller {
    address calleeAddress;
    address public caller;
    address public eoaaddress;

    constructor(address _callee) {
        calleeAddress = _callee;
    }

    function setCalleeX(uint _x) public {
        caller = msg.sender;        // EOA 地址
        eoaaddress = tx.origin;     // EOA 地址
        Callee callee = Callee(calleeAddress);
        callee.setX(_x);
        // 在 Callee 中:msg.sender = Caller 地址,tx.origin = EOA 地址
    }
}

調用鏈分析(EOA -> Caller -> Callee):

位置msg.sendertx.origin
Caller 中EOA 地址EOA 地址
Callee 中Caller 合約地址EOA 地址

external 關鍵字與內部調用

通過 this 調用 external 函數,會產生一次合約間調用,生成新的 message:

// SPDX-License-Identifier: MIT
pragma solidity >=0.8.2 <0.9.0;

contract ExternalDemo {
    address public caller;

    function first() public {
        this.second(); // 通過 this 調用 external 函數,產生新的 message
    }

    function second() external {
        caller = msg.sender; // 這裏 msg.sender 是 ExternalDemo 自己的地址!
    }
}

設計原則

  1. 合約內部函數之間的調用,應避免產生新的上下文(不要用 this.xxx()
  2. 如果 external 函數需要被內部調用,應改爲 public,避免使用 this 關鍵字
  3. external 僅用於確實只需要外部調用的場景(節省 gas,因爲參數直接從 calldata 讀取)