Introduction
In the evolving landscape of blockchain-based applications, ensuring fair and transparent token distribution is critical—especially in scenarios involving investors, teams, and contributors. Token vesting contracts solve this challenge by locking tokens and releasing them over time, thus preventing early dumps and fostering long-term commitment.
In this article, we will walk through the complete development of a token vesting decentralized application (dApp) deployed on the Polygon network. We will start by writing two robust smart contracts using Solidity: one for a custom ERC-20 token and another for managing time-based token vesting schedules. We’ll then deploy these contracts using the Hardhat framework and finally create an intuitive and responsive frontend using React.js that allows users to connect their MetaMask wallets and view their vesting details in real time. Whether you're a smart contract developer, a frontend engineer, or a Web3 enthusiast, this guide will help you understand the technical underpinnings and architectural decisions involved in building a real-world dApp from scratch.
Prerequisites
Ensure you have the following installed:
- Node.js (v18+ recommended)
- Hardhat (npm install --save-dev hardhat)
- MetaMask extension in your browser
- Polygon wallet funded with test or real POL
- Polygon RPC URL
Step 1. Setting Up the Project Directory
Create a folder for your project and initialize it
mkdir vesting-dapp
cd vesting-dapp
npm init -y
Install Hardhat
npm install --save-dev hardhat
Initialize Hardhat project
npx hardhat
Choose “Create a basic sample project” and install dependencies when prompted.
Install OpenZeppelin contracts
npm install @openzeppelin/contracts
Install dotenv for environment variables
npm install dotenv

Step 2. Writing Smart Contracts
Inside contracts/ directory, create two files
Token.sol
This Solidity contract defines a simple ERC-20 token named "MyToken" (symbol: MTK) using OpenZeppelin's standard. It mints an initial supply to the deployer's address during deployment.
// SPDX-License-Identifier: MIT
pragma solidity ^ 0.8 .20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MyToken is ERC20 {
constructor(uint256 initialSupply) ERC20("MyToken", "MTK") {
_mint(msg.sender, initialSupply);
}
}
VestingContract.sol
- This Solidity smart contract implements a secure time-based token vesting system using OpenZeppelin libraries.
- It allows the contract owner to lock a specified amount of ERC-20 tokens for a beneficiary, releasing them periodically over a predefined schedule.
- The VestingSchedule struct tracks each beneficiary's vesting parameters, including total tokens, release interval, and progress.
- The createTimeBasedVesting() function initializes a vesting plan by transferring tokens into the contract, while releaseTokens() lets users claim the tokens they've earned based on elapsed time.
- It includes helper view functions like getReleasableAmount and getVestingInfo for frontend integration and transparency.
// SPDX-License-Identifier: MIT
pragma solidity ^ 0.8 .20;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract VestingContract is Ownable {
struct VestingSchedule {
uint256 totalAmount;
uint256 amountReleased;
uint256 startTime;
uint256 interval;
uint256 amountPerInterval;
uint256 numberOfIntervals;
bool revoked;
}
IERC20 public immutable token;
mapping(address => VestingSchedule) public vestingSchedules;
event VestingCreated(address indexed beneficiary, uint256 totalAmount);
event TokensReleased(address indexed beneficiary, uint256 amount);
event VestingRevoked(address indexed beneficiary);
constructor(IERC20 _token) Ownable(msg.sender) {
token = _token;
}
function createTimeBasedVesting(
address beneficiary,
uint256 totalAmount,
uint256 startTime,
uint256 endTime,
uint256 interval
) external onlyOwner {
require(vestingSchedules[beneficiary].totalAmount == 0, "Already exists");
require(totalAmount > 0 && interval > 0);
require(endTime > startTime);
uint256 duration = endTime - startTime;
uint256 numberOfIntervals = duration / interval;
require(totalAmount % numberOfIntervals == 0);
uint256 amountPerInterval = totalAmount / numberOfIntervals;
vestingSchedules[beneficiary] = VestingSchedule({
totalAmount,
amountReleased: 0,
startTime,
interval,
amountPerInterval,
numberOfIntervals,
revoked: false
});
require(token.transferFrom(msg.sender, address(this), totalAmount));
emit VestingCreated(beneficiary, totalAmount);
}
function releaseTokens() external {
VestingSchedule storage schedule = vestingSchedules[msg.sender];
require(schedule.totalAmount > 0 && !schedule.revoked);
uint256 elapsed = block.timestamp - schedule.startTime;
uint256 intervals = elapsed / schedule.interval;
if (intervals > schedule.numberOfIntervals) {
intervals = schedule.numberOfIntervals;
}
uint256 releasable = (intervals * schedule.amountPerInterval) - schedule.amountReleased;
require(releasable > 0);
schedule.amountReleased += releasable;
require(token.transfer(msg.sender, releasable));
emit TokensReleased(msg.sender, releasable);
}
function getReleasableAmount(address beneficiary) external view returns(uint256) {
VestingSchedule memory schedule = vestingSchedules[beneficiary];
if (schedule.totalAmount == 0 || schedule.revoked) return 0;
uint256 elapsed = block.timestamp - schedule.startTime;
uint256 intervals = elapsed / schedule.interval;
if (intervals > schedule.numberOfIntervals) {
intervals = schedule.numberOfIntervals;
}
uint256 totalReleasable = intervals * schedule.amountPerInterval;
return totalReleasable - schedule.amountReleased;
}
function getVestingInfo(address beneficiary) external view returns(VestingSchedule memory) {
return vestingSchedules[beneficiary];
}
Step 3. Deployment Scripts
Creating the ERC-20 Token Smart Contract
- This Hardhat script is used to deploy a standard ERC-20 token named TestToken. It first accesses the contract factory using hre.ethers.getContractFactory and deploys the contract without any constructor parameters.
- After deployment, it waits until the contract is fully mined on the blockchain using token.deployed(). Once the deployment is complete, it prints the deployed contract address to the console.
- The main function is wrapped with error handling to catch and log any deployment errors.
const hre = require("hardhat");
async function main() {
const Token = await hre.ethers.getContractFactory("TestToken");
const token = await Token.deploy();
await token.deployed();
console.log("ERC20 Token deployed to:", token.address);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
Creating the Monthly Token Vesting Smart Contract
- This Hardhat script deploys the MonthlyTokenVesting smart contract using a previously deployed ERC-20 token address. It first retrieves the contract factory using hre.ethers.getContractFactory, then deploys the vesting contract by passing the token address to the constructor.
- Once deployed, it logs the newly created vesting contract's address to the console. The main function is wrapped with error handling to gracefully catch and log any deployment issues.





Join the conversation! Your thoughts help the community grow.