Mass Token Distribution System Development
Mass token distribution is not just a loop with transfer() in a smart contract. When recipients number in tens of thousands and each transaction costs gas, improper architecture turns airdrop into losing proposition for the project and nightmare for users who can't get tokens for hours due to network congestion.
The task of mass distribution system: deliver tokens to maximum number of addresses with minimal gas costs, with protection against abuse and with audit trail for every payout.
Push vs Pull: Key Architectural Choice
First decision — who initiates token transfer.
Push (project → user): project sends tokens to addresses directly. Simple for users, expensive for project. With 50,000 recipients and standard transfer (21,000 gas) on Ethereum mainnet — millions of dollars in gas at any network load.
Pull (user claims): recipient calls claim() themselves. Gas costs fall on user. Additional barrier to entry, but economically more fair.
Merkle drop (pull with proof): gold standard for large airdrops. Recipient list published as Merkle tree, only root stored on-chain. User provides proof of belonging to the list.
contract MerkleDistributor {
address public immutable token;
bytes32 public immutable merkleRoot;
// Bitmap for tracking claimed without storing address mappings
mapping(uint256 => uint256) private claimedBitMap;
event Claimed(uint256 indexed index, address indexed account, uint256 amount);
constructor(address token_, bytes32 merkleRoot_) {
token = token_;
merkleRoot = merkleRoot_;
}
function isClaimed(uint256 index) public view returns (bool) {
uint256 claimedWordIndex = index / 256;
uint256 claimedBitIndex = index % 256;
uint256 claimedWord = claimedBitMap[claimedWordIndex];
uint256 mask = (1 << claimedBitIndex);
return claimedWord & mask == mask;
}
function claim(
uint256 index,
address account,
uint256 amount,
bytes32[] calldata merkleProof
) external {
require(!isClaimed(index), "Already claimed");
bytes32 node = keccak256(abi.encodePacked(index, account, amount));
require(
MerkleProof.verify(merkleProof, merkleRoot, node),
"Invalid proof"
);
_setClaimed(index);
IERC20(token).transfer(account, amount);
emit Claimed(index, account, amount);
}
function _setClaimed(uint256 index) private {
uint256 claimedWordIndex = index / 256;
uint256 claimedBitIndex = index % 256;
claimedBitMap[claimedWordIndex] |= (1 << claimedBitIndex);
}
}
Bitmap instead of mapping(address => bool) saves significant storage, especially with hundreds of thousands of recipients.
Merkle Tree Generation and Verification
Off-chain tree generation is critical. Error in recipient list means unable to get tokens or double payouts.
const { MerkleTree } = require('merkletreejs');
const keccak256 = require('keccak256');
const { ethers } = require('ethers');
function generateMerkleTree(recipients) {
// recipients: [{ index, address, amount }, ...]
const leaves = recipients.map(({ index, address, amount }) => {
return ethers.solidityPackedKeccak256(
['uint256', 'address', 'uint256'],
[index, address, amount]
);
});
const tree = new MerkleTree(leaves, keccak256, { sortPairs: true });
const root = tree.getHexRoot();
// Generate proofs for each recipient
const proofs = recipients.map(({ index, address, amount }, i) => ({
index,
address,
amount: amount.toString(),
proof: tree.getHexProof(leaves[i])
}));
return { root, proofs };
}
// Publish proofs via IPFS or API
// User gets their proof and calls claim()
Protection Against Sybil and Abuse
Mass distribution without protection is invitation to sybil attack. Typical scenario: attacker creates thousands of wallets, meets minimum requirements with each, and gets disproportionate share of airdrop.
Protection levels:
On-chain criteria: consider only wallets with real history — first transaction before certain date, fee volume above threshold, interaction with specific protocols.
Off-chain verification: Gitcoin Passport (identity proof aggregator), Proof of Humanity, verification through social proof (Twitter/Github account linked to address). Integrated via signature: user signs message with their wallet confirming ownership.
Tiered airdrop: different amounts for different activity categories. Early users get more than those who started a week before snapshot.
| Category | Criteria | Multiplier |
|---|---|---|
| OG users | First transaction > 12 months ago | 3x |
| Active users | > 10 transactions in last 6 months | 2x |
| Regular users | At least 1 transaction in last 3 months | 1x |
| Snapshot hunters | Transaction in last 2 weeks | 0.5x |
Batch Distribution for Push Scenarios
When push is necessary (e.g., compensating exploit victims, retroactive rewards for verified addresses), use batch transfer contracts.
Simple pattern: multicall with multiple transfer() in one transaction. More efficient: Disperse.app pattern — one contract call that iterates over recipient array.
function disperseToken(
IERC20 token,
address[] calldata recipients,
uint256[] calldata amounts
) external {
uint256 total = 0;
for (uint256 i = 0; i < amounts.length; i++) {
total += amounts[i];
}
token.transferFrom(msg.sender, address(this), total);
for (uint256 i = 0; i < recipients.length; i++) {
token.transfer(recipients[i], amounts[i]);
}
}
Optimal batch size — 200–500 addresses per transaction depending on network. Exceeding block gas limit causes reverts.
Vesting on Top of Airdrop
For teams and investors often combine mass distribution with vesting: tokens are claimed but not immediately available, unlocked by schedule.
Implementation: upon claim user doesn't receive tokens directly, personal VestingWallet is deployed (or entry created in shared vesting contract) with schedule. More gas-expensive, but provides complete flexibility for individual vesting schedules.
Monitoring and Analytics
After launching airdrop track: percentage claimed from total pool, how many addresses claimed in first 24/48/72 hours, addresses immediately selling (on-chain via DEX events), average token retention after 30 days.
These metrics directly relate to audience quality and allow adjusting next airdrop.
Development Timeline
Merkle distributor with off-chain tree generation and basic claim frontend — 3–4 weeks. Full system with sybil protection, tiered distribution, vesting integration and analytics dashboard — 2–3 months.







