Claim Contract Development for Token Distribution

Claim Contract Development for Token Distribution You deployed an airdrop, but the gas to deploy a list of 10,000 addresses ate half your budget. Or worse — someone found a way to claim tokens twice with the same proof. These issues appear in production all the time. A claim contract is a standar

Blockchain Development Services

Frequently Asked Questions

Latest works

  • image_website-b2b-advance_0.webp
    B2B ADVANCE company website development
    1441
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1301
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    998
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1267
  • image_logo-advance_0.webp
    B2B Advance company logo design
    713
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    1003

Claim Contract Development for Token Distribution

You deployed an airdrop, but the gas to deploy a list of 10,000 addresses ate half your budget. Or worse — someone found a way to claim tokens twice with the same proof. These issues appear in production all the time. A claim contract is a standard tool, but its implementation requires precision. We, a team with over 5 years of blockchain experience, provide turnkey claim contract creation with security guarantees and gas optimization. In this article, we'll break down why the Merkle tree is the industry standard and how to avoid costly mistakes.

Why Merkle Tree Over On-Chain List?

Parameter On-chain whitelist Merkle tree
Gas on deploy (10k addresses) ~0.5 ETH ~0.01 ETH
Gas on claim ~50k (SLOAD) ~30k (SLOAD + 2x SHA3)
Storage 10k storage slots 1 bytes32
Updateability requires contract migration root change

Gas savings on deployment up to 90% — for large projects this translates to thousands of dollars.

How to Implement Merkle-Based Claim

Building the Tree (Off-Chain)

import { StandardMerkleTree } from "@openzeppelin/merkle-tree"; // Leaves: [address, amount] const values = [ ["0xAddress1...", ethers.parseEther("100")], ["0xAddress2...", ethers.parseEther("250")], // ... ]; const tree = StandardMerkleTree.of(values, ["address", "uint256"]); console.log("Merkle Root:", tree.root); // Save the tree for proof generation fs.writeFileSync("tree.json", JSON.stringify(tree.dump())); // For a specific address, generate the proof for (const [i, v] of tree.entries()) { if (v[0] === "0xAddress1...") { const proof = tree.getProof(i); console.log("Proof:", proof); } } 

Contract

// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; contract MerkleClaim is Ownable { IERC20 public immutable token; bytes32 public immutable merkleRoot; uint256 public immutable claimDeadline; // Packed bitmap for gas efficiency instead of mapping(address => bool) mapping(uint256 => uint256) private claimedBitMap; event Claimed(address indexed account, uint256 amount, uint256 index); constructor( address _token, bytes32 _merkleRoot, uint256 _claimWindowDays ) Ownable(msg.sender) { token = IERC20(_token); merkleRoot = _merkleRoot; claimDeadline = block.timestamp + (_claimWindowDays * 1 days); } 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 _setClaimed(uint256 index) private { uint256 claimedWordIndex = index / 256; uint256 claimedBitIndex = index % 256; claimedBitMap[claimedWordIndex] = claimedBitMap[claimedWordIndex] | (1 << claimedBitIndex); } function claim( uint256 index, address account, uint256 amount, bytes32[] calldata merkleProof ) external { require(block.timestamp <= claimDeadline, "Claim period ended"); require(!isClaimed(index), "Already claimed"); bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(index, account, amount)))); require(MerkleProof.verify(merkleProof, merkleRoot, leaf), "Invalid proof"); _setClaimed(index); token.transfer(account, amount); emit Claimed(account, amount, index); } // Recover unclaimed tokens after deadline function recoverUnclaimed() external onlyOwner { require(block.timestamp > claimDeadline, "Claim period active"); uint256 balance = token.balanceOf(address(this)); token.transfer(owner(), balance); } } 

Using a bitmap instead of mapping(address => bool) is an important optimization. One storage slot (32 bytes) stores 256 flags. For 10,000 participants, about 40 slots are needed instead of 10,000. The first claim in a slot costs 20,000 gas (SSTORE cold), subsequent ones 5,000 (SSTORE warm). The savings are significant.

