Skip to content
On this page

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.safeTransferFrom to 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

PointDescription
ETH transfer patterncall{value}("") > transfer()
ERC20 transfer patternsafeTransferFrom > transferFrom
AtomicityIn Solidity, any revert automatically rolls back all state changes
Gas optimizationArray iteration is O(n); watch the gas limit with many recipients
SafeERC20In OZ v5 it is a library, requiring a using statement

Built with AiAda