欢迎来到《Solidity面试修炼之道》专栏💎。
专栏核心理念:
核心 Slogan💸💸:从面试题到实战精通,你的 Web3 开发进阶指南。
一句话介绍🔬🔬: 150+ 道面试题 × 103 篇深度解析 = 你的 Solidity 修炼秘籍。
- ✅ 名称有深度和系统性
- ✅ "修炼"体现进阶过程
- ✅ 适合中文技术社区
- ✅ 记忆度高,易于传播
- ✅ 全场景适用
Q19: fallback 和 receive 有什么区别?
简答:
receive 专门处理纯 ETH 转账(无数据),fallback 处理所有其他情况(包括带数据的调用或找不到匹配函数时)。receive 优先级高于 fallback。
详细分析:
fallback 和 receive 是 Solidity 中的特殊函数,用于处理合约接收 ETH 和未知函数调用的情况。
receive() external payable:
- 专门用于接收纯 ETH 转账
- 当 msg.data 为空时触发
- 必须是 external 和 payable
- 不能有参数和返回值
- Gas 限制:通过 transfer/send 调用时只有 2300 gas
fallback() external [payable]:
- 处理所有其他情况
- 当调用不存在的函数时触发
- 当 msg.data 不为空但没有 receive 时触发
- 可以是 payable 或非 payable
- 可以有参数和返回值(Solidity 0.6.0+)
调用优先级:
接收 ETH + msg.data 为空
├─ 有 receive() -> 调用 receive()
└─ 无 receive() -> 调用 fallback()
接收 ETH + msg.data 不为空
└─ 调用 fallback()(必须是 payable)
不接收 ETH + 调用不存在的函数
└─ 调用 fallback()
代码示例:
solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
/**
* @title FallbackReceiveExamples
* @notice 演示 fallback 和 receive 的区别
*/
// 示例 1:同时有 receive 和 fallback
contract BothFunctions {
event Received(address sender, uint256 amount, string message);
event Fallback(address sender, uint256 amount, bytes data);
/**
* @notice 接收纯 ETH 转账
* @dev 当 msg.data 为空时调用
*/
receive() external payable {
emit Received(msg.sender, msg.value, "receive called");
}
/**
* @notice 处理其他所有情况
* @dev 当调用不存在的函数或 msg.data 不为空时调用
*/
fallback() external payable {
emit Fallback(msg.sender, msg.value, msg.data);
}
/**
* @notice 测试函数:演示不同的调用方式
*/
function getBalance() public view returns (uint256) {
return address(this).balance;
}
}
// 示例 2:只有 receive
contract OnlyReceive {
event Received(address sender, uint256 amount);
receive() external payable {
emit Received(msg.sender, msg.value);
}
// 注意:没有 fallback
// 如果调用不存在的函数,交易会回滚
// 如果发送 ETH 但带有数据,交易会回滚
}
// 示例 3:只有 fallback
contract OnlyFallback {
event Fallback(address sender, uint256 amount, bytes data);
fallback() external payable {
emit Fallback(msg.sender, msg.value, msg.data);
}
// 注意:没有 receive
// 纯 ETH 转账会调用 fallback
// 调用不存在的函数也会调用 fallback
}
// 示例 4:非 payable fallback
contract NonPayableFallback {
event FallbackCalled(bytes data);
// 非 payable:不能接收 ETH
fallback() external {
emit FallbackCalled(msg.data);
}
// 如果发送 ETH,交易会回滚
// 只能处理不带 ETH 的未知函数调用
}
/**
* @title CallScenarios
* @notice 演示不同调用场景
*/
contract CallScenarios {
BothFunctions public target;
constructor() {
target = new BothFunctions();
}
/**
* @notice 场景 1:使用 transfer 发送 ETH
* @dev 触发 receive()
*/
function scenario1_Transfer() public payable {
// msg.data 为空,触发 receive()
payable(address(target)).transfer(msg.value);
}
/**
* @notice 场景 2:使用 send 发送 ETH
* @dev 触发 receive()
*/
function scenario2_Send() public payable {
// msg.data 为空,触发 receive()
bool success = payable(address(target)).send(msg.value);
require(success, "Send failed");
}
/**
* @notice 场景 3:使用 call 发送 ETH(无数据)
* @dev 触发 receive()
*/
function scenario3_CallNoData() public payable {
// msg.data 为空,触发 receive()
(bool success, ) = address(target).call{value: msg.value}("");
require(success, "Call failed");
}
/**
* @notice 场景 4:使用 call 发送 ETH(带数据)
* @dev 触发 fallback()
*/
function scenario4_CallWithData() public payable {
// msg.data 不为空,触发 fallback()
(bool success, ) = address(target).call{value: msg.value}(
abi.encodeWithSignature("nonExistentFunction()")
);
require(success, "Call failed");
}
/**
* @notice 场景 5:调用不存在的函数(无 ETH)
* @dev 触发 fallback()
*/
function scenario5_CallNonExistent() public {
// 调用不存在的函数,触发 fallback()
(bool success, ) = address(target).call(
abi.encodeWithSignature("nonExistentFunction()")
);
require(success, "Call failed");
}
}
/**
* @title GasLimitations
* @notice 演示 gas 限制
*/
contract GasLimitations {
event ReceivedWithGas(uint256 gasLeft);
event FallbackWithGas(uint256 gasLeft);
/**
* @notice receive 的 gas 限制
* @dev transfer/send 只提供 2300 gas
*/
receive() external payable {
// 只有 2300 gas(通过 transfer/send 时)
// 只能做简单操作:emit event, 简单赋值
emit ReceivedWithGas(gasleft());
// ❌ 不能做复杂操作:
// - 写入多个存储槽
// - 调用其他合约
// - 复杂计算
}
fallback() external payable {
emit FallbackWithGas(gasleft());
}
}
/**
* @title ProxyPattern
* @notice 使用 fallback 实现代理模式
*/
contract ProxyPattern {
address public implementation;
constructor(address _implementation) {
implementation = _implementation;
}
/**
* @notice 代理所有调用到实现合约
*/
fallback() external payable {
address impl = implementation;
assembly {
// 复制 calldata
calldatacopy(0, 0, calldatasize())
// 委托调用实现合约
let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
// 复制返回数据
returndatacopy(0, 0, returndatasize())
// 返回或回滚
switch result
case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}
/**
* @notice 接收 ETH
*/
receive() external payable {}
}
/**
* @title BestPractices
* @notice 最佳实践
*/
contract BestPractices {
event Received(address sender, uint256 amount);
event FallbackCalled(address sender, bytes data);
/**
* @notice ✅ 推荐:明确的 receive 和 fallback
*/
receive() external payable {
// 处理纯 ETH 转账
emit Received(msg.sender, msg.value);
}
fallback() external payable {
// 处理未知函数调用
emit FallbackCalled(msg.sender, msg.data);
// 可选:回滚未知调用
// revert("Function not found");
}
/**
* @notice ✅ 推荐:在 receive/fallback 中保持简单
*/
mapping(address => uint256) public deposits;
// ❌ 不推荐:在 receive 中做复杂操作
// receive() external payable {
// deposits[msg.sender] += msg.value;
// // 如果通过 transfer 调用,可能 gas 不足
// }
// ✅ 推荐:使用显式函数
function deposit() public payable {
deposits[msg.sender] += msg.value;
}
}
/**
* @title DecisionTree
* @notice 决策树:何时使用 receive/fallback
*/
contract DecisionTree {
/**
* @notice 使用 receive 的场景:
* - 合约需要接收 ETH
* - 简单的 ETH 接收逻辑
* - 不需要处理函数调用
*
* @notice 使用 fallback 的场景:
* - 实现代理模式
* - 处理未知函数调用
* - 需要访问 msg.data
* - 实现通用的调用处理器
*
* @notice 同时使用两者:
* - 需要区分纯 ETH 转账和函数调用
* - 需要不同的处理逻辑
* - 代理合约(fallback)+ 接收 ETH(receive)
*
* @notice 都不使用:
* - 合约不应该接收 ETH
* - 所有函数都明确定义
* - 避免意外的 ETH 转入
*/
}
理论补充:
调用流程图:
外部调用合约
↓
检查函数选择器
↓
找到匹配的函数?
├─ 是 → 调用该函数
└─ 否 ↓
msg.data 为空?
├─ 是 ↓
│ 有 receive()?
│ ├─ 是 → 调用 receive()
│ └─ 否 → 调用 fallback()
└─ 否 → 调用 fallback()
如果没有对应的函数:
→ 交易回滚
Gas 成本对比:
| 操作 | Gas 限制 | 适用场景 |
|---|---|---|
| transfer | 2300 | 简单 ETH 转账 |
| send | 2300 | 简单 ETH 转账 |
| call | 所有剩余 gas | 复杂操作 |
历史演进:
- Solidity 0.6.0 之前:只有 fallback
- Solidity 0.6.0+:引入 receive,分离关注点
- 现在:receive 处理 ETH,fallback 处理调用
相关问题:
- Q23: 如何向没有 payable 函数、receive 或 fallback 的合约发送 Ether?
- Q5: 代理合约需要什么特殊的 CALL 才能工作?
