Appearance
L1-2: Multisend
1. Problem
Create a contract that sends ETH or ERC20 tokens to multiple addresses in a single transaction. All transfers must either all succeed or all revert (atomicity).
2. Why
On Ethereum, an EOA can only send one transfer per transaction. Batch transfers require a contract intermediary:
- DeFi protocols distributing rewards
- DAOs paying multiple contributors
- Airdropping tokens to multiple addresses
Atomicity ensures there is no inconsistent state where "some transfers succeed and some fail".
3. Solution
ETH Batch Transfer
solidity
function sendETH(address payable[] calldata receivers, uint256[] calldata amounts)
- Validate
msg.value == sum(amounts) - Execute
call{value: amount}("")for each recipient - If any one fails, the entire transaction reverts
ERC20 Batch Transfer
solidity
function sendTokens(address[] calldata receivers, uint256[] calldata amounts, address token)
- Use
SafeERC20.safeTransferFromto transfer one by one - Caller must pre-approve the contract
4. Pitfalls Encountered
4.1 Using transfer() instead of call()
transfer() is fixed at 2300 gas. If the recipient is a contract whose fallback requires more gas, the transfer will fail.
4.2 Missing msg.value validation
Without validating msg.value == sum(amounts), the contract's own ETH could be accidentally transferred out.
4.3 SafeERC20 import
In OpenZeppelin v5, safeTransferFrom lives in the SafeERC20 library and requires using SafeERC20 for IERC20.
5. Why the Pitfalls Happen
5.1
After Ethereum EIP-1884, the gas cost of certain opcodes increased. 2300 gas is insufficient to execute any meaningful contract logic. call{value}("") forwards all available gas.
5.2
If only array lengths are validated without checking the total amount, an attacker could send more ETH than the sum and make the contract cover the difference, or send less than the sum and cause the contract's own funds to be transferred out.
6. How to Resolve the Pitfalls
6.1
Use the low-level call{value: amount}("") instead of .transfer() or .send().
6.2
Calculate the total amount before the loop and validate msg.value == totalSent.
7. Technical Highlights
| Point | Description |
|---|---|
| ETH transfer pattern | call{value}("") > transfer() |
| ERC20 transfer pattern | safeTransferFrom > transferFrom |
| Atomicity | In Solidity, any revert automatically rolls back all state changes |
| Gas optimization | Array iteration is O(n); watch the gas limit with many recipients |
| SafeERC20 | In OZ v5 it is a library, requiring a using statement |