- Merkle Airdrop with EIP-712 Signatures
A gas-efficient airdrop distribution system using Merkle trees to verify claim eligibility and EIP-712 typed signatures for secure claim authorization. This implementation allows users to claim pre-allocated airdrop tokens with minimal on-chain verification overhead.
- Merkle Tree Verification: Gas-efficient eligibility verification using Merkle proofs
- EIP-712 Signatures: Typed signature scheme for secure claim authorization
- Immutable Token Contract: Secure ERC20 token for airdrop distribution
- Claim Tracking: Prevents duplicate claims with one-time claim verification per address
- Multi-network Support: Deployable on Ethereum, Arbitrum, and Base networks
Tech Stack:
- Solidity 0.8.33
- Foundry (Forge for building and testing)
- forge-std version (v1.11.0)
- OpenZeppelin Contracts (ERC20, EIP-712, Merkle proof utilities, ECDSA signature recovery)
- openzeppelin-contracts version (v5.5.0)
- Murky (Merkle tree generation for testing)
- murky version (v0.1.0)
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Whitelisted Users/EOAs β
ββββββββββββ¬ββββββββββββββββββββββββββββββ¬ββββββββββββββββββ
β β
β Direct claim β Authorize signature
β with signature β (delegate claim)
β βΌ
β ββββββββββββββββββββββββββββ
β β Authorized Claimer β
β β (Non-whitelisted EOA) β
β ββββββββββ¬ββββββββββββββββββ
β β
β claim(address, β claim(address,
β amount, proof, β amount, proof,
β v, r, s) β v, r, s)
β β
βββββββββββββββ¬ββββββββββββββββ
βΌ
ββββββββββββββββββββββββββββββββββββββββββββ
β β
β MerkleAirdrop Contract β
β β
β ββββββββββββββββββββ ββββββββββββββββββ β
β β Merkle Root β β EIP-712 Domain β β
β β (Eligibility) β β(Signature Ver) β β
β ββββββββββββββββββββ ββββββββββββββββββ β
β β
β ββββββββββββββββββββββββββββββββββββββ β
β β Claim Status Tracking (per address)β β
β β(Prevents duplicate claims) β β
β ββββββββββββββββββββββββββββββββββββββ β
β β
βββββββββββββββββ¬βββββββββββββββββββββββββββ
β safeTransfer()
β
βΌ
βββββββββββββββββββββββββββ
β AirdropToken (ERC20) β
β Token Distribution β
βββββββββββββββββββββββββββ
Repository Structure:
merkle-airdrop-eip712/
βββ src/
β βββ AirdropToken.sol # ERC20 token for airdrop
β βββ MerkleAirdrop.sol # Core airdrop claim contract with EIP-712
βββ script/
β βββ Deploy.s.sol # Deployment script
β βββ Interactions.s.sol # Claim interaction scripts
β βββ GenerateInput.s.sol # Generate merkle tree input data
β βββ MerkleBuilder.s.sol # Build merkle tree from input
β βββ HelperConfig.s.sol # Network configuration
β βββ SplitSignature.s.sol # Split full signature into v,r,s
β βββ target/ # Generated merkle tree outputs
βββ test/
β βββ unit/
β β βββ MerkleBuilderTest.t.sol # Merkle tree generation tests
β βββ integration/
β βββ DeployTest.t.sol # Deployment tests
β βββ MerkleAirdropTest.t.sol # Airdrop claim functionality tests
β βββ InteractionsTest.t.sol # Full integration tests
βββ lib/ # Dependencies
βββ foundry.toml # Foundry configuration
βββ Makefile # Convenient make targets
βββ README.md # This file
git clone https://github.com/0xGearhart/merkle-airdrop-eip712
cd merkle-airdrop-eip712
make-
Copy the environment template:
cp .env.example .env
-
Configure your
.envfile:ETH_SEPOLIA_RPC_URL=your_sepolia_rpc_url_here ETH_MAINNET_RPC_URL=your_mainnet_rpc_url_here ARB_SEPOLIA_RPC_URL=your_arbitrum_sepolia_rpc_url_here ARB_MAINNET_RPC_URL=your_arbitrum_mainnet_rpc_url_here BASE_SEPOLIA_RPC_URL=your_base_sepolia_rpc_url_here BASE_MAINNET_RPC_URL=your_base_mainnet_rpc_url_here ETHERSCAN_API_KEY=your_etherscan_api_key_here DEFAULT_KEY_ADDRESS=public_address_of_your_encrypted_private_key_here SECONDARY_ADDRESS=secondary_address_for_whitelisting
-
Get testnet ETH:
- Ethereum Sepolia: cloud.google.com/application/web3/faucet/ethereum/sepolia
- Base Sepolia & Arbitrum Sepolia (requires mainnet Chainlink balance): faucets.chain.link
-
Configure Makefile
- Change account name in Makefile to the name of your desired encrypted key
- Change
--account defaultKeyto--account <YOUR_ENCRYPTED_KEY_NAME> - Check encrypted key names stored locally with:
cast wallet list
- If no encrypted keys found, encrypt private key to be used securely within foundry:
cast wallet import <account_name> --interactive
- Never commit your
.envfile - Never use your mainnet private key for testing
- Use a separate wallet with only testnet funds
Compile the contracts:
forge buildRun the test suite:
forge testRun tests with verbosity:
forge test -vvvRun specific test:
forge test --mt testFunctionNameGenerate coverage report:
forge coverageCreate test coverage report and save to .txt file:
make coverage-reportStart a local Anvil node:
make anvilDeploy to local node (in another terminal):
make deployGenerate Merkle Tree: Build the merkle tree from input data and generate the root:
make merkleGet Claim Digest: Get the EIP-712 typed hash digest for signing:
make get-digestSign Digest: Sign the digest with your private key:
make sign-digestClaim Airdrop (Streamlined): Automatically create digest, sign, and claim in one script:
make claim-airdropClaim with Full Signature: Use a pre-generated full signature (requires splitting first):
make claim-airdrop-with-full-sigSplit Full Signature: Split a full signature into v, r, and s components:
make split-signatureDeploy to Sepolia:
make deploy ARGS="--network eth sepolia"Deploy to Arbitrum Sepolia:
make deploy ARGS="--network arb sepolia"Deploy to Base Sepolia:
make deploy ARGS="--network base sepolia"Or using forge directly:
forge script script/Deploy.s.sol:Deploy --rpc-url $ETH_SEPOLIA_RPC_URL --account defaultKey --broadcast --verify --etherscan-api-key $ETHERSCAN_API_KEY -vvvvIf automatic verification fails:
forge verify-contract <CONTRACT_ADDRESS> src/MerkleAirdrop.sol:MerkleAirdrop --chain-id 11155111 --etherscan-api-key $ETHERSCAN_API_KEY| Network | MerkleAirdrop Address | AirdropToken Address | Block |
|---|---|---|---|
| Arbitrum Sepolia | 0x0292d5E5A58a1BE1603a6fAb2D893eb1b6039D6F |
0x04CD94DE2733Bff4A359c3D595573F479430cac9 |
View on Arbiscan |
For production use, consider:
- Professional security audit
- Bug bounty program
- Gradual rollout with monitoring
This protocol does not implement role-based access control through OpenZeppelin's AccessControl. The contracts follow a stateless, permissionless design:
MerkleAirdrop Contract:
- No Owner: The contract is deployed without an owner. Once deployed, it operates autonomously with no administrative functions.
- Permissionless Claims: Any address with a valid Merkle proof and corresponding EIP-712 signature can claim their airdrop.
- One-Time Claims Per Address: Uses
s_hasClaimedmapping to prevent duplicate claims, but does not restrict who can call the function.
AirdropToken Contract:
- Initial Minter: The deployer of the contract receives the initial token supply upon deployment.
- Standard ERC20: No special roles; functions follow standard ERC20 behavior (transfer, approve, etc.).
- Tokens Transferred to MerkleAirdrop: All airdrop tokens are transferred to the
MerkleAirdropcontract during deployment for distribution.
Key Security Properties:
- β Immutable Parameters: Merkle root and token address are immutable once set
- β EIP-712 Compliance: Signature verification uses standardized typed data hashing
- β Gas Optimization: Merkle proofs minimize on-chain computation
β οΈ No Recovery: Once tokens are in theMerkleAirdropcontract, unclaimed tokens cannot be withdrawn (by design)β οΈ Whitelist Immutability: The Merkle root (whitelist) cannot be updated after deployment
Centralization Risks:
- Whitelist Generation: The Merkle tree whitelist is generated off-chain. A malicious whitelist could be deployed, but users can verify their own eligibility before claiming.
- Signature Verification: Claims require valid EIP-712 signatures. If the signer's private key is compromised, unauthorized claims are possible for addresses in the Merkle proof.
Dependencies:
- OpenZeppelin Contracts: Uses audited libraries for ERC20, EIP-712, ECDSA, and Merkle proof verification
- Murky: External library for Merkle tree generation in tests
- Merkle Root Immutability: Cannot update the whitelist after deployment. A new contract must be deployed to change eligible addresses.
- No Batch Claims: Users can claim their own airdrop, or on behalf of others with their signature.
- Merkle Leaf Hashing: Current implementation uses
keccak256(bytes.concat(keccak256(abi.encode(account, amount))))which is less gas-efficient than assembly-based methods. Consider using Solady'sEfficientHashLibfor production. - No Claim Deadline: Users can claim their airdrop at any time after deployment. Consider adding a deadline in production environments.
| Function | Description | Optimizations |
|---|---|---|
claim() |
Main airdrop claim function | Uses Merkle proofs (log n verification), EIP-712 signature verification, one-time claim tracking |
getDigest() |
Returns EIP-712 typed data hash | Lazy evaluation, no storage reads |
getClaimStatus() |
Check if address has claimed | Single storage read |
Key Gas Optimizations Implemented:
- Merkle Trees: Instead of storing all eligible addresses (O(n) storage), uses Merkle root (O(1) storage) with O(log n) verification
- Immutable Variables: Token and Merkle root use
immutablekeyword, reducing SSTOREs and optimizing reads - SafeERC20: Uses OpenZeppelin's
SafeERC20for safe transfers with minimal overhead - No Loop-Based Operations: Merkle proof verification is non-iterative
- Single Storage Slot for Claims:
s_hasClaimedmapping efficiently tracks claimed status
Gas Report:
Generate a gas report for this project:
make gas-reportGenerate gas snapshot:
forge snapshotCompare gas changes:
forge snapshot --diffContributions are welcome! Please follow these steps:
- 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
This project is licensed under the MIT License - see the LICENSE file for details.
Disclaimer: This software is provided "as is", without warranty of any kind. Use at your own risk.
Built with Foundry