RedDuck
Copied

Vesting contract implemented in 3 different ways_

Written by

Mark Virchenko

Posted: Jun 18, 2022
UPD: Aug 6, 2026
8 min reading
53

Hi everyone, in this article, we will show you 3 different approaches to implementing a Vesting contract.

A vesting contract is a contract that allows owners of projects to allocate tokens for their users but makes the users themselves take the tokens out of the vesting contract. Such an operation is called a “claim”.

To claim, of course, users need to prove to the contract, that they have the right to do so. For that, the owner of the project has to perform some action that will make the contract confident that the certain user indeed can claim.

There are three ways to do it that we know so far: 1) Populate a mapping (address => uint256) that will return the uint256 amount that a user with a specific address can claim. 2) Use the Merkle Tree root hash as storage (it’s just a compressed bytes32 field) and provide the cryptographic proof that an address is contained in the bytes32 hashed variable every time a user makes a claim. Reference: https://redduck.medium.com/off-chain-storage-pattern-in-solidity-merkle-tree-d277be4453f4 3) Use the owner’s signatures so that the contract can verify that the claim is legitimate. The messages like “Address 0x.…11 can claim 100 tokens” will be signed by the owner’s private key and the contract will be able to verify that, if both signature and message is provided by the user during claim.

Let’s take a look at all the implementation of all the three methods above.

On-chain solution (mapping)

A default, normal solution, would be to use a mapping of addresses and their corresponding claimable amounts:

solidity
contract DefaultVesting is Ownable {
    mapping (address => uint256) claimableAmount;
    IERC20 immutable _token;
    constructor(IERC20 vestedToken) {
        _token = vestedToken;
    }
    function vestTokensMany(address[] calldata toArray, uint256[] calldata amountArray) external onlyOwner { 
       for (uint256 i = 0; i < toArray.length; i++) {
           claimableAmount[toArray[i]] = amountArray[i];
       }
    }
    function claimTokens(uint256 amount) external {
        require(canClaimTokens(amount), "cannot claim");
        claimableAmount[msg.sender] -= amount;
        _token.transfer(msg.sender, amount);
    }
    function canClaimTokens(uint256 amount) private view returns (bool) {
       return claimableAmount[msg.sender] >= amount;
    }
}

Pros: 1. Simplicity. It’s really very easy to implement that. 2. Everything is stored on-chain, so there is a bit more transparency and security.

Cons: 1. VestTokensMany is not optimised to high amount of data (like 1000 addresses). It could cost up to 10000$ dollars on Ethereum chain to initialize many addresses. 2. Lack of transparency: If the amount of data is high and we can’t initialise the mapping from one transaction, it means that we can always change the vested amounts/addresses and it’s not transparent to users.

Gas-Optimised solution: Merkle Tree

We need to get rid of the code that makes us execute N steps in order to add N addresses. Then we will be able to get rid of that con about costly vestTokensMany function.

So, let’s make it work with less data. Merkle Trees can help us with that. This approach uses the Merkle root tree algorithm described in this article: https://redduck.medium.com/off-chain-storage-pattern-in-solidity-merkle-tree-d277be4453f4)

The code below, using the Merkle root trees, is demonstrated.

solidity
contract MerkleVesting is Ownable {
   bytes32 claimMerkleRoot;
   mapping (address => bool) addressClaim;
   IERC20 immutable _token;

   constructor(IERC20 vestedToken) {
      _token = vestedToken;
   }
   function vestTokens(bytes32 merkleRoot) external onlyOwner {
      claimMerkleRoot = merkleRoot;
   }
   function claimTokens(uint256 amount, bytes32[] calldata merkleProof) external {
       require(canClaimTokens(amount, merkleProof), "cannot claim");
       addressClaim[msg.sender] = true;
       _token.transfer(msg.sender, amount);
   }
   function canClaimTokens(uint256 amount, bytes32[] calldata merkleProof) private view returns (bool) {
    return addressClaim[msg.sender] == false &&
           MerkleProof.verify(merkleProof, claimMerkleRoot,
           keccak256(abi.encodePacked(msg.sender, amount)));
    }
}

We avoid spending N on-chain steps to initialise N addresses, because we shrink all those addresses into one hash off-chain, and we just spend one step to initialise the data on-chain (claimMerkleRoot field). However, this field won’t be able to work stand-alone, unlike the mapping. It will require an additional argument to be always provided during a claim — bytes32[] merkleProof. This proof will contain log2(N) amount of addresses that are connected to the msg.sender address in the tree, and it is required to prove that the pair {msg.sender, amount} exists in the tree.

Pros: 1. Fast speed: the complexities are O(1) for vestTokens, O(logN) for claimTokens where N is the amount of vested addresses. 2. Security: The merkle root trees could be used with a multi signature wallet, and it would mean high security, just like with on-chain solution. 3. Fast invalidation of vested amounts and addresses. To change the vested addresses or amounts, we can easily change just one field (as opposed to iterating all keys in the mapping). 4. Decentralization / Transparency - We could disallow to modify the vested addresses/amounts if we want to. So, #3 is optional, and we can disallow to modify anything, which was even not possible with the mapping solution.

Cons: 1. It would require keeping storage somewhere off-chain. 2. Usually it’s harder for the users to use the smart contracts directly (although it’s not very hard).

Alternative off-chain solution: Owner signatures

