Disperse
0xddb6a9b9e2d6c5636b444c0cda907c9944c6cec7
Verification
Verified
v0.8.24+commit.e11b9ed9
Type
Contract
2,117 bytes
ABI entries
6
0 read · 2 write
License
none
Contract information
- Address
- 0xddb6a9b9e2d6c5636b444c0cda907c9944c6cec7
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.24+commit.e11b9ed9
- Optimization
- Enabled
- Creator
- 0xc14C897c6b…9230205125
- Creation tx
- 0xd5edbd45cb…b5bfaf9b3a
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (0)
No read functions
The ABI is unavailable or exposes no view functions.
ABI
[
{
"inputs": [],
"name": "EmptyRecipients",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "recipient",
"type": "address"
}
],
"name": "EtherTransferFailed",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "required",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "provided",
"type": "uint256"
}
],
"name": "InsufficientValue",
"type": "error"
},
{
"inputs": [],
"name": "LengthMismatch",
"type": "error"
},
{
"inputs": [
{
"internalType": "address[]",
"name": "recipients",
"type": "address[]"
},
{
"internalType": "uint256[]",
"name": "amounts",
"type": "uint256[]"
}
],
"name": "disperseEther",
"outputs": [],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [
{
"internalType": "contract IERC20",
"name": "token",
"type": "address"
},
{
"internalType": "address[]",
"name": "recipients",
"type": "address[]"
},
{
"internalType": "uint256[]",
"name": "amounts",
"type": "uint256[]"
}
],
"name": "disperseToken",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]Source code
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import { IERC20 } from "../library/IERC20.sol";
import { SafeERC20 } from "../library/SafeERC20.sol";
import { ReentrancyGuard } from "../library/ReentrancyGuard.sol";
/**
* @title Disperse
*
* HookSwap's batch-send ("multisender" / disperse) utility — send one ERC-20 (or the
* native currency) to many recipients in a SINGLE transaction. A fork of the canonical,
* public-domain Disperse.app contract, modernised to solc 0.8.24.
*
* Design (trustless + minimal):
* • Stateless — no storage, no owner, no admin, no fees. There is nothing to configure
* and no privileged party.
* • NEVER custodies funds. The token path pulls tokens PER RECIPIENT straight from the
* caller via `transferFrom(msg.sender, recipients[i], amounts[i])`, so the contract
* never holds a balance between transfers. The ether path forwards each `amounts[i]`
* with the call and refunds any unused `msg.value` dust back to the caller.
*
* Token approvals: the caller approves this contract for AT LEAST the sum of `amounts`
* (a plain ERC-20 allowance — NO Permit2) before calling {disperseToken}; each
* `transferFrom` then draws against that allowance.
*
* NOT audited. Vendors a minimal `IERC20` + `SafeERC20` (no OpenZeppelin import), matching
* the sibling `contracts/referral` / `contracts/locker` kits.
*/
contract Disperse is ReentrancyGuard {
using SafeERC20 for IERC20;
/// @dev `recipients` and `amounts` must be the same length.
error LengthMismatch();
/// @dev Nothing to send (empty recipient list).
error EmptyRecipients();
/// @dev A native-currency transfer to `recipient` failed (recipient reverted / OOG).
error EtherTransferFailed(address recipient);
/// @dev `msg.value` sent with {disperseEther} is less than the sum of `amounts`.
error InsufficientValue(uint256 required, uint256 provided);
/**
* @notice Batch-send an ERC-20 to many recipients, pulling PER RECIPIENT from the caller.
* @dev Uses `SafeERC20.safeTransferFrom` so non-standard tokens (e.g. USDT, which returns
* no bool) work. The contract never holds the tokens — each transfer goes directly
* from `msg.sender` to `recipients[i]`. Requires the caller to have approved this
* contract for at least `sum(amounts)`.
* @param token The ERC-20 to distribute.
* @param recipients Destination addresses.
* @param amounts Amount (in token base units) for each corresponding recipient.
*/
function disperseToken(
IERC20 token,
address[] calldata recipients,
uint256[] calldata amounts
) external nonReentrant {
uint256 len = recipients.length;
if (len != amounts.length) {
revert LengthMismatch();
}
if (len == 0) {
revert EmptyRecipients();
}
for (uint256 i = 0; i < len; ) {
token.safeTransferFrom(msg.sender, recipients[i], amounts[i]);
unchecked {
++i;
}
}
}
/**
* @notice Batch-send the native currency to many recipients in one tx.
* @dev Forwards each `amounts[i]` with a low-level call. Any unused `msg.value` (the
* caller may over-send to cover rounding) is refunded to `msg.sender` at the end,
* so the contract never retains a balance.
* @param recipients Destination addresses.
* @param amounts Amount (in wei) for each corresponding recipient.
*/
function disperseEther(address[] calldata recipients, uint256[] calldata amounts)
external
payable
nonReentrant
{
uint256 len = recipients.length;
if (len != amounts.length) {
revert LengthMismatch();
}
if (len == 0) {
revert EmptyRecipients();
}
// Sum the requested amounts up front (checked add) and require the caller sent
// AT LEAST that much. This makes the refund a computed constant below, so a
// malicious recipient can NOT re-enter and drain `address(this).balance`.
uint256 total = 0;
for (uint256 i = 0; i < len; ) {
total += amounts[i];
unchecked {
++i;
}
}
if (total > msg.value) {
revert InsufficientValue(total, msg.value);
}
for (uint256 i = 0; i < len; ) {
(bool ok, ) = recipients[i].call{ value: amounts[i] }("");
if (!ok) {
revert EtherTransferFailed(recipients[i]);
}
unchecked {
++i;
}
}
// Refund EXACTLY the over-sent leftover (a computed constant, NOT
// address(this).balance) so re-entrancy can never sweep more than the caller's
// own dust. Skip entirely when the caller sent exactly `total`.
uint256 refund = msg.value - total;
if (refund > 0) {
(bool refunded, ) = msg.sender.call{ value: refund }("");
if (!refunded) {
revert EtherTransferFailed(msg.sender);
}
}
}
}
Chain explorer3108msChain node81ms