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:
- Directly minting ERC20 tokens to investors.
- 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:
- Users recieve tokens immediately.
- Users cannot grief the project before the LP has been created.
- Users cannot claim tokens before the vesting start datetime.
- Users cannot immediately dump their whole allocation before the vesting end datetime.
- 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
- Decide which vesting type you want to create.
- Call
createon Liquivest Proxy for the required type. - Vesting NFT is minted to the msg.sender, which is the user calling it.
- 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
createfunction within a for loop. - You do not need to handle metadata, it is dynamically created onchain via the Art Proxy.
