Smart Contract Details

Smart contract variable, storage and function mechanics

The TokenVestingPlans and VotingTokenVestingPlans contracts are built on top of the ERC721Enumerate contracts by OpenZeppelin. The backbone is that each individual vesting plan is a unique NFT issue to the beneficiary, with the details of the vesting plan stored in storage within a Struct object. When creating a vesting plan, the creator inputs all of the details of the vesting plan, that will be stored in the Struct object, and then the beneficiary is minted an NFT, mapped by its tokenId to the struct that is stored in storage by the same tokenId uint256, connecting the two objects together.

Both contracts are unique, however, in how they handle token voting and delegation. The TokenVestingPlans are optimized for snapshot (or offchain) voting, and use a special ERC721Delegate contract that allows plan owners to delegate their NFT to another wallet, which when combined with the read functions allows Snapshot to simply and elegantly calculate all of the tokens delegated to a specific wallet across all NFTs.

In contrast, the VotingTokenVestingPlans do not use the ERC721Delegate contract, but instead have a built in method to physically separate tokens from the main ERC721 escrow contract, and transfer to a special VotingVault contract. This segregation of tokens enables the owners to directly delegate their tokens, assuming those tokens follow the OpenZeppelin ERC20Votes standard. When a plan beneficiary wants to delegate their vesting tokens, they will call the smart contract to setupVoting or delegate their tokens, which then creates a unique VotingVault contract, mapped to the NFT, and then allows the beneficiary and owner of the NFT to delegate the tokens held inside of the VotingVault.

For more detailed information and architecture of the voting and delegation system, read more in the Delegation Architecture section.

Vesting Plan Parameters:

PlanId: The NFT TokenId and planId mapped in storage linking the NFT and Vesting Plan Token: The address of the ERC20 Token that is locked in the contract and vesting Amount: The total amount of tokens remaining in the vesting plan (this resets on each claim) Start: The date in which the currently vesting amount started (this resets on each claim) Cliff: An optional cliff date, this represents a date after the start in which tokens remain unvested, but anything from the start to the cliff date vest in bulk fashion upon the cliff date Rate: The rate at which tokens vest, per period Period: The time of each period (in seconds), for streaming style this is 1, but for daily it is 86,400 as an example. The period dictates how often the 'rate' of tokens vest to the beneficiary. VestingAdmin: The address of the admin, typically creator, who is solely responsible for revoking the plan AdminTransferOBO: A special boolean parameter, which when toggled to 'true' allows the VestingAdmin to transfer the NFT from the current owner to a new owner. This does not trigger a claim or revoke of tokens, it simply transfers the NFT.

The Plan is a struct mapped in storage by the NFT token Id (also commonly referred to as the planId). You can query the details of a specific Plan using the public getter function created by the 'plans' mapping, which will return the Plan (struct) details for that particular NFT.

