在 Solidity 中,合约接收以太币(ETH)的方式主要有三种:显式 payable 函数、receive() 函数和 fallback() 函数。这三种方式适用于不同场景,核心是通过 payable 修饰符允许合约接收 ETH,同时处理资金相关的业务逻辑。下面详细介绍每种方式的用法、场景和实操示例。

一、显式 payable 函数(最常用)

显式 payable 函数是最灵活的接收方式,允许在接收 ETH 时加入自定义验证逻辑(如金额检查、权限控制等),是业务场景中最常用的方式。

1. 语法与核心作用

定义一个带 payable 修饰符的函数,用户通过调用该函数向合约转入 ETH,函数内部可处理资金(如记录余额、验证条件等)。

function 函数名(参数) external payable {
    // 业务逻辑(如验证、状态更新等)
}
  • external:限制函数只能从外部调用(不能在合约内部调用)。
  • payable:标记函数可接收 ETH,否则调用时发送 ETH 会失败。
2. 适用场景

需要对转入的 ETH 进行条件验证或业务处理时使用,例如:

  • 众筹合约中,要求每次转入的 ETH 不低于某个最小值;
  • 会员系统中,只有付费用户才能调用函数。
3. 实操示例

以下是一个简单的“捐款合约”,使用显式 payable 函数接收 ETH,并记录每个捐款人的金额:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

contract Donation {
    // 记录每个地址的捐款金额
    mapping(address => uint256) public donorToAmount;

    // 显式 payable 函数:接收捐款并记录
    function donate() external payable {
        // 验证:捐款金额不能为 0
        require(msg.value > 0, "至少转入 1 wei");
        // 记录捐款金额(msg.value 是当前调用转入的 ETH 数量,单位为 wei)
        donorToAmount[msg.sender] += msg.value;
    }

    // 查看合约总余额
    function getContractBalance() external view returns (uint256) {
        return address(this).balance;
    }
}

调用方式:
用户通过钱包向合约地址调用 donate() 函数,并在调用时指定转入的 ETH 数量(例如转入 0.1 ETH),合约会自动执行 donate() 中的逻辑。

二、receive() 函数(被动接收纯 ETH)

receive() 是 Solidity 0.6 后新增的特殊函数,专门用于接收无数据的纯 ETH 转账(例如用户直接向合约地址“打款”,不调用任何函数)。

1. 语法与核心作用
receive() external payable {
    // 可选:简单的处理逻辑(如记录日志)
}
  • 特点:
    • 无参数、无返回值,必须标记 external payable;
    • 仅在用户“直接转账”(不携带任何数据)时触发;
    • 每个合约最多只能有一个 receive() 函数。
2. 适用场景

需要被动接收 ETH(用户可能忘记调用显式函数,直接转账)时使用,例如:

  • 慈善捐款合约,允许任何人直接向合约地址打款;
  • 钱包合约,需要接收外部转入的 ETH。
3. 实操示例

给上面的 Donation 合约添加 receive() 函数,允许用户直接向合约地址转账(不调用 donate()):

contract Donation {
    mapping(address => uint256) public donorToAmount;

    // 显式捐款函数
    function donate() external payable {
        require(msg.value > 0, "至少转入 1 wei");
        donorToAmount[msg.sender] += msg.value;
    }

    // receive():处理直接转账(无数据)
    receive() external payable {
        // 直接转账时也记录金额(和 donate() 逻辑一致)
        donorToAmount[msg.sender] += msg.value;
    }

    function getContractBalance() external view returns (uint256) {
        return address(this).balance;
    }
}

触发方式:
用户直接向合约地址转账(例如在钱包中输入合约地址,转账 0.1 ETH,不调用任何函数),此时 receive() 会自动执行,记录捐款金额。

三、fallback() 函数(默认接收带数据的转账)

fallback() 是“默认函数”,当合约收到无法匹配其他函数的调用(包括带数据的转账)时触发,可用于接收带数据的 ETH 转账。

1. 语法与核心作用
fallback() external payable {
    // 处理逻辑(如解析数据、记录日志等)
}
  • 特点:
    • 无参数、无返回值,需标记 external payable(若要接收 ETH);
    • 触发场景:
      • 用户调用合约中不存在的函数(带数据);
      • 用户向合约转账时携带数据(此时若没有 receive() 函数,fallback() 会触发)。
2. 适用场景

需要处理带数据的转账或作为“默认逻辑”时使用,例如:

  • 兼容旧版本合约的调用(可能携带过时数据);
  • 处理特殊格式的转账数据(如解析自定义编码的信息)。
3. 实操示例

给 Donation 合约添加 fallback() 函数,处理带数据的转账:

contract Donation {
    mapping(address => uint256) public donorToAmount;
    event FallbackTriggered(address sender, uint256 amount, bytes data); // 记录触发时的信息

    function donate() external payable {
        require(msg.value > 0, "至少转入 1 wei");
        donorToAmount[msg.sender] += msg.value;
    }

    receive() external payable {
        donorToAmount[msg.sender] += msg.value;
    }

    // fallback():处理带数据的转账或调用不存在的函数
    fallback() external payable {
        // 记录带数据的转账信息
        emit FallbackTriggered(msg.sender, msg.value, msg.data);
        // 即使带数据,也记录捐款金额
        donorToAmount[msg.sender] += msg.value;
    }

    function getContractBalance() external view returns (uint256) {
        return address(this).balance;
    }
}

触发方式:
用户向合约转账时携带数据(例如调用一个不存在的函数 invalidFunction(123)),此时 fallback() 会触发,执行内部逻辑。

四、三种方式的触发优先级

当合约同时定义了 receive()、fallback() 和显式 payable 函数时,触发顺序如下:

  1. 调用显式 payable 函数:用户主动调用时,优先执行显式函数(如 donate())。
  2. 纯 ETH 转账(无数据):
    • 若存在 receive(),触发 receive();
    • 若不存在 receive(),触发 fallback()(需 payable)。
  3. 带数据的转账:
    • 若调用的函数不存在,触发 fallback()(需 payable)。

五、安全注意事项

  1. 避免无限制接收 ETH:
    若合约不应接收 ETH(如纯代币合约),可禁用 receive() 和 fallback(),或在其中主动报错:

    receive() external payable {
        revert("本合约不接收 ETH");
    }
    fallback() external payable {
        revert("本合约不接收 ETH");
    }
    
  2. 防止重入攻击:
    接收函数中若涉及状态更新(如 donorToAmount[msg.sender] += msg.value),需遵循“先更新状态,再外部交互”,必要时使用 ReentrancyGuard(OpenZeppelin 库)。

  3. 控制 gas 消耗:
    receive() 和 fallback() 在直接转账时可能仅获得 2300 gas(如用 transfer() 转账),因此逻辑需极简(避免复杂计算或日志),否则会失败。

总结

接收方式适用场景核心特点
显式 payable 函数需要验证条件、主动触发的场景可自定义逻辑,最灵活
receive() 函数被动接收纯 ETH 转账(无数据)无参数,仅处理纯转账
fallback() 函数处理带数据的转账或默认逻辑通用默认函数,可处理数据

根据业务需求选择合适的方式:需要验证时用显式函数,被动接收纯 ETH 用 receive(),处理带数据的转账用 fallback()。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