拓冰建站拓冰建站
首页 / 资讯中心 / 正文

WTF-Solidity 第 19 讲:Solidity 合约接收 ETH 的 receive() 与 fallback() 函数深度解析

WTF-Solidity 第 19 讲Solidity 合约接收 ETH 的 receive() 与 fallback() 函数深度解析【免费下载链接】WTF-SolidityWTF Solidity 极简入门教程供小白们使用。Now supports English! 官网: https://wtf.academy项目地址: https://gitcode.com/GitHub_Trending/wt/WTF-Solidityreceive()与fallback()是 Solidity 中两个特殊的回调函数分别在「合约收到 ETH 转账」与「调用不存在于合约中的函数」两种场景下被触发是任何会持有原生资产或承担代理转发职责的合约都无法回避的基础设施。本文以 WTF-Solidity 第 19 讲的英文版教程Languages/en/19_Fallback_en/readme.md为骨架结合仓库内的 Fallback.sol、SendETH.sol、ProxyContract.sol 等源码系统梳理两个函数的声明规则、触发判定流程、2300 gas 限制、安全风险以及它们在接收 ETH 与代理合约中的实际应用读完即可在真实合约中正确写出、部署并验证这两个函数。一、为什么需要 receive() 与 fallback()两个特殊函数的前世今生Solidity 支持两种特殊的回调函数receive()和fallback()它们主要在两种情况下被使用接收 ETH当合约地址收到原生代币转账时触发处理合约中不存在的函数调用当调用数据msg.data中的函数选择器无法匹配合约里任何已声明的函数时兜底执行这是代理合约proxy contract得以实现的基础。这里有一个重要的历史背景原文中有明确提示在 Solidity 0.6.x 版本之前语法上只有fallback()一个函数它同时承担「接收用户发送的 ETH」和「在被调用函数签名未匹配时响应调用」两个职责从 0.6 版本开始Solidity 才将fallback()拆分成receive()和fallback()两个函数让「收钱」与「兜底转发」各司其职。因此在阅读老项目源码时遇到 0.6 之前的fallback()要意识到它在当时同时承担了如今receive()的角色。本讲聚焦于「接收 ETH」这一核心场景代理合约场景则作为fallback()的重要工程应用一并展开。二、接收 ETH 的专用入口receive()2.1 声明规则receive()是合约收到 ETH 转账时被调用的函数。它的声明方式与普通函数完全不同一个合约最多只能有一个receive()函数声明时不需要function关键字直接写receive() external payable { ... }不能有任何参数不能返回任何值必须包含external可见性修饰符和payable状态可变性修饰符否则无法接收 ETH编译也会报错。2.2 gas 限制为什么 receive() 里不宜做复杂逻辑当合约接收 ETH 时receive()会被触发。但要注意如果别人用send()和transfer()方法发送 ETHgas会被限制在 2300只够记录日志等极简操作此时如果receive()里逻辑太复杂例如写存储、循环、再调用其他合约会触发Out of Gas报错导致转账失败而如果用call发送 ETH则可以自定义gas从而在receive()里执行更复杂的逻辑。这三种发送 ETH 的方法在仓库第 20 讲 SendETH.sol 的注释中给出了权威对照// 3种方法发送ETH // transfer: 2300 gas, revert // send: 2300 gas, return bool // call: all gas, return (bool, data)即transfer()固定 2300 gas 且失败直接 revertsend()同样 2300 gas 但失败只返回boolcall()可携带全部 gas 并返回(bool, data)。这正是receive()内「轻量」原则的底层原因。2.3 在 receive() 中发送事件虽然不建议做复杂操作但在receive()中记录一条事件是安全且常见的最佳实践。原文给出的概念示例如下// 定义事件 event Received(address Sender, uint Value); // 接收ETH时释放 Received 事件 receive() external payable { emit Received(msg.sender, msg.value); }仓库中可部署的完整实现位于 Languages/en/19_Fallback_en/Fallback.sol其中事件命名为receivedCalled中文版 19_Fallback/Fallback.sol 与之完全一致// Emit Received event when receiving ETH receive() external payable { emit receivedCalled(msg.sender, msg.value); }通过事件中的msg.sender发送方与msg.value转账金额单位 wei链下可以完整审计每一笔进入合约的 ETH。2.4 安全提醒恶意 receive() 的拒绝服务风险原文特别警告有些恶意合约会在receive()函数0.6 之前则是fallback()中嵌入大量消耗 gas 的内容或者故意让执行失败revert的代码导致包含退款、转账逻辑的合约无法正常工作。例如一个合约要向某个地址退款而该地址是一个部署了恶意receive()的合约若退款使用transfer()/send()这类固定 2300 gas 的调用就可能因接收方receive()故意 revert 或耗尽 gas 而永久失败。因此在编写包含退款等逻辑的合约时必须假定接收方可能是恶意合约并采取call 返回值检查、或者「拉取式支付pull payment」等防御手段。三、兜底回退函数fallback()3.1 声明规则与触发时机fallback()函数会在调用合约中不存在的函数时被触发同时也可用于接收 ETH或用于代理合约proxy contract。它的声明规则声明时不需要function关键字必须由external修饰一般也会用payable修饰以接收 ETHfallback() external payable { ... }若不需要收 ETH也可以不加payable。3.2 带数据的兜底示例定义一个fallback()被触发时释放fallbackCalled事件并输出msg.sender、msg.value和msg.data调用数据event fallbackCalled(address Sender, uint Value, bytes Data); // fallback fallback() external payable { emit fallbackCalled(msg.sender, msg.value, msg.data); }仓库中对应的实际实现见 Languages/en/19_Fallback_en/Fallback.sol。注意msg.data是bytes类型它保存了本次调用的完整 calldata——这正是代理合约能够「原样转发」调用数据的关键。3.3 工程应用代理合约的流量入口fallback()在代理合约中扮演「统一转发入口」所有无法匹配的函数调用即所有业务调用都会落入fallback()再由它把msg.data通过delegatecall委托给逻辑合约执行。仓库第 46 讲 ProxyContract.sol 的实现非常典型/** * dev 回调函数调用_delegate()函数将本合约的调用委托给 implementation 合约 */ fallback() external payable { _delegate(); }其_delegate()内部通过内联汇编执行delegatecall(gas(), _implementation, 0, calldatasize(), 0, 0)把调用者发来的 calldata 完整转发给逻辑合约并将返回值原样返回见 ProxyContract.sol。第 47 讲的可升级合约 Upgrade.sol 采用了同样的模式// fallback函数将调用委托给逻辑合约 fallback() external payable { (bool success, bytes memory data) implementation.delegatecall(msg.data); }由此可见fallback()不只是「接收 ETH」的工具更是整个可升级代理体系46 讲 ProxyContract、47 讲 Upgrade、48 讲 TransparentProxy、49 讲 UUPS的基石入口。四、receive 与 fallback 的触发判定一张流程图说清楚receive和fallback都能够用于接收 ETH但触发规则并不相同。原文给出了权威判定流程触发fallback() 还是 receive()? 接收ETH | msg.data是空 / \ 是 否 / \ receive()存在? fallback() / \ 是 否 / \ receive() fallback()用文字归纳就是合约接收 ETH 时若msg.data为空且合约中存在receive()→ 触发receive()若msg.data不为空例如带着 calldata 调用合约→ 无论是否有receive()都触发fallback()若msg.data为空但合约中没有声明receive()→ 触发fallback()此时fallback()必须为payable否则无法接收 ETH若合约中既没有receive()也没有payable fallback()那么向合约直接发送 ETH 将会报错但仍可以通过带有payable的函数向合约转入 ETH。这段判定逻辑同样以注释形式固化在仓库源码中见 Languages/en/19_Fallback_en/Fallback.sol 与中文版 19_Fallback/Fallback.sol可以作为编写合约时的「速查卡」随时查阅。为了便于记忆将两者的差异汇总如下对比维度receive()fallback()声明关键字不需要function不需要function参数/返回值无参数、无返回值无参数、无返回值必备修饰符external payableexternal通常加payable触发条件①msg.data为空且已声明msg.data非空或msg.data为空但未声明 receive()触发条件②纯 ETH 转账调用不存在的函数如代理合约转发用途专门接收 ETH接收 ETH 代理转发/兜底五、Remix 实操演示从部署到事件验证教程的完整实验环境是 Remix IDE仓库中提供了四张与之对应的操作截图。以仓库根目录为基准图片位于 Languages/en/19_Fallback_en/img/ 目录。完整步骤如下第一步部署合约。在 Remix 中新建文件粘贴 Fallback.sol或中文版 19_Fallback/Fallback.sol的全部代码并编译随后部署合约Fallback。第二步触发 receive()。在「VALUE」栏填入要发送给合约的金额单位是 Wei例如 100保持 CALLDATA 为空然后点击「Transact」。第三步验证 receivedCalled 事件。交易成功后在交易日志中可以看到receivedCalled事件被释放事件参数记录了发送方地址与金额 100 Wei——证明空 calldata 的纯转账确实落入了receive()。第四步触发 fallback()。再次在「VALUE」栏填入金额例如 99 Wei并在「CALLDATA」栏填入任意非空数据例如0xabcd然后点击「Transact」。第五步验证 fallbackCalled 事件。交易成功后日志中会释放fallbackCalled事件其参数包含发送方地址、金额 99 Wei 以及原始调用数据0xabcd——证明非空 calldata 的调用被fallback()完整接管。两轮实验恰好构成一组对照同样的合约、同样地填入 VALUE唯一变量是 CALLDATA 是否为空最终分别触发了receive()与fallback()与第四节中的判定流程完全吻合。读者在本地复现时只需观察事件名称即可快速确认当前触发的是哪个函数。六、总结本讲介绍了 Solidity 中的两种特殊函数receive()和fallback()。回顾核心要点receive()是接收 ETH 的专用入口无参数、无返回值、必须external payable且每个合约最多一个由于send/transfer只提供 2300 gasreceive()内应保持轻量推荐只发事件并警惕恶意合约在receive()中消耗 gas 或故意 revert 造成的退款拒绝服务。fallback()在函数签名不匹配或无可匹配调用时兜底执行必须external、通常payable它既是接收 ETH 的通道也是代理合约中delegatecall转发的统一入口可对照 ProxyContract.sol 与 Upgrade.sol 加深理解。触发顺序接收 ETH 时msg.data为空且有receive()→ 触发receive()msg.data非空或没有receive()→ 触发payable 的fallback()两者皆无则直接转账失败。0.6 版本之前只有单一fallback()0.6 之后才拆分为receive()与fallback()阅读旧合约时需注意这一语义差异。掌握这两个函数是理解 ETH 入账链路、防御恶意接收方攻击以及编写可升级代理合约的第一步也为后续第 20 讲「三种发送 ETH 的方式」、第 4649 讲「代理与可升级合约」系列打下了直接基础。【免费下载链接】WTF-SolidityWTF Solidity 极简入门教程供小白们使用。Now supports English! 官网: https://wtf.academy项目地址: https://gitcode.com/GitHub_Trending/wt/WTF-Solidity创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门