How Vesting Claim Works

For team and investors, claim usually works together with vesting. Cliff + linear unlock is the standard scheme:

struct VestingSchedule { uint256 totalAmount; uint256 cliffEnd; // timestamp when cliff ends uint256 vestingEnd; // timestamp when fully unlocked uint256 claimed; // already claimed } mapping(address => VestingSchedule) public schedules; function claimVested() external { VestingSchedule storage schedule = schedules[msg.sender]; require(block.timestamp >= schedule.cliffEnd, "Cliff not reached"); uint256 vested = _calculateVested(schedule); uint256 claimable = vested - schedule.claimed; require(claimable > 0, "Nothing to claim"); schedule.claimed += claimable; token.transfer(msg.sender, claimable); } function _calculateVested(VestingSchedule memory s) private view returns (uint256) { if (block.timestamp >= s.vestingEnd) return s.totalAmount; if (block.timestamp < s.cliffEnd) return 0; uint256 vestingDuration = s.vestingEnd - s.cliffEnd; uint256 elapsed = block.timestamp - s.cliffEnd; return (s.totalAmount * elapsed) / vestingDuration; } 

Common Vulnerabilities

Vulnerability Consequences Solution
Double-claim without bitmap Token loss on repeated claim with different amount Bitmap with unique index
Griefing through claim on behalf Forced token transfer to unwanted address Require msg.sender == account
Missing recoverUnclaimed Tokens locked forever Add recovery function with timelock
Frontrunning proof Theft of tokens by swapping account Include account in leaf

Double-Claim Without Bitmap

If you use mapping(address => bool) instead of bitmap, and the same address appears with different amounts — the proof is valid for each variation, the claimed[address] = true flag is set once, but the second claim with a different amount still passes. Bitmap with index as key prevents this: each index is unique.

Griefing Through Claim on Behalf

If claim(account, ...) is called by someone other than account — tokens can be force-sent to an address that hasn't passed KYC or a contract without receive(). For protocols with compliance requirements, restrict: require(msg.sender == account).

No RecoverUnclaimed

Tokens are permanently locked on the contract if the deadline is not handled. Always add a recovery function.

Frontrunning Proof

The proof is public; anyone can see it in the mempool and submit it with their own address. Protection: include account in the leaf (already implemented above) — the proof works only for that specific address.

Multi-Round Claims

For airdrops with multiple rounds (e.g., retroactive + ongoing rewards), use several Merkle roots — one per round, or a mutable root with a timelock on updates:

bytes32[] public merkleRoots; // index = round number mapping(uint256 => mapping(uint256 => uint256)) private claimedBitMaps; // round => bitmap function addRound(bytes32 root) external onlyOwner { merkleRoots.push(root); } 

What’s Included in the Work?

  • Smart contract source code (Solidity 0.8.20) with full test coverage (Foundry, including fuzzing).
  • Deploy and verification scripts for Etherscan.
  • Integration documentation (ABI, interfaces, call examples).
  • Post-deployment support: consultations, help with Merkle root updates.
  • Optional: external security audit (from 2 weeks).

Claim Contract Development Process

  1. Requirements analysis — determine the participant list, amounts, vesting schedule, need for multi-round.
  2. Architecture design — choose Merkle tree, bitmap, vesting, additional functions.
  3. Implementation — write the smart contract in Solidity 0.8.20 with full test coverage (Foundry, including fuzzing).
  4. Audit — internal review + optional external audit (from 2 weeks).
  5. Deployment — deploy with optimized parameters (solid gas efficiency guarantee).
  6. Documentation and integration — provide integration guide and support.

Timeline: from 5 working days to 3 weeks depending on complexity. Cost is calculated individually.

We have implemented over 30 claim contracts for projects on Ethereum, Arbitrum, and Polygon. Contact us for a consultation — we will evaluate your project in 2 days. Order turnkey development and get a reliable claim contract protected against all typical vulnerabilities.