動態調用與Fallback機制
學習目標
- 掌握 call 動態調用的語法與使用場景
- 理解 calldata 數據結構(selector + 參數編碼)
- 掌握 fallback / receive 函數的觸發機制
- 理解 tx、msg、block 三種上下文變量的區別
call 動態調用
基本語法
(bool success, bytes memory data) = <address>.call(bytes calldata);
call是address類型的方法,用於在運行時動態調用目標合約的函數- 返回兩個值:
success表示調用是否成功,data是返回的字節數據 - 必須檢查返回值
success,忽視返回值會造成嚴重的安全問題——調用失敗時程序不會自動 revert,而是繼續執行後續邏輯
calldata 數據結構
calldata 是傳遞給合約函數的二進制編碼數據,由兩部分組成:
- 前 4 字節:函數選擇器(selector)
selector = bytes4(keccak256("函數簽名"))- 例如
bytes4(keccak256("setX(uint256)"))得到setX函數的選擇器 - 剩餘字節: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.number、block.timestamp |
Message 上下文的變化規則
- 合約間調用產生新的 message(msg.sender 變爲調用方合約地址)
- 直接被 EOA 調用時,message 是 transaction 的拷貝(msg.sender == tx.origin)
- 合約內部調用(非 external),message 不變
- 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 自己的地址!
}
}
設計原則
- 合約內部函數之間的調用,應避免產生新的上下文(不要用
this.xxx()) - 如果 external 函數需要被內部調用,應改爲
public,避免使用this關鍵字 external僅用於確實只需要外部調用的場景(節省 gas,因爲參數直接從 calldata 讀取)
主題測試文章,只做測試使用。發佈者:Walker,轉轉請注明出處:https://walker-learn.xyz/archives/7494
