T
iTokenly

Token Vesting Contract: Build One in Solidity Step by Step

Marcus Reynolds··Web3 & Development·Guide
Token Vesting Contract: Build One in Solidity Step by Step

Token Vesting Contract: Build One in Solidity Step by Step

Introduction: What You’ll Build in This Guide

A token vesting contract locks an ERC-20 token allocation on-chain and releases it to a beneficiary over time. In this guide, you will build a single-beneficiary vesting contract, test the open up math, deploy it to a testnet, verify the source code, and learn when you should use OpenZeppelin instead of writing custom Solidity.

The practical take is simple: production teams should usually start from OpenZeppelin VestingWallet and customize only when the agreement needs a cliff, revocation, staking rights, stepped releases, or many beneficiaries. That caution fits the security-first design culture promoted by Vitalik Buterin, Co-founder at Ethereum Foundation, where simple, reviewable code is preferred when real value is locked.

As of May 2026, this guide uses Solidity 0.8.24 as a stable reference point (Solidity release blog, January 2024) and OpenZeppelin Contracts 5.0.0+ as the library baseline (OpenZeppelin GitHub release, October 2023). VestingWallet itself was added in OpenZeppelin Contracts 4.3.0 (OpenZeppelin GitHub release, June 2021).

The final result

You will finish with a contract that holds one ERC-20 token for one beneficiary. It stores a token address, beneficiary address, start timestamp, cliff duration, total duration, total allocation, and a release() function that transfers only the amount currently vested.

The VEST-7 build framework

Use this original VEST-7 framework whenever you review a vesting schedule solidity implementation:

  1. Verify parameters: token, beneficiary, start, cliff, duration, allocation, and revocation rule.
  2. Establish the formula: write the vesting math before writing Solidity.
  3. Secure transfers: use checked ERC-20 transfer wrappers.
  4. Track releases: store the amount already released before external calls.
  5. Time-test boundaries: test before start, at cliff, midpoint, and final timestamp.
  6. Prove deployment: verify source code and constructor arguments.
  7. Document operations: record who can fund, revoke, recover, and administer the contract.

This framework gives you a repeatable checklist instead of a vague sense that the contract looks correct.

Prerequisites: Set Up Your Solidity Vesting Environment

Before you write the token vesting contract, set up a clean Solidity project. A predictable environment prevents confusing test failures later.

What you’ll need

  • Node.js 20 or newer: use it if you choose Hardhat.
  • Hardhat or Foundry: Hardhat is familiar for JavaScript tests; Foundry is fast for Solidity-native tests.
  • OpenZeppelin Contracts: install with npm install @openzeppelin/contracts.
  • A browser wallet: use MetaMask or another test wallet for Sepolia deployment.
  • A testnet explorer: Sepolia Etherscan helps you inspect and verify contracts.
  • A Sepolia RPC endpoint: use a provider such as Alchemy or Infura and store the URL in your local environment file.
  • Sepolia ETH and mock ERC-20 tokens: never test the first deployment with mainnet assets.

Deployment fees depend on network gas prices and bytecode size. Sepolia uses test ETH, while mainnet costs real ETH. Review how ETH gas fees work before you plan a production launch.

Knowledge to refresh

  1. ERC-20 transfers: your vesting contract will release tokens with a checked transfer call.
  2. block.timestamp: Solidity reads time as Unix seconds, not calendar dates.
  3. Integer math: Solidity has no floating-point division, so multiplication order matters.
  4. Addresses: one wrong character in a beneficiary or token address can be unrecoverable.
  5. Immutability: a normal deployed contract cannot be edited after deployment.

Warning: Never test vesting with real funds first. A wrong token address, wrong beneficiary, or bad timestamp can lock tokens permanently. Simulate the full schedule on Sepolia, including cliff expiry and final release, before you connect a mainnet wallet.

Step 1: Define the Vesting Schedule in Solidity

Start by defining the agreement in plain language. A vesting schedule solidity implementation is only correct if the schedule inputs are correct.

