Skip to content
On this page

L1-2: Multisend(批量转账)

1. 问题

创建合约实现单笔交易中向多个地址批量发送 ETH 或 ERC20 代币。要求所有转账全部成功或全部回滚(原子性)。

2. 原因

在以太坊上,EOA 每次交易只能发送一笔转账。批量转账需要合约中转:

  • DeFi 协议分发奖励
  • DAO 向多个贡献者支付
  • 空投代币给多个地址

原子性确保不会出现「部分转账成功、部分失败」的不一致状态。

3. 方案

ETH 批量转账

solidity
function sendETH(address payable[] calldata receivers, uint256[] calldata amounts)
  • 验证 msg.value == sum(amounts)
  • 对每个接收者执行 call{value: amount}("")
  • 任一失败则全部回滚

ERC20 批量转账

solidity
function sendTokens(address[] calldata receivers, uint256[] calldata amounts, address token)
  • 使用 SafeERC20.safeTransferFrom 逐个转账
  • 调用者需预先 approve 合约

4. 遇到的陷阱

4.1 使用 transfer() 而非 call()

transfer() 固定 2300 gas,接收方如果是合约且 fallback 需要更多 gas 则会失败。

4.2 msg.value 验证缺失

如果没有验证 msg.value == sum(amounts),合约自身的 ETH 可能被意外转走。

4.3 SafeERC20 导入

OpenZeppelin v5 中 safeTransferFromSafeERC20 library 中,需要 using SafeERC20 for IERC20

5. 陷阱的原因

5.1

以太坊 EIP-1884 后某些操作码 gas 成本增加,2300 gas 不足以执行任何有意义的合约逻辑。call{value}("") 转发所有可用 gas。

5.2

如果只验证数组长度而不验证总金额,攻击者可以发送超过 sum 的 ETH 让合约承担差额,或发送不足金额导致合约自有资金被转出。

6. 如何解决陷阱

6.1

使用低阶 call{value: amount}("") 代替 .transfer().send()

6.2

在循环前计算总金额并验证 msg.value == totalSent

7. 技术要点

要点说明
ETH 转账模式call{value}("") > transfer()
ERC20 转账模式safeTransferFrom > transferFrom
原子性Solidity 中任一 revert 自动回滚所有状态变更
Gas 优化数组遍历 O(n),大量接收者需注意 Gas 上限
SafeERC20OZ v5 中为 library,需 using 语句

Built with AiAda