Git Product home page Git Product logo

vesting's Introduction

Solidity unit test

Simple Vesting Contract

The contracts allow to read from a CSV file, and whitelist three different ways: batch, merkle tree and ECDSA.

In the contracts folder there are three different whitelist mechanism:

  • Vesting.sol batch of adddress[] users whitelist
  • MerkleVesting.sol whitelist users by merkle tree
  • SignVesting.sol whitelist users by ECDSA

There are some unit tests provided for each of the smart contracts, along with some additional helper functions in the test/utils folder.

Running unit test

To get started make sure to install the npm modules. The tests beeen run on a node v18.4.0

$ npm i

Followed by the command:

$ npm test

Additional details

Vesting.sol contract

  • Making batch transaction you need to keep in track of gas. If you attempt to transact a large arrray it may potentially run out of gas.
  • Ideally you would batch smaller list of addresses.
  • It can be very expensive when you are doing a large amount of users.

Here is an implementation of the whitelist mechanism for batch

function whitelist(
    address[] memory _user, 
    uint256[] memory _amount, 
    uint256[] memory _lockPeriod
) public onlyOwner {
    require(
        _user.length != 0 &&
        _amount.length != 0 &&
        _lockPeriod.length != 0,
        "Vesting: params cannot be empty"
    );
    require(
        _user.length == _amount.length && 
        _user.length == _lockPeriod.length && 
        _amount.length == _lockPeriod.length, 
        "Vesting: params length are not equal"
    );

    for (uint256 i; i < _user.length;) {
        userWhitelist[_user[i]].amount = _amount[i];
        userWhitelist[_user[i]].lockPeriod = block.timestamp + _lockPeriod[i];
        userWhitelist[_user[i]].updatedAt = block.timestamp + _lockPeriod[i];
        userWhitelist[_user[i]].claimedAmount = 0;

        emit WhitelistEvent(_user[i], _amount[i], _lockPeriod[i]);
        unchecked { i++; }
    }
}

Usage, let's say you have 2 addresses to be whitelisted alice (0x08C8...36f) and bob (0x2b22...038):

  • Ensure that your amount is converted to wei before being calling whitelist function
import { ethers } from "hardhat"
const ONE_WEEK = 60 * 60 * 24 * 7
/**
 * whitelist[0] = addresses to be whitelisted
 * whitelist[1] = total amount to be claimed in wei
 * whitelist[2] = lock period time (current block timestamp + lockPeriod)
 */
const whitelist = [
    ["0x08C8e533722578834BC844413d3B11e834f1e36f", "0x2b221d0aFB3309b7E7A6e61a24eFd4B12Adc1038"],
    [ethers.utils.parseEther("5000"), ethers.utils.parseEther("9000")],
    [ONE_WEEK, ONE_WEEK]
]

// Only the deployer can invoke this function, since it has onlyOwner modifier
await vestingInstance.whitelist(...whitelist)
  • Alice can claim a total of 5000 DToken after one week passes
  • Bob can claim a total of 9000 DToken after one week passes

An implementation of batch whitelist is seen at test/utils/index.ts at createWhitelist() function

Along with a unit test provided at test/Vesting.test.ts

  • should allow users to claim token after one month