A token vesting schedule needs these inputs

  1. Beneficiary: the wallet address that receives vested tokens.
  2. Token address: the ERC-20 contract being locked.
  3. Total allocation: the full token amount committed to this schedule.
  4. Start timestamp: the Unix time in seconds when vesting begins.
  5. Cliff: the lock-up period before any tokens are claimable.
  6. Duration: the total vesting period measured from the start timestamp.
  7. Release frequency: every second, daily, monthly, or fixed tranches.
  8. Revocation rule: whether an admin can cancel unvested tokens and where they go.

Choose the schedule type

Most beginner contracts should start with linear vesting because the formula is easy to test. More complex schedules need stronger review.

Schedule type

How it works

Best for

Linear

Tokens vest at a constant rate after any cliff.

Team allocations and investor rounds.

Cliff plus linear

Nothing vests until the cliff, then linear vesting continues.

Employee grants with a one-year cliff.

Stepped

Fixed percentages release at fixed dates.

Advisor deals and staged community rewards.

Milestone

Unlocks depend on an external event or oracle input.

Grants tied to measurable delivery targets.

Nick Szabo, computer scientist and originator of the smart-contracts concept, is a useful reference point here because vesting works best when the contract can verify the rule deterministically. If a milestone requires off-chain judgment, you need governance or oracle design, not only a vesting formula.

Convert dates into timestamps

Solidity understands Unix timestamps. A year written as 365 days compiles to 31,536,000 seconds (Solidity time-units documentation, accessed May 2026).

Do not hard-code today’s date in tests. Use Hardhat time helpers or Foundry vm.warp() so your test suite can jump to the cliff date, midpoint, and final release date in seconds.

Write the formula before the contract

Open a spreadsheet before you open your Solidity file. Write this formula first:

vestedAmount = totalAllocation × (currentTime − startTime) / duration

Then test it with concrete values. For example, if totalAllocation is 1,000e18, duration is 31,536,000 seconds, and elapsed time is half the duration, the vested amount should be 500e18.

Step 2: Choose Your Contract Architecture

Next, decide whether to inherit from audited code or write custom logic. This choice affects audit cost, testing burden, and how much can go wrong.

Architecture decision table

Approach

Best for

Tradeoffs

OpenZeppelin VestingWallet

Single beneficiary, linear vesting, ETH or ERC-20 release.

Lowest custom-code risk, but no revocation or multi-beneficiary accounting by default.

Custom single-beneficiary contract

One recipient with revocation, custom cliffs, staking hooks, or stepped math.

Flexible, but every branch must be tested and reviewed.

Multi-beneficiary vesting pool

Teams, investor rounds, grants, or airdrops with many recipients.

Efficient for many schedules, but state accounting and access control become harder.

Use OpenZeppelin when possible

Use OpenZeppelin VestingWallet when your schedule is linear and has one beneficiary. If you need a cliff, check the current OpenZeppelin releases page before you select the exact package version because constructor interfaces can change between major versions.

Customize only when the schedule demands it

Write a custom token vesting contract only when the agreement requires logic that the base contract does not provide. Common reasons include revocable grants, many beneficiaries, non-linear tranches, staking rights during lock-up, or governance restrictions.

If a milestone release depends on off-chain facts, involve oracle design early. That is where the work of Sergey Nazarov, Co-founder at Chainlink Labs, is relevant: oracle systems exist because smart contracts need reliable external data when the chain cannot verify a condition by itself.

Step 3: Write the Token Vesting Contract

Now you can write the contract. The example below is a single-beneficiary ERC-20 vesting contract with a cliff and linear vesting.

Install the dependencies

npm install @openzeppelin/contracts

Add these imports at the top of your Solidity file:

import '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol';

SafeERC20 wraps token transfers so non-standard ERC-20 behavior is handled more safely. That is important because not every token returns a clean boolean from transfer().

Declare state variables

using SafeERC20 for IERC20; IERC20 public immutable token; address public immutable beneficiary; uint256 public immutable start; uint256 public immutable duration; uint256 public immutable cliff; uint256 public immutable totalAllocation; uint256 public released;

