// SPDX-License-Identifier: MIT pragma solidity 0.8.28; /// @title BatchTransfer – minimal multi-send for native coin and ERC-20 tokens. /// @notice Same interface as the widely used "disperse" contract. Holds no funds between calls. /// Used by https://crypto.xvvxvv.online/evm-batch-transfer (testnets only). interface IERC20 { function transfer(address to, uint256 amount) external returns (bool); function transferFrom(address from, address to, uint256 amount) external returns (bool); } contract BatchTransfer { error LengthMismatch(); error NativeSendFailed(address to); error TokenTransferFailed(address to); /// @notice Send native coin to many recipients. msg.value must cover the sum; any surplus is refunded. function disperseEther(address[] calldata recipients, uint256[] calldata values) external payable { if (recipients.length != values.length) revert LengthMismatch(); for (uint256 i = 0; i < recipients.length; i++) { (bool ok, ) = recipients[i].call{value: values[i]}(""); if (!ok) revert NativeSendFailed(recipients[i]); } uint256 left = address(this).balance; if (left > 0) { (bool ok, ) = msg.sender.call{value: left}(""); if (!ok) revert NativeSendFailed(msg.sender); } } /// @notice Pull the total once with transferFrom, then push to each recipient (one approval, lowest gas). function disperseToken(IERC20 token, address[] calldata recipients, uint256[] calldata values) external { if (recipients.length != values.length) revert LengthMismatch(); uint256 total = 0; for (uint256 i = 0; i < values.length; i++) total += values[i]; _call(token, abi.encodeWithSelector(IERC20.transferFrom.selector, msg.sender, address(this), total), msg.sender); for (uint256 i = 0; i < recipients.length; i++) { _call(token, abi.encodeWithSelector(IERC20.transfer.selector, recipients[i], values[i]), recipients[i]); } } /// @notice transferFrom directly to each recipient (works for tokens that block contract-held balances). function disperseTokenSimple(IERC20 token, address[] calldata recipients, uint256[] calldata values) external { if (recipients.length != values.length) revert LengthMismatch(); for (uint256 i = 0; i < recipients.length; i++) { _call(token, abi.encodeWithSelector(IERC20.transferFrom.selector, msg.sender, recipients[i], values[i]), recipients[i]); } } /// @dev Tolerates tokens that return nothing (USDT-style) but reverts on `false` or on a revert. function _call(IERC20 token, bytes memory data, address who) private { (bool ok, bytes memory ret) = address(token).call(data); if (!ok || (ret.length != 0 && !abi.decode(ret, (bool)))) revert TokenTransferFailed(who); } }