动态调用与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