The immutable variables are set once in the constructor. released stays mutable because it records how many tokens have already left the contract.

Add constructor validation

constructor( address _token, address _beneficiary, uint256 _start, uint256 _duration, uint256 _cliff, uint256 _totalAllocation ) { require(_token != address(0), 'zero token'); require(_beneficiary != address(0), 'zero beneficiary'); require(_duration > 0, 'duration zero'); require(_cliff <= _duration, 'cliff too long'); require(_totalAllocation > 0, 'allocation zero'); token = IERC20(_token); beneficiary = _beneficiary; start = _start; duration = _duration; cliff = _cliff; totalAllocation = _totalAllocation; }

The cliff check matters. If the cliff is longer than the full duration, the schedule can behave in a way no beneficiary expects.

Add events

event TokensReleased(address indexed beneficiary, uint256 amount);

Emit the event whenever tokens move. Block explorers and internal dashboards can then display the release history without reading every storage value manually.

Use the complete starter contract

// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; contract SimpleTokenVesting { using SafeERC20 for IERC20; IERC20 public immutable token; address public immutable beneficiary; uint256 public immutable start; uint256 public immutable duration; uint256 public immutable cliff; uint256 public immutable totalAllocation; uint256 public released; event TokensReleased(address indexed beneficiary, uint256 amount); constructor(address _token, address _beneficiary, uint256 _start, uint256 _duration, uint256 _cliff, uint256 _totalAllocation) { require(_token != address(0), 'zero token'); require(_beneficiary != address(0), 'zero beneficiary'); require(_duration > 0, 'duration zero'); require(_cliff <= _duration, 'cliff too long'); require(_totalAllocation > 0, 'allocation zero'); token = IERC20(_token); beneficiary = _beneficiary; start = _start; duration = _duration; cliff = _cliff; totalAllocation = _totalAllocation; } function vestedAmount(uint256 timestamp) public view returns (uint256) { if (timestamp < start + cliff) { return 0; } if (timestamp >= start + duration) { return totalAllocation; } uint256 elapsed = timestamp - start; return (totalAllocation * elapsed) / duration; } function releasable() public view returns (uint256) { return vestedAmount(block.timestamp) - released; } function release() external { uint256 amount = releasable(); require(amount > 0, 'nothing to release'); released += amount; token.safeTransfer(beneficiary, amount); emit TokensReleased(beneficiary, amount); } }

Step 4: Add the Enable and Release Mechanism

A token vesting contract calculates releasable tokens by taking the total allocation, computing the vested amount from elapsed time, cliff, and duration, then subtracting the amount already released. Before the cliff it returns zero. During vesting it releases the linear share. After the duration it releases the full remaining allocation.

Calculate vestedAmount

Your vesting function must handle four time windows:

  1. Before start: return 0.
  2. Before cliff: return 0.
  3. During vesting: return (totalAllocation * elapsed) / duration.
  4. After duration: return totalAllocation.

Use >= for boundary checks. Ethereum proof-of-stake slots are 12 seconds (ethereum.org block documentation, accessed May 2026), so you should not write logic that depends on one exact second.

Implement release()

Follow checks, effects, interactions:

  1. Calculate the releasable amount.
  2. Revert if the amount is zero.
  3. Increase released before calling the token contract.
  4. Transfer tokens with safeTransfer.
  5. Emit TokensReleased.

Warning: token decimals are not percentages

If you want to vest 1,000 tokens and the token uses 18 decimals, pass 1000 * 10**18, not 1000. Tokens such as USDC use 6 decimals, so always check the token’s decimals() value before deployment.

Mentor note: Add a deployment check that confirms the contract is funded with the expected allocation before the schedule is announced. Underfunded vesting contracts confuse beneficiaries and create avoidable support work.

Step 5: Test the Vesting Contract Before Deployment

Testing is where you prove the math. Do not rely on visual inspection of the Solidity code.

Use the VEST-7 test matrix

The table below is a generated test dataset you can copy into Hardhat or Foundry. It uses a 1,000-token allocation, a 100-second duration, and a 25-second cliff.

