From beb3019cbcea41f8bf1c66c53c091cb5d466361c Mon Sep 17 00:00:00 2001 From: Jason D Date: Fri, 6 Mar 2026 12:13:43 -0700 Subject: [PATCH 1/3] deprecate stake program unbonding period --- programs/vault-stake/src/account_structs.rs | 67 +---- programs/vault-stake/src/error.rs | 10 - programs/vault-stake/src/events.rs | 17 -- programs/vault-stake/src/lib.rs | 52 ++-- programs/vault-stake/src/processor.rs | 123 ++------- programs/vault-stake/src/state.rs | 5 +- scripts/common.sh | 1 - scripts/deploy.sh | 4 - scripts/vault-stake/initialize.ts | 10 +- tests/vault-stake.test.ts | 278 ++++++++------------ 10 files changed, 156 insertions(+), 411 deletions(-) diff --git a/programs/vault-stake/src/account_structs.rs b/programs/vault-stake/src/account_structs.rs index 2b1af31..5b1fc39 100644 --- a/programs/vault-stake/src/account_structs.rs +++ b/programs/vault-stake/src/account_structs.rs @@ -78,24 +78,6 @@ pub struct Pause<'info> { pub signer: Signer<'info>, } -#[derive(Accounts)] -pub struct UpdateConfig<'info> { - #[account( - mut, - seeds = [b"stake_config"], - bump = stake_config.bump - )] - pub stake_config: Account<'info, StakeConfig>, - - /// CHECK: This is the program data account that contains the update authority - #[account( - constraint = program_data.key() == get_program_data_address(&crate::id()) @ CustomErrorCode::InvalidProgramData - )] - pub program_data: UncheckedAccount<'info>, - - pub signer: Signer<'info>, -} - #[derive(Accounts)] pub struct Deposit<'info> { #[account( @@ -172,46 +154,10 @@ pub struct Deposit<'info> { pub token_program: Program<'info, Token>, } -#[derive(Accounts)] -pub struct Unbond<'info> { - #[account( - seeds = [b"stake_config"], - bump = stake_config.bump - )] - pub stake_config: Account<'info, StakeConfig>, - - #[account(mut)] - pub signer: Signer<'info>, - - #[account( - constraint = mint.key() == stake_config.mint @ CustomErrorCode::InvalidMint - )] - pub mint: Account<'info, Mint>, - - #[account( - token::mint = stake_config.mint, - constraint = user_mint_token_account.mint == stake_config.mint @ CustomErrorCode::InvalidMint, - constraint = user_mint_token_account.owner == signer.key() @ CustomErrorCode::InvalidMintAuthority - - )] - pub user_mint_token_account: Account<'info, TokenAccount>, - - #[account( - init, - payer = signer, - space = UnbondingTicket::LEN, - seeds = [b"ticket", signer.key().as_ref()], - bump - )] - pub ticket: Account<'info, UnbondingTicket>, - - pub system_program: Program<'info, System>, -} - #[derive(Accounts)] pub struct Redeem<'info> { #[account( - seeds = [b"stake_config"], + seeds = [b"stake_config"], bump = stake_config.bump )] pub stake_config: Account<'info, StakeConfig>, @@ -245,19 +191,22 @@ pub struct Redeem<'info> { #[account(mut)] pub signer: Signer<'info>, + /// Optional legacy unbonding ticket from the old two-step flow. + /// If present (pass the ticket PDA address), it will be closed and rent returned to signer. + /// If no ticket exists, pass the program's own ID — Anchor 0.31 treats it as None and skips all constraints. #[account( mut, - close = signer, // return rent to user when done + close = signer, seeds = [b"ticket", signer.key().as_ref()], bump, )] - pub ticket: Account<'info, UnbondingTicket>, + pub ticket: Option>, #[account( mut, token::mint = stake_config.vault, constraint = user_vault_token_account.mint == stake_config.vault @ CustomErrorCode::InvalidVaultMint, - constraint = user_vault_token_account.owner == signer.key() @ CustomErrorCode::InvalidTicketOwner + constraint = user_vault_token_account.owner == signer.key() @ CustomErrorCode::InvalidTokenOwner )] pub user_vault_token_account: Account<'info, TokenAccount>, @@ -265,7 +214,7 @@ pub struct Redeem<'info> { mut, token::mint = stake_config.mint, constraint = user_mint_token_account.mint == stake_config.mint @ CustomErrorCode::InvalidMint, - constraint = user_mint_token_account.owner == signer.key() @ CustomErrorCode::InvalidTicketOwner + constraint = user_mint_token_account.owner == signer.key() @ CustomErrorCode::InvalidTokenOwner )] pub user_mint_token_account: Account<'info, TokenAccount>, diff --git a/programs/vault-stake/src/error.rs b/programs/vault-stake/src/error.rs index 4fbc1c1..3f0af70 100644 --- a/programs/vault-stake/src/error.rs +++ b/programs/vault-stake/src/error.rs @@ -12,19 +12,11 @@ pub enum CustomErrorCode { InvalidAuthority = 4, #[msg("Insufficient balance")] InsufficientBalance = 5, - #[msg("Unbonding period not elapsed")] - UnbondingPeriodNotElapsed = 6, - #[msg("Insufficient unbonding balance")] - InsufficientUnbondingBalance = 7, - #[msg("Unbonding is currently in progress")] - UnbondingInProgress = 8, #[msg("Invalid mint provided")] InvalidMint = 9, #[msg("Invalid vault mint provided")] InvalidVaultMint = 10, - #[msg("Invalid ticket owner")] - InvalidTicketOwner = 11, #[msg("Invalid mint authority")] InvalidMintAuthority = 12, @@ -52,8 +44,6 @@ pub enum CustomErrorCode { VaultAndMintCannotBeSame = 26, #[msg("Protocol is paused")] ProtocolPaused = 27, - #[msg("Invalid bonding period")] - InvalidBondingPeriod = 28, #[msg("Invalid token owner")] InvalidTokenOwner = 29, #[msg("Invalid mint program owner")] diff --git a/programs/vault-stake/src/events.rs b/programs/vault-stake/src/events.rs index 0340ecb..200d41a 100644 --- a/programs/vault-stake/src/events.rs +++ b/programs/vault-stake/src/events.rs @@ -14,14 +14,6 @@ pub struct DepositEvent { pub totals_last_update_slot: u64, } -#[event] -pub struct UnbondEvent { - pub user: Pubkey, - pub amount: u64, - pub mint: Pubkey, - pub vault: Pubkey, -} - #[event] pub struct RedeemEvent { pub user: Pubkey, @@ -37,15 +29,6 @@ pub struct RedeemEvent { pub totals_last_update_slot: u64, } -#[event] -pub struct UnbondingPeriodUpdated { - pub admin: Pubkey, - pub old_period: i64, - pub new_period: i64, - pub mint: Pubkey, - pub vault: Pubkey, -} - #[event] pub struct RewardsPublished { pub admin: Pubkey, diff --git a/programs/vault-stake/src/lib.rs b/programs/vault-stake/src/lib.rs index 2822ce1..abd75f2 100644 --- a/programs/vault-stake/src/lib.rs +++ b/programs/vault-stake/src/lib.rs @@ -5,7 +5,7 @@ pub mod account_structs; /// /// 1. Initial Setup: /// - Admin creates two token types: Vault (wYLDS), Stake (PRIME) -/// - Admin initializes program with token addresses and unbonding period +/// - Admin initializes program with token addresses /// - Admin configures vault token account to hold deposited tokens /// /// 2. User Staking Flow: @@ -15,22 +15,15 @@ pub mod account_structs; /// - User receives stake tokens (PRIME) based on their wYLDS pool share /// /// 3. Withdrawal Flow: -/// a. Unbonding Initiation: -/// - User initiates withdrawal by burning stake tokens (PRIME) -/// - System creates an unbonding ticket attached to the user -/// - Unbonding period timer starts -/// -/// b. Waiting Period: -/// - User holds an unbonding ticket during lock period -/// - Can query remaining time via status check -/// -/// c. Redemption: -/// - After the unbonding period expires, the user can redeem -/// - Vault tokens (wYLDS) returned to user based on their minted PRIME share -/// - Unbonding ticket is invalidated +/// - User calls redeem(amount) with the amount of PRIME to burn +/// - Program computes wYLDS owed using virtual shares formula +/// - PRIME is burned; wYLDS is transferred to user immediately +/// - Any legacy unbonding ticket from v1 is automatically closed (rent returned) +/// when the optional ticket account is provided /// /// 4. Administrative Functions: -/// - Update token configurations if needed +/// - Pause/unpause protocol operations +/// - Update freeze and rewards administrators /// - Monitor vault token accounts /// /// Security is maintained through PDAs (Program Derived Addresses) and strict @@ -54,16 +47,13 @@ pub mod vault_stake { /// Initializes the vault program with the required token configurations: /// - vault_mint: The token that users deposit (e.g., wYLDS) /// - stake_mint: The token users receive when staking (e.g., PRIME) - /// - unbonding_period: Time in seconds users must wait before redeeming pub fn initialize( ctx: Context, - unbonding_period: i64, freeze_administrators: Vec, rewards_administrators: Vec, ) -> Result<()> { processor::initialize( ctx, - unbonding_period, freeze_administrators, rewards_administrators, ) @@ -74,12 +64,6 @@ pub mod vault_stake { pub fn pause(ctx: Context, pause: bool) -> Result<()> { processor::pause(ctx, pause) } - - /// Updates the program configuration with new token addresses: - /// - new_unbonding_period: New unbonding period in seconds - pub fn update_config(ctx: Context, new_unbonding_period: i64) -> Result<()> { - processor::update_config(ctx, new_unbonding_period) - } /// Handles user deposits of vault tokens (e.g., wYLDS): /// - Transfers vault tokens to program vault account @@ -88,18 +72,12 @@ pub mod vault_stake { processor::deposit(ctx, amount) } - /// Initiates the unbonding process: - /// - Burns user's stake tokens (e.g., PRIME) - /// - Starts unbonding period timer via user ticket - pub fn unbond(ctx: Context, amount: u64) -> Result<()> { - processor::unbond(ctx, amount) - } - - /// Completes the unbonding process after the period expires: - /// - Burns unbonding tokens (e.g., uwYLDS) - /// - Returns vault tokens (e.g., wYLDS) to user - pub fn redeem(ctx: Context) -> Result<()> { - processor::redeem(ctx) + /// Redeems stake tokens (PRIME) for vault tokens (wYLDS): + /// - Burns the specified amount of PRIME from the user's account + /// - Transfers the proportional wYLDS from the vault to the user immediately + /// - Optionally closes a legacy unbonding ticket (from v1) and returns rent to user + pub fn redeem(ctx: Context, amount: u64) -> Result<()> { + processor::redeem(ctx, amount) } pub fn update_freeze_administrators( @@ -140,7 +118,7 @@ pub mod vault_stake { } pub fn exchange_rate(ctx: Context) -> Result { - processor::exchange_rate(ctx) + processor::exchange_rate(ctx) } } diff --git a/programs/vault-stake/src/processor.rs b/programs/vault-stake/src/processor.rs index c015f19..b32c036 100644 --- a/programs/vault-stake/src/processor.rs +++ b/programs/vault-stake/src/processor.rs @@ -3,7 +3,7 @@ use crate::error::*; use crate::events::*; use crate::guard::validate_program_update_authority; use crate::state::{calculate_assets_to_shares, calculate_exchange_rate, calculate_shares_to_assets, - MAX_ADMINISTRATORS, MAX_UNBONDING_PERIOD, MIN_UNBONDING_PERIOD, VIRTUAL_ASSETS, VIRTUAL_SHARES}; + MAX_ADMINISTRATORS, VIRTUAL_ASSETS, VIRTUAL_SHARES}; use anchor_lang::prelude::*; use anchor_spl::token::spl_token::instruction::AuthorityType; use anchor_spl::token::{self, Burn, MintTo, Transfer}; @@ -59,7 +59,6 @@ Proper PDA Authority: Vault is controlled by PDA, not externally pub fn initialize( ctx: Context, - unbonding_period: i64, freeze_administrators: Vec, rewards_administrators: Vec, ) -> Result<()> { @@ -72,14 +71,6 @@ pub fn initialize( rewards_administrators.len() <= MAX_ADMINISTRATORS, CustomErrorCode::TooManyAdministrators ); - require!( - unbonding_period >= MIN_UNBONDING_PERIOD, - CustomErrorCode::InvalidBondingPeriod - ); - require!( - unbonding_period <= MAX_UNBONDING_PERIOD, - CustomErrorCode::InvalidBondingPeriod - ); require!( ctx.accounts.vault_token_mint.key() != ctx.accounts.mint.key(), CustomErrorCode::VaultAndMintCannotBeSame @@ -88,7 +79,7 @@ pub fn initialize( let config = &mut ctx.accounts.stake_config; config.vault = ctx.accounts.vault_token_mint.key(); config.mint = ctx.accounts.mint.key(); - config.unbonding_period = unbonding_period; + config.unbonding_period = 0; // DEPRECATED: unbonding period removed in v0.0.5 config.freeze_administrators = freeze_administrators; config.rewards_administrators = rewards_administrators; config.bump = ctx.bumps.stake_config; @@ -140,31 +131,6 @@ pub fn pause(ctx: Context, pause: bool) -> Result<()> { Ok(()) } -pub fn update_config(ctx: Context, new_unbonding_period: i64) -> Result<()> { - validate_program_update_authority(&ctx.accounts.program_data, &ctx.accounts.signer)?; - require!( - new_unbonding_period >= MIN_UNBONDING_PERIOD, - CustomErrorCode::InvalidBondingPeriod - ); - require!( - new_unbonding_period <= MAX_UNBONDING_PERIOD, - CustomErrorCode::InvalidBondingPeriod - ); - - let config = &mut ctx.accounts.stake_config; - config.unbonding_period = new_unbonding_period; - - emit!(UnbondingPeriodUpdated { - admin: ctx.accounts.signer.key(), - old_period: ctx.accounts.stake_config.unbonding_period, - new_period: new_unbonding_period, - mint: ctx.accounts.stake_config.mint, - vault: ctx.accounts.stake_config.vault, - }); - - Ok(()) -} - pub fn deposit(ctx: Context, amount: u64) -> Result<()> { require!(amount > 0, CustomErrorCode::InvalidAmount); require!( @@ -258,84 +224,34 @@ pub fn deposit(ctx: Context, amount: u64) -> Result<()> { Ok(()) } -// Create an unbonding ticket for the user. They are unbonding 'amount' of mint tokens. -pub fn unbond(ctx: Context, amount: u64) -> Result<()> { - msg!("Starting unbond process"); +// Redeem stake tokens (PRIME) for vault tokens (wYLDS). +// Burns the user's PRIME and transfers the proportional share of wYLDS from the vault. +// Any legacy unbonding ticket (from the old two-step flow) is automatically closed +// and rent returned to the user when the optional ticket account is provided. +pub fn redeem(ctx: Context, amount: u64) -> Result<()> { + msg!("Starting redeem process"); require!(amount > 0, CustomErrorCode::InvalidAmount); require!( !ctx.accounts.stake_config.paused, CustomErrorCode::ProtocolPaused ); - let current_mint_amount = ctx.accounts.user_mint_token_account.amount; - require!( - amount <= current_mint_amount, - CustomErrorCode::InsufficientUnbondingBalance - ); - - let ticket = &mut ctx.accounts.ticket; - ticket.owner = ctx.accounts.signer.key(); - ticket.requested_amount = amount; - ticket.start_balance = current_mint_amount; - ticket.start_ts = Clock::get()?.unix_timestamp; - - msg!("Emitting UnbondEvent"); - emit!(UnbondEvent { - user: ctx.accounts.signer.key(), - amount, - mint: ctx.accounts.mint.key(), - vault: ctx.accounts.stake_config.vault, - }); - msg!("Emitted UnbondingEvent"); - - Ok(()) -} - -// Redeem the user's unbonded tokens after the unbonding period has elapsed. -// The user is allowed to redeem up to the amount they requested to unbond, -// capped by their current mint token balance. They are entitled to their -// share of the vault tokens based on the current exchange rate. -// Burn the user's mint tokens and transfer the corresponding vault tokens -// from the vault to the user. -// -pub fn redeem(ctx: Context) -> Result<()> { - msg!("Starting redeem process"); + let user_share_mint_balance = ctx.accounts.user_mint_token_account.amount; require!( - !ctx.accounts.stake_config.paused, - CustomErrorCode::ProtocolPaused - ); - - let now = Clock::get()?.unix_timestamp; - let ticket = &ctx.accounts.ticket; - - require_keys_eq!( - ticket.owner, - ctx.accounts.signer.key(), - CustomErrorCode::InvalidTicketOwner + amount <= user_share_mint_balance, + CustomErrorCode::InsufficientBalance ); - let stake_config = &ctx.accounts.stake_config; let total_assets = ctx.accounts.vault_token_account.amount; let total_shares = ctx.accounts.mint.supply; msg!("total_assets: {}", total_assets); msg!("total_shares: {}", total_shares); - - require!( - now - ticket.start_ts >= stake_config.unbonding_period, - CustomErrorCode::UnbondingPeriodNotElapsed - ); - - let user_share_mint_balance = ctx.accounts.user_mint_token_account.amount; - let requested_shares_to_withdraw = ticket.requested_amount.min(user_share_mint_balance); - require!( - requested_shares_to_withdraw > 0, - CustomErrorCode::InsufficientUnbondingBalance - ); + msg!("redeem amount (shares): {}", amount); // Calculate redemption amount using virtual offsets // Formula: assets = (shares * (vault_balance + VIRTUAL_ASSETS)) / (supply + VIRTUAL_SHARES) - // VIRTUAL_SHARES determines the minimum cost to execute an attack - let numerator = (requested_shares_to_withdraw as u128) + // VIRTUAL_SHARES/VIRTUAL_ASSETS prevent inflation attacks (ERC4626 virtual shares pattern) + let numerator = (amount as u128) .checked_mul( (total_assets as u128) .checked_add(VIRTUAL_ASSETS) @@ -357,6 +273,9 @@ pub fn redeem(ctx: Context) -> Result<()> { msg!("Amount to withdraw calculated: {}", amount_to_withdraw); + // Guard against dust amounts rounding down to zero + require!(amount_to_withdraw > 0, CustomErrorCode::InvalidAmount); + require!( ctx.accounts.vault_token_account.amount >= amount_to_withdraw as u64, CustomErrorCode::InsufficientVaultBalance @@ -369,7 +288,7 @@ pub fn redeem(ctx: Context) -> Result<()> { }; token::burn( CpiContext::new(ctx.accounts.token_program.to_account_info(), burn_accounts), - requested_shares_to_withdraw, + amount, )?; let seeds: &[&[u8]] = &[b"vault_authority", &[ctx.bumps.vault_authority]]; @@ -392,7 +311,7 @@ pub fn redeem(ctx: Context) -> Result<()> { .checked_sub(amount_to_withdraw as u64) .ok_or(CustomErrorCode::Overflow)?; let result_total_shares = total_shares - .checked_sub(requested_shares_to_withdraw) + .checked_sub(amount) .ok_or(CustomErrorCode::Overflow)?; let totals_last_update_slot = Clock::get()?.slot; @@ -400,12 +319,12 @@ pub fn redeem(ctx: Context) -> Result<()> { emit!(RedeemEvent { user: ctx.accounts.signer.key(), mint: ctx.accounts.mint.key(), - requested_mint_amount: requested_shares_to_withdraw, + requested_mint_amount: amount, mint_supply: ctx.accounts.mint.supply, vault: ctx.accounts.vault_token_account.key(), redeemed_vault_amount: amount_to_withdraw as u64, vault_balance: ctx.accounts.vault_token_account.amount, - shares_burned: requested_shares_to_withdraw, + shares_burned: amount, total_assets: result_total_assets, total_shares: result_total_shares, totals_last_update_slot, diff --git a/programs/vault-stake/src/state.rs b/programs/vault-stake/src/state.rs index 3ace1ad..14f056a 100644 --- a/programs/vault-stake/src/state.rs +++ b/programs/vault-stake/src/state.rs @@ -1,7 +1,5 @@ use anchor_lang::prelude::*; -pub const MAX_UNBONDING_PERIOD: i64 = 31536000; // 365 days in seconds -pub const MIN_UNBONDING_PERIOD: i64 = 1; // 1 second pub const MAX_ADMINISTRATORS: usize = 5; // max number of freeze/rewards administrators pub const VIRTUAL_SHARES: u128 = 1_000_000; // multiplier to prevent inflation attacks pub const VIRTUAL_ASSETS: u128 = 1_000_000; // multiplier to prevent inflation attacks @@ -10,6 +8,7 @@ pub const VIRTUAL_ASSETS: u128 = 1_000_000; // multiplier to prevent inflation a pub struct StakeConfig { pub vault: Pubkey, pub mint: Pubkey, + // DEPRECATED: unbonding period removed in v2. Kept for on-chain account layout compatibility. pub unbonding_period: i64, pub freeze_administrators: Vec, pub rewards_administrators: Vec, @@ -22,6 +21,8 @@ impl StakeConfig { pub const LEN: usize = 8 + 32 + 32 + 8 + (4 + (32 * MAX_ADMINISTRATORS)) + (4 + (32 * MAX_ADMINISTRATORS)) + 1 + 1; } +// DEPRECATED: No new tickets are created (unbond instruction removed in v2). +// Kept so Anchor can deserialize existing on-chain tickets for closure during redeem. #[account] pub struct UnbondingTicket { pub owner: Pubkey, diff --git a/scripts/common.sh b/scripts/common.sh index e4bf8c1..4ea99cb 100755 --- a/scripts/common.sh +++ b/scripts/common.sh @@ -158,5 +158,4 @@ show_accounts_and_pdas() { echo "Freeze Authority PDA: $(get_pda "$VAULT_STAKE_PROGRAM_ID" "freeze_authority")" echo "Freeze Administrators: $FREEZE_ADMINISTRATORS" echo "Rewards Administrators: $REWARDS_ADMINISTRATORS" - echo "Unbonding Period (in seconds): $UNBONDING_PERIOD" } diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 44fc382..c816ff1 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -187,9 +187,6 @@ initialize_stake_program() { if [ -z "$REWARDS_ADMINISTRATORS" ]; then prompt_with_default REWARDS_ADMINISTRATORS "Enter comma-separated list of Rewards Administrator addresses" fi - if [ -z "$UNBONDING_PERIOD" ]; then - prompt_with_default UNBONDING_PERIOD "Enter Unbonding Period (in seconds)" - fi if [ -z "$STAKE_PROG_MINT_TOKEN" ]; then prompt_with_default STAKE_PROG_MINT_TOKEN "Enter Stake Program mint token (staking token minted, PRIME)" fi @@ -199,7 +196,6 @@ initialize_stake_program() { --vault "$MINT_PROG_MINT_TOKEN" \ --vault_token_account "$STAKE_PROG_VAULT_TOKEN_ACCOUNT" \ --mint "$STAKE_PROG_MINT_TOKEN" \ - --unbonding_period "$UNBONDING_PERIOD" \ --freeze_administrators "$FREEZE_ADMINISTRATORS" \ --rewards_administrators "$REWARDS_ADMINISTRATORS") diff --git a/scripts/vault-stake/initialize.ts b/scripts/vault-stake/initialize.ts index c695424..f0f8362 100755 --- a/scripts/vault-stake/initialize.ts +++ b/scripts/vault-stake/initialize.ts @@ -2,7 +2,6 @@ import * as anchor from "@coral-xyz/anchor"; import {Program} from "@coral-xyz/anchor"; import {VaultStake} from "../../target/types/vault_stake"; import yargs from "yargs"; -import BN from "bn.js"; import {PublicKey} from "@solana/web3.js"; const provider = anchor.AnchorProvider.env(); @@ -21,11 +20,6 @@ const args = yargs(process.argv.slice(2)) description: "Token that will be minted upon receipt of the vaulted asset", required: true, }) - .option("unbonding_period", { - type: "string", - description: "Unbonding period in seconds", - required: true, - }) .option("vault_token_account", { type: "string", description: "Token account that will hold the vaulted asset (e.g. wYLDS). Must be owned by the program-derived address.", @@ -81,7 +75,6 @@ const main = async () => { ); const vault = new anchor.web3.PublicKey(args.vault); const mint = new anchor.web3.PublicKey(args.mint); - const unbondingPeriod = new BN(parseInt(args.unbonding_period)); const vaultTokenAccount = new anchor.web3.PublicKey(args.vault_token_account); const freezeAdministrators: PublicKey[] = (args.freeze_administrators.split(",")).map((s: string) => new anchor.web3.PublicKey(s)); if (freezeAdministrators.length > 5) { @@ -95,7 +88,6 @@ const main = async () => { console.log("Program ID:", program.programId.toBase58()); console.log("Vault (accepted token):", vault.toBase58()); console.log("Mint (token to be minted):", mint.toBase58()); - console.log("Unbonding Period (seconds):", unbondingPeriod); console.log("Vault Token Account:", vaultTokenAccount.toBase58()); console.log("Stake Config PDA:", stakeConfigPda.toBase58()); console.log("Stake Vault Token Account Config PDA:", stakeVaultTokenAccountConfigPda.toBase58()); @@ -108,7 +100,7 @@ const main = async () => { // Call initialize await program.methods - .initialize(unbondingPeriod, freezeAdministrators, rewardsAdministrators) + .initialize(freezeAdministrators, rewardsAdministrators) .accountsStrict({ stakeConfig: stakeConfigPda, vaultAuthority: vaultAuthorityPda, diff --git a/tests/vault-stake.test.ts b/tests/vault-stake.test.ts index da8ea8a..366773d 100644 --- a/tests/vault-stake.test.ts +++ b/tests/vault-stake.test.ts @@ -57,8 +57,6 @@ describe("vault-stake", () => { let freezeAdmin: Keypair; let rewardsAdmin: Keypair; - let unbondingPeriod: BN; - let publishRewardsId = 0; const ONE_BIG_SHARE = createBigInt(1_000_000); @@ -178,8 +176,6 @@ describe("vault-stake", () => { mintProgram.programId ); - unbondingPeriod = new BN(10); // seconds - // Airdrop SOL await provider.connection.requestAirdrop(user.publicKey, 100 * LAMPORTS_PER_SOL); await provider.connection.requestAirdrop(user2.publicKey, 100 * LAMPORTS_PER_SOL); @@ -383,7 +379,7 @@ describe("vault-stake", () => { const tooManyAdmins = Array(6).fill(Keypair.generate().publicKey); try { await program.methods - .initialize(unbondingPeriod, tooManyAdmins, [rewardsAdmin.publicKey]) + .initialize(tooManyAdmins, [rewardsAdmin.publicKey]) .accounts({ signer: provider.wallet.publicKey, vaultTokenAccount: vaultTokenAccount, @@ -402,26 +398,7 @@ describe("vault-stake", () => { const tooManyAdmins = Array(6).fill(Keypair.generate().publicKey); try { await program.methods - .initialize(unbondingPeriod, [freezeAdmin.publicKey], tooManyAdmins) - .accounts({ - signer: provider.wallet.publicKey, - vaultTokenAccount: vaultTokenAccount, - vaultTokenMint: vaultedToken, - mint: mintedToken, - programData: programDataPda, - }) - .rpc(); - assert.fail("Should have thrown error"); - } catch (err) { - expect(err).to.exist; - } - }); - - it("fails with invalid unbonding period", async () => { - const tooManyAdmins = Array(6).fill(Keypair.generate().publicKey); - try { - await program.methods - .initialize(new BN(0), [freezeAdmin.publicKey], tooManyAdmins) + .initialize([freezeAdmin.publicKey], tooManyAdmins) .accounts({ signer: provider.wallet.publicKey, vaultTokenAccount: vaultTokenAccount, @@ -438,7 +415,7 @@ describe("vault-stake", () => { it("initializes the vault config", async () => { await program.methods - .initialize(unbondingPeriod, [freezeAdmin.publicKey], [rewardsAdmin.publicKey]) + .initialize([freezeAdmin.publicKey], [rewardsAdmin.publicKey]) .accountsStrict({ stakeConfig: stakeConfigPda, vaultAuthority: vaultAuthorityPda, @@ -460,14 +437,14 @@ describe("vault-stake", () => { assert.ok(config.freezeAdministrators[0].equals(freezeAdmin.publicKey)); assert.equal(config.rewardsAdministrators.length, 1); assert.ok(config.rewardsAdministrators[0].equals(rewardsAdmin.publicKey)); - assert.ok(config.unbondingPeriod.eq(unbondingPeriod)); + assert.equal(config.unbondingPeriod.toNumber(), 0, "unbondingPeriod deprecated field should be 0"); assert.ok(!config.paused); }); it("fails when called twice", async () => { try { await program.methods - .initialize(unbondingPeriod, [freezeAdmin.publicKey], [rewardsAdmin.publicKey]) + .initialize([freezeAdmin.publicKey], [rewardsAdmin.publicKey]) .accounts({ signer: provider.wallet.publicKey, vaultTokenAccount: vaultTokenAccount, @@ -603,39 +580,20 @@ describe("vault-stake", () => { assert.equal(vaultBalanceBefore, ONE_BIG_TOKEN * createBigInt(20_001), "Vault should have all deposits"); assert.isTrue(vaultBalanceBefore > (user1Assets + user2Assets), "There will be rounding dust"); - // step 4 - attacker redeems - // first they need to unbond - const [ticketPda] = anchor.web3.PublicKey.findProgramAddressSync( - [Buffer.from("ticket"), user.publicKey.toBuffer()], - program.programId - ); - await program.methods.unbond(new BN(user1Shares)) - .accountsStrict({ - stakeConfig: stakeConfigPda, - mint: mintedToken, - signer: user.publicKey, - userMintTokenAccount: userMintTokenAccount, - ticket: ticketPda, - systemProgram: anchor.web3.SystemProgram.programId, - }) - .signers([user]) - .rpc(); - - // wait for >10 seconds unbonding period - await new Promise(resolve => setTimeout(resolve, 15000)); - await program.methods.redeem() + // step 4 - attacker redeems immediately (no unbonding period) + await program.methods.redeem(new BN(user1Shares)) .accountsStrict({ stakeConfig: stakeConfigPda, vaultTokenAccount: vaultTokenAccount, stakeVaultTokenAccountConfig: stakeVaultTokenAccountConfigPda, vaultAuthority: vaultAuthorityPda, signer: user.publicKey, + ticket: program.programId, // no legacy ticket userVaultTokenAccount: userVaultTokenAccount, userMintTokenAccount: userMintTokenAccount, mint: mintedToken, vaultMint: vaultedToken, tokenProgram: anchor.utils.token.TOKEN_PROGRAM_ID, - ticket: ticketPda }).signers([user]) .rpc() @@ -664,37 +622,19 @@ describe("vault-stake", () => { assert.equal(user2MintTokenBefore, createBigInt(1_999_600), "User 2 should still have 1,999,600 shares"); assert.equal(user2VaultBalanceBefore, BIG_ZERO, "User 2 should not have any vault tokens"); - const [ticketPda] = anchor.web3.PublicKey.findProgramAddressSync( - [Buffer.from("ticket"), user2.publicKey.toBuffer()], - program.programId - ); - await program.methods.unbond(new BN(user2MintTokenBefore)) - .accountsStrict({ - stakeConfig: stakeConfigPda, - mint: mintedToken, - signer: user2.publicKey, - userMintTokenAccount: user2MintTokenAccount, - ticket: ticketPda, - systemProgram: anchor.web3.SystemProgram.programId, - }) - .signers([user2]) - .rpc().catch(e => console.dir(e)); - - // wait for >10 seconds unbonding period - await new Promise(resolve => setTimeout(resolve, 15000)); - await program.methods.redeem() + await program.methods.redeem(new BN(user2MintTokenBefore)) .accountsStrict({ stakeConfig: stakeConfigPda, vaultTokenAccount: vaultTokenAccount, stakeVaultTokenAccountConfig: stakeVaultTokenAccountConfigPda, vaultAuthority: vaultAuthorityPda, signer: user2.publicKey, + ticket: program.programId, // no legacy ticket userVaultTokenAccount: user2VaultTokenAccount, userMintTokenAccount: user2MintTokenAccount, mint: mintedToken, vaultMint: vaultedToken, tokenProgram: anchor.utils.token.TOKEN_PROGRAM_ID, - ticket: ticketPda }).signers([user2]) .rpc() @@ -845,144 +785,119 @@ describe("vault-stake", () => { }); }); - describe("unbond", () => { - it("unbond ticket closes", async () => { - const [ticketPda] = anchor.web3.PublicKey.findProgramAddressSync( - [Buffer.from("ticket"), user.publicKey.toBuffer()], - program.programId - ); - await program.methods.unbond(new BN(1000)) - .accountsStrict({ - stakeConfig: stakeConfigPda, - mint: mintedToken, - signer: user.publicKey, - userMintTokenAccount: userMintTokenAccount, - ticket: ticketPda, - systemProgram: anchor.web3.SystemProgram.programId, - }) - .signers([user]) - .rpc(); + describe("redeem", () => { + it("redeems a partial amount immediately (no waiting)", async () => { + const redeemAmount = new BN(1000); + const mintBalanceBefore = (await getAccount(provider.connection, userMintTokenAccount)).amount; + const vaultBalanceBefore = (await getAccount(provider.connection, vaultTokenAccount)).amount; + const userVaultBalanceBefore = (await getAccount(provider.connection, userVaultTokenAccount)).amount; - const t = await program.account.unbondingTicket.fetch( - ticketPda - ); - assert.equal(t.requestedAmount.toNumber(), new BN(1000).toNumber(), "Unbonding ticket should reflect requested amount"); + assert.ok(mintBalanceBefore >= BigInt(redeemAmount.toNumber()), "User must have enough PRIME to redeem"); - // wait for >10 seconds unbonding period - await new Promise(resolve => setTimeout(resolve, 15000)); - await program.methods.redeem() + await program.methods.redeem(redeemAmount) .accountsStrict({ stakeConfig: stakeConfigPda, vaultTokenAccount: vaultTokenAccount, stakeVaultTokenAccountConfig: stakeVaultTokenAccountConfigPda, vaultAuthority: vaultAuthorityPda, signer: user.publicKey, + ticket: program.programId, // no legacy ticket userVaultTokenAccount: userVaultTokenAccount, userMintTokenAccount: userMintTokenAccount, mint: mintedToken, vaultMint: vaultedToken, tokenProgram: anchor.utils.token.TOKEN_PROGRAM_ID, - ticket: ticketPda - }).signers([user]) - .rpc() - - try { - await program.account.unbondingTicket.fetch( - ticketPda - ); - assert.fail("Redemption request should be closed"); - } catch (err) { - expect(err).to.exist; - expect(err.message).to.include("Account does not exist or has no data"); - } - }); - - it("unbond twice fails", async () => { - const [ticketPda] = anchor.web3.PublicKey.findProgramAddressSync( - [Buffer.from("ticket"), user.publicKey.toBuffer()], - program.programId - ); - await program.methods.unbond(new BN(1000)) - .accountsStrict({ - stakeConfig: stakeConfigPda, - mint: mintedToken, - signer: user.publicKey, - userMintTokenAccount: userMintTokenAccount, - ticket: ticketPda, - systemProgram: anchor.web3.SystemProgram.programId, }) .signers([user]) .rpc(); + const mintBalanceAfter = (await getAccount(provider.connection, userMintTokenAccount)).amount; + const vaultBalanceAfter = (await getAccount(provider.connection, vaultTokenAccount)).amount; + const userVaultBalanceAfter = (await getAccount(provider.connection, userVaultTokenAccount)).amount; + + assert.equal(mintBalanceAfter, mintBalanceBefore - BigInt(redeemAmount.toNumber()), "PRIME should be burned"); + assert.ok(vaultBalanceAfter < vaultBalanceBefore, "Vault balance should decrease"); + assert.ok(userVaultBalanceAfter > userVaultBalanceBefore, "User should receive wYLDS"); + }); + + it("fails with zero amount", async () => { try { - await program.methods.unbond(new BN(1000)) + await program.methods.redeem(new BN(0)) .accountsStrict({ stakeConfig: stakeConfigPda, - mint: mintedToken, + vaultTokenAccount: vaultTokenAccount, + stakeVaultTokenAccountConfig: stakeVaultTokenAccountConfigPda, + vaultAuthority: vaultAuthorityPda, signer: user.publicKey, + ticket: program.programId, + userVaultTokenAccount: userVaultTokenAccount, userMintTokenAccount: userMintTokenAccount, - ticket: ticketPda, - systemProgram: anchor.web3.SystemProgram.programId, + mint: mintedToken, + vaultMint: vaultedToken, + tokenProgram: anchor.utils.token.TOKEN_PROGRAM_ID, }) .signers([user]) .rpc(); - assert.fail("Unbond request should have failed"); + assert.fail("Should have thrown error"); } catch (err) { expect(err).to.exist; + expect(err.toString()).to.include("InvalidAmount"); } }); - it("redeem without unbond", async () => { - const [ticketPda] = anchor.web3.PublicKey.findProgramAddressSync( - [Buffer.from("ticket"), user.publicKey.toBuffer()], - program.programId - ); + it("fails with more than user balance", async () => { + const mintBalance = (await getAccount(provider.connection, userMintTokenAccount)).amount; + const tooMuch = new BN(mintBalance.toString()).add(new BN(1)); try { - await program.methods.redeem() + await program.methods.redeem(tooMuch) .accountsStrict({ stakeConfig: stakeConfigPda, vaultTokenAccount: vaultTokenAccount, stakeVaultTokenAccountConfig: stakeVaultTokenAccountConfigPda, vaultAuthority: vaultAuthorityPda, signer: user.publicKey, + ticket: program.programId, userVaultTokenAccount: userVaultTokenAccount, userMintTokenAccount: userMintTokenAccount, mint: mintedToken, vaultMint: vaultedToken, tokenProgram: anchor.utils.token.TOKEN_PROGRAM_ID, - ticket: ticketPda - }).signers([user]) - .rpc() + }) + .signers([user]) + .rpc(); assert.fail("Should have thrown error"); } catch (err) { expect(err).to.exist; + expect(err.toString()).to.include("InsufficientBalance"); } }); - it("closes out unbonding tickets", async () => { - const [ticketPda] = anchor.web3.PublicKey.findProgramAddressSync( - [Buffer.from("ticket"), user.publicKey.toBuffer()], - program.programId - ); + it("redeems full balance in one call", async () => { + const mintBalance = (await getAccount(provider.connection, userMintTokenAccount)).amount; + if (mintBalance === BigInt(0)) { + return; // user already fully redeemed in a prior test; skip + } - // wait for >10 seconds unbonding period which was created in the double bond test - await new Promise(resolve => setTimeout(resolve, 15000)); - await program.methods.redeem() + await program.methods.redeem(new BN(mintBalance.toString())) .accountsStrict({ stakeConfig: stakeConfigPda, vaultTokenAccount: vaultTokenAccount, stakeVaultTokenAccountConfig: stakeVaultTokenAccountConfigPda, vaultAuthority: vaultAuthorityPda, signer: user.publicKey, + ticket: program.programId, userVaultTokenAccount: userVaultTokenAccount, userMintTokenAccount: userMintTokenAccount, mint: mintedToken, vaultMint: vaultedToken, tokenProgram: anchor.utils.token.TOKEN_PROGRAM_ID, - ticket: ticketPda - }).signers([user]) - .rpc() + }) + .signers([user]) + .rpc(); + + const mintBalanceAfter = (await getAccount(provider.connection, userMintTokenAccount)).amount; + assert.equal(mintBalanceAfter, BigInt(0), "All PRIME should be burned"); }); }); @@ -1041,20 +956,57 @@ describe("vault-stake", () => { expect(err.toString()).to.include("ProtocolPaused"); } }); - it("prevents unbond when paused", async () => { + it("prevents redeem when paused", async () => { + // First deposit some tokens so user has shares to redeem + const userMintBalance = (await getAccount(provider.connection, userMintTokenAccount)).amount; + if (userMintBalance === BigInt(0)) { + // Make a small deposit to give user shares for the redeem attempt + const userVaultBalance = (await getAccount(provider.connection, userVaultTokenAccount)).amount; + if (userVaultBalance > BigInt(0)) { + await program.methods + .pause(false) // temporarily unpause to deposit + .accountsStrict({ stakeConfig: stakeConfigPda, signer: freezeAdmin.publicKey }) + .signers([freezeAdmin]) + .rpc(); + await program.methods + .deposit(new BN(10_000_000)) // 1000 is too small when vault dust ~5B units; need > vault_dust / VIRTUAL_SHARES ≈ 5001 units + .accountsStrict({ + stakeConfig: stakeConfigPda, + vaultTokenAccount: vaultTokenAccount, + stakeVaultTokenAccountConfig: stakeVaultTokenAccountConfigPda, + vaultAuthority: vaultAuthorityPda, + mint: mintedToken, + vaultMint: vaultedToken, + mintAuthority: mintAuthorityPda, + signer: user.publicKey, + userVaultTokenAccount: userVaultTokenAccount, + userMintTokenAccount: userMintTokenAccount, + tokenProgram: anchor.utils.token.TOKEN_PROGRAM_ID + }) + .signers([user]) + .rpc(); + await program.methods + .pause(true) // re-pause + .accountsStrict({ stakeConfig: stakeConfigPda, signer: freezeAdmin.publicKey }) + .signers([freezeAdmin]) + .rpc(); + } + } + try { - const [ticketPda] = anchor.web3.PublicKey.findProgramAddressSync( - [Buffer.from("ticket"), user.publicKey.toBuffer()], - program.programId - ); - await program.methods.unbond(new BN(1000)) + await program.methods.redeem(new BN(1000)) .accountsStrict({ stakeConfig: stakeConfigPda, - mint: mintedToken, + vaultTokenAccount: vaultTokenAccount, + stakeVaultTokenAccountConfig: stakeVaultTokenAccountConfigPda, + vaultAuthority: vaultAuthorityPda, signer: user.publicKey, + ticket: program.programId, + userVaultTokenAccount: userVaultTokenAccount, userMintTokenAccount: userMintTokenAccount, - ticket: ticketPda, - systemProgram: anchor.web3.SystemProgram.programId, + mint: mintedToken, + vaultMint: vaultedToken, + tokenProgram: anchor.utils.token.TOKEN_PROGRAM_ID, }) .signers([user]) .rpc(); @@ -1625,20 +1577,6 @@ describe("vault-stake", () => { } }); - it("unbonding update by upgrade authority", async () => { - await program.methods - .updateConfig(new BN(240)) - .accountsStrict({ - stakeConfig: stakeConfigPda, - signer: provider.wallet.publicKey, - programData: programData, - }) - .rpc(); - - //fetch config and verify - const config = await program.account.stakeConfig.fetch(stakeConfigPda); - assert.equal(config.unbondingPeriod.toNumber(), new BN(240).toNumber()); - }); }); describe("overflow deposits", () => { From 90852e9a0cdfe6fc4234cb3d492ddcb86bc64ac1 Mon Sep 17 00:00:00 2001 From: Jason D Date: Fri, 6 Mar 2026 16:01:40 -0700 Subject: [PATCH 2/3] update scripts for deprecated stake program unbonding period --- .../query_all_unbonding_requests.ts | 57 +++++++++++---- scripts/vault-stake/redeem.ts | 27 ++++--- scripts/vault-stake/unbond.ts | 68 ----------------- scripts/vault-stake/update_config.ts | 73 ------------------- 4 files changed, 62 insertions(+), 163 deletions(-) delete mode 100755 scripts/vault-stake/unbond.ts delete mode 100755 scripts/vault-stake/update_config.ts diff --git a/scripts/vault-stake/query_all_unbonding_requests.ts b/scripts/vault-stake/query_all_unbonding_requests.ts index 5e775df..b6175be 100644 --- a/scripts/vault-stake/query_all_unbonding_requests.ts +++ b/scripts/vault-stake/query_all_unbonding_requests.ts @@ -1,3 +1,17 @@ +/** + * DEPRECATED — Unbonding period removed in v0.0.5 + * + * The two-step unbond → withdraw flow no longer exists. Redeem is now immediate. + * No new UnbondingTicket accounts will be created by the current program. + * + * This script is retained solely for auditing any LEGACY tickets that were + * created before the upgrade. If any are found, they can be closed (and rent + * recovered) by calling `redeem` with the ticket PDA — the updated redeem + * instruction will close the account and return rent to the signer automatically. + * + * If no tickets appear, the on-chain state is fully migrated and this script + * can be removed. + */ import * as anchor from "@coral-xyz/anchor"; import {Program} from "@coral-xyz/anchor"; import {VaultStake} from "../../target/types/vault_stake"; @@ -9,26 +23,45 @@ anchor.setProvider(provider); const program = anchor.workspace.VaultStake as Program; const main = async () => { + // UnbondingTicket may have been removed from the IDL in a future cleanup. + // Guard the lookup so the script fails with a clear message rather than a + // cryptic TypeError if the account type no longer exists. + const ticketIdlEntry = STAKE_IDL.accounts.find((a: { name: string }) => a.name === "UnbondingTicket"); + if (!ticketIdlEntry) { + console.log("UnbondingTicket is no longer present in the IDL — migration complete, no legacy tickets possible."); + return; + } + const accounts = await provider.connection.getProgramAccounts(program.programId, { filters: [ { - // Filter by account size (UnbondingTicket::LEN) + // Filter by account size (UnbondingTicket::LEN = discriminator + owner + amounts + timestamp) dataSize: 8 + 32 + 8 + 8 + 8, }, - // filter by discriminator to only get UnbondingTicket accounts { memcmp: { offset: 0, - bytes: STAKE_IDL.accounts.filter(a => a.name === "UnbondingTicket")[0].discriminator + bytes: ticketIdlEntry.discriminator, } } ] }); + + if (accounts.length === 0) { + console.log("No legacy UnbondingTicket accounts found — migration complete."); + return; + } + + console.log(`Found ${accounts.length} legacy UnbondingTicket account(s).`); + console.log("These can be closed by calling redeem with the ticket PDA (rent will be returned to the owner)."); + console.log("-".repeat(60)); + const coder = new anchor.BorshAccountsCoder(program.idl); const unbondingRequests = accounts.map(({ pubkey, account }) => { try { const decoded = coder.decode("unbondingTicket", account.data); return { + pubkey, owner: decoded.owner, requestedAmount: decoded.requestedAmount, startBalance: decoded.startBalance, @@ -40,16 +73,14 @@ const main = async () => { } }).filter(x => x !== null); - console.log("-".repeat(60)); unbondingRequests.forEach(r => { - console.log(`Owner: ${r.owner.toBase58()}`); - console.log(`Requested Amount:${r.requestedAmount.toString()}`); - console.log(`Start Balance: ${r.startBalance.toString()}`); - console.log(`Start Time: ${new Date(r.startTime.toNumber() * 1000).toISOString()}`); - }) - console.log("-".repeat(60)); - - -} + console.log(`Ticket PDA: ${r.pubkey.toBase58()}`); + console.log(`Owner: ${r.owner.toBase58()}`); + console.log(`Requested Amount: ${r.requestedAmount.toString()}`); + console.log(`Start Balance: ${r.startBalance.toString()}`); + console.log(`Start Time: ${new Date(r.startTime.toNumber() * 1000).toISOString()}`); + console.log("-".repeat(60)); + }); +}; main().catch(console.error); diff --git a/scripts/vault-stake/redeem.ts b/scripts/vault-stake/redeem.ts index 93e6f9f..61731a8 100755 --- a/scripts/vault-stake/redeem.ts +++ b/scripts/vault-stake/redeem.ts @@ -2,6 +2,7 @@ import * as anchor from "@coral-xyz/anchor"; import yargs from "yargs"; import {Program} from "@coral-xyz/anchor"; import {VaultStake} from "../../target/types/vault_stake"; +import BN from "bn.js"; const provider = anchor.AnchorProvider.env(); anchor.setProvider(provider); @@ -34,14 +35,18 @@ const args = yargs(process.argv.slice(2)) description: "User's mint token account address where the staking mint tokens (e.g. PRIME) will be burned. Must be associated token account for the mint token (e.g. PRIME)", required: true, }) - + .option("amount", { + type: "number", + description: "Amount to redeem. Must be less than or equal to the amount staked.", + required: true, + }) .parseSync(); const main = async () => { const signer = provider.wallet.publicKey; // Derive PDAs - const [stakeConfigPda, bump] = anchor.web3.PublicKey.findProgramAddressSync( + const [stakeConfigPda] = anchor.web3.PublicKey.findProgramAddressSync( [Buffer.from("stake_config")], program.programId ); @@ -59,10 +64,17 @@ const main = async () => { program.programId ); + // The unbonding flow was removed in v0.0.5. The ticket account is now optional: + // - If a legacy UnbondingTicket PDA exists on-chain, pass its address so the + // program closes it and returns rent to the signer. + // - If no ticket exists, pass program.programId as the Anchor 0.31 None sentinel + // (Anchor treats pubkey == programId as Option::None and skips all constraints). const [ticketPda] = anchor.web3.PublicKey.findProgramAddressSync( [Buffer.from("ticket"), signer.toBuffer()], program.programId ); + const legacyTicketInfo = await provider.connection.getAccountInfo(ticketPda); + const ticketAccount = legacyTicketInfo !== null ? ticketPda : program.programId; // Program args const mint = new anchor.web3.PublicKey(args.mint); @@ -71,34 +83,31 @@ const main = async () => { const userVaultTokenAccount = new anchor.web3.PublicKey(args.user_vault_token_account); const userMintTokenAccount = new anchor.web3.PublicKey(args.user_mint_token_account); - console.log(`Signer: ${mint.toBase58()}`); + console.log(`Signer: ${signer.toBase58()}`); console.log(`Mint (token to be burned e.g. PRIME): ${mint.toBase58()}`); console.log(`Vault Token Account (e.g. wYLDS): ${vaultTokenAccount.toBase58()}`); console.log(`User Vault Token Account: ${userVaultTokenAccount.toBase58()}`); console.log(`Stake Config PDA: ${stakeConfigPda.toBase58()}`); console.log(`Vault Authority PDA: ${vaultAuthorityPda.toBase58()}`); - console.log(`Ticket PDA: ${ticketPda.toBase58()}`); + console.log(`Legacy Ticket PDA: ${ticketPda.toBase58()} (${legacyTicketInfo !== null ? "found — will be closed and rent returned" : "not found — skipped"})`); const tx = await program.methods - .redeem() + .redeem(new BN(args.amount, 10, "le")) .accountsStrict({ stakeConfig: stakeConfigPda, stakeVaultTokenAccountConfig: stakeVaultTokenAccountConfigPda, vaultTokenAccount: vaultTokenAccount, vaultAuthority: vaultAuthorityPda, signer: signer, + ticket: ticketAccount, userVaultTokenAccount: userVaultTokenAccount, userMintTokenAccount: userMintTokenAccount, mint: mint, vaultMint: vaultMint, tokenProgram: anchor.utils.token.TOKEN_PROGRAM_ID, - ticket: ticketPda }).rpc(); console.log("Transaction:", tx); }; main().catch(console.error); - - - diff --git a/scripts/vault-stake/unbond.ts b/scripts/vault-stake/unbond.ts deleted file mode 100755 index f920e01..0000000 --- a/scripts/vault-stake/unbond.ts +++ /dev/null @@ -1,68 +0,0 @@ -import * as anchor from "@coral-xyz/anchor"; -import yargs from "yargs"; -import {Program} from "@coral-xyz/anchor"; -import {VaultStake} from "../../target/types/vault_stake"; -import {getAssociatedTokenAddress} from "@solana/spl-token"; - -const provider = anchor.AnchorProvider.env(); -anchor.setProvider(provider); - -const program = anchor.workspace.VaultStake as Program; - -const args = yargs(process.argv.slice(2)) - .option("mint", { - type: "string", - description: "Mint token that will be burned (e.g. PRIME) after unbonding period.", - required: true, - }) - .option("amount", { - type: "number", - description: "Amount of mint tokens to burn at unbond", - required: true, - }) - .parseSync(); - -const main = async () => { - const signer = provider.wallet.publicKey; - - // Derive PDAs - const [stakeConfigPda, bump] = anchor.web3.PublicKey.findProgramAddressSync( - [Buffer.from("stake_config")], - program.programId - ); - - // Derive ticket PDA - const [ticketPda] = anchor.web3.PublicKey.findProgramAddressSync( - [Buffer.from("ticket"), signer.toBuffer()], - program.programId - ); - - // Program args - const mint = new anchor.web3.PublicKey(args.mint); - const amount = new anchor.BN(args.amount); - const userMintTokenAccount = await getAssociatedTokenAddress(mint,signer) - - console.log("Burned Mint (token to be burned e.g. PRIME)", mint.toBase58()); - console.log("Amount:", amount.toString()); - console.log("User Mint Token Account to be burned:", userMintTokenAccount.toBase58()); - console.log("User Unbonding Ticket:", ticketPda.toBase58()); - console.log("Stake Config PDA:", stakeConfigPda.toBase58()); - - const tx = await program.methods - .unbond(amount) - .accountsStrict({ - stakeConfig: stakeConfigPda, - mint: mint, - signer: signer, - userMintTokenAccount: userMintTokenAccount, - ticket: ticketPda, - systemProgram: anchor.web3.SystemProgram.programId, - }).rpc(); - - console.log("Transaction:", tx); -}; - -main().catch(console.error); - - - diff --git a/scripts/vault-stake/update_config.ts b/scripts/vault-stake/update_config.ts deleted file mode 100755 index 086d0a1..0000000 --- a/scripts/vault-stake/update_config.ts +++ /dev/null @@ -1,73 +0,0 @@ -import * as anchor from "@coral-xyz/anchor"; -import {Program} from "@coral-xyz/anchor"; -import {VaultStake} from "../../target/types/vault_stake"; -import yargs from "yargs"; -import {PublicKey} from "@solana/web3.js"; -import { - TOKEN_PROGRAM_ID, -} from "@solana/spl-token"; - -const provider = anchor.AnchorProvider.env(); -anchor.setProvider(provider); - -const program = anchor.workspace.VaultStake as Program; - -const args = yargs(process.argv.slice(2)) - .option("unbonding_period", { - type: "number", - description: "Unbonding period in seconds", - required: true, - }) - .parseSync(); - -const main = async () => { - const signer = provider.wallet.publicKey; - - // Create PDA (if needed) - const [stakeConfigPda, bump] = anchor.web3.PublicKey.findProgramAddressSync( - [Buffer.from("stake_config")], - program.programId - ); - - // bpf_loader_upgradeable program id - const BPF_LOADER_UPGRADEABLE_ID = new PublicKey( - "BPFLoaderUpgradeab1e11111111111111111111111" - ); - // derive ProgramData PDA - const [programData] = PublicKey.findProgramAddressSync( - [program.programId.toBuffer()], - BPF_LOADER_UPGRADEABLE_ID - ); - - const unbondingPeriod = new anchor.BN(args.unbonding_period); - - console.log("Calling function:", "updateConfig"); - console.log("Available methods:", Object.keys(program.methods)); - // Print the accounts for your specific method - console.log("IDL accounts for updateConfig:", - program.idl.instructions.find(ix => ix.name === "updateConfig")?.accounts - ); - - console.log("Stake Config PDA:", stakeConfigPda.toBase58()); - console.log("Unbonding Period:", unbondingPeriod.toString()); - console.log("ProgramData PDA:", programData.toBase58()); - console.log("Signer:", signer.toBase58()); - console.log("Token Program:", TOKEN_PROGRAM_ID.toBase58()); - - try { - const tx = await program.methods - .updateConfig(unbondingPeriod) - .accounts({ - signer: signer, - programData: programData, - }) - .rpc(); - console.log("Transaction:", tx); - } catch (error) { - throw error; - } - - -}; - -main().catch(console.error); From 61a6da8211f697ac3afd93ede58ba290dfe7cca0 Mon Sep 17 00:00:00 2001 From: Jason D Date: Mon, 9 Mar 2026 10:03:33 -0600 Subject: [PATCH 3/3] pr review finding updates to clean up comments and tests --- programs/vault-stake/src/state.rs | 4 ++-- scripts/vault-stake/redeem.ts | 5 ++--- tests/vault-stake.test.ts | 6 +++--- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/programs/vault-stake/src/state.rs b/programs/vault-stake/src/state.rs index 14f056a..af76d73 100644 --- a/programs/vault-stake/src/state.rs +++ b/programs/vault-stake/src/state.rs @@ -8,7 +8,7 @@ pub const VIRTUAL_ASSETS: u128 = 1_000_000; // multiplier to prevent inflation a pub struct StakeConfig { pub vault: Pubkey, pub mint: Pubkey, - // DEPRECATED: unbonding period removed in v2. Kept for on-chain account layout compatibility. + // DEPRECATED: unbonding period removed. Kept for on-chain account layout compatibility. pub unbonding_period: i64, pub freeze_administrators: Vec, pub rewards_administrators: Vec, @@ -21,7 +21,7 @@ impl StakeConfig { pub const LEN: usize = 8 + 32 + 32 + 8 + (4 + (32 * MAX_ADMINISTRATORS)) + (4 + (32 * MAX_ADMINISTRATORS)) + 1 + 1; } -// DEPRECATED: No new tickets are created (unbond instruction removed in v2). +// DEPRECATED: No new tickets are created (unbond instruction removed). // Kept so Anchor can deserialize existing on-chain tickets for closure during redeem. #[account] pub struct UnbondingTicket { diff --git a/scripts/vault-stake/redeem.ts b/scripts/vault-stake/redeem.ts index 61731a8..46f36c7 100755 --- a/scripts/vault-stake/redeem.ts +++ b/scripts/vault-stake/redeem.ts @@ -1,8 +1,7 @@ import * as anchor from "@coral-xyz/anchor"; -import yargs from "yargs"; import {Program} from "@coral-xyz/anchor"; +import yargs from "yargs"; import {VaultStake} from "../../target/types/vault_stake"; -import BN from "bn.js"; const provider = anchor.AnchorProvider.env(); anchor.setProvider(provider); @@ -92,7 +91,7 @@ const main = async () => { console.log(`Legacy Ticket PDA: ${ticketPda.toBase58()} (${legacyTicketInfo !== null ? "found — will be closed and rent returned" : "not found — skipped"})`); const tx = await program.methods - .redeem(new BN(args.amount, 10, "le")) + .redeem(new anchor.BN(args.amount, 10, "le")) .accountsStrict({ stakeConfig: stakeConfigPda, stakeVaultTokenAccountConfig: stakeVaultTokenAccountConfigPda, diff --git a/tests/vault-stake.test.ts b/tests/vault-stake.test.ts index 366773d..9c3f7c1 100644 --- a/tests/vault-stake.test.ts +++ b/tests/vault-stake.test.ts @@ -581,7 +581,7 @@ describe("vault-stake", () => { assert.isTrue(vaultBalanceBefore > (user1Assets + user2Assets), "There will be rounding dust"); // step 4 - attacker redeems immediately (no unbonding period) - await program.methods.redeem(new BN(user1Shares)) + await program.methods.redeem(new BN(user1Shares.toString())) .accountsStrict({ stakeConfig: stakeConfigPda, vaultTokenAccount: vaultTokenAccount, @@ -622,7 +622,7 @@ describe("vault-stake", () => { assert.equal(user2MintTokenBefore, createBigInt(1_999_600), "User 2 should still have 1,999,600 shares"); assert.equal(user2VaultBalanceBefore, BIG_ZERO, "User 2 should not have any vault tokens"); - await program.methods.redeem(new BN(user2MintTokenBefore)) + await program.methods.redeem(new BN(user2MintTokenBefore.toString())) .accountsStrict({ stakeConfig: stakeConfigPda, vaultTokenAccount: vaultTokenAccount, @@ -876,7 +876,7 @@ describe("vault-stake", () => { it("redeems full balance in one call", async () => { const mintBalance = (await getAccount(provider.connection, userMintTokenAccount)).amount; if (mintBalance === BigInt(0)) { - return; // user already fully redeemed in a prior test; skip + assert.fail("Test precondition violated: expected non-zero mint balance before redeeming full balance"); } await program.methods.redeem(new BN(mintBalance.toString()))