fallback 和 receive 函数是什么? 图 1
fallback 和 receive 函数是什么? · 图 1

结论先说

当一笔调用”没有匹配到具体函数”时,EVM 把执行交给兜底逻辑:带 calldata 但选择器不匹配(或无 calldata 但有 value 的旧式合约)→ fallback 函数;纯 ETH 转账(无 calldata + 有 value)→ receive 函数(有则用之,无则需 fallback 接收)。它们是合约的”默认入口”:钱包直接转账、其它合约转发调用、意图不明的交互,都落在这里。理解 fallback/receive 的触发规则与 gas 限制,就理解了”为什么有的合约收不了转账”、“为什么 fallback 里不能做重活”、以及”proxy/委托调用为什么依赖 fallback”。

触发规则:调用怎么被分发

合约收到外部调用时,EVM 按规则分发:一,有 calldata 且前 4 字节(选择器)匹配某函数 → 执行该函数;二,无 calldata + 有 value → 执行 receive(若定义),否则 fallback(若定义且能接收 ETH),否则 revert(“打不开”——转账失败回滚);三,有 calldata 但选择器不匹配 → 执行 fallback(若定义),否则 revert。关键推论:一,“随便发个 data 到合约” = 触发 fallback(不是”调用一个不存在的函数”——EVM 没有”函数不存在”错误,只有”选择器不匹配 → fallback/revert”);二,向”没有 fallback/receive 的合约”转 ETH = 必然失败(revert)——“这个合约不收裸币”是代码决定的;三,向 EOA(个人地址)发任意 data = 直接忽略 data(EOA 没有代码,data 不执行)——“给个人地址发合约调用”是无效操作(常见误操作:把合约交互发到个人地址)。

fallback 的 gas 限制:为什么不能做重活

规则:fallback 被”选择器不匹配”的调用触发时,调用者只保证 2300 gas(历史遗留的”转发性调用 gas 上限”)——这 2300 gas 只够”记录接收(event)+ 简单的 balance 检查”,不够 SSTORE(存储写入)或外部调用。设计动机(历史):防”重入/恶意调用”——调用者用不匹配选择器触发 fallback 时,不能指望 fallback 做复杂状态变更(gas 不够跑完)。含义:一,fallback 里做 SSTORE/emit 大事件/外部调用 = 在”不匹配调用”路径下 gas 不足 revert(但在”纯转账”路径下 receive 的 gas 规则不同——receive 是转账专用,gas 由转账交易的 gasLimit 决定,不受 2300 限制,但仍应保持轻量);二,“fallback 收币 + 记账”的设计要分开:receive(轻量收币)+ 单独的显式函数(记账/复杂逻辑)——不要指望 fallback 干重活。

fallback 的三个真实用途

一,接收与转发:合约收 ETH 后转发给其它地址(支付合约、收款地址)——receive/fallback 里 forward(注意转发的气与重入防护)。二,proxy/委托模式(最重要):代理合约的”整个逻辑”就是 fallback——所有调用都”选择器不匹配代理的函数”(代理不定义业务函数),fallback 里 delegatecall 到逻辑合约——“代理不执行自己的业务,全部委托”,这是可升级合约/最小代理(EIP-1167)的实现基础;三,意图/通用接口:一些合约把 fallback 当”多用途入口”(按 calldata 自定义解码路由)——非标准用法,审计要点(任意 calldata 触发任意逻辑 = 攻击面扩大)。三者的共同点:fallback 是”入口层”,重逻辑应该在”被委托/被路由到的地方”,入口保持薄。

安全面:fallback 是攻击者最先看的门

一,“意外触发”:攻击者向合约发”垃圾 calldata”(不匹配选择器)触发 fallback——如果 fallback 有状态变更/外部调用/权限检查缺失,就是攻击面(“这个合约的 fallback 做了什么”是审计必查项);二,ETH 接收假设:依赖”该合约不收 ETH”假设的逻辑(如”余额 > 0 = 异常”的检查)可能被”主动转入 ETH”(任何人可以转 ETH 给有 receive 的合约)破坏——“别人给我转钱”是外部事件,状态机要能消化;三,重入经典路径:fallback/receive 里”先转账后改状态”的顺序 = 重入入口(攻击合约的 receive 里回调)——CEI 模式(检查-效果-交互)在兜底函数里同样适用;四,gas 假设攻击:依赖”fallback 一定 revert(gas 不够)“的逻辑(“这个合约收不了币所以余额恒 0”)——规则变更/路径差异可能打破假设,“依赖 revert 的安全性”是反模式。审计 checklist 里”fallback/receive 的行为 + 它被谁/什么触发”是独立小节。

开发者实践

一,明确意图:合约”该不该收 ETH”要显式表达——不收:不定义 receive/fallback(或显式 revert + 注释);收:定义 receive(轻量)+ 文档化;二,fallback 保持薄:记录(log)或委托(delegatecall),状态变更/外部调用移到显式函数;三,proxy 设计:代理的 fallback = 纯 delegatecall(无其它逻辑——“代理里加判断”会改变委托语义,升级模型复杂化);四,测试”恶意交互”:向合约发垃圾 calldata、转 1 wei、带 value 的不匹配调用——行为按设计(revert/记录/忽略),无意外状态变更;五,文档:fallback/receive 的”存在 + 行为”写进合约文档(链下使用者需要知道”这个合约收不收币、收到后发生什么”)。

常见误读

“调用不存在的函数 = 报错”——EVM 分发是”选择器匹配”,不匹配走 fallback(有)或 revert(无),没有”函数不存在”这个错误类别;“fallback 和 receive 是一回事”——触发路径不同(receive 仅纯转账,fallback 覆盖不匹配 calldata + 无 receive 时的转账),gas 规则不同(2300 限制只作用于”不匹配调用”路径);“代理合约的业务在代理里”——代理只有 fallback(委托入口),业务在逻辑合约(delegatecall 的目标)——“升级”换的是逻辑合约地址,代理不变。

风险提示

分发规则与 gas 限制是 EVM/Solidity 语义(稳定,但 Solidity 版本对 fallback/receive 的语法/默认行为有演进——旧版本的”modifier 式 fallback”与新版本的”函数式”行为细节有差,引用以所用编译器版本文档为准)。“合约收不收 ETH、收到后做什么”是交互前的必查项(读源码/验证状态);与未验证合约交互时,fallback 行为是黑箱——信任成本按”未审计”计。本文为语义解释,不构成对任何合约/模式的安全评价。

小结

一句话记忆:调用分发 = 选择器匹配 → 函数;纯转账 → receive;不匹配 → fallback(2300 gas 限制,保持薄);三者定义与否决定”合约收不收币、垃圾调用发生什么”。fallback 的真实工作:收款转发、proxy 委托(可升级合约的地基)、以及攻击者最先探的门——审计查它的行为与触发面,开发者让它薄、意图显式、测试恶意交互。