WTF-Solidity 第22讲:Solidity call 详解——低级调用与任意合约交互
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
call 是 address 类型的低级(low-level)成员函数,用来与其他合约交互。它的返回值为 (bool, bytes memory) 元组:
- 第一个值
bool success:表示call是否执行成功; - 第二个值
bytes memory data:表示目标函数返回值的 ABI 编码(若目标函数无返回值,则为空字节串0x)。
在使用 call 之前,需要先明确三条关键结论(这也是官方文档与 WTF-Solidity 教程反复强调的):
call是 Solidity 官方推荐的发送 ETH 的方式:通过触发目标合约的fallback或receive函数完成转账,详见本仓库第 20 讲:发送 ETH。- 不推荐用
call调用另一个合约:当你调用一个不安全合约的函数时,等于把执行控制权交给了对方(包括代码执行、剩余 gas 与调用上下文),存在安全隐患。推荐的做法仍是声明合约变量后调用函数,见第 21 讲:调用其他合约。 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 上限。若目标函数声明为 payable,value 指定的 ETH 才能成功转入。
二、目标合约 OtherContract
先部署一个简单的目标合约 OtherContract,代码与第 21 讲中的目标合约基本相同,唯一区别是新增了 fallback 函数,为后续"调用不存在函数"的实验做准备。以下为日文版源码 Languages/ja/22_Call_ja/Call.sol(pragma 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 返回的 success 与 data 原样输出到链上日志中:
// Response イベントは`call`の結果`success`と`data`を出力します
event Response(bool success, bytes data);
2. 调用 setX 函数:call + value
定义 callSetX 函数,调用目标合约的 setX(),同时转入 msg.value 数额的 ETH,并释放 Response 事件输出 success 与 data:
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 事件输出的 data 为 0x(空字节),同时 success 为 true:
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 事件的输出可以看到,data 为 0x0000000000000000000000000000000000000000000000000000000000000005(256 位左对齐的 uint256 编码),经过 abi.decode(data, (uint256)) 后,函数的最终返回值为 5:
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.sol 与 22_Call/Call.sol)均声明
pragma solidity ^0.8.34;,即abi.encodeWithSignature、abi.decode、call{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 中部署
OtherContract与Call两个合约后即可交互验证(见上文两张运行截图)。本仓库同时是 Foundry 工程(根目录存在 foundry.toml),也可用forge build对合约进行编译校验。 - 与 delegatecall 的区分:
call在目标合约的上下文中执行(msg.sender为调用方,修改的是目标合约的状态变量);而delegatecall则在调用方自己的上下文中执行逻辑。两者语法几乎一致但语义完全不同,详细对比见第 23 讲:delegatecall。
五、安全提醒与总结
总结本讲的核心结论:
call是address类型的低级成员函数,返回(bool success, bytes memory data);- 调用格式为
目标地址.call{value: 金额, gas: gas量}(abi.encodeWithSignature("签名", 参数...)); - 返回值需要配合
abi.decode(data, (类型))才能还原为结构化数据; - 调用不存在的函数不会直接失败,而是触发目标合约的
fallback函数——在未定义或会 revert 的fallback场景下,success可能为false,因此使用call时务必要检查success返回值,切勿盲目信任调用结果; call不是调用合约的推荐方法,因为不安全(调用不可信合约等于交出控制权);但当不知道目标合约的源代码或ABI时,它提供了唯一可行的低层交互途径,非常有用。
下一讲将介绍与 call 极其相似、但执行上下文完全不同的 delegatecall,它是代理合约与可升级合约(见本仓库第 46 讲:代理合约)的基石。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust4.26 K641- DDeepSeek-V4.1-FlashDeepSeek-V4.1-Flash 是一个多模态混合专家(MoE)模型,拥有 5520 亿骨干参数,并支持最多一百万 token 的上下文长度。该模型原生支持图像和文本输入,并以自回归方式生成文本Python860
SlideSCIPPT插件,支持素材库、AI助手、一键添加图片标题,复制粘贴位置、一键图片对齐、一键插入Markdown(加粗、超链接等行内样式、代码块、LaTeX等块级样式)、便捷导出图片!C#621
Agent-Reach给你的 AI Agent 一键装上互联网能力。13 个平台(网页/GitHub/YouTube/小红书/B站/Twitter/Reddit 等)多后端路由,当下最稳的接入方式替你选好、装好、体检好。GitHub 主仓库同步镜像。Python1304
new-apiAI模型聚合管理中转分发系统,一个应用管理您的所有AI模型,支持将多种大模型转为统一格式调用,支持OpenAI、Claude、Gemini等格式,可供个人或者企业内部管理与分发渠道使用。🍥 A Unified AI Model Management & Distribution System. Aggregate all your LLMs into one app and access them via an OpenAI-compatible API, with native support for Claude (Messages) and Gemini formats.Go23446
JeecgBoot🔥企业级低代码平台集成了AI应用平台,帮助企业快速实现低代码开发和构建AI应用!前后端分离架构 SpringBoot,SpringCloud、Mybatis,Ant Design4、 Vue3.0、TS+vite!强大的代码生成器让前后端代码一键生成,无需写任何代码! 引领AI低代码开发模式: AI生成->OnlineCoding-> 代码生成-> 手工MERGE,显著的提高效率,又不失灵活~Java37451

