Create Vesting In Smart Contract

Summary

Consider the scenario whereby you are creating an ICO which you handle via your ICO smart contract. Currently you are either:

  1. Directly minting ERC20 tokens to investors.
  2. Allowing investors to claim after LP is created or by a certain date/block.

You can integrate Liquivest to wrap the ERC-20 tokens into a vesting schedule which you then forward to user. Liquivest allows you to set a unix datetime for the vesting to start, so you can ensure that:

  1. Users recieve tokens immediately.
  2. Users cannot grief the project before the LP has been created.
  3. Users cannot claim tokens before the vesting start datetime.
  4. Users cannot immediately dump their whole allocation before the vesting end datetime.
  5. Developers can lock tokens for the dev/treasury supply within a vesting schedule.

All of these functions increase the trust that investors have by recieving tokens immediatelys and that developers allocation is also vested alongside.

Additionally the price performance of the project is also increased by not allowing investors to immediately dump their whole allocation on LP creation.

High Level Steps

  1. Decide which vesting type you want to create.
  2. Call create on Liquivest Proxy for the required type.
  3. Vesting NFT is minted to the msg.sender, which is the user calling it.
  4. If Vesting NFT is to be held by a smart contract, program logic to either claim and/or transfer it.

Actors

  • msg.sender
    • User calling.
  • LiquivestMinter
    • The example contract below, showing how to mint a vesting token from a contract.
  • LiquivestProxy
    • Liquivest proxy used to manage calls to the different vesting types.
  • ConcreteVestingNFT
    • The specific vesting chosen.

Basic flow

// Approval tx 1
msg.sender         --ERC20::Approve--> LiquivestMinter                         // You allow LiquivestMinter to move tokens from you, to itself

// Approval tx 2
LiquivestMinter    --LiquivestMinter::approveTokenAllowance--> LiquivestProxy  // You allow LiquivestProxy to move tokens from LiquivestMinter to itself

// Create tx
msg.sender         --ILiquivestProxy::createX--> LiquivestMinter               // LiquivestMinter moves tokens from msg.sender to itself

// The rest of the sequence is handled by Liquivest

LiquivestMinter    --IERC5725::create--> LiquivestProxy                        // LiquivestProxy moves tokens from LiquivestMinter to itself
LiquivestProxy     --IERC5725::create--> ConcreteVestingNFT                    // ConcreteVestingNFT moves tokens from LiquivestMinter to itself

ConcreteVestingNFT --IERC721-mint--> msg.sender                                // ConcreteVestingNFT mints a token to msg.sender for the sent token amount

LiquivestMinter Example

The LiquivestMinter contract is an example and are not audited or battle-tested, you should test your intergration thoroughly.

In this example, you should set an allowance for LiquivestMinter to move tokens from yourself to LiquivestMinter. This is done manually on the tokens contract page (ERC20::approve(address spender, uint256 amount) -> LiquivestMinter).

You should then call LiquivestMinter::approveTokenAllowance(address token) which allows LiquivestProxy to move tokens from LiquivestMinter to itself and then onwards to the vesting contract.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract LiquivestMinter {
    address public liquivestProxy;

    constructor(address _liquivestProxy) {
        require(_liquivestProxy != address(0), "Invalid proxy address");
        liquivestProxy = _liquivestProxy;
            token.approve(liquivestProxy, amount);
    }

    /// @notice Approves LiquivestProxy to spend `amount` of `token` on behalf of this contract
    function approveTokenAllowance(IERC20 token) external {
        token.approve(liquivestProxy, type(uint256).max);

    }

    /// @notice Create reverse exponential vesting NFT
    function createReverseExponential(
        uint128 reverseExponent,
        uint256 amount,
        uint128 startTime,
        uint128 endTime,
        IERC20 token
    ) external {
        token.transferFrom(msg.sender, address(this), amount);
        token.approve(liquivestProxy, amount);

        bytes memory data = abi.encodeWithSignature(
            "create(uint128,address,uint256,uint128,uint128,address)",
            reverseExponent,
            msg.sender,
            amount,
            startTime,
            endTime,
            address(token)
        );

        _callProxy(data);
    }

    /// @notice Create exponential vesting NFT
    function createExponential(
        uint256 amount,
        uint128 startTime,
        uint128 endTime,
        IERC20 token,
        uint128 exponent
    ) external {
        token.transferFrom(msg.sender, address(this), amount);
        token.approve(liquivestProxy, amount);

        bytes memory data = abi.encodeWithSignature(
            "create(address,uint256,uint128,uint128,address,uint128)",
            msg.sender,
            amount,
            startTime,
            endTime,
            address(token),
            exponent
        );

        _callProxy(data);
    }

    /// @notice Create linear vesting NFT
    function createLinear(
        uint256 amount,
        uint128 startTime,
        uint128 duration,
        IERC20 token
    ) external {
        token.transferFrom(msg.sender, address(this), amount);
        token.approve(liquivestProxy, amount);

        bytes memory data = abi.encodeWithSignature(
            "create(address,uint256,uint128,uint128,address)",
            msg.sender,
            amount,
            startTime,
            duration,
            address(token)
        );

        _callProxy(data);
    }

    /// @notice Create cliff vesting NFT
    function createCliff(
        uint128[] memory cliffTimes,
        uint256[] memory cliffAmounts,
        IERC20 token
    ) external {
        uint256 totalAmount;
        for (uint256 i = 0; i < cliffAmounts.length; i++) {
            totalAmount += cliffAmounts[i];
        }

        token.transferFrom(msg.sender, address(this), totalAmount);
        token.approve(liquivestProxy, totalAmount);

        bytes memory data = abi.encodeWithSignature(
            "create(address,uint128[],uint256[],address)",
            msg.sender,
            cliffTimes,
            cliffAmounts,
            address(token)
        );

        _callProxy(data);
    }

    /// @notice Helper to safely forward call and handle errors
    function _callProxy(bytes memory data) internal {
        (bool success, bytes memory res) = address(liquivestProxy).call(data);
        if (!success) {
            if (res.length > 0) {
                assembly {
                    revert(add(res, 32), mload(res))
                }
            } else {
                revert("LiquivestProxy call failed");
            }
        }
    }
}

Further points

  • You can batch create vesting NFT’s by putting the create function within a for loop.
  • You do not need to handle metadata, it is dynamically created onchain via the Art Proxy.