動態調用與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 局部,每次跨合約調用產生新的 msg msg.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.sender tx.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 讀取)

主題測試文章,只做測試使用。發佈者:Walker,轉轉請注明出處:https://walker-learn.xyz/archives/7494

(0)
Walker的頭像Walker
上一篇 5天前
下一篇 1天前

相關推薦

  • 存儲位置與拷貝機制:storage、memory、calldata

    存儲位置與拷貝機制:storage、memory、calldata 學習目標 理解 EVM 中三種數據存儲位置的特點,以及引用類型在不同存儲位置之間賦值時的拷貝規則。 前置知識 已學習值類型和引用類型(數組、結構體、映射、字符串)。 三種存儲位置 storage —— 持久化存儲 類似數據庫,數據永久保存在區塊鏈上 成員變量(狀態變量)默認存儲在 stora…

  • 繼承多態與庫合約

    繼承多態與庫合約 學習目標 掌握 Solidity 繼承機制與多態 理解 C3 線性化算法 掌握庫合約(library)的定義和使用 繼承基礎 繼承定義 使用 is 關鍵字 繼承的實現方式是代碼拷貝:部署後變成一個合約 可見性與繼承 private:子合約不可見,但不能定義同名成員 internal:子合約可見 public:完全可見 event 和 mod…

    Web3與WASM 20小時前
    300
  • 引用類型詳解:數組、結構體、映射、字符串

    引用類型詳解:數組、結構體、映射、字符串 學習目標 掌握 Solidity 中四種引用類型(數組、結構體、映射、字符串/變長字節數組)的定義和使用方法。 前置知識 已學習值類型(整型、布爾、地址、定長字節數組等)。 數組(Array) storage 中的數組 Solidity 中數組分爲兩種: 靜態數組 T[K]:長度固定,編譯時確定 動態數組 T[]:長…

  • Gas機制與轉賬設計

    Gas機制與轉賬設計 學習目標 理解區塊鏈的經濟模型與激勵機制 掌握 Gas、Gas Price、Gas Fee 的概念與關係 理解 Ether 單位與轉換 掌握合約轉賬設計(receive / fallback / payable) 區塊鏈的經濟系統 爲什麼需要經濟模型? 計算與存儲資源是稀缺的:每個節點都要執行和存儲所有交易 共識和 trustless …

    22小時前
    300
  • Solidity 值類型詳解

    Solidity 值類型詳解 學習目標 掌握 Solidity 的整型、布爾、地址、定長字節數組、枚舉等值類型 理解 EVM 256 位機器架構對類型設計的影響 掌握類型轉換規則(隱式 vs 顯式) 瞭解溢出問題的歷史與解決方案 理解 Solidity 中所有類型的默認值機制 一、整型(int / uint) 1.1 基本概念 Solidity 提供有符號整…

簡體中文 繁體中文 English