Timestamp

Expected vested tokens

Reason

start - 1

0

Schedule has not started.

start + 24

0

Cliff has not passed.

start + 25

250

Cliff boundary reached.

start + 50

500

Half the duration has elapsed.

start + 100

1000

Full duration has elapsed.

Write time-travel tests

In Hardhat, use the network helper package and call time.increaseTo(targetTimestamp). In Foundry, use vm.warp(timestamp). Test the cliff boundary, midpoint, final timestamp, and a second release in the same block.

Compare against expected output

Your local test output should look similar to this transcript when the boundary cases pass:

vesting math before cliff: expected 0, got 0 vesting math at midpoint: expected 500e18, got 500e18 vesting math at end: expected 1000e18, got 1000e18 repeated release: expected 0 newly releasable, got 0

If your contract output differs from the spreadsheet, assume the contract is wrong until you find the reason.

Test edge cases

  • Start equals current timestamp: confirm vesting begins immediately.
  • Cliff equals duration: confirm no tokens release until the end.
  • Duration fully elapsed: confirm one call can release the remaining allocation.
  • Late funding: confirm the schedule still uses the original start time.
  • Zero balance: confirm the call does not silently record a false release.
  • Repeated release: confirm the same vested amount cannot be paid twice.

Add security-focused tests

  • Access control: decide whether anyone can call release() or only the beneficiary, then test that rule.
  • Reentrancy order: confirm internal accounting changes before the token transfer.
  • Unusual ERC-20 behavior: test with a fee-on-transfer mock if your token might charge fees.
  • Timestamp tolerance: test near the cliff boundary instead of only exact timestamps.
  • Denial of service: confirm no unrelated external call can block beneficiary releases.

The OWASP Smart Contract Top 10 risks are worth reviewing with your test suite because access control failures and unexpected reverts apply directly to vesting contracts.

Step 6: Deploy and Verify the Vesting Contract

Deploy only after tests pass. The safe order is testnet deploy, testnet funding, source-code verification, then production deployment.

Deploy to a testnet first

Run a deployment command such as:

npx hardhat run scripts/deploy.js --network sepolia

Before you press Enter, check these constructor arguments:

  • Token address: the ERC-20 contract you are vesting.
  • Beneficiary address: the recipient wallet, not the deployer by accident.
  • Start time: a Unix timestamp in seconds.
  • Cliff duration: for example, 15,552,000 seconds for about six months.
  • Total duration: for example, 126,144,000 seconds for four years.

Fund the contract

Deploying the vesting contract does not move tokens into it. From the token owner wallet, send the full allocation to the vesting contract address with transfer(vestingContractAddress, totalAmount).

After the transfer, open the contract address on the block explorer and confirm the token balance. Do this before you send the address to investors, employees, or community members.

Verify the contract on a block explorer

Source-code verification lets anyone read the deployed code. Use Etherscan Verify and Publish or your framework’s verify task.

  1. Select the compiler type that matches your deployment artifact.
  2. Select the exact compiler version, such as v0.8.24.
  3. Match optimization settings and run count.
  4. Submit the constructor arguments exactly as deployed.

If you used imports, prefer standard JSON verification from your build tool instead of manual copy-paste. Manual flattening can introduce mistakes if duplicate license lines or import paths are handled incorrectly.

Security Checklist for a Production Token Vesting Contract

Mainnet deployment is a one-way door for most teams. Run this checklist before you lock meaningful token value.

  • Validate addresses: check token, beneficiary, owner, and treasury addresses against the intended chain.
  • Test timestamps: simulate before start, cliff, midpoint, final release, and repeated calls.
  • Use SafeERC20: avoid raw transfer() calls for ERC-20 releases.
  • Verify source code: publish source and constructor arguments on the relevant block explorer.
  • Audit release math: compare Solidity output with an independent spreadsheet model.
  • Protect admin keys: use a multisig wallet or hardware-backed admin process, not a single hot wallet.
  • Document revocation rules: state who can revoke, when they can revoke, and where unvested tokens go.

Audit the release math