struct Plan {
    address token;
    uint256 amount;
    uint256 start;
    uint256 cliff;
    uint256 rate;
    uint256 period;
    address vestingAdmin;
    bool adminTransferOBO;
mapping(uint256 => Plan) public plans;
// getter function created automatically by the public plans mapping
function plans(uint256 planId) external view returns (Plan);

Creating A Vesting Plan

Creating a vesting plan involves inputting the parameters (except the PlanId which is assigned) into the createPlan function on the smart contract. This function will transfer tokens from the msg.sender to the ERC721 contract, create a struct in storage, and mint the recipient an NFT. The NFT tokenId is mapped in storage to the struct that has all of the vesting plan parameters.

The tokens, having been pulled from the msg.sender address, are kept in escrow inside the ERC721 contract until they are claimed via vesting redemption by the beneficiary, or revoked by the vesting admin.

Below is the create function used when creating a new vesting plan. The contract will only mint a single plan at a time, so to create them in bulk there is a separate batching contract that loops through an array of plans to mint them.

function createPlan(
    address recipient,
    address token,
    uint256 amount,
    uint256 start,
    uint256 cliff,
    uint256 rate,
    uint256 period,
    address vestingAdmin,
    bool adminTransferOBO
  ) external returns (uint256 newPlanId);

Redeeming and claiming tokens from Vesting Plan

Redeeming a plan is for the beneficiary to claim vested tokens from their vesting plan. They do this via calling one of three contract calls, which are laid out below in more detail. Calling any of these functions will use an internal library to calculate the amount of tokens that are vested and claimable, and then deliver those tokens to the beneficiary. Then the contract will update its storage of the vesting plan to reduce the amount by what was claimed, so the Amount parameter only reflects the remainder, and will update the Start date, so that the start date reflects the block timestamp of the claim. This storage update is the most efficient way to keep the vesting plan clean so that tokens are redeemed only on the predefined schedule, but without waste of storing additional variables that reflect original amounts and dates.

A vesting plan beneficiary with multiple plans may redeem multiple or all of them at the same time, and the contract will only process the plans that have tokens available to be claimed, otherwise it will skip those entirely.

The first way to redeem tokens is using the redeemPlans method, which takes an array of the planIds (NFT TokenIds). The function will use an internal function _redeemPlan to process each plan individually, calculating the balance that is vested, un vested, and returning the amount to be redeemed. If the vested balance is greater than 0, then it will withdraw the tokens from the escrow contract (either the ERC721 contract itself or the VotingVault contract if voting was setup) and transfer them to the beneficiary. The function will then update the storage of the remaining amount still to be vested, and the timestamp applicable of the last redemption.

function redeemPlans(uint256[] calldata planIds) external;

The redeemAllPlans function works similarly to the redeemPlans function, except that it leverages the ERC721Enumerable iteration functionality to iterate through all of the plans owned by a single wallet address. Each plan is then processed by the same _redeemPlan function. Because the function does not require any inputs, it is very effective for users manually interacting with the contract but who may not know their vesting plan tokenIDs offhand.

function redeemAllPlans() external;

The final redemption function is the partialRedeemPlans function, which takes an additional input of the redemptionTime, and the array of planIds to be redeemed. The redemptionTime parameter allows an owner to partially redeem tokens from their plan(s) with a backdate. The function will revert if a timestamp is put into the future. The internal library will calculate the amount of tokens that have vested as of the redemptionTime, and then withdraw that balance to the beneficiary.

function partialRedeemPlans(uint256[] calldata planIds, uint256 redemptionTime) external;

Revoking a Vesting Plan

Revoking a vesting plan is only allowable by the Vesting Admin of that particular vesting plan. When the Admin revokes the plan, the plan is effectively cancelled go-forward, such that the un-vested tokens are returned back to the vesting admin, and no more can be vested beyond that point. They do not automatically send the tokens to the vesting plan beneficiary, however, in case there are tax benefits or other reasons why they would not want to immediately claim the tokens. The plan beneficiary still can claim their vested tokens. Once they claim anything remaining, the NFT will be burned and the Plan stored in storage deleted. If the vesting admin revokes a plan before anything is vested, then it will burn the NFT and return all of the tokens back to the admin. The Vesting Admin has two choices in how to revoke a plan or plans, which can be done one at a time or in a batch group, so long as the wallet is the vesting admin for all of those plans; they can revoke at the current timestamp, which will revoke at the second of processing anything unvested, or they can future revoke a plan. The future revoke may be useful for admins that want to allow the beneficiary to vest tokens 'through the end of the week' or 'through the end of the month', and can pick that timestamp accordingly to their effective termination date.

The revokePlans function takes an array of planIds and then will use and internal function to process each. The function allows for a vesting plan Admin to revoke many plans in a single transaction, whether for the same beneficiary who has multiple, or for revoking an entire team's set of tokens. The function will iterate through the array, and the internal _revokePlan function will check that the vestingAdmin is the msg.sender, and then calculate the un-vested balances with the library method. It will then return the un-vested amount of tokens to the vesting admin, and update the storage Plan data so that if there is anything vested but unclaimed, the beneficiary can still claim those tokens, and otherwise the NFT will be burned and plan storage deleted. The time calculation for the vested and un-vested balances is based on the block.timestamp of the transaction at the time.

function revokePlans(uint256[] calldata planIds) external;

Separately, vesting admins can revoke the vesting plan(s) with a future timestamp, rather than the current block.timestamp. This will perform similar calculations as the typical revokePlans function, but using the revokeTime in the calculation instead of the block.timestamp. This is useful for when letting a team member or contributor go as of a certain date, like the end of the week or end of the month. Instead of waiting until that date to process the revoke, admins can effectively future date the revoke transaction, processing it now but as of the desired future date.

function futureRevokePlans(uint256[] calldata planIds, uint256 revokeTime) external;

Admin Transfer Functions

Administrators also have the special ability to transfer vesting plans on behalf of their users, if their users allow it to happen. The ability is a toggle that only the vesting beneficiary can turn on or off. It is NFT specific, so it can be managed by each vesting plan beneficiary locally, rather than at an administrator level, allowing for beneficiaries who are comfortable with self custody to take full control of their vesting plan, and for those who trust their admin to act on their behalf in case of emergency can keep the toggle on. The purpose is to serve both sophisticated and newer users, while still giving the vesting tool all of the requirements to make it as decentralized and trustless as possible.

Admins can also individually assign a different address to a vesting plan they are the administrator on. This is useful for when a token project first launches, they may have a multisig wallet that manages everything but want to transition that admin authority to a DAO governance contract at a later date - they can adjust their admin address after the fact.

Helpful Links Technical Audit Documentation:

Smart Contracts:

Last updated