首页
/ WTF-Solidity 第22讲:Solidity call 详解——低级调用与任意合约交互

WTF-Solidity 第22讲:Solidity call 详解——低级调用与任意合约交互

2026-09-14 14:06:16作者:咎竹峻Karen

call 是 Solidity 中 address 类型的低级成员函数,既可用于发送 ETH,也可在不掌握目标合约源码或 ABI 的情况下调用其任意函数。本讲以 WTF-Solidity 仓库第 22 讲(22_Call)为骨架,结合日文版源码 Languages/ja/22_Call_ja/Call.sol 与中文版源码 22_Call/Call.sol,完整演示 call 的语法、abi.encodeWithSignature 编码、返回值解码、value/gas 指定,以及调用不存在函数时触发 fallback 的行为。读完本讲,你将掌握 call 的正确姿势、适用场景与其安全性边界。

一、什么是 call

calladdress 类型的低级(low-level)成员函数,用来与其他合约交互。它的返回值为 (bool, bytes memory) 元组:

  • 第一个值 bool success:表示 call 是否执行成功;
  • 第二个值 bytes memory data:表示目标函数返回值的 ABI 编码(若目标函数无返回值,则为空字节串 0x)。

在使用 call 之前,需要先明确三条关键结论(这也是官方文档与 WTF-Solidity 教程反复强调的):

  1. call 是 Solidity 官方推荐的发送 ETH 的方式:通过触发目标合约的 fallbackreceive 函数完成转账,详见本仓库第 20 讲:发送 ETH
  2. 不推荐用 call 调用另一个合约:当你调用一个不安全合约的函数时,等于把执行控制权交给了对方(包括代码执行、剩余 gas 与调用上下文),存在安全隐患。推荐的做法仍是声明合约变量后调用函数,见第 21 讲:调用其他合约
  3. call 的不可替代场景:当不知道对方合约的源代码或 ABI 时,无法生成合约变量,此时仍可以通过 call 调用对方合约的函数。这是 call 最具价值的使用场景。

call 的使用规则

call 的基础语法如下:

目标合约地址.call(字节码);

其中的 字节码 通常由结构化编码函数 abi.encodeWithSignature 生成:

abi.encodeWithSignature("函数签名", 逗号分隔的具体参数)

函数签名 的格式为 "函数名(逗号分隔的参数类型)"。例如要调用 f(uint256,address) 并传入 _x_addr

abi.encodeWithSignature("f(uint256,address)", _x, _addr)

此外,call 在调用合约时可以指定交易发送的 ETH 数额与 gas 数额:

目标合约地址.call{value: 发送数额, gas: gas数额}(字节码);

其中 value 控制随调用转入的 ETH 数量,gas 控制本次调用可用的 gas 上限。若目标函数声明为 payablevalue 指定的 ETH 才能成功转入。

二、目标合约 OtherContract

先部署一个简单的目标合约 OtherContract,代码与第 21 讲中的目标合约基本相同,唯一区别是新增了 fallback 函数,为后续"调用不存在函数"的实验做准备。以下为日文版源码 Languages/ja/22_Call_ja/Call.solpragma solidity ^0.8.34;)中的完整实现:

contract OtherContract {
    uint256 private _x = 0; // 状态变量_x
    // ethを受け取るイベント、amountとgasを記録
    event Log(uint256 amount, uint256 gas);

    fallback() external payable {}

    // コントラクトのETH残高を返す関数
    function getBalance() public view returns (uint256) {
        return address(this).balance;
    }

    // _xの値を設定できる関数。同時にコントラクトへETHを送信することもできる(payable)
    function setX(uint256 x) external payable {
        _x = x;
        // もしETHの送信がある場合のみLogイベントを放出
        if (msg.value > 0) {
            emit Log(msg.value, gasleft());
        }
    }

    // xの値を取得する関数
    function getX() external view returns (uint256 x) {
        x = _x;
    }
}

该合约包含一个状态变量 _x、一个收到 ETH 时触发的事件 Log,以及三个函数:

  • getBalance():返回合约的 ETH 余额(即 address(this).balance);
  • setX(uint256 x)external payable 函数,可以设置 _x 的值,并在 msg.value > 0 时通过 Log(msg.value, gasleft()) 记录转入金额与剩余 gas;
  • getX():读取并返回 _x 的值。

fallback() external payable {} 是一个空的、可接收 ETH 的回退函数,任何未匹配到函数签名的调用都会落入其中,并且不会主动回滚。

三、用 call 调用目标合约

Call 合约中,我们逐一演示如何通过 call 调用目标合约的不同函数。

1. 定义 Response 事件

为了便于观察 call 的返回值,首先定义一个 Response 事件,将 call 返回的 successdata 原样输出到链上日志中:

// Response イベントは`call`の結果`success`と`data`を出力します
event Response(bool success, bytes data);

2. 调用 setX 函数:call + value

定义 callSetX 函数,调用目标合约的 setX(),同时转入 msg.value 数额的 ETH,并释放 Response 事件输出 successdata

function callSetX(address payable _addr, uint256 x) public payable {
    // setX()をcallし、ETHを送信
    (bool success, bytes memory data) = _addr.call{value: msg.value}(abi.encodeWithSignature("setX(uint256)", x));

    emit Response(success, data); // イベントを放出
}

