Skip to content

Repository files navigation

Multi-Tenant Token Vesting Protocol

A production-style token vesting program built on Solana using the Anchor framework. Any organization can permissionlessly spin up its own isolated vesting pool, allocate linear vesting schedules (with cliffs) to employees or contributors, and let beneficiaries claim vested tokens over time - all enforced entirely on-chain.

Built as a deep dive into Anchor account design, PDA architecture, and Solana security patterns beyond tutorial-level programs.

Why this exists

Most vesting contract examples online are single-tenant (one admin, one set of employees, hardcoded). This program is designed as a factory pattern: one deployed program instance can serve unlimited independent organizations, each with completely isolated pools, employees, and token vaults - no shared state, no admin approval needed to onboard.

Architecture

Account model

VestingPool (one per organization)
├── authority: Pubkey      - the org's admin wallet
├── mint: Pubkey           - which SPL token this pool vests
├── vault: Pubkey          - PDA-owned token account holding the tokens
├── bump / vault_bump

VestingProtocol (one per beneficiary, per pool)
├── pool: Pubkey           - parent pool reference
├── beneficiary: Pubkey    - the employee/recipient wallet
├── bump
├── total_allocation       - tokens granted
├── start_time             - vesting start (on-chain clock, not client-supplied)
├── cliff_time              - absolute timestamp before which nothing vests
├── duration                - total vesting length in seconds
├── amount_claimed          - running total withdrawn
├── revoked_time / is_revoked
└── initialized

Both account types are PDAs, namespaced so different organizations and different beneficiaries can never collide:

pool     = PDA(["pool"], authority)
vault    = PDA(["vault"], pool)
vesting  = PDA(["vesting"], pool, beneficiary)

Instructions

Instruction Caller Effect
create_pool anyone Permissionlessly creates a new pool + token vault for the caller
initialize pool authority Creates an empty vesting schedule for a named beneficiary
fund_pool pool authority Deposits SPL tokens from the authority's wallet into the pool's vault
allocate pool authority Sets the allocation amount and starts the vesting clock (on-chain time)
claim beneficiary Withdraws currently vested-but-unclaimed tokens via a PDA-signed CPI transfer
revoke pool authority Freezes further vesting accrual at the moment of revocation

Vesting math

Linear vesting with a cliff gate:

  • Before cliff_time: nothing is claimable, regardless of elapsed time since start_time.
  • Between cliff and start_time + duration: claimable amount grows linearly.
  • After start_time + duration: the full allocation is vested.
  • If revoked: accrual freezes permanently at revoked_time - the beneficiary keeps whatever vested up to that point, nothing more.
vested = if now < cliff_time {
    0
} else if now >= start_time + duration {
    total_allocation
} else {
    (total_allocation * (now - start_time)) / duration   // u128 intermediate to avoid overflow
};

claimable = vested - amount_claimed

Security design

  • PDA-derived identity, not client-trusted addresses. Every account's address is recomputed from seeds and checked against what's supplied - accounts can't be swapped or spoofed.
  • Tamper-proof timing. start_time and revoked_time are always read from Solana's Clock sysvar, never accepted as client input - no one can fake elapsed time to force early vesting.
  • Ownership enforced at the CPI layer, not just application logic. The token vault is owned by the pool's PDA, which has no private key. Tokens can only leave via a CPI signed with the pool's own seeds inside the program's code - no admin, including the pool's own authority, can manually withdraw from it.
  • Strict beneficiary isolation. claim requires a live signature from the exact beneficiary stored on the vesting account, checked via has_one, and the destination token account's ownership is independently verified. Neither the pool authority nor any other party can claim on a beneficiary's behalf.
  • Checked arithmetic throughout - no raw +/- on token amounts or timestamps; overflow/underflow is handled explicitly.

Tests

Full integration test suite (TypeScript, node:test) against a local validator, covering:

  • Happy path: pool creation → employee onboarding → funding → allocation → claiming
  • Revocation freezing accrual correctly (claim before and after revoke, confirmed no further accrual after real time passes)
  • Adversarial cases: pool authority cannot claim a beneficiary's tokens, unrelated wallets cannot claim, double-claims are rejected once nothing new has vested
anchor test

Screenshots

Tests passing Solana Logs

Installation / Running Locally

Prerequisites

  • Rust
  • Solana CLI (solana --version)
  • Anchor CLI via avm, pinned to the version in Anchor.toml:
    avm install 1.1.2
    avm use 1.1.2
  • Node.js + npm (or Bun)

Setup

# Clone the repo
git clone https://github.com/Enmilo-dev/vesting.git
cd vesting

# Install JS dependencies
npm install
# or: bun install

# Build the on-chain program
anchor build

# Run the full test suite (spins up a local validator automatically)
anchor test

That's it - no .env file or manual configuration needed. anchor test handles starting a local validator, deploying the program, funding the test wallet, and running the full instruction/integration test suite described above.

Useful commands

# Just build without running tests
anchor build

# Deploy to devnet (requires a funded devnet wallet)
anchor deploy --provider.cluster devnet

# Check your program's deployed address
anchor keys list

Stack

  • Anchor (Rust) - on-chain program
  • @coral-xyz/anchor + @solana/spl-token - TypeScript client/tests
  • node:test - test runner

Status / possible extensions

This is a portfolio/learning project, not audited for production use. Natural next steps if extended further:

  • Reclaim-unvested-tokens instruction for the authority after a revoke
  • Multiple concurrent vesting schedules per beneficiary within one pool
  • Fee-on-claim or vesting-NFT representations for secondary transferability

About

A vesting protocol on Solana chain made with Anchor and tested with TS.

Topics

Resources

Stars

Watchers

Forks

Contributors

Languages