RSS Amplifier

Commitment to Security · Sep 30, 2024

Token Vesting Contracts

0
Sign in to vote or save

0xCommit · Commitment to Security

In the decentralized Web3 ecosystem, token vesting is a critical tool used to lock tokens and align incentives across stakeholders. Vesting ensures that tokens are released gradually over time, which helps projects avoid massive token dumps that can destabilize the tokenomics. However, improper implementations of vesting contracts expose projects to a range of security vulnerabilities that can lead to serious financial and reputational damage.

In this article, we will explore common token vesting patterns and offer code snippets focused on addressing potential security risks. By the end, you'll have a comprehensive understanding of how to implement secure vesting mechanisms for your smart contracts.

Token vesting refers to the gradual release of tokens over a set period, often used for purposes such as:

  • Founder or team lockups

  • Investor token allocations

  • Staking rewards

The primary objective of vesting is to align long-term incentives, ensuring that stakeholders cannot immediately liquidate all their tokens. Yet, without a security-first mindset, vesting mechanisms can be exploited by bad actors.

Linear vesting distributes tokens at a constant rate over a predefined period. For example, 1,000 tokens could be released monthly over two years.

  1. Reentrancy Attacks: If the vesting contract allows external interactions, it could be susceptible to reentrancy attacks, where an attacker calls the claim function multiple times before the state is updated.

  2. Gas Limit Constraints: Releasing tokens for many users in a single transaction can exceed gas limits, causing the transaction to fail.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract SecureLinearVesting is ReentrancyGuard {
    IERC20 public token;
    mapping(address => uint256) public balances;
    mapping(address => uint256) public lastClaim;
    uint256 public vestingPeriod = 30 days;
    constructor(IERC20 _token) {
        token = _token;
    }
    function claim() external nonReentrant {
        require(balances[msg.sender] > 0, "No tokens to claim");
        uint256 timePassed = block.timestamp - lastClaim[msg.sender];
        require(timePassed >= vestingPeriod, "Vesting period not over");
        uint256 amount = balances[msg.sender] * timePassed / vestingPeriod;
        balances[msg.sender] -= amount;
        lastClaim[msg.sender] = block.timestamp;
        token.transfer(msg.sender, amount);
    }
}

Code Example: Linear Vesting with Reentrancy Protection

  1. ReentrancyGuard ensures the function cannot be called multiple times in the same transaction.

  2. Checks-effects-interactions pattern ensures that state changes happen before any token transfer, reducing vulnerability to reentrancy attacks.

In cliff vesting, tokens are locked for a fixed period (the "cliff"), after which a percentage or the total allocation is released.

  1. Front-running: Attackers may exploit time delays in transaction processing to claim tokens before legitimate holders.

  2. Timestamp Dependence: Contracts that rely on block timestamps can be vulnerable to miner manipulation.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract CliffVesting {
    IERC20 public token;
    uint256 public cliffEndBlock;
    mapping(address => uint256) public balances;
    mapping(address => bool) public hasClaimed;
    constructor(IERC20 _token, uint256 _cliffEndBlock) {
        token = _token;
        cliffEndBlock = _cliffEndBlock;
    }
    function claim() external {
        require(block.number >= cliffEndBlock, "Cliff period not ended");
        require(!hasClaimed[msg.sender], "Already claimed");
        uint256 amount = balances[msg.sender];
        require(amount > 0, "No tokens to claim");
        hasClaimed[msg.sender] = true;
        token.transfer(msg.sender, amount);
    }
}

Code Example: Cliff Vesting Using Block Numbers

  1. Block numbers are used instead of timestamps to prevent miner manipulation.

  2. A hasClaimed flag ensures that users can only claim tokens once.

Milestone vesting releases tokens only when specific achievements or goals are met, such as product launches or key milestones in project development.

  1. Milestone Validation: If milestones are validated off-chain, insiders could falsely claim that a milestone has been met.

  2. Oracle Manipulation: If oracles are used to validate milestones, compromising the oracle could lead to premature or fraudulent token releases.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/access/Ownable.sol";
contract MilestoneVesting is Ownable {
    IERC20 public token;
    uint256 public milestoneCount;
    mapping(address => bool) public approvedSigners;
    mapping(uint256 => bool) public milestonesCompleted;
    event MilestoneCompleted(uint256 milestoneId);
    constructor(IERC20 _token) {
        token = _token;
    }
    modifier onlySigners() {
        require(approvedSigners[msg.sender], "Not an approved signer");
        _;
    }
    function addSigner(address signer) external onlyOwner {
        approvedSigners[signer] = true;
    }
    function completeMilestone(uint256 milestoneId) external onlySigners {
        require(!milestonesCompleted[milestoneId], "Milestone already completed");
        milestonesCompleted[milestoneId] = true;
        emit MilestoneCompleted(milestoneId);
    }
    function claimVestedTokens(uint256 milestoneId) external {
        require(milestonesCompleted[milestoneId], "Milestone not completed");
        uint256 amount = calculateReward(milestoneId, msg.sender);
        token.transfer(msg.sender, amount);
    }
    function calculateReward(uint256 milestoneId, address user) internal view returns (uint256) {
        // Logic for calculating vested tokens
        return 1000; // Example: 1000 tokens
    }
}

Code Example: Milestone Vesting with Multi-Signature Validation

  1. Multi-signature (multi-sig) verification ensures milestones are validated by multiple authorized parties.

  2. Event emissions (`MilestoneCompleted`) allow transparency and help with audit trails.

Hybrid vesting combines multiple vesting patterns, such as combining a cliff with linear vesting, to allow more flexibility while maintaining security.

  1. Increased Complexity: As contract logic becomes more complex, auditing and security become more challenging, increasing the risk of hidden vulnerabilities.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract HybridVesting {
    IERC20 public token;
    mapping(address => uint256) public balances;
    mapping(address => uint256) public lastClaim;
    mapping(address => bool) public cliffClaimed;
    uint256 public cliffEndBlock;
    uint256 public vestingPeriod = 30 days;
    constructor(IERC20 _token, uint256 _cliffEndBlock) {
        token = _token;
        cliffEndBlock = _cliffEndBlock;
    }
    function claimCliff() external {
        require(block.number >= cliffEndBlock, "Cliff period not ended");
        require(!cliffClaimed[msg.sender], "Cliff already claimed");
        uint256 amount = balances[msg.sender] / 2;
        cliffClaimed[msg.sender] = true;
        token.transfer(msg.sender, amount);
    }
    function claimLinear() external {
        require(cliffClaimed[msg.sender], "Cliff must be claimed first");
        uint256 timePassed = block.timestamp - lastClaim[msg.sender];
        require(timePassed >= vestingPeriod, "Vesting period not over");
        uint256 amount = (balances[msg.sender] / 2) * timePassed / vestingPeriod;
        lastClaim[msg.sender] = block.timestamp;
        token.transfer(msg.sender, amount);
    }
}

Code Example: Hybrid Vesting with Modular Design

  1. Modular design separates logic for cliff and linear vesting, simplifying the auditing process.

  2. The contract ensures users can only claim tokens after the cliff is completed, maintaining the intended flow.

In large-scale projects, a pull-based model allows users to trigger their own token release, reducing gas load for the contract and ensuring scalability.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract PullVesting {
    IERC20 public token;
    mapping(address => uint256) public balances;
    mapping(address => uint256) public lastClaim;
    uint256 public vestingPeriod = 30 days;
    function claim() external {
        uint256 timePassed = block.timestamp - lastClaim[msg.sender];
        require(timePassed >= vestingPeriod, "Vesting period not over");
        uint256 amount = balances[msg.sender] * timePassed / vestingPeriod;
        balances[msg.sender] -= amount;
        lastClaim[msg.sender] = block.timestamp;
        token.transfer(msg.sender, amount);
    }
}

Code Example: Pull-Based Vesting

  1. The pull-based model reduces gas costs for the contract and ensures that users bear the gas costs of claiming their tokens.

  1. Reentrancy Protection: Always implement reentrancy guards using patterns like OpenZeppelin’s `ReentrancyGuard`.

  2. Gas Efficiency: Use pull-based models and avoid loops that could exceed gas limits, especially in large contracts with many participants.

  3. Time Manipulation Mitigation: Use block numbers instead of timestamps to avoid miner manipulation.

  4. Multi-Signature Validation: For milestone-based vesting, require multiple trusted signers to confirm milestones.

Token vesting is a powerful mechanism to ensure long-term alignment in Web3 projects. However, poorly designed vesting contracts can be exploited, leading to catastrophic losses. By understanding common token vesting patterns and applying the right security practices, you can implement vesting mechanisms that not only align incentives but also ensure the security and integrity of your project.

Website: 0xCommit.com

X: 0xCommitAudits

Telegram: 0xCommitAudits

Read the original on 0xcommit.substack.com

Comments

Nothing yet. Say the first thing.

    Sign in to join the conversation.