MerkleVesting.sol contract

  • It requires the merkle tree to be balanced (if you're planning to make your own implementation, however a library like merkletreejs will ensure that the tree is balanced).
  • Requires a storage, since merkle tree is one way hash. You need to map each user to proof hash, in order to be a successful transaction
  • If a mistake is made after generating and deploying the merkle root. You are required to re-deploy a new contract with a new root tree.

Here is an example of whitelist mechanism for merkle tree implementation

function whitelist(
    address _user,
    uint256 _amount,
    uint256 _lockPeriod, 
    bytes32[] calldata merkleProof
) public nonReentrant {
    require(_user != address(0), "MerkleVesting: cannot be zero address");
    require(!blacklist[_user], "MerkleVesting: address is blacklisted");
    require(!isWhitelist[_user], "MerkleVesting: already whitelisted");

    bytes32 node = keccak256(abi.encodePacked(_user, _amount, _lockPeriod));
    require(MerkleProof.verify(merkleProof, merkleRoot, node), "MerkleVesting: invalid proof");

    userWhitelist[_user].amount = _amount;
    userWhitelist[_user].lockPeriod = block.timestamp + _lockPeriod;
    userWhitelist[_user].updatedAt = block.timestamp + _lockPeriod;
    userWhitelist[_user].claimedAmount = 0;

    isWhitelist[_user] = true;
    emit WhitelistEvent(_user, _amount, _lockPeriod);
}

Usage, let's take same example as before where 2 addresses to be whitelisted alice (0x08C8...36f) and bob (0x2b22...038)

import { ethers } from "hardhat"
import { keccak256 } from "@ethersproject/keccak256"
import { MerkleTree } from "merkletreejs"

const ONE_WEEK = 60 * 60 * 24 * 7

// Same whitelist as previous example
const whitelist = [
    ["0x08C8e533722578834BC844413d3B11e834f1e36f", "0x2b221d0aFB3309b7E7A6e61a24eFd4B12Adc1038"],
    [ethers.utils.parseEther("5000"), ethers.utils.parseEther("9000")],
    [ONE_WEEK, ONE_WEEK]
]
const nodeLeaves = []

// convert amount to wei
whitelist[1] = whitelist[1].map(amount => ethers.utils.parseEther(val))
for (let i = 0; i < whitelist[0].length; i++) {
    nodeLeaves.push(ethers.utils.solidityKeccack256(
        ["address", "uint256", "uint256"],
        [whitelist[0][i], whitelist[1][i], whitelist[2][i]]
    ))
}

// Generate the merkle tree
const merkleTree = new MerkleTree(nodeLeaves, keccak256, { sortPairs: true })

/**
 * proof[0] is the hash proof for alice
 * proof[1] is the hash proof for bob
 * If any params are given wrong will throw "MerkleVesting: invalid proof" error
 */
const proof = nodeLeaves.map(leaf => merkleTree.getHexProof(leaf))

// @note: You can also delegate to whitelist for other users
await merkleVestingInstance.whitelist(
    whitelist[0][0],
    whitelist[1][0],
    whitelist[2][0],
    proof[0]
)

The MerkleVesting.sol has the same implementation for the remaining functions as Vesting.sol except for whitelist function.

An implementation of batch whitelist is seen at test/utils/index.ts at generateMerkleTree() function. Unlike Vesting.sol where you remove users by calling delist(), for MerkleVesting.sol is required to call addBlacklist() function to prevent users from whitelist again.

Note: Once the users have whitelisted, you need to keep in track to prevent the users to reclaim tokens again, you need to update isWhitelist.

Along with a unit test provided at test/MerkleVesting.test.ts

  • should provide the correct leaf hash to whitelist user

SignVesting.sol contract

  • ECDSA can potentially can have security risks, if not implemented correctly in a smart contract (e.g.: replay attack, can easily be solved by providing a nonce)
  • Easy to setup, and allows to create new offline signature, to allow new whitelist addresses, unlike MerkleVesting.sol where it requires to change the tree root. Or Vesting.sol requiring additional gas cost. With SignVesting.sol you are able to create offline signatures, and let the user verify it.
  • If the signer's private key gets compromised, than the entire smart contract gets compromised. Since the attacker will be able to create valid signatures, putting the smart contract at a risk! For security reason, never re-use signer's private key when generating new offline transactions.

Here is an example of whitelist mechanism for ECDSA / sign implementation

function whitelist(
    address _user, 
    uint256 _amount, 
    uint256 _lockPeriod,
    uint256 _nonce,
    uint8 v,
    bytes32 r,
    bytes32 s
) public nonReentrant pausable {
    require(!blacklist[_user], "SignVesting: address is blacklisted");
    require(!nonce[_user][_nonce], "SignVesting: nonce already been used");
    bytes32 hash = keccak256(abi.encodePacked(_user, _amount, _lockPeriod, _nonce));
    bytes32 hashMessage = hash.toEthSignedMessageHash();
    address ecRecover = ECDSA.recover(hashMessage, v, r, s);

    require(ecRecover == signerAddress, "SignVesting: invalid signature");
    userWhitelist[_user].amount = _amount;
    userWhitelist[_user].lockPeriod = block.timestamp + _lockPeriod;
    userWhitelist[_user].updatedAt = block.timestamp + _lockPeriod;
    userWhitelist[_user].claimedAmount = 0;

    nonce[_user][_nonce] = true;
    emit WhitelistEvent(_user, _amount, _lockPeriod);
}

Usage, let's take same example as before where 2 addresses to be whitelisted alice (0x08C8...36f) and bob (0x2b22...038)

import { ethers } from "hardhat"

const ONE_WEEK = 60 * 60 * 24 * 7

// Same whitelist as previous example
const whitelist = [
    ["0x08C8e533722578834BC844413d3B11e834f1e36f", "0x2b221d0aFB3309b7E7A6e61a24eFd4B12Adc1038"],
    [ethers.utils.parseEther("5000"), ethers.utils.parseEther("9000")],
    [ONE_WEEK, ONE_WEEK]
]
const signatureList = []
const signer = new ethers.Wallet(process.env.SIGNER_PRIVATE_KEY)

// convert amount to wei
whitelist[1] = whitelist[1].map(amount => ethers.utils.parseEther(val))
for (let i = 0; i < whitelist[0].length; i++) {
    const listSignMessages = ethers.utils.solidityKeccack256(
        ["address", "uint256", "uint256", "uint256"],
        [whitelist[0][i], whitelist[1][i], whitelist[2][i], 1]
    )
    const msgHashBinary = ethers.utils.arrayify(listSignMessages)

    // sign the message
    const flatSig = await ethers.signMessage(msgHashBinary)
    const {r, s, v} = ethers.utils.splitSignature(flatSig)
    signatureList.push({r, s, v})
}

await signVestingInstance.whitelist(
    whitelist[0][0],
    whitelist[1][0],
    whitelist[2][0],
    1
    signatureList[0].v,
    signatureList[0].r,
    signatureList[0].s
)

An implementation of ECDSA / sign whitelist is seen at test/utils/index.ts at generateSignature() function

Note: Once the users have whitelisted, you need to keep in track of nonce, to prevent replay attacks.

Along with a unit test provided at test/SignVesting.test.ts

  • should get a valid signature to whitelist user

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.