什么是 receive 和 fallback?给合约转账时,到底会执行哪个函数

receive 处理空 calldata 的原生币转账,fallback 承接无法匹配的调用。本文讲清入口分流、payable、代理转发、Gas 与排查方法。

什么是 receive 和 fallback?给合约转账时,到底会执行哪个函数

你向一个合约地址直接转入 ETH,没有点击任何函数,合约却可能记录事件;你调用了一个合约里不存在的函数,交易也不一定立刻失败。这不是链上“自动猜测”了你的意图,而是 Solidity 为两类特殊入口设计了分流规则:receive 和 fallback。

理解它们的关键不是背语法,而是先看一次调用带了什么:calldata 是否为空、函数选择器能否匹配、是否同时携带原生币、目标入口是否 payable。这几项共同决定调用进入普通函数、receive、fallback,还是直接回退。

receive 主要承接空 calldata 的原生币转账;fallback 主要承接无法匹配函数的调用。

合约收到的不是“转账”,而是一次消息调用

一次 EVM 调用可以带目标地址、原生币数量、calldata 和 Gas。普通函数调用的 calldata 通常以前四字节选择器开头,合约按选择器寻找入口。直接发送 ETH 时,calldata 往往为空。特殊函数就是在普通匹配无法覆盖时接住这些调用。

receive 什么时候执行

Solidity 官方文档规定,合约最多定义一个 receive() external payable。它没有参数和返回值。当调用的 calldata 为空时,若合约定义了 receive,就进入这里。这是处理普通 ETH 转入最明确的入口。

receive 必须是 payable,因为它的目的就是接收原生币。常见实现只记录一个事件,或进行非常轻量的记账。不要因为函数名像“收款”就假设资金会自动分配、退款或铸造代币,那些逻辑必须由代码明确实现。

空 calldata 调用优先进入 receive 函数的分流规则
空 calldata 先找 receive;没有它时,才可能进入 payable fallback。

fallback 什么时候执行

fallback 同样最多一个,并且必须是 external。主要有两种情况会进入它:calldata 不为空但没有任何普通函数匹配;或者 calldata 为空、合约又没有 receive。fallback 如果还要接收 ETH,必须标记 payable,否则携带资金的调用会回退。

现代 Solidity 还允许 fallback 接收完整的 bytes calldata input 并返回原始字节。这让代理合约可以把未知调用转发给实现合约,也让路由器能按自定义规则处理数据。但灵活性越高,审计难度也越高。

一张表看清调用分流

calldata函数匹配优先入口携带 ETH 的要求
非空匹配对应普通函数该函数必须 payable
非空不匹配fallbackfallback 必须 payable
空不适用receivereceive 天生 payable
空且无 receive不适用fallbackfallback 必须 payable
无可用入口不适用交易回退资金不会按该调用转入

未知函数没有报错,可能是 fallback 接住了

钱包使用了错误 ABI、前端连错地址或选择器拼错时,调用可能进入 fallback。如果 fallback 只是接受并返回成功,上层会误以为操作完成。因此官方文档建议:若只是想接收普通 ETH,明确实现 receive;不要用宽松的 payable fallback 掩盖接口混淆。

未知函数选择器进入 fallback 但不代表调用目标正确
fallback 能执行,只说明存在兜底入口,不代表原本想调用的函数成功。

为什么代理合约离不开 fallback

代理地址通常保存状态,而具体逻辑位于实现合约。用户把不同函数的 calldata 发给代理时,代理本身未必声明这些函数,于是 fallback 接住数据,再通过 delegatecall 交给实现合约。调用者看到的是同一个地址,实际执行逻辑却可来自另一个地址。

这也意味着查看代理源码时不能只读 fallback。还要确认实现地址、升级管理员、存储布局和当前实现版本。一个“只有转发代码”的代理,不等于功能简单或风险较低。

2300 Gas 不是通用安全边界

历史上 send 和 transfer 会附带有限的 Gas,receive 或代替它的 fallback 在最坏情况下只能依赖 2300 Gas,复杂存储写入和外部调用可能失败。当前 Solidity 官方文档已将 send 和 transfer 标为弃用并计划移除,开发者不应把固定 Gas 补贴当成长期安全假设。

使用低级 call{value: amount}("") 转账时,接收方可能获得更多 Gas 并执行任意逻辑。发送方应检查返回值、考虑重入,并优先采用先更新状态后外部交互、提款模式和重入保护等设计。

合约余额可能绕过 receive 增加

即使合约没有 receive 和 payable fallback,也不能据此断言余额永远为零。协议层存在不经过这两个入口的余额变化方式,合约无法在这些情况下执行代码或拒绝资金。因此 address(this).balance 可能高于合约内部手工累计的收款数字,业务逻辑不应依赖两者永远相等。

发送失败时怎样排查

  1. 确认发送的是原生币还是 ERC-20 代币,两者路径完全不同。
  2. 查看 calldata 是否为空,交易是否调用了具体函数。
  3. 在验证源码中查找 receive、fallback 和 payable。
  4. 确认代理地址的当前实现及其调用逻辑。
  5. 查看回退数据、内部调用和事件,而不只看顶层状态。
  6. 先用模拟和小额测试验证,不盲目重复发送。

开发者检查清单

  • 只收普通 ETH 时是否明确实现 receive?
  • fallback 是否对未知选择器默认成功?
  • 收款入口是否做了可能超出 Gas 的复杂操作?
  • 低级调用是否检查 success 和返回数据?
  • 外部转账前是否先更新内部状态并考虑重入?
  • 测试是否覆盖空/非空 calldata、带/不带 value 的组合?

普通用户要记住什么

向合约地址转入原生币,不等于调用了存款或兑换功能;交易显示成功,也不等于协议给你记了余额。使用前应从项目官方入口发起操作,核对网络、目标地址、调用方法和模拟结果。误把代币转到不支持提取的合约,即使链上转账成功也可能无法找回。

官方资料

本文调用规则依据 Solidity:Receive Ether Function 与 Solidity:Fallback Function。语法和编译器行为会演进,开发与审计时请结合项目锁定的 Solidity 版本。

receive 和 fallback 都是入口,不是安全保证。先根据 calldata、选择器和 value 判断调用会走到哪里,再分析入口内部做了什么,才不会把“合约收到了钱”和“业务操作完成”混为一谈。


本文为链上指南原创科普内容,仅用于解释智能合约机制,不构成任何投资建议。链上操作具有不可逆和合约风险,请核对地址、网络与调用内容,并先进行小额验证。