A on-chain batch transfer executor for ERC-20 tokens and native chain currency. One contract, one transaction: many recipients receive funds atomically from a single sender. Built for Web3 Kit / Nexus-style products that need explorer-friendly entry points, operational guardrails, and predictable gas behavior.
- Single-transaction batch payouts — Send to many recipients in one call instead of many separate transactions
- ERC-20 and native support — Distribute any standard token or chain-native currency (ETH, BNB, etc.)
- Atomic execution — The entire batch succeeds or reverts; no partial token transfers
- Explorer-friendly aliases — Public entry points named Airdrop, MultiSender, Disperse, and Transfer for clear block explorer labels
- Configurable batch limits —
maxRecipientscap set at deploy and adjustable by the owner - Emergency pause — Owner can pause all batch operations when needed
- Reentrancy protection — Every external batch function is guarded against reentrancy
- Safe token transfers — Uses OpenZeppelin SafeERC20 for reliable ERC-20 handling
- Rich on-chain events — Emits indexed events for token and native batches for easy indexing and analytics
- Multi-network ready — Hardhat setup for Ethereum (mainnet, Sepolia) and BSC (mainnet, testnet)
- Deploy and verify tooling — Scripts with constructor-aware verify command output
- Test coverage — Automated tests for token batch, native batch, validation, and recipient limits
Copy env.example to .env and fill in your RPC URLs, private key, and API keys before deploy or verify.
npm installnpm run compilenpm testGas report during tests:
set REPORT_GAS=true && npm testOn macOS / Linux:
REPORT_GAS=true npm testBSC testnet (npm script):
npm run deploy:bscTestnetBSC mainnet (npm script):
npm run deploy:bscMainnetOther networks (Hardhat CLI):
npx hardhat run scripts/deploy.js --network sepolia
npx hardhat run scripts/deploy.js --network eth
npx hardhat run scripts/deploy.js --network bsc
npx hardhat run scripts/deploy.js --network bscTestnetAfter deploy, the script prints a ready-to-run verify command with the contract address and MAX_RECIPIENTS constructor argument.
Replace <network>, <contract_address>, and <max_recipients> with your deploy values. MAX_RECIPIENTS defaults to 200 if not set in .env.
BSC testnet / mainnet:
npx hardhat verify --network bscTestnet <contract_address> <max_recipients>
npx hardhat verify --network bsc <contract_address> <max_recipients>Example:
npx hardhat verify --network bscTestnet 0xYourContractAddress 200Ethereum (requires ETHERSCAN_API_KEY in hardhat.config.js):
npx hardhat verify --network sepolia <contract_address> <max_recipients>
npx hardhat verify --network eth <contract_address> <max_recipients>npx hardhat nodeThis document is the project brain: purpose, architecture, behavior, operations, and design rationale — without implementation code.
Distributing tokens or native currency to many addresses usually means many separate transactions. That costs more gas, increases failure surface, and complicates UX for airdrops, payroll, rewards, and treasury disbursements.
BatchExecutor is a single smart contract that:
- Accepts one call with parallel arrays of recipients and amounts
- Moves ERC-20 tokens via
transferFrom(sender must approve first) - Moves native currency (BNB, ETH, etc.) via a single
msg.valuethat must equal the sum of amounts - Enforces batch size limits, pause controls, and reentrancy protection
- Exposes multiple public function aliases so block explorers and frontends can show familiar names (Airdrop, MultiSender, Disperse, Transfer)
- No token minting, staking, or vesting
- No fee collection or routing logic inside the contract
- No off-chain indexing service (events are emitted for external indexers)
- MockToken exists only for local testing, not production deployment
flowchart TB
subgraph Clients
UI[Web3 Kit / Nexus UI]
Script[Deploy & ops scripts]
Wallet[User wallet]
end
subgraph OffChain["Off-chain layer"]
RPC[RPC providers]
Env[Environment config]
Explorer[Block explorers]
end
subgraph OnChain["On-chain layer"]
BE[BatchExecutor]
ERC20[ERC-20 tokens]
Native[Native currency]
end
UI --> Wallet
Wallet --> BE
Script --> RPC
RPC --> BE
BE --> ERC20
BE --> Native
BE --> Explorer
Env --> Script
Env --> RPC
| Principle | How it is applied |
|---|---|
| Single responsibility | Contract only batches transfers; no business logic beyond validation and safety |
| Fail fast | Invalid input reverts the entire batch — no partial silent failures for token paths |
| Explicit trust | Token sends pull from msg.sender; native sends require exact msg.value |
| Operational control | Owner can pause, adjust maxRecipients, but cannot seize user funds |
| Explorer ergonomics | Duplicate entry-point names map to the same internal execution paths |
classDiagram
class BatchExecutor {
+uint256 maxRecipients
+setMaxRecipients()
+pause()
+unpause()
+Airdrop()
+MultiSender()
+Disperse()
+Transfer()
}
class Ownable {
+owner()
}
class Pausable {
+paused()
}
class ReentrancyGuard {
}
BatchExecutor --|> Ownable
BatchExecutor --|> Pausable
BatchExecutor --|> ReentrancyGuard
Inherited capabilities (OpenZeppelin)
- Ownable — deployment account becomes owner; only owner updates limits and pause state
- Pausable — global emergency stop; all batch entry points respect
whenNotPaused - ReentrancyGuard —
nonReentranton every external batch function - SafeERC20 — safe token transfers with standard-compliant error handling
Configurable state
maxRecipients— upper bound on array length per batch; set at deploy time, adjustable by owner
Events (indexer / analytics surface)
TokenBatchExecuted— sender, token, recipient count, total amountNativeBatchExecuted— sender, recipient count, total amountMaxRecipientsUpdated— previous and new limit
All aliases are thin wrappers over two private execution paths. Naming supports block explorer readability and product vocabulary — not different behavior.
flowchart LR
subgraph TokenBatch["Token batch path"]
T1[Airdrop]
T2[MultiSender]
T3[Disperse token]
T4[Transfer token]
TE[_executeTokenBatch]
end
subgraph NativeBatch["Native batch path"]
N1[Disperse native]
N2[Transfer native]
NE[_executeNativeBatch]
end
T1 --> TE
T2 --> TE
T3 --> TE
T4 --> TE
N1 --> NE
N2 --> NE
| Entry name | Asset type | Sender funding model |
|---|---|---|
| Airdrop | ERC-20 | Approve contract, then call |
| MultiSender | ERC-20 | Same as Airdrop |
| Disperse (token overload) | ERC-20 | Same as Airdrop |
| Transfer (token overload) | ERC-20 | Same as Airdrop |
| Disperse (native overload) | Native | Send msg.value = sum of amounts |
| Transfer (native overload) | Native | Send msg.value = sum of amounts |
sequenceDiagram
participant User
participant Token as ERC-20 token
participant BE as BatchExecutor
User->>Token: approve(BatchExecutor, total)
User->>BE: batch call(recipients[], amounts[])
BE->>BE: validate lengths, limits, addresses
loop each recipient
BE->>Token: transferFrom(User, recipient, amount)
end
BE-->>User: TokenBatchExecuted event
Behavioral rules
tokenaddress must be non-zerorecipients.lengthmust equalamounts.length- Length must be between 1 and
maxRecipientsinclusive - No zero-address recipients
- Entire transaction reverts if any
transferFromfails (insufficient allowance, balance, etc.) - Zero amounts are allowed per recipient (still iterates; no skip logic on token path)
sequenceDiagram
participant User
participant BE as BatchExecutor
participant R as Recipients
User->>BE: batch call + msg.value
BE->>BE: sum amounts, compare to msg.value
alt msg.value mismatch
BE-->>User: revert InvalidNativeValue
end
loop each recipient with amount > 0
BE->>R: native transfer
alt transfer fails
BE-->>User: revert NativeTransferFailed
end
end
BE-->>User: NativeBatchExecuted event
Behavioral rules
msg.valuemust exactly equal the sum of all amounts (no overpay, no underpay)- Zero-amount entries are skipped for the actual send loop (gas optimization)
- Failed low-level native transfers revert with recipient and amount context
Custom errors (gas-efficient, explicit) cover every guard:
| Error | Trigger |
|---|---|
| InvalidLength | Recipients and amounts arrays differ in length |
| ZeroRecipients | Empty batch or zero maxRecipients |
| TooManyRecipients | Batch length exceeds maxRecipients |
| ZeroAddressRecipient | Any recipient is address(0) |
| ZeroTokenAddress | Token address is zero on ERC-20 path |
| InvalidNativeValue | msg.value ≠ sum of native amounts |
| NativeTransferFailed | Low-level send to recipient failed |
Atomicity: one failed validation or transfer reverts the whole transaction. No state is partially committed.
flowchart TB
subgraph ThreatMitigation
R[ReentrancyGuard on all externals]
P[Pausable emergency stop]
S[SafeERC20 for token calls]
V[Strict input validation]
O[Owner-only admin functions]
end
subgraph TrustBoundaries
Sender[Caller msg.sender]
Owner[Contract owner]
Recipients[Recipient addresses]
end
Sender -->|funds only their own tokens/native| BE[BatchExecutor]
Owner -->|cannot withdraw user balances| BE
BE -->|no arbitrary call/delegate| Recipients
| Concern | Mitigation |
|---|---|
| Reentrancy during native sends | nonReentrant modifier |
| Malicious token contracts | SafeERC20; still subject to token-specific quirks |
| Admin abuse | Owner can pause or change limits only — no fund withdrawal hook |
| Oversized batches / griefing | maxRecipients cap |
| Wrong native payment | Exact msg.value check before any sends |
| Explorer / UX confusion | Aliases share one implementation — no hidden code paths |
Residual risks (documented, not hidden)
- Owner can pause the contract (availability risk)
- Owner can lower
maxRecipients(operational constraint change) - Smart contract wallets or contracts that reject native transfers may cause
NativeTransferFailed - ERC-20 tokens with fees-on-transfer or rebasing behavior are not specially handled
flowchart LR
subgraph Repo
Contracts[contracts/]
Scripts[scripts/]
Tests[test/]
Config[hardhat.config]
end
subgraph Runtime
HH[Hardhat]
Ethers[ethers v6]
OZ[OpenZeppelin contracts]
end
subgraph Networks
Local[hardhat local]
ETH[Ethereum mainnet / Sepolia]
BSC[BSC mainnet / testnet]
end
Contracts --> HH
Scripts --> HH
Tests --> HH
Config --> Networks
HH --> Ethers
Contracts --> OZ
| Area | Role |
|---|---|
| contracts/BatchExecutor.sol | Production contract |
| contracts/MockToken.sol | Test-only ERC-20 fixture |
| scripts/deploy.js | Deploy BatchExecutor with configurable maxRecipients |
| test/test.js | Behavioral coverage for token batch, native batch, validation, limits |
| hardhat.config.js | Solidity 0.8.28, optimizer, network definitions, gas reporter, explorer keys |
| env.example | Template for secrets and RPC URLs — copy to .env |
| Network key | Chain | Typical use |
|---|---|---|
| hardhat | Local | Development and automated tests |
| sepolia | Ethereum testnet | Pre-mainnet Ethereum testing |
| mainnet | Ethereum | Production Ethereum |
| bscTestnet | BNB Smart Chain testnet | Pre-mainnet BSC testing |
| bscMainnet | BNB Smart Chain | Production BSC |
Package scripts ship for BSC deploy targets; other networks are configured and usable via Hardhat CLI with the matching --network flag.
Configuration is loaded from .env (never committed). See env.example for the full variable list.
| Variable group | Purpose |
|---|---|
| PRIVATE_KEY | Deployer / transaction signer for live networks |
| RPC URLs | Per-network JSON-RPC endpoints |
| BSCSCAN_API_KEY | Contract verification on BSC explorers |
| MAX_RECIPIENTS | Initial on-chain batch limit at deploy (default 200) |
- Solidity 0.8.28 with optimizer enabled (200 runs) — balance of deploy size and runtime gas
- hardhat-gas-reporter — gas visibility during tests
- @nomicfoundation/hardhat-toolbox — compile, test, verify, and network tooling bundle
sequenceDiagram
participant Ops as Operator
participant Env as .env
participant Script as deploy.js
participant Chain as Target network
Ops->>Env: configure PRIVATE_KEY, RPC, MAX_RECIPIENTS
Ops->>Script: hardhat run deploy.js --network <target>
Script->>Chain: deploy BatchExecutor(initialMaxRecipients)
Chain-->>Ops: contract address + tx hash logged
Deploy-time decisions
- Deployer becomes owner (Ownable
msg.senderat construction) initialMaxRecipientsis immutable intent at launch but owner can update later viasetMaxRecipients- No proxy / upgrade pattern — deployed bytecode is final for that address
Post-deploy checklist
- Verify contract on the target block explorer
- Record deployed address and initial
maxRecipients - Communicate address to frontend / Web3 Kit integration
- Confirm pause policy and owner key custody
Token batch
- User holds sufficient token balance
- User approves BatchExecutor for at least the sum of all amounts in the batch
- User submits batch transaction through any alias (Airdrop, MultiSender, Disperse, Transfer)
Native batch
- User computes total = sum of amounts
- User submits batch transaction with
value= total exactly
Use the alias that best matches user-facing language. Behavior is identical within token vs native families. Overloaded names (Disperse, Transfer) are distinguished by argument types: token address present → ERC-20 path; only arrays → native path.
Subscribe to TokenBatchExecuted and NativeBatchExecuted for analytics, receipts, and activity feeds. Indexed fields include sender and (for tokens) token address.
Tests run on the local Hardhat network with a deployed BatchExecutor and MockToken.
| Scenario | Expected outcome |
|---|---|
| Valid ERC-20 batch | Balances updated; event emitted with correct aggregates |
| Valid native batch | Event emitted; correct total |
| Length mismatch | Revert InvalidLength |
Wrong msg.value on native |
Revert InvalidNativeValue |
Batch over maxRecipients |
Revert TooManyRecipients after owner lowers limit |
Tests assert behavior and events, not implementation internals — aligned with how integrators experience the contract.
See the Commands section at the top of this README for setup, compile, test, deploy, verify, and local node usage.
Ensure .env is populated from env.example before any network deploy or contract verification.
| Decision | Rationale |
|---|---|
| Multiple public aliases | Explorer and marketing-friendly labels without duplicate logic |
| Private execution helpers | One validation and transfer implementation; reduces audit surface |
Owner-adjustable maxRecipients |
Ops can react to gas limits or abuse without redeploying |
| Pausable | Emergency stop for incidents; explicit tradeoff vs immutability |
| No upgrade proxy | Simplicity and trust minimization for a transfer utility |
Exact msg.value for native |
Prevents stuck ETH in contract from overpayment |
| Custom errors over strings | Lower gas, clearer revert data for tooling |
| Default 200 max recipients | Sensible batch size for deploy; tunable per environment |
| Term | Meaning |
|---|---|
| Batch | Single transaction distributing to N recipients |
| Alias | Public function name sharing the same execution path |
| Native | Chain native currency (ETH, BNB, etc.) |
| Owner | Deployer-controlled admin role (pause, limits) |
| Sender | msg.sender — the account whose tokens or native currency is distributed |
| maxRecipients | On-chain cap on array length per call |
Update this README when:
- New networks or deploy scripts are added
- Public API aliases change
- Security assumptions or owner capabilities change
- Integration requirements for Web3 Kit / Nexus evolve
This file is the canonical architecture and behavior reference. Implementation details live in the contracts and scripts; this brain explains what the system is, how it is shaped, and why it is shaped that way.