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.
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.
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)
| 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 |
Linear vesting with a cliff gate:
- Before
cliff_time: nothing is claimable, regardless of elapsed time sincestart_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- 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_timeandrevoked_timeare always read from Solana'sClocksysvar, 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.
claimrequires a live signature from the exact beneficiary stored on the vesting account, checked viahas_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.
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- Rust
- Solana CLI (
solana --version) - Anchor CLI via
avm, pinned to the version inAnchor.toml:avm install 1.1.2 avm use 1.1.2
- Node.js + npm (or Bun)
# 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 testThat'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.
# 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- Anchor (Rust) - on-chain program
@coral-xyz/anchor+@solana/spl-token- TypeScript client/testsnode:test- test runner
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

