在 Solidity 中,向其他地址发送 ETH 主要有三种方式:transfer、send 和 call。
1. transfer(不推荐用于新项目)
- 语法:
payableAddress.transfer(amount); - 特点:
- 自动转发 2300 gas,足够记录日志,但不足以指向复杂的逻辑或状态更改。
- 如果转账失败,会 自动回滚 交易(revert)。
- 从 Solidity 0.6 开始,目标地址必须是
address payable类型。
- 适用场景: 简单转账,接收方为普通钱包或简单合约,无需担心重入攻击(因为 gas 限制)。
- 缺点: gas 固定为 2300,未来以太坊硬分叉可能该变 gas 成本(如 lstanbul 升级后某些操作码 gas 增加),导致
transfer可能失败。因此,官方逐渐建议弃用。
2. send (不推荐)
- 语法:
bool success = payableAddress.send(amount); - 特点:
- 同样只提供 2300 gas。
- 转账失败时 不会自动回滚 ,而是返回
false。开发者需要手动检查返回值并处理失败情况。
- 适用场景: 极少使用,因为需要手动处理失败,且 gas 限制问题与
transfer相同。 - 注意: 必须检查返回值,否则即使失败,交易也会继续执行,可能导致状态不一致。
3. call (目前推荐的方式)
- 语法:
(bool success,) = payableAddress.call{value: amount}(""); - 特点:
- 转发所有可用 gas(默认),也可以手动指定 gas,如
{value:amount,gas: gasAmount}。 - 失败时返回
false不会自动回滚 ,需要开发者检查success并决定是否revert。 - 可以调用任意函数(通过编码数据),所以更灵活。
- 目标地址无需时
payable,因为call会将地址视为address,但若需要接收 ETH,接收方仍需有receive()或fallback()函数。
- 转发所有可用 gas(默认),也可以手动指定 gas,如
- 安全性: 由于转发所有 gas,容易引发 重入攻击 。因此,使用
call发送 ETH 时, 必须 遵循"检查-生效-交互" 模式,并考虑使用重入锁(如OpenZeppelin 的ReentrancyGuard)。
示例代码:使用 call 发送 ETH
solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SendEther {
// 事件记录转账
event Sent(address indexed to, uint256 amount);
// 向指定地址发送 ETH
function sendViaCall(address payable _to) external payable {
// 使用 call 发送 msg.value(调用函数时附带的 ETH)
(bool success, ) = _to.call{value: msg.value}("");
require(success, "ETH transfer failed");
emit Sent(_to, msg.value);
}
// 接收 ETH 的函数(必须存在,否则合约无法接收直接转账)
receive() external payable {
// 可以记录收到 ETH 的事件
}
}
解释:
sendViaCall是一个payable函数,调用时可以附带 ETH(msg.value)。- 使用
_to.call{value: msg.value}("")将 ETH 转发给_to地址。 - 检查返回值,失败时
revert。 - 合约必须定义
receive()或fallback()才能接收直接转账(如通过send、transfer或call发送的普通转账)。
接收 ETH 的合约要求
如果目标地址是一个合约,它必须实现以下函数之一才能成功接收 ETH(否则转账会失败):
receive() external payable:当调用数据为空时执行(即纯 ETH 转账)。fallback() external payable:当没有匹配的函数且调用数据非空时执行,也可以接收 ETH。
示例接收合约:
solidity
contract Receiver {
event Received(address indexed from, uint256 amount);
receive() external payable {
emit Received(msg.sender, msg.value);
}
}
安全性总结
- 重入攻击 :使用
call时,如果接收方是恶意合约,它可以在receive中回调发送方函数,导致未预期的状态更改。解决方案:- 先更新余额等状态,再发送 ETH("检查-生效-交互"模式)。
- 使用重入锁修饰器。
- Gas 限制 :
transfer和send的 2300 gas 可能不足,未来分叉可能更不可靠。call默认转发所有 gas,更灵活,但需防范重入。 - 返回值检查 :
send和call必须检查返回值,transfer不需要。
建议 :在 Solidity 0.8.x 及以上版本,优先使用 call 并配合重入保护,或使用 OpenZeppelin 的 Address.sendValue 库函数(内部也是用 call,并处理了返回值)。