Skip to content

shri-prakhar/LST_CONTRACT

Repository files navigation

LST Platform (Liquid Staking Token) — Anchor Program

A Solana Anchor program that provides a simple liquid staking mechanism: users stake SOL into a program-controlled vault and receive a 1:1-pegged token (pSOL) minted to their token account. The program supports: staking SOL, requesting an unstake (starts a cooldown), claiming unstaked SOL after the cooldown (burn pSOL), and updating yield which adjusts the exchange rate.

This repository contains the on-chain program, migrations, test harness, and generated TypeScript types.

Key files

  • programs/lst-platform/src/lib.rs — Anchor program implementation (instructions, accounts, events, errors).
  • Anchor.toml — Anchor configuration.
  • migrations/deploy.ts — deployment migration script.
  • tests/lst-platform.ts — TypeScript test file / integration tests.
  • idl/lst_platform.json — program IDL (generated by Anchor build).
  • types/lst_platform.ts — generated typescript types for the IDL.

Quick summary (what it does)

  • Stake SOL: user transfers SOL to the program vault PDA; the program mints pSOL tokens to the user.
  • Request unstake: user declares how many pSOL to redeem and starts a cooldown timer.
  • Claim unstake: after cooldown, user burns pSOL and receives SOL from the vault.
  • Update yield: admin can deposit reward SOL which updates the exchange rate between SOL and pSOL.

Accounts & PDAs

The program uses a few PDAs and account structs:

  • global state PDA (seed: b"globalState") — GlobalState holds admin, total_staked_sol, total_psol_supply, exchange_rate, cooldown_period.
  • vault PDA (seed: b"vault") — a system account that holds SOL deposited by stakers and acts as mint authority for the pSOL mint.
  • pSOL mint — mint account whose authority is the vault PDA.
  • user state PDA (seed: [b"user", user_pubkey]) — UserState tracks per-user stake, pending_unstake, cooldown_start.

See programs/lst-platform/src/lib.rs for exact types and layouts.

Instructions (program API)

  • initialiseglobalstate(ctx, cooldown_period: i64) — initialize GlobalState, sets admin, exchange_rate (initially 1_000_000_000), cooldown_period, and total counters.

  • stakesol(ctx, amount: u64) — user sends native SOL to the vault; the program mints pSOL based on current exchange_rate. Validates amount > 0.

  • requestunstake(ctx, psol_amount: u64) — mark user's pending_unstake and set cooldown_start = now. Prevents immediate claim.

  • claimunstake(ctx) — after cooldown finished, burns pending_unstake pSOL from user token account and transfers corresponding SOL from the vault to the user.

  • updateyield(ctx, new_reward: u64) — admin deposits new_reward SOL (or the value is added to total_staked_sol), recalculates exchange_rate = total_staked_sol * 1e9 / total_psol_supply.

Events

The program emits events for monitoring:

  • StakeEvent { user, sol_amount, psol_minted }
  • UnstakeRequestedevent { user, psol_amount, unloack_time }
  • ClaimUnstakedEvent { user, sol_amount }
  • YieldUpdateEvent { admin, new_exchange_rate, total_staked_sol }

Errors

Custom program errors exposed by the contract (see LstError):

  • InvalidAmount — amount must be > 0.
  • Overflow — arithmetic overflow on checked operations.
  • NoStake — user has no active stake.
  • NothingToClaim — user has no pending_unstake.
  • CooldownNotFinished — cooldown period not passed yet.
  • VaultInsufficient — vault does not have enough SOL to satisfy an unstake.

Build / Test / Deploy (local dev)