调用 callSetX 时传入 OtherContract 的地址和 5,即可把目标合约的状态变量 _x 改为 5。由于 setX() 没有返回值,Response 事件输出的 data0x(空字节),同时 successtrue

Remix 中调用 callSetX 的运行结果:传入 OtherContract 地址与 x=5,Response 事件输出 success=true 与空 data(0x)

3. 调用 getX 函数:call + abi.decode

接下来调用 getX(),该函数返回目标合约 _x 的值(类型 uint256)。call 返回的原始 data 是 ABI 编码后的字节串,需要使用 abi.decode 解码才能还原出数值:

function callGetX(address _addr) external returns (uint256) {
    // call getX()
    (bool success, bytes memory data) = _addr.call(abi.encodeWithSignature("getX()"));

    emit Response(success, data); // イベントを放出
    return abi.decode(data, (uint256));
}

Response 事件的输出可以看到,data0x0000000000000000000000000000000000000000000000000000000000000005(256 位左对齐的 uint256 编码),经过 abi.decode(data, (uint256)) 后,函数的最终返回值为 5

Remix 中调用 callGetX 的运行结果:decoded output 显示 uint256: 5,Response 事件输出 success=true 与编码后的 data

4. 调用不存在的函数:触发 fallback

如果给 call 传入的目标合约并不存在的函数签名,目标合约的 fallback 函数会被触发:

function callNonExist(address _addr) external {
    // 存在しない関数を呼び出す
    (bool success, bytes memory data) = _addr.call(abi.encodeWithSignature("foo(uint256)"));

    emit Response(success, data); // イベントを放出
}

上面的例子中,我们 call 了一个并不存在的 foo 函数。因为目标合约 OtherContract 定义了空的 fallback() external payable {},且该 fallback 不会主动 revert,所以 call 仍然执行成功并返回 success = true,但真正执行的是目标合约的 fallback 函数,而不是任何真实业务函数。这正是低级调用的"容错"特性:它只关心调用是否在 EVM 层面成功(未回滚),并不校验函数签名是否真实存在。

四、源码级要点与运行前提

以下细节可从仓库源码中得到印证:

  • 版本约束:两份源码(Languages/ja/22_Call_ja/Call.sol22_Call/Call.sol)均声明 pragma solidity ^0.8.34;,即 abi.encodeWithSignatureabi.decodecall{value: ...} 等语法需要 0.8.34 及以上编译器支持(0.8.x 系列均可正常编译运行)。
  • 签名编码与选择器abi.encodeWithSignature("setX(uint256)", x) 内部等价于"函数选择器(keccak256("setX(uint256)") 的前 4 字节)+ 参数 ABI 编码",因此 call 不需要任何接口定义即可精确命中目标函数。关于函数选择器的深入讲解,可参考本仓库第 29 讲:函数选择器 Selector
  • gas 观测setX() 中通过 gasleft() 记录调用发生时的剩余 gas,配合 Log(msg.value, gasleft()) 事件,可直观观测低层调用中的 gas 消耗情况;这也是 call 支持 {gas: ...} 选项的现实意义——调用方可以限制被调合约可消耗的 gas 上限。
  • 运行环境:本讲示例在 Remix 中部署 OtherContractCall 两个合约后即可交互验证(见上文两张运行截图)。本仓库同时是 Foundry 工程(根目录存在 foundry.toml),也可用 forge build 对合约进行编译校验。
  • 与 delegatecall 的区分call 在目标合约的上下文中执行(msg.sender 为调用方,修改的是目标合约的状态变量);而 delegatecall 则在调用方自己的上下文中执行逻辑。两者语法几乎一致但语义完全不同,详细对比见第 23 讲:delegatecall

五、安全提醒与总结

总结本讲的核心结论:

  1. calladdress 类型的低级成员函数,返回 (bool success, bytes memory data)
  2. 调用格式为 目标地址.call{value: 金额, gas: gas量}(abi.encodeWithSignature("签名", 参数...))
  3. 返回值需要配合 abi.decode(data, (类型)) 才能还原为结构化数据;
  4. 调用不存在的函数不会直接失败,而是触发目标合约的 fallback 函数——在未定义或会 revert 的 fallback 场景下,success 可能为 false,因此使用 call 时务必要检查 success 返回值,切勿盲目信任调用结果;
  5. call 不是调用合约的推荐方法,因为不安全(调用不可信合约等于交出控制权);但当不知道目标合约的源代码或 ABI 时,它提供了唯一可行的低层交互途径,非常有用。

下一讲将介绍与 call 极其相似、但执行上下文完全不同的 delegatecall,它是代理合约与可升级合约(见本仓库第 46 讲:代理合约)的基石。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
34
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.21 K
2.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
953
1.87 K
docsdocs
暂无描述
Markdown
906
5.84 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
538
615
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
864
1.37 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
4.29 K
1.04 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.4 K
1.49 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
550
403
flutter_flutterflutter_flutter
本仓库是 Flutter SDK 与 Flutter Engine 的 OpenHarmony 适配版本,由 CPF-Flutter 团队维护。开发者可使用熟悉的 Flutter 技术栈开发 OpenHarmony 应用,3.35.7 及以后的适配版本可基于本仓库源码构建支持 OpenHarmony 的 Flutter Engine。
Dart
1.19 K
348