Skip to main content

Command Palette

Search for a command to run...

Designing a Gas Efficient ERC20 Merkle Vesting Contract

Updated
4 min read
H

I am a software engineer with a strong background in competitive programming and backend focused development.

I graduated in IT from Army Institute of Technology, Pune (2025) and currently work at NxtWave, where I build and review technical content around data structures, algorithms, and problem solving.

My recent work is focused on smart contract and protocol development using Solidity and Foundry. I enjoy building systems where correctness, security, and clear invariants matter such as token vesting, staking mechanisms, and upgradeable protocols.

Earlier, I spent several years in competitive programming (Codeforces Expert, 5★ CodeChef), which shaped how I approach problem solving, edge cases, and testing.

On this blog, I write about the design and implementation of smart contracts, testing strategies, and engineering tradeoffs I encounter while building Web3 protocols.

Token distributions are deceptively hard to get right on Ethereum.
At scale, storing per user allocations on chain quickly becomes expensive, and time based vesting logic is prone to subtle bugs if not tested carefully.

In this article, I’ll walk through the design of a Merkle based ERC20 vesting contract that focuses on gas efficiency, correctness, and auditability, along with the testing strategy used to validate it.


The Problem

A typical vesting contract needs to answer two questions:

  1. Who is eligible and how many tokens do they get?

  2. When can they receive those tokens?

A naïve solution stores all user allocations on chain:

mapping(address => uint256) allocation;

This approach does not scale well:

  • High storage costs

  • High deployment gas

  • Large attack surface

For large distributions (airdrops, community grants, investor vesting), this quickly becomes impractical.


Design Overview

To address this, the contract separates concerns:

  • Eligibility & allocation → handled using a Merkle tree

  • Time based release → handled via a global vesting schedule

Core ideas:

  • Store only a single Merkle root on chain

  • Let users prove eligibility off chain using Merkle proofs

  • Apply a global vesting schedule (start, cliff, duration) for simplicity

This keeps the contract:

  • Gas efficient

  • Easy to reason about

  • Easier to audit


Merkle Based Allocation

Each user’s allocation is encoded off chain as a Merkle leaf:

leaf = keccak256(userAddress, allocation)

All leaves are combined into a Merkle tree, producing a single merkleRoot that is stored on chain.

The contract never stores the full user list.


Claim Flow

When a user wants to participate, they call claim() with:

  • their allocation

  • a Merkle proof

At a high level, claim() does the following:

  1. Ensures the user has not already claimed

  2. Recomputes the leaf (user, allocation)

  3. Verifies the Merkle proof against the stored root

  4. Stores the user’s total allocation on chain

After a successful claim, the user is registered and eligible for vesting.

This is a one time operation per user.


Vesting Logic

The vesting model is intentionally simple and predictable:

  • A single global schedule:

    • start

    • cliff

    • duration

  • Linear vesting after the cliff

The releasable amount is computed as:

releasable = vestedAmount − alreadyReleased

Where:

  • Before the cliff → vestedAmount = 0

  • After full duration → vestedAmount = totalAllocation

  • Otherwise → linear proportion of time elapsed

This design avoids per user schedules, reducing storage and complexity.


Release Flow

Users can call release() at any time after claiming.

The function:

  1. Computes the currently releasable amount

  2. Updates internal state (released)

  3. Transfers tokens to the user

The implementation follows the checks–effects–interactions (CEI) pattern to avoid reentrancy and accounting issues.


Security Considerations

Several failure cases are explicitly handled:

  • Double claim prevention
    A user can claim only once.

  • Invalid allocation protection
    Any mismatch in (user, amount) causes Merkle verification to fail.

  • Early release prevention
    Releasing before the cliff returns zero.

  • Over release protection
    Released tokens are tracked per user.

The contract intentionally avoids:

  • admin mutation

  • late joiners

  • upgradeability

This minimizes trust assumptions and reduces audit complexity.


Testing Strategy

Testing focused on time based edge cases and adversarial behavior, using Foundry.

Key scenarios covered:

  • Claim with valid and invalid Merkle proofs

  • Double claim attempts

  • Releasing before the cliff

  • Releasing at the exact cliff

  • Partial vesting during the schedule

  • Full vesting after duration

  • Ensuring no over release occurs

For unit tests, a single leaf Merkle tree was used to isolate on chain logic without introducing off chain complexity.


Design Tradeoffs

Some conscious tradeoffs were made:

  • Fixed Merkle root
    Late joiners are not supported. New users require a new distribution round.

  • Global vesting schedule
    Per user schedules were avoided to reduce gas costs and invariant complexity.

  • No upgradeability
    Vesting contracts benefit from immutability and predictability.

These choices prioritize correctness and auditability over flexibility.


Conclusion

This project demonstrates how Merkle proofs and time based vesting can be combined to build a scalable, gas efficient token distribution system without sacrificing security.

The emphasis throughout was on:

  • clear separation of concerns

  • defensive design

  • rigorous testing

The full implementation and tests are available on my GitHub.
codebase link : https://github.com/harshkumarrai/ethereum-solidity-projects-/tree/master/Projects/erc20_vesting_merkle


ERC20 Merkle Vesting Contract