A modular, gas-efficient, and production-ready Merkle tree-based token airdrop system
Built with OpenZeppelin contracts β’ Supports fixed & variable amounts β’ CSV import β’ Full TypeScript tooling
- π³ Merkle Tree Verification β Gas-efficient claims using cryptographic proofs
- π° Fixed & Variable Amounts β Support for equal distribution or custom amounts per address
- β° Deadline Control β Optional claim deadline with extension capability
- π’ Max Claims Limit β Optional cap on total number of claims (FCFS)
- π CSV Import β Generate Merkle trees directly from CSV files
- π Security First β Built on battle-tested OpenZeppelin contracts
- π¦ Export Ready β Generate proof files ready for frontend integration
- π§ͺ Fully Tested β Comprehensive test suite included
- Architecture
- Installation
- Quick Start
- Contracts
- Merkle Tree Generation
- Deployment
- Testing
- Integration Guide
- Security
- Contributing
- License
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Airdrop System β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β ββββββββββββββββ ββββββββββββββββββββ β
β β Airdrop βββββΆβ AirdropDeadline β β
β β (Base) β β (+ deadline) β β
β ββββββββββββββββ ββββββββββββββββββββ β
β β β β
β βΌ β β
β ββββββββββββββββββββ β β
β β AirdropMaxClaims β β β
β β (+ max claims) βββββββββΌββββββββββββββββββββ β
β ββββββββββββββββββββ β β β
β βΌ βΌ β
β ββββββββββββββββββββββββββββββββββββ β
β β AirdropWithDeadlineAndMaxClaims β β
β β (all features combined) β β
β ββββββββββββββββββββββββββββββββββββ β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Clone the repository
git clone https://github.com/yourusername/lemonjet-airdrop.git
cd lemonjet-airdrop
# Install dependencies
npm install- Node.js 18+ (v20+ recommended)
- npm or yarn
Note: This project uses ESM modules (
"type": "module"in package.json)
# Create your airdrop list in data/airdrop-list.csv
npx hardhat run scripts/load-from-csv.tsnpx hardhat test# Update ignition/modules/parameters.json with your values
npx hardhat ignition deploy ignition/modules/Airdrop.ts --network sepoliaThe foundation for all airdrop variants. Features:
- Merkle proof verification for claims
- Support for both fixed and variable amount airdrops
- BitMap for gas-efficient claim tracking
- Owner can withdraw unclaimed tokens
- Owner can update Merkle root
// Fixed amount claim (amount set in constructor)
function claim(bytes32[] calldata proof, uint256 index) external;
// Variable amount claim (amount encoded in Merkle tree)
function claim(bytes32[] calldata proof, uint256 index, uint256 amount) external;Extends base with time-limited claiming:
// Claims revert after deadline
function claim(bytes32[] calldata proof, uint256 index) external;
// Owner can extend (not shorten) the deadline
function extendClaimDeadline(uint64 newDeadline) external;Extends base with a cap on total claims (first-come-first-served):
// Claims revert when claimsRemaining hits 0
function claim(bytes32[] calldata proof, uint256 index) external;
// Owner can adjust remaining claims
function setClaimsRemaining(uint256 newClaimsRemaining) external;Combines both deadline and max claims restrictions. Perfect for limited-time, limited-quantity airdrops.
The easiest way to generate a Merkle tree:
Step 1: Create your CSV file at data/airdrop-list.csv:
# For fixed amounts (everyone gets the same):
address
0x70997970C51812dc3A010C7d01b50e0d17dc79C8
0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC
# For variable amounts (include amount column):
address,amount
0x70997970C51812dc3A010C7d01b50e0d17dc79C8,100
0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC,200Step 2: If using variable amounts, update scripts/load-from-csv.ts:
const HAS_AMOUNTS = true; // Set to true for variable amountsStep 3: Generate the tree:
npx hardhat run scripts/load-from-csv.tsThis creates three files in ./data/:
airdrop-tree.jsonβ Full tree data (for restoration)airdrop-metadata.jsonβ Human-readable formatairdrop-proofs.jsonβ All proofs (for frontend)
Create a custom script (e.g., scripts/my-airdrop.ts):
import {
createFixedAmountEntries,
generateFixedAmountTree,
saveTree,
exportProofsToFile,
} from "./merkle/index.js";
const addresses = [
"0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
"0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC",
];
// Create tree
const entries = createFixedAmountEntries(addresses);
const tree = generateFixedAmountTree(entries);
console.log("Merkle Root:", tree.root);
// Save for deployment & frontend
saveTree(tree, "./data/tree.json");
exportProofsToFile(tree, "fixed", "./data/proofs.json");Then run with:
npx hardhat run scripts/my-airdrop.ts# Lookup proof for a specific address
npx hardhat run scripts/get-proof.tsOr programmatically (in a script under scripts/):
import { loadTree, getProofByAddress } from "./merkle/index.js";
const tree = loadTree("./data/airdrop-tree.json");
const result = getProofByAddress(tree, "0x70997970C51812dc3A010C7d01b50e0d17dc79C8");
if (result) {
console.log({
proof: result.proof,
index: result.index,
valid: result.valid,
});
}Edit ignition/modules/parameters.json:
{
"$global": {
"asset": "0xYourTokenAddress",
"merkleRoot": "0xYourMerkleRoot",
"claimAmount": "1000000000000000000",
"claimDeadline": "1735689600",
"claimsRemaining": "1000"
}
}Hardhat 3 uses the keystore for secure credential management:
# Set your Sepolia RPC URL
npx hardhat keystore set SEPOLIA_RPC_URL
# Set your deployer private key
npx hardhat keystore set SEPOLIA_PRIVATE_KEY
# Set Etherscan API key for verification
npx hardhat keystore set ETHERSCAN_API_KEYAlternatively, you can use environment variables (less secure):
export SEPOLIA_RPC_URL="https://sepolia.infura.io/v3/YOUR_KEY"
export SEPOLIA_PRIVATE_KEY="your_private_key"
export ETHERSCAN_API_KEY="your_etherscan_key"# Choose the contract variant you need:
# Basic airdrop
npx hardhat ignition deploy ignition/modules/Airdrop.ts --network sepolia
# With deadline
npx hardhat ignition deploy ignition/modules/AirdropWithDeadline.ts --network sepolia
# With max claims
npx hardhat ignition deploy ignition/modules/AirdropWithMaxClaims.ts --network sepolia
# With both deadline and max claims
npx hardhat ignition deploy ignition/modules/AirdropWithDeadlineAndMaxClaims.ts --network sepoliaAfter deployment, transfer tokens to the airdrop contract:
# Using cast (foundry)
cast send $TOKEN_ADDRESS "transfer(address,uint256)" $AIRDROP_ADDRESS $TOTAL_AMOUNT --private-key $PRIVATE_KEY# Basic verification
npx hardhat verify --network sepolia $AIRDROP_ADDRESS $TOKEN_ADDRESS $MERKLE_ROOT $CLAIM_AMOUNT
# Or use Ignition's built-in verification
npx hardhat ignition verify $DEPLOYMENT_ID --network sepolia# Run all tests
npx hardhat test
# Run with coverage
npx hardhat test --coverageThe project includes 24 comprehensive tests covering:
| Contract | Tests |
|---|---|
| Airdrop | Basic claim functionality, proof verification |
| AirdropWithDeadline | Deadline enforcement, extension logic |
| AirdropWithMaxClaims | Claims limit, decrementing, owner controls |
| AirdropWithDeadlineAndMaxClaims | Combined feature tests, edge cases |
The airdrop-proofs.json file is designed for easy frontend integration:
{
"root": "0x1234...",
"format": "fixed",
"proofs": {
"0x70997970C51812dc3A010C7d01b50e0d17dc79C8": {
"index": "0",
"proof": ["0xabc...", "0xdef..."]
}
}
}Example React hook:
import { useContractWrite } from 'wagmi';
import proofs from './data/airdrop-proofs.json';
function useClaim(address: string) {
const userProof = proofs.proofs[address];
const { write: claim } = useContractWrite({
address: AIRDROP_ADDRESS,
abi: AIRDROP_ABI,
functionName: 'claim',
args: [userProof.proof, BigInt(userProof.index)],
});
return { claim, eligible: !!userProof };
}// Check if address is in the airdrop
const isEligible = address in proofs.proofs;
// Check if already claimed (on-chain)
const isClaimed = await airdropContract.read.isClaimed([index]);
β οΈ This code has not been audited. Use at your own risk.
- β Uses OpenZeppelin's battle-tested contracts
- β BitMap for efficient double-claim prevention
- β Double-hashed leaves (prevents second preimage attacks)
- β Owner-only administrative functions
- β Zero-address validation
- Token Allowance: Ensure the contract holds enough tokens before users claim
- Merkle Root Updates: Changing the root doesn't reset claim status
- Deadline Extension: Can only extend, never shorten
- Gas Costs: Proof verification costs ~25-35k gas depending on tree depth
We welcome contributions! Here's how to get started:
# Fork and clone the repository
git clone https://github.com/yourusername/lemonjet-airdrop.git
cd lemonjet-airdrop
# Install dependencies
npm install
# Create a branch for your feature
git checkout -b feature/your-feature-name- We use TypeScript for all scripts
- Solidity follows the Solidity Style Guide
- Run linting before commits:
npx hardhat compile
npx hardhat test- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
- Provide a clear description of the changes
- Include tests for new functionality
- Update documentation if needed
- Ensure all tests pass
- Keep commits atomic and well-described
Found a bug? Please open an issue with:
- A clear title and description
- Steps to reproduce
- Expected vs actual behavior
- Solidity/Hardhat versions
lemonjet-airdrop/
βββ contracts/ # Solidity contracts
β βββ Airdrop.sol
β βββ AirdropWithDeadline.sol
β βββ AirdropWithMaxClaims.sol
β βββ AirdropWithDeadlineAndMaxClaims.sol
β βββ Asset.sol # Test ERC20 token
βββ scripts/
β βββ merkle/ # Merkle tree utilities
β β βββ index.ts # Main exports
β β βββ generate.ts # Tree generation
β β βββ load.ts # Tree loading
β β βββ save.ts # Tree persistence
β β βββ proof.ts # Proof generation
β β βββ types.ts # TypeScript types
β βββ generate-airdrop-tree.ts # Example: full workflow demo
β βββ load-from-csv.ts # Generate tree from CSV
β βββ get-proof.ts # Lookup proof for address
βββ test/ # Test files
βββ ignition/
β βββ modules/ # Deployment modules
βββ data/ # Generated Merkle trees
βββ hardhat.config.ts
This project is licensed under the MIT License β see the LICENSE file for details.
Built with π by LemonJet