Vesting contract implemented in 3 different ways.

--

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:

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.

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

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:

Comparison table

References

The repo where you can play with the smart contracts described above is located at this URL:

Cases

In many cases we had to use Merkle Root Tree approach and it is for instance the CrimeCash P2E game. It is a game, where users compete against each other, and at the end of the game season, top players of the game receive rewards in the form of the CRIME token. The project is pretty solid and popular, so it has to comply with high security standards, but at the same time, be usable by its the large user-base.

So, based on the requirements above, we decided to choose the Merkle Tree solution. It has been working fine for the last 2 seasons and it is proving that it is the best solution for a mix of Security and Scalability.

Contacts

If you have any questions about this or want a project to be developed, you could contact me:

Telegram — @RedDuckUA
Email — mark.virchenko@redduck.io

Or our other partners:
sales@redduck.io

--

--

RedDuck: Your Blockchain Development Partner 🏆
RedDuck: Your Blockchain Development Partner 🏆

Written by RedDuck: Your Blockchain Development Partner 🏆

RedDuck is a fast-growing, innovative Web3 and financial technology company. We offer customized solutions for the blockchain industry, regardless of complexity

Responses (2)