Dynamic Calls and Fallback Mechanism
Learning Objectives
- Master the syntax and use cases of
calldynamic calls - Understand the
calldatadata structure (selector + parameter encoding) - Master the trigger mechanism of
fallback/receivefunctions - Understand the differences between
tx,msg, andblockcontext variables
call Dynamic Calls
Basic Syntax
(bool success, bytes memory data) = <address>.call(bytes calldata);
callis a method of theaddresstype, used to dynamically call functions of a target contract at runtime.- It returns two values:
successindicates whether the call was successful, anddatais the returned byte data. - The return value
successmust be checked. Ignoring the return value can lead to serious security issues – if the call fails, the program will not automatically revert but will continue to execute subsequent logic.
calldata Data Structure
calldata is binary encoded data passed to contract functions, consisting of two parts:
- First 4 bytes: Function selector
selector = bytes4(keccak256("函数签名"))- For example,
bytes4(keccak256("setX(uint256)"))gets the selector for thesetXfunction. - Remaining bytes: ABI-encoded parameters
Generation and Decoding
- Generate
calldata:abi.encodeWithSignature(sig, params...) - Decode return values:
abi.decode(bytes, (types...))
ABI Encoding/Decoding Example
// 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 Example
The following example demonstrates how to dynamically call a function of another contract using 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;
}
}
Key points:
- abi.encodeWithSignature automatically calculates the selector and encodes parameters.
- After calling, succ must be checked; otherwise, if it fails, the program will continue to execute, leading to inconsistent states.
- The returned result is of type bytes and needs to be restored to its specific type using abi.decode.
fallback Function
fallback is a special "fallback" function that is automatically triggered when the called function does not exist in the target contract.
Core Use Cases
- Proxy Pattern: The core mechanism for upgradeable contracts, where all calls are forwarded to the logic contract via
fallback. - Transfer Functionality: Plays an important role when receiving Ether (see next chapter for details).
Example
// 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;
}
}
Note: If the function signature is written incorrectly in Caller (e.g., writing
"setY(uint256)"instead of"setX(uint256)"), the call will still "succeed" – but it will trigger the fallback function, and x will be set to 100 million. The fallback function is not for handling typos, but for specifically designed application scenarios (such as proxy patterns).
Context Variables: tx, msg, block
Relationship between Transaction and Block
- Any contract function call is ultimately triggered by a transaction sent by an EOA (Externally Owned Account).
- Calls between contracts form a call chain (A calls B, B calls C...).
- The entire call chain shares the same transaction and is packaged in the same block.
Three Types of Context
| Context | Scope | Key Attributes |
|---|---|---|
| tx | Global, shared across the entire call chain | tx.origin (address of the EOA that initiated the transaction) |
| msg | Local, a new msg is generated for each cross-contract call |
msg.sender (direct caller), msg.value (attached ETH), msg.data (calldata) |
| block | Current block information | block.number, block.timestamp |
Rules for Message Context Changes
- Inter-contract calls generate a new message (
msg.senderbecomes the calling contract's address). - When directly called by an EOA, the message is a copy of the transaction (
msg.sender == tx.origin). - Internal contract calls (non-
external) do not change the message. externalandthiswill turn an internal call into an inter-contract call (generating a new message).
Example: Observing Changes in msg.sender and 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 地址
}
}
Call Chain Analysis (EOA -> Caller -> Callee):
| Location | msg.sender | tx.origin |
|---|---|---|
| In Caller | EOA Address | EOA Address |
| In Callee | Caller Contract Address | EOA Address |
external Keyword and Internal Calls
Calling an external function via this will result in an inter-contract call, generating a new 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 自己的地址!
}
}
Design Principles
- Calls between internal contract functions should avoid generating new contexts (do not use
this.xxx()). - If an
externalfunction needs to be called internally, it should be changed topublic, avoiding the use of thethiskeyword. externalis only used for scenarios where only external calls are truly needed (saves gas because parameters are read directly fromcalldata).
主题测试文章,只做测试使用。发布者:Walker,转转请注明出处:https://walker-learn.xyz/archives/7494
