Skip to content

talhadaar/multitoken-vesting

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

14 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

MultiTokenVesting

A gas-optimized, multi-token vesting contract written in Solidity. This contract allows an admin (Owner) to create revocable, linear vesting schedules with optional cliffs for multiple beneficiaries across different ERC20 tokens.

⚡ Features

  • Multi-Token Support: One contract can handle vesting for any number of different ERC20 tokens (USDC, WETH, UNI, etc.).

  • Revocable: The Owner can revoke a schedule at any time.

  • Vested tokens are immediately sent to the beneficiary.

  • Unvested tokens are refunded to the Owner.

  • Gas Optimized:

  • Packed Structs: Booleans (revoked, claimed) and addresses are tightly packed to save storage costs.

  • Custom Errors: Uses error ScheduleWasRevoked() instead of expensive string revert messages.

  • Direct Indexing: Uses array indices for O(1) gas efficiency during claims.

  • Safety Features:

  • Solvency Check: Tokens are transferred into the contract at creation, guaranteeing funds are available.

  • Excess Withdrawal: Owner can recover accidentally sent tokens that are not part of any schedule.

🪙 Supported Tokens

This contract is designed exclusively for ERC20 tokens.

  • Supported: Any standard ERC20 token (e.g., USDC, UNI, LINK, WETH).
  • Not Supported: Native chain currencies (e.g., AGNG, PEAQ) are not supported directly.

📐 Vesting Logic

The vesting logic follows a standard linear release schedule:

  1. Before Cliff: 0 tokens are releasable.
  2. After Cliff, Before End: Tokens are released linearly based on time passed since start.
  3. After End: 100% of tokens are releasable.

Formula:

VestedAmount = (TotalAmount * (CurrentTime - StartTime)) / Duration

Example Scenario

  • Total: 1,000 Tokens
  • Duration: 1,000 Seconds
  • Cliff: 250 Seconds (25%)
Time (s) Status Vested Amount
0s Started 0
200s Inside Cliff 0
250s Cliff Ends 250 (25%)
500s Linear Vesting 500 (50%)
1000s Finished 1,000 (100%)

🚀 Installation & Setup

This project uses Foundry. Ensure you have Foundry installed.

1. Clone the Repo

git clone <your-repo-url>
cd multi-token-vesting

2. Install Dependencies

forge install OpenZeppelin/openzeppelin-contracts --no-commit

3. Build

forge build

4. Run Tests

forge test -vv

🛠 Usage

1. Creating a Schedule (Owner Only)

Before creating a schedule, you must approve the vesting contract to spend your tokens.

Step A: Approve Tokens Call approve() on the Token Contract (e.g., USDC).

// Call this on the TOKEN contract, NOT the vesting contract
IERC20(tokenAddress).approve(vestingContractAddress, amount);

Step B: Create Schedule Call createVestingSchedule on the Vesting Contract. The contract will pull the tokens from your wallet automatically.

Currently, there is no validation to ensure that schedule.start is set in the future. This could lead to incorrect vestedAmount calculation in calculateReleasableAmount. Please ensure a valid start timing.

vestingContract.createVestingSchedule(
    0xBeneficiary...,   // Beneficiary Address
    0xToken...,         // Token Address
    1000 * 10**18,      // Amount
    block.timestamp,    // Start Time
    2592000,            // Cliff Duration (e.g., 30 days)
    31536000            // Total Duration (e.g., 1 year)
);

2. Claiming Tokens (Beneficiary Only)

Beneficiaries claim their available tokens by passing the scheduleIndex.

// The index is emitted in the ScheduleCreated event
uint256 scheduleIndex = 0; 
vestingContract.claim(scheduleIndex);

3. Revoking a Schedule (Owner Only)

The owner can stop a schedule early.

vestingContract.revoke(scheduleIndex);
  • Result: The beneficiary receives all tokens earned up to this exact second. The remaining tokens are sent back to the Owner's wallet.

4. Withdraw Excess (Owner Only)

If you accidentally send tokens to the contract without creating a schedule, you can recover them.

// Withdraws only tokens that are NOT locked in active schedules
vestingContract.withdrawExcess(tokenAddress);

📦 API Reference

Structs

The VestingSchedule struct is packed to minimize storage costs.

struct VestingSchedule {
    address beneficiary;    // Address of the user
    uint64 start;           // Start timestamp
    bool revoked;           // Has the schedule been revoked?
    bool claimed;           // Have all tokens been claimed?
    address token;          // Token contract address
    uint64 duration;        // Duration of vesting in seconds
    uint64 cliff;           // Cliff duration in seconds
    uint256 totalAmount;    // Total tokens allocated
    uint256 amountClaimed;  // Total tokens withdrawn so far
}

Errors

Error Description
InvalidAddress Beneficiary or Token address is address(0).
InvalidAmount Vesting amount is 0.
InvalidDuration Duration is 0.
InvalidCliff Cliff is longer than the Duration.
Unauthorized Caller is not the beneficiary.
ScheduleClaimed All tokens have already been claimed.
NothingToClaim Current releasable amount is 0 (e.g., inside cliff).
InvalidIndex The provided schedule index does not exist.
ScheduleWasRevoked The schedule has been revoked and is closed.
InsufficientExcessBalance Attempted to withdraw tokens that are locked in vesting.

🛡 Security & Audit Info

  • Solidity Version: ^0.8.20
  • Centralization Risk: This contract allows the Owner to revoke schedules. This means beneficiaries must trust the Owner not to revoke schedules maliciously.
  • SafeMath: Not required (Solidity 0.8+ has built-in overflow protection).
  • SafeERC20: Used for all token transfers to handle non-compliant ERC20s (tokens that don't return bools).
  • Reentrancy: The claim function follows the Checks-Effects-Interactions pattern, updating state before transferring tokens.

📜 License

SPDX-License-Identifier: MIT

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors