WTF-Solidity 第21讲:在 Solidity 中调用其他合约——四种交互方式与 ETH 转账实战
在区块链上,合约之间相互调用不仅让代码具备跨合约的可复用性,也是整个 Ethereum DApp 生态得以运转的基础——许多 Web3 应用(例如 yield farming 协议)都依赖调用另一个合约来完成核心逻辑。本篇文章基于 WTF-Solidity 极简入门教程第 21 讲,完整讲解如何在已知目标合约地址与源码(或 ABI)的前提下,从 Solidity 中创建合约引用并调用其函数,并通过四种典型写法演示传地址、传合约变量、创建合约变量以及调用时附带转账 ETH 的完整流程,读完即可在 Remix 中独立复现。
调用已部署合约的基本思路
在 Solidity 中,一个合约调用另一个合约的函数,核心前提是知道目标合约的地址和它的代码(或接口/ABI)。有了这两样,就可以在调用方合约中创建一个"引用",进而调用目标合约暴露出来的任何函数。
创建引用的语法非常直观:
_Name(_Address)
_Name:目标合约名,必须与目标合约源码(或 ABI)中声明的合约名保持一致;_Address:目标合约的部署地址。
拿到引用后,用点号调用目标函数即可:
_Name(_Address).f()
其中 f() 就是要调用的目标函数。
目标合约 OtherContract 解析
为了方便演示,先准备一个简单的、专门用于被其他合约调用的目标合约 OtherContract。该合约完整实现在仓库源码 21_CallContract/CallContract.sol(西语版源码位于 Languages/es/21_LlamarContrato_es/CallContract.sol),内容如下:
contract OtherContract {
uint256 private _x = 0; // 状态变量 _x
// 收到 ETH 时触发的事件,记录转入金额和剩余 gas
event Log(uint amount, uint gas);
// 返回合约的 ETH 余额
function getBalance() view public returns(uint) {
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(uint x){
x = _x;
}
}
这个合约包含一个状态变量 _x、一个在收到 ETH 时触发的事件 Log,以及三个函数:
getBalance():view public函数,通过address(this).balance返回合约当前的 ETH 余额;setX(uint256 x):external payable函数,既能把状态变量_x设置为传入的x,还能顺便接收转入的 ETH;当msg.value > 0时触发Log事件,记录转入金额与gasleft();getX():external view函数,读取_x的当前值。
从源码结构可以看出,OtherContract 故意同时提供了"读"(getX、getBalance)与"写/收币"(setX)两种函数,覆盖了调用方在交互时可能遇到的绝大多数场景:读取状态、写入状态、附带转账。
调用方合约 CallContract 与四种交互方式
接下来在 Remix 中同时编译并部署 OtherContract 和 CallContract 两个合约,其中 CallContract 包含四种典型的调用写法,全部实现在 21_CallContract/CallContract.sol:
contract CallContract{
function callSetX(address _Address, uint256 x) external{
OtherContract(_Address).setX(x);
}
function callGetX(OtherContract _Address) external view returns(uint x){
x = _Address.getX();
}
function callGetX2(address _Address) external view returns(uint x){
OtherContract oc = OtherContract(_Address);
x = oc.getX();
}
function setXTransferETH(address otherContract, uint256 x) payable external{
OtherContract(otherContract).setX{value: msg.value}(x);
}
}
部署阶段在 Remix 中依次选择 OtherContract 与 CallContract 合约进行 Deploy 操作,得到两个合约实例地址:
方式一:传入合约地址,即时创建引用
这是最直接的方式:在函数参数中接收目标合约的 address,在函数体内用 OtherContract(_Address) 创建引用并立刻调用目标函数。以调用 OtherContract.setX 为例:
function callSetX(address _Address, uint256 x) external{
OtherContract(_Address).setX(x);
}
实操步骤:
- 复制已部署的
OtherContract合约地址,作为callSetX的第一个参数_Address填入; - 第二个参数
x填入希望写入的值,例如123; - 点击 transact 发起交易,交易成功后调用
OtherContract自己的getX函数,可以看到返回值变为123,证明调用生效。
方式二:直接传入合约变量
方式一要求参数类型是 address;而 Solidity 允许直接把参数类型声明为合约名 OtherContract,把"合约引用"作为函数参数传递。下面例子演示了通过这种方式调用目标合约的 getX():
function callGetX(OtherContract _Address) external view returns(uint x){
x = _Address.getX();
}
注意一个底层细节:参数 OtherContract _Address 的底层类型仍然是 address。也就是说,在生成的 ABI 中、以及在调用 callGetX 时传入的参数,全部都是 address 类型——合约类型参数只是编译期层面的语法糖,链上交互层面传递的依然是 20 字节地址。
实操步骤:复制 OtherContract 合约地址填入 callGetX 参数,调用后即可成功读取到 x 的值(此函数声明为 view,不会产生状态变更,只做链上读取)。
方式三:创建合约变量,再通过变量调用
除了在参数位置临时构造引用,还可以先把引用保存到合约类型的局部变量里,再通过该变量调用目标函数。示例将 OtherContract 的引用存入变量 oc:
function callGetX2(address _Address) external view returns(uint x){
OtherContract oc = OtherContract(_Address);
x = oc.getX();
}
这种方式与方式一的区别在于:方式一是 OtherContract(_Address).f() 的"一行流"写法,方式三则把引用赋值给显式变量 oc,适合需要对同一个目标合约连续调用多个函数、或需要把引用传入其他内部逻辑的场合,可读性和复用性更好。
方式四:调用合约并附带转账 ETH
当目标函数声明为 payable 时,调用方可以在调用同时向目标合约转账 ETH,语法为:
_Name(_Address).f{value: _Value}()
_Name:目标合约名;_Address:目标合约地址;f:目标函数名;_Value:要转出的 ETH 数额,单位是wei。
OtherContract.setX 恰好是 payable 函数,因此下面这个 setXTransferETH 就通过调用 setX 实现了"设置状态 + 转账"两步合一:
function setXTransferETH(address otherContract, uint256 x) payable external{
OtherContract(otherContract).setX{value: msg.value}(x);
}
这里把调用方收到的 msg.value 原封不动地通过 {value: msg.value} 转发给目标合约。
实操步骤:
- 复制
OtherContract合约地址,填入setXTransferETH的参数otherContract; - 参数
x填入目标状态值; - 在 Remix 的 VALUE 输入框中填入
10(单位选择 ETH),再点击 transact,即会在调用setX的同时向OtherContract转入 10 ETH。
转账完成后,验证余额变化有两条途径:
- 事件验证:
OtherContract.setX在收到 ETH 时会触发Log(msg.value, gasleft()),在 Remix 的事件日志区(Logs)可以查到转入金额与剩余 gas; - 函数验证:调用
OtherContract的getBalance(),返回值为10000000000000000000(即 10 ETH,单位 wei),与转入金额吻合。
调用交互中的几个关键细节
结合上面四种写法和源码,有几个值得注意的工程细节:
- 地址必须与 ABI 匹配:
_Name(_Address)中的_Name决定 Solidity 如何解析函数签名,如果实际部署在_Address上的合约与声明的 ABI 不一致(例如函数签名对不上),调用会回滚或产生不可预期的行为; - 底层仍是
address:如方式二所强调,合约类型参数在 ABI 层面对应address,理解这一点有助于阅读生成的 ABI 或与其他工具链交互; payable是转账的前提:只有目标函数声明为payable,{value: ...}才能生效;对非payable函数附带value会导致调用失败;msg.value与gasleft():msg.value是本次调用实际携带的 ETH 数额(wei),gasleft()返回调用点剩余的 gas,二者被组合进Log事件用于链上审计;- 异常会向上传播:被调用合约执行失败时,整个调用链会一并回滚,调用方合约应在上层设计好防护(例如 try/catch 或前置校验);
- 交易与读取的区分:
callSetX、setXTransferETH修改目标合约状态,需要发送交易;callGetX、callGetX2是view函数,直接本地读取即可,无需消耗 gas 发起交易。
总结
本讲围绕"已知合约代码(或 ABI)和地址,如何调用已部署合约"这一主题,介绍了创建合约引用的核心语法 _Name(_Address),并通过 CallContract 中的四个函数展示了四种交互写法:传入地址即时调用(callSetX)、传入合约类型参数(callGetX)、创建合约变量后调用(callGetX2),以及使用 {value: ...} 在调用同时转账 ETH(setXTransferETH)。相关完整代码可在仓库 21_CallContract/CallContract.sol 中查看,西语版对应 Languages/es/21_LlamarContrato_es/CallContract.sol,建议在 Remix 中配合上述截图步骤亲手部署验证一遍,以加深对合约间调用机制的理解。
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#601
Agent-Reach给你的 AI Agent 一键装上互联网能力。13 个平台(网页/GitHub/YouTube/小红书/B站/Twitter/Reddit 等)多后端路由,当下最稳的接入方式替你选好、装好、体检好。GitHub 主仓库同步镜像。Python1284
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.Go23245
JeecgBoot🔥企业级低代码平台集成了AI应用平台,帮助企业快速实现低代码开发和构建AI应用!前后端分离架构 SpringBoot,SpringCloud、Mybatis,Ant Design4、 Vue3.0、TS+vite!强大的代码生成器让前后端代码一键生成,无需写任何代码! 引领AI低代码开发模式: AI生成->OnlineCoding-> 代码生成-> 手工MERGE,显著的提高效率,又不失灵活~Java37451