solidity
contract SignatureVesting is Ownable {
   using ECDSA for bytes32;
   mapping (bytes => bool) signatureClaim;
   IERC20 immutable _token;
   constructor(IERC20 vestedToken) {
        _token = vestedToken;
    }
    function claimTokens(uint256 amount, uint256 nonce, bytes calldata signature) external {
        require(canClaimTokens(amount, nonce, signature), "sig");
        signatureClaim[signature] = true;
        _token.transfer(msg.sender, amount);
   }
   function canClaimTokens(uint256 amount, uint256 nonce, bytes calldata signature) private view returns (bool) {
        bytes32 message = keccak256(abi.encodePacked(msg.sender, amount, nonce, address(this)));
        return signatureClaim[signature] == false && message.toEthSignedMessageHash().recover(signature) == owner();
    }
}

This approach is fundamentally different. See, we don’t even have a vestTokens function. The reason for that is that it is basically like the mapping solution, but a virtual one. When a mapping is populated on-chain, you sign a transaction and send it immediately. In this scenario, you just sign a transaction and give it to the user that would claim later. And they provide the signature of that transaction to the contract, and contract basically executes both transactions at the same time: vest and claim transactions. This way the owner does not need to spend their funds in order to initialise the contract with a state, only users do, and they only pay for their record in the mapping.

So, instead of us sending a transaction every time we want to populate a user’s address into the vesting contract, we just sign this transaction data, give it to the user outside of Solidity smart contracts, and whenever this user wants to claim, they can do so by providing the signature we gave them. The contract will check the signature and if it’s valid, it will allow the user to claim tokens. It is like a virtual mapping, that exists off-chain.

It’s obvious pro is that it’s complexity is O(1) for both initialisation and claim functions, unlike the previous solutions.

Of course, it sounds cool, but there are a lot of drawbacks when using the signatures approach. The cons are listed below:

Replay attacks

Firstly, signatures could be re-used by a user many times. But this has a solution - firstly, we need to store in a mapping a register of used/unused signatures (like we do with the signatureClaim field). Also, we need to store more data for security, like the contract address and the nonce of the signature, to avoid replay attacks. But, even if we do so, we still can get a replay attack if we deploy the same contract with the same address onto another chain and don’t specify a different nonce in the new contract, that is different from the previous contract’s nonce.

Centralisation

We need a private key to produce the signature. So, the whole idea is circling around one guy with access to one private key. Gnosis safe won’t help here (even if it had EIP 1271 implemented) because a signature is at the end produced by just one private key by one person. If it gets stolen, then the hacker could generate whatever signature they want, and claim all the tokens. Whereas Merkle root and mapping solutions could be used with gnosis safe multi-signature wallet for more security.

So, regarding an off-chain solution, the Merkle root tree would be more secure, and also robust, because if we decide to change user’s claim amounts, it will be much harder to patch the smart contract to ignore previously generated signatures, and only accept new signatures with new claim amounts, whereas for the merkle tree solution, we just need to update the root hash.

Unsolvable lack of transparency

The fact that all the signatures exist off-chain, means that there can be more signatures produced by the owner, for instance, that would allow himself or his friend to drain the whole balance of the contract. This is unsolvable due to the nature of the solution — we don’t have the vestTokens function, we don’t know where the data exists.

Summary

In general, Merkle Trees seem to be the most relevant way. They have low computational complexity, high decentralisation, easy invalidation of data, transparency towards users, low security risks, and can contain high amount of addresses. The only case when they are not applicable is when you have limited development time or skillset on the project, and you don’t want to bother because the amount of addresses you need to vest is not high.

Here’s a table to compare the three solutions:

Press enter or click to view image in full size

Comparison table

Reference: https://github.com/RedDuckTeam/Vesting

_Our Cases

Since 2020, RedDuck has been building Web3 products end to end – smart contracts, indexers, backends and the interfaces people actually trade in. Two of our public projects show the range: Midas, a platform issuing regulated real-world assets onchain, and Injex, the first liquidity aggregator in the Injective ecosystem.

Midas — tokenized investment products dashboard
EVM
RWATokenization

Midas: Tokenized Treasuries & Yield Strategies

Midas (midas.app) came to us to put regulated, institution-grade yield products onchain: mTBILL, a tokenized short-duration US Treasury product, and mBASIS, a market-neutral basis-trading strategy — issued as ERC-20 tokens that an investor can hold, move and redeem without ever leaving DeFi.

The hard part is not the token — it is everything around it. Minting and redemption are priced against the fund's net asset value, which has to reach the contracts through an oracle; access is permissioned, so the compliance perimeter lives in the contract rather than in a form; and redemptions have to feel instant while the underlying assets settle on a banking timeline. We built that stack end to end and took it through multiple external audits.

Today Midas runs a growing family of yield tokens that stay fully composable across dozens of EVM networks — supplied as collateral and routed through other protocols like any ERC-20, still redeemable at NAV on demand — with nine-figure TVL and, in an EU first, tokenized treasuries open to retail investors.

Explore more
Injex DEX aggregator landing page
Injective L1
DeFiDEX aggregator

Injex: The First Liquidity Aggregator on Injective

Liquidity on Injective sits in a dozen separate venues, so getting a fair price used to mean opening every DEX in turn and comparing by hand. Injex removes that step: it is the first aggregator in the ecosystem, and it quotes the whole market at once.

Under the swap card there is a routing layer that reads reserves across DojoSwap, Helix, Astroport, Neptune, White Whale and Hydro, splits an order across whichever pools give the best combined rate, and settles it in a single transaction — later extended with limit orders, staking and on-chain analytics.

The interface stays deliberately plain: one card, the best price, minimal slippage. Newcomers can use it like an ordinary exchange, while traders who care about execution can still see the full route.

Explore more