A one-line arithmetic error can overpay on day one or underpay forever. Test exact values at the cliff timestamp, midpoint, and start + duration. If any value is off by one wei, stop and debug before deployment.

For anything beyond simple linear vesting, get another reviewer involved. You can build a stronger baseline with free smart contract security training before paying for a professional audit.

Decide whether revocation is allowed

Revocation protects a project when a contributor leaves early, but it weakens beneficiary certainty. If you include it, restrict the function to a multisig and consider a time delay. Also confirm the on-chain rule matches the signed agreement.

Team and investor allocations can have legal consequences. Read when to consult a crypto lawyer before shipping a revocable schedule tied to valuable tokens.

Check operational risks

Risk

Mitigation

Compromised deployer key

Deploy from a hardware-backed wallet and transfer ownership to a multisig process.

Wrong token address

Send a small test transfer before funding the full allocation.

Unverified source code

Add verification to the deployment script.

No recovery path

If needed, add recovery only for unrelated tokens, never for vested beneficiary tokens.

Governance exposure

Review DAO security best practices if the vested token controls votes.

Run static analysis, fuzzing, and peer review before mainnet. Tooling will not replace judgment, but it catches mistakes that manual review often misses.

Frequently Asked Questions

What is token vesting?
Token vesting is a mechanism that locks tokens and releases them gradually over a set period. It's commonly used for founder allocations, employee grants, investor unlocks, and ecosystem incentives. By controlling when tokens become accessible, vesting aligns long-term incentives and prevents sudden large sell-offs that could destabilize a token's price.
What is a vesting contract?
A vesting contract is a smart contract that holds tokens and releases them according to predefined rules covering the beneficiary, start time, cliff, duration, and release function. Once deployed on-chain, bugs or misconfigured parameters are difficult or impossible to correct, so thorough testing before deployment is essential.
What is the primary purpose of a token vesting schedule?
The main purpose is to control when recipients can access their tokens. Vesting schedules encourage long-term commitment from teams, advisors, investors, and grant recipients. They also make token unlocks transparent and predictable, helping communities accurately model future circulating supply and plan accordingly.
What is a token contract?
A token contract is a smart contract that creates and manages a token's balances, transfers, approvals, and total supply rules. A vesting contract is typically a separate contract — it holds tokens issued by the token contract and releases them to beneficiaries over time according to a defined schedule.
What is a vesting schedule?
A vesting schedule is the timeline and formula that determines how an allocation unlocks. Key components include a start date, cliff period, total duration, release frequency, and total allocation size. A common example: 25% unlocks after a one-year cliff, with the remaining 75% releasing linearly over the following three years.
Is Solidity still in demand?
Yes, Solidity remains widely used in 2026. Ethereum and EVM-compatible chains continue to host the majority of DeFi protocols, NFT platforms, DAOs, and token infrastructure. Demand is strongest for developers who combine Solidity proficiency with solid skills in security auditing, testing frameworks, gas optimization, and production deployment workflows.
What is a 10 20 30 40 vesting schedule?
A 10/20/30/40 schedule typically means tokens unlock in stepped increments — 10%, 20%, 30%, and 40% — at separate milestones or dates. The percentages alone are incomplete without defined trigger dates or conditions. Both the contract logic and any accompanying legal agreement must specify exactly when each tranche becomes available.
How is the vesting schedule calculated?
Linear vesting uses this formula: vested amount equals total allocation multiplied by elapsed time divided by total duration, applied only after any cliff has passed. Stepped schedules instead unlock fixed amounts at predefined milestones. Always write tests that verify the exact vested amounts at multiple timestamps to catch calculation errors early.

Author

Marcus Reynolds - Crypto analyst and blockchain educator
Marcus Reynolds

Crypto analyst and blockchain educator with over 8 years of experience in the digital asset space. Former fintech consultant at a major Wall Street firm turned full-time crypto journalist. Specializes in DeFi, tokenomics, and blockchain technology. His writing breaks down complex cryptocurrency concepts into actionable insights for both beginners and seasoned investors.

Related articles