Prerequisites

  • Rust toolchain (stable or the one specified in rust-toolchain.toml)
  • Solana CLI
  • Anchor CLI (install via cargo install --git https://github.com/coral-xyz/anchor --tag <version> or recommended install)
  • Node.js (for tests and migrations)
  • Yarn or npm

Common commands

Build the program (Anchor):

anchor build

Run tests (this will start a local test validator and run the TypeScript integration tests):

anchor test

Deploy to a local validator or configured cluster (make sure Anchor.toml has correct cluster/provider):

anchor deploy

You can also run the Solana test validator manually:

solana-test-validator --reset
# then in another terminal
anchor deploy

Using the TypeScript client / IDL

After anchor build, the IDL is available at idl/lst_platform.json. Generated TypeScript types are in types/lst_platform.ts.

A typical interaction flow using Anchor/TypeScript (pseudo):

  1. Call initialiseglobalstate as admin to create GlobalState, mint pSOL and vault PDAs.
  2. User calls stakesol with SOL lamports.
  3. User receives pSOL in their token account.
  4. User calls requestunstake(psol_amount) to begin cooldown.
  5. After cooldown passes, user calls claimunstake() which burns pSOL and receives SOL.

Refer to tests/lst-platform.ts for concrete examples of using the Anchor test client.

Usage examples (TypeScript / Anchor)

The snippets below use Anchor's TypeScript client (v0.24+ method-style) and the generated types in types/lst_platform.ts. They assume you already built the program (anchor build) and have a local provider.

Note: SOL amounts are in lamports (1 SOL = 1_000_000_000 lamports). pSOL uses 9 decimals (mint decimals = 9), so token amounts are the raw integer representation.

import * as anchor from '@project-serum/anchor';
import { TOKEN_PROGRAM_ID } from '@solana/spl-token';
import { LstPlatform } from './types/lst_platform';

const provider = anchor.AnchorProvider.local();
anchor.setProvider(provider);
const program = anchor.workspace.LstPlatform as anchor.Program<LstPlatform>;

// helper: derive PDAs
const [globalStatePDA] = await anchor.web3.PublicKey.findProgramAddress([
	Buffer.from('globalState'),
], program.programId);
const [vaultPDA] = await anchor.web3.PublicKey.findProgramAddress([
	Buffer.from('vault'),
], program.programId);

// 1) Initialise global state (admin)
const cooldownSeconds = 60 * 60 * 24; // 1 day
await program.methods
	.initialiseglobalstate(new anchor.BN(cooldownSeconds))
	.accounts({
		globalstate: globalStatePDA,
		vault: vaultPDA,
		psolMint: /* pubkey for the pSOL mint (created by init) */,
		admin: provider.wallet.publicKey,
		systemProgram: anchor.web3.SystemProgram.programId,
		tokenProgram: TOKEN_PROGRAM_ID,
		rent: anchor.web3.SYSVAR_RENT_PUBKEY,
	})
	.rpc();

// 2) Stake SOL (user)
// Make sure the user has an associated token account for pSOL (ATA) before staking.
const user = provider.wallet.publicKey;
const userPsolAta = /* create/get ATA for psolMint for `user` */;
const oneSol = anchor.web3.LAMPORTS_PER_SOL;
await program.methods
	.stakesol(new anchor.BN(oneSol))
	.accounts({
		user,
		globalstate: globalStatePDA,
		vault: vaultPDA,
		psolMint: /* psol mint pubkey */, // `psol_minted` account in ctx
		userstake: /* derived user state PDA: [b"user", user.toBuffer()] */,
		userPsolAccount: userPsolAta,
		systemProgram: anchor.web3.SystemProgram.programId,
		tokenProgram: TOKEN_PROGRAM_ID,
		rent: anchor.web3.SYSVAR_RENT_PUBKEY,
	})
	.rpc();

// 3) Request unstake (start cooldown)
const psolToUnstake = new anchor.BN(/* amount in pSOL base units (u64) */);
await program.methods
	.requestunstake(psolToUnstake)
	.accounts({
		user,
		globalstate: globalStatePDA,
		userstake: /* user state PDA */,
	})
	.rpc();

// 4) Claim unstake (after cooldown)
await program.methods
	.claimunstake()
	.accounts({
		user,
		userstake: /* user state PDA */,
		globalstate: globalStatePDA,
		vault: vaultPDA,
		psolMint: /* psol mint pubkey */,
		userPsolAccount: userPsolAta,
		systemProgram: anchor.web3.SystemProgram.programId,
		tokenProgram: TOKEN_PROGRAM_ID,
	})
	.rpc();

// 5) Admin: update yield (deposit reward and recalculate exchange rate)
const rewardLamports = new anchor.BN(0.5 * anchor.web3.LAMPORTS_PER_SOL); // 0.5 SOL
await program.methods
	.updateyield(rewardLamports)
	.accounts({
		globalstate: globalStatePDA,
		vault: vaultPDA,
		admin: provider.wallet.publicKey,
	})
	.rpc();

Small notes:

  • Use @solana/spl-token helpers to create/get the user's associated token account for the pSOL mint before staking.
  • When deriving PDAs, include the same seeds used in the program (b"globalState", b"vault", b"user").
  • Amounts: SOL values are lamports; pSOL token amounts use the mint's decimals (9).

Security notes & considerations

  • Arithmetic uses checked operations and returns Overflow on wrapping. Always validate inputs at the client layer.
  • The pSOL mint authority is the program vault PDA. Keep the PDA derivation and seeds consistent when interacting.
  • The program stores SOL in the vault PDA lamports — ensure the vault has enough SOL before allowing claims.
  • Carefully manage the admin signer key. Admin-only instruction is updateyield (and initialiseglobalstate on setup).

Development and contribution

  • Extend tests in tests/ to cover edge cases: concurrent requests, fractional exchange rates, admin misuse, and insufficient vault balance.
  • Add CI to run anchor test on PRs.
  • Add a security audit checklist and fuzz tests before production deployment.

About

An LST anchor in anchor with all test suites

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors