Token Vesting Contracts
An overview from security perspective
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.
What is Token Vesting?
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.
Common Token Vesting Patterns and Their Security Risks
1. Linear Vesting
Linear vesting distributes tokens at a constant rate over a predefined period. For example, 1,000 tokens could be released monthly over two years.
Security Risks:
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.
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
Key Security Measures:
ReentrancyGuard ensures the function cannot be called multiple times in the same transaction.
Checks-effects-interactions pattern ensures that state changes happen before any token transfer, reducing vulnerability to reentrancy attacks.
2. Cliff Vesting
In cliff vesting, tokens are locked for a fixed period (the "cliff"), after which a percentage or the total allocation is released.
Security Risks:
Front-running: Attackers may exploit time delays in transaction processing to claim tokens before legitimate holders.
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
Key Security Measures:
Block numbers are used instead of timestamps to prevent miner manipulation.
A hasClaimed flag ensures that users can only claim tokens once.
3. Milestone Vesting
Milestone vesting releases tokens only when specific achievements or goals are met, such as product launches or key milestones in project development.
Security Risks:
Milestone Validation: If milestones are validated off-chain, insiders could falsely claim that a milestone has been met.
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
Key Security Measures:
Multi-signature (multi-sig) verification ensures milestones are validated by multiple authorized parties.
Event emissions (`MilestoneCompleted`) allow transparency and help with audit trails.
4. Hybrid Vesting
Hybrid vesting combines multiple vesting patterns, such as combining a cliff with linear vesting, to allow more flexibility while maintaining security.
Security Risks:
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
Key Security Measures:
Modular design separates logic for cliff and linear vesting, simplifying the auditing process.
The contract ensures users can only claim tokens after the cliff is completed, maintaining the intended flow.
5. Pull-Based Model for Gas Efficiency
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
Key Security Measures:
The pull-based model reduces gas costs for the contract and ensures that users bear the gas costs of claiming their tokens.
Best Practices for Secure Token Vesting
Reentrancy Protection: Always implement reentrancy guards using patterns like OpenZeppelin’s `ReentrancyGuard`.
Gas Efficiency: Use pull-based models and avoid loops that could exceed gas limits, especially in large contracts with many participants.
Time Manipulation Mitigation: Use block numbers instead of timestamps to avoid miner manipulation.
Multi-Signature Validation: For milestone-based vesting, require multiple trusted signers to confirm milestones.
Conclusion
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.
Connect With Us:
Website: 0xCommit.com
Telegram: 0xCommitAudits


