动态调用与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小时前
    400
  • 引用类型详解:数组、结构体、映射、字符串

    引用类型详解:数组、结构体、映射、字符串 学习目标 掌握 Solidity 中四种引用类型(数组、结构体、映射、字符串/变长字节数组)的定义和使用方法。 前置知识 已学习值类型(整型、布尔、地址、定长字节数组等)。 数组(Array) storage 中的数组 Solidity 中数组分为两种: 静态数组 T[K]:长度固定,编译时确定 动态数组 T[]:长…

  • Gas机制与转账设计

    Gas机制与转账设计 学习目标 理解区块链的经济模型与激励机制 掌握 Gas、Gas Price、Gas Fee 的概念与关系 理解 Ether 单位与转换 掌握合约转账设计(receive / fallback / payable) 区块链的经济系统 为什么需要经济模型? 计算与存储资源是稀缺的:每个节点都要执行和存储所有交易 共识和 trustless …

    22小时前
    400
  • Solidity 值类型详解

    Solidity 值类型详解 学习目标 掌握 Solidity 的整型、布尔、地址、定长字节数组、枚举等值类型 理解 EVM 256 位机器架构对类型设计的影响 掌握类型转换规则(隐式 vs 显式) 了解溢出问题的历史与解决方案 理解 Solidity 中所有类型的默认值机制 一、整型(int / uint) 1.1 基本概念 Solidity 提供有符号整…

简体中文 繁体中文 English