Appearance
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 中 safeTransferFrom 在 SafeERC20 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 上限 |
| SafeERC20 | OZ v5 中为 library,需 using 语句 |