diff --git a/Anchor.toml b/Anchor.toml index d9cac31..22c1f7e 100644 --- a/Anchor.toml +++ b/Anchor.toml @@ -12,9 +12,16 @@ cluster = "Localnet" wallet = "~/.config/solana/id.json" [scripts] -# test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts -r tests/hooks.ts" -#test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/initialize-stake-pool.ts -r tests/hooks.ts" - test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/token-2022/*.ts -r tests/token-2022/hooks22.ts" +# Classic Token program tests +# test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/*.ts -r tests/hooks.ts" +# test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/initialize-stake-pool.ts -r tests/hooks.ts" +# test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/claim-all.ts -r tests/hooks.ts" + +# Token 2022 specific tests (currently do not play nice with classic tests) +test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/token-2022/*.ts -r tests/token-2022/hooks22.ts" +# test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/token-2022/01*.ts -r tests/token-2022/hooks22.ts" +# test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/token-2022/03*.ts -r tests/token-2022/hooks22.ts" +# test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/token-2022/04*.ts -r tests/token-2022/hooks22.ts" [test.validator] [[test.genesis]] diff --git a/Cargo.lock b/Cargo.lock index aa2577f..0aa05b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2413,7 +2413,7 @@ dependencies = [ [[package]] name = "spl-token-staking" -version = "2.0.0" +version = "2.0.1" dependencies = [ "anchor-lang", "anchor-spl", diff --git a/packages/token-staking/src/idl.ts b/packages/token-staking/src/idl.ts index 7515b44..5900868 100644 --- a/packages/token-staking/src/idl.ts +++ b/packages/token-staking/src/idl.ts @@ -3,7 +3,7 @@ type Mutable = { }; export const _SplTokenStakingIDL = { - version: "2.0.0", + version: "2.0.1", name: "spl_token_staking", instructions: [ { @@ -209,6 +209,11 @@ export const _SplTokenStakingIDL = { "from the account staking.", ], }, + { + name: "mint", + isMut: false, + isSigner: false, + }, { name: "from", isMut: true, @@ -386,6 +391,12 @@ export const _SplTokenStakingIDL = { isSigner: false, docs: ["Token account to transfer the previously staked token to"], }, + { + name: "mint", + isMut: false, + isSigner: false, + docs: ["Staking pool's mint"], + }, ], args: [], }, @@ -736,10 +747,14 @@ export const _SplTokenStakingIDL = { docs: ["latest amount of tokens in the vault"], type: "u64", }, + { + name: "decimals", + type: "u8", + }, { name: "padding0", type: { - array: ["u8", 8], + array: ["u8", 7], }, }, ], diff --git a/packages/token-staking/src/instructions.ts b/packages/token-staking/src/instructions.ts index aa87132..252eb59 100644 --- a/packages/token-staking/src/instructions.ts +++ b/packages/token-staking/src/instructions.ts @@ -15,6 +15,7 @@ import { SplTokenStakingV0 } from "./idl_v0"; * @param maxDuration * @param authority - defaults to `program.provider.publicKey` * @param registar - Registrar key if this StakePool uses SPL-Governance + * @param tokenProgram */ export const initStakePool = async ( program: anchor.Program, @@ -25,6 +26,7 @@ export const initStakePool = async ( maxDuration = new anchor.BN("18446744073709551615"), authority?: anchor.Address, registrar: anchor.web3.PublicKey | null = null, + tokenProgram: anchor.web3.PublicKey = SPL_TOKEN_PROGRAM_ID ) => { const _authority = authority ? new anchor.web3.PublicKey(authority) @@ -50,7 +52,7 @@ export const initStakePool = async ( stakePool: stakePoolKey, mint, vault: vaultKey, - tokenProgram: SPL_TOKEN_PROGRAM_ID, + tokenProgram: tokenProgram, rent: anchor.web3.SYSVAR_RENT_PUBKEY, systemProgram: anchor.web3.SystemProgram.programId, }) @@ -65,6 +67,7 @@ export const initStakePool = async ( * @param rewardMint * @param rewardPoolIndex * @param authority + * @param tokenProgram * @returns */ export const addRewardPool = async ( @@ -73,7 +76,8 @@ export const addRewardPool = async ( stakePoolMint: anchor.Address, rewardMint: anchor.web3.PublicKey, rewardPoolIndex = 0, - authority?: anchor.Address + authority?: anchor.Address, + tokenProgram: anchor.web3.PublicKey = SPL_TOKEN_PROGRAM_ID, ) => { const _authority = authority ? new anchor.web3.PublicKey(authority) @@ -103,7 +107,7 @@ export const addRewardPool = async ( rewardMint, stakePool: stakePoolKey, rewardVault: rewardVaultKey, - tokenProgram: SPL_TOKEN_PROGRAM_ID, + tokenProgram: tokenProgram, rent: anchor.web3.SYSVAR_RENT_PUBKEY, systemProgram: anchor.web3.SystemProgram.programId, }) @@ -115,6 +119,7 @@ export const addRewardPool = async ( * @param program * @param payer * @param owner + * @param mint * @param stakePoolKey * @param from * @param amount @@ -127,6 +132,7 @@ export const createStakeBuilder = ( program: anchor.Program, payer: anchor.web3.PublicKey, owner: anchor.web3.PublicKey, + mint: anchor.web3.PublicKey, stakePoolKey: anchor.Address, from: anchor.Address, amount: anchor.BN, @@ -157,6 +163,7 @@ export const createStakeBuilder = ( .accounts({ payer, owner, + mint: mint, from, stakePool: stakePoolKey, vault: vaultKey, @@ -178,6 +185,7 @@ export const createStakeBuilder = ( * @param program * @param payer * @param owner + * @param mint * @param stakePoolKey * @param from * @param amount @@ -190,6 +198,7 @@ export const createStakeInstruction = async ( program: anchor.Program, payer: anchor.web3.PublicKey, owner: anchor.web3.PublicKey, + mint: anchor.web3.PublicKey, stakePoolkey: anchor.Address, from: anchor.Address, amount: anchor.BN, @@ -201,6 +210,7 @@ export const createStakeInstruction = async ( program, payer, owner, + mint, stakePoolkey, from, amount, @@ -227,6 +237,7 @@ export const deposit = async ( program: anchor.Program, payer: anchor.web3.PublicKey, owner: anchor.web3.PublicKey, + mint: anchor.web3.PublicKey, stakePoolKey: anchor.Address, from: anchor.Address, amount: anchor.BN, @@ -245,6 +256,7 @@ export const deposit = async ( program, payer, owner, + mint, stakePoolKey, from, amount, diff --git a/programs/spl-token-staking/Cargo.toml b/programs/spl-token-staking/Cargo.toml index c89b8d3..6fd1653 100644 --- a/programs/spl-token-staking/Cargo.toml +++ b/programs/spl-token-staking/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "spl-token-staking" -version = "2.0.0" +version = "2.0.1" description = "Created with Anchor" edition = "2021" diff --git a/programs/spl-token-staking/src/instructions/add_reward_pool.rs b/programs/spl-token-staking/src/instructions/add_reward_pool.rs index 406e456..f9aa01f 100644 --- a/programs/spl-token-staking/src/instructions/add_reward_pool.rs +++ b/programs/spl-token-staking/src/instructions/add_reward_pool.rs @@ -1,33 +1,35 @@ use anchor_lang::prelude::*; -use anchor_spl::token::{Mint, Token, TokenAccount}; +use anchor_spl::token_interface::{ + Mint as MintInterface, TokenAccount as TokenAccountInterface, TokenInterface, +}; -use crate::state::{RewardPool, StakePool}; use crate::errors::ErrorCode; +use crate::state::{RewardPool, StakePool}; #[derive(Accounts)] #[instruction(index: u8)] pub struct AddRewardPool<'info> { - /// Payer of rent - #[account(mut)] - pub payer: Signer<'info>, - - /// Authority of the StakePool - pub authority: Signer<'info>, - - /// SPL Token Mint of the token that will be distributed as rewards - pub reward_mint: Account<'info, Mint>, - - /// StakePool where the RewardPool will be added - #[account( - mut, - has_one = authority @ ErrorCode::InvalidAuthority, - constraint = stake_pool.load()?.reward_pools[usize::from(index)].reward_vault == Pubkey::default() - @ ErrorCode::RewardPoolIndexOccupied, - )] - pub stake_pool: AccountLoader<'info, StakePool>, - - /// An SPL token Account for holding rewards to be claimed - #[account( + /// Payer of rent + #[account(mut)] + pub payer: Signer<'info>, + + /// Authority of the StakePool + pub authority: Signer<'info>, + + /// SPL Token Mint of the token that will be distributed as rewards + pub reward_mint: InterfaceAccount<'info, MintInterface>, + + /// StakePool where the RewardPool will be added + #[account( + mut, + has_one = authority @ ErrorCode::InvalidAuthority, + constraint = stake_pool.load()?.reward_pools[usize::from(index)].reward_vault == Pubkey::default() + @ ErrorCode::RewardPoolIndexOccupied, + )] + pub stake_pool: AccountLoader<'info, StakePool>, + + /// An SPL token Account for holding rewards to be claimed + #[account( init, seeds = [stake_pool.key().as_ref(), reward_mint.key().as_ref(), b"rewardVault"], bump, @@ -35,17 +37,20 @@ pub struct AddRewardPool<'info> { token::mint = reward_mint, token::authority = stake_pool, )] - pub reward_vault: Account<'info, TokenAccount>, + pub reward_vault: InterfaceAccount<'info, TokenAccountInterface>, - pub token_program: Program<'info, Token>, - pub rent: Sysvar<'info, Rent>, - pub system_program: Program<'info, System>, + pub token_program: Interface<'info, TokenInterface>, + pub rent: Sysvar<'info, Rent>, + pub system_program: Program<'info, System>, } pub fn handler(ctx: Context, index: u8) -> Result<()> { - let mut stake_pool = ctx.accounts.stake_pool.load_mut()?; - let reward_pool = RewardPool::new(&ctx.accounts.reward_vault.key()); - stake_pool.reward_pools[usize::from(index)] = reward_pool; - - Ok(()) -} \ No newline at end of file + let mut stake_pool = ctx.accounts.stake_pool.load_mut()?; + let reward_pool = RewardPool::new( + &ctx.accounts.reward_vault.key(), + ctx.accounts.reward_mint.decimals, + ); + stake_pool.reward_pools[usize::from(index)] = reward_pool; + + Ok(()) +} diff --git a/programs/spl-token-staking/src/instructions/claim_all.rs b/programs/spl-token-staking/src/instructions/claim_all.rs index cd39039..52082b8 100644 --- a/programs/spl-token-staking/src/instructions/claim_all.rs +++ b/programs/spl-token-staking/src/instructions/claim_all.rs @@ -1,4 +1,5 @@ use anchor_lang::prelude::*; +use anchor_spl::token_2022::Token2022; use super::claim_base::*; @@ -10,7 +11,12 @@ pub struct ClaimAll<'info> { pub fn handler<'info>(ctx: Context<'_, '_, 'info, 'info, ClaimAll<'info>>) -> Result<()> { { let mut stake_pool = ctx.accounts.claim_base.stake_pool.load_mut()?; - stake_pool.recalculate_rewards_per_effective_stake(&ctx.remaining_accounts, 2usize)?; + let step = if ctx.accounts.claim_base.token_program.key() == Token2022::id() { + 3usize + } else { + 2usize + }; + stake_pool.recalculate_rewards_per_effective_stake(&ctx.remaining_accounts, step)?; } let claimed_amounts = ctx diff --git a/programs/spl-token-staking/src/instructions/claim_base.rs b/programs/spl-token-staking/src/instructions/claim_base.rs index a18efc2..24d8bee 100644 --- a/programs/spl-token-staking/src/instructions/claim_base.rs +++ b/programs/spl-token-staking/src/instructions/claim_base.rs @@ -1,5 +1,7 @@ use anchor_lang::prelude::*; -use anchor_spl::token::{self, Token, Transfer}; +use anchor_spl::token::{self, Transfer}; +use anchor_spl::token_2022::{self, Token2022, TransferChecked}; +use anchor_spl::token_interface::TokenInterface; use crate::errors::ErrorCode; use crate::math::U256; @@ -24,7 +26,7 @@ pub struct ClaimBase<'info> { )] pub stake_deposit_receipt: Account<'info, StakeDepositReceipt>, - pub token_program: Program<'info, Token>, + pub token_program: Interface<'info, TokenInterface>, } impl<'info> ClaimBase<'info> { @@ -33,20 +35,45 @@ impl<'info> ClaimBase<'info> { &self, reward_vault_info: AccountInfo<'info>, owner_reward_account_info: AccountInfo<'info>, + mint_account_info: Option>, amount: u64, + index: Option, ) -> Result<()> { let stake_pool = self.stake_pool.load()?; - let cpi_ctx = CpiContext { - program: self.token_program.to_account_info(), - accounts: Transfer { - from: reward_vault_info, - to: owner_reward_account_info, - authority: self.stake_pool.to_account_info(), - }, - remaining_accounts: Vec::new(), - signer_seeds: &[stake_pool_signer_seeds!(stake_pool)], - }; - token::transfer(cpi_ctx, amount) + + if self.token_program.key() == Token2022::id() { + if mint_account_info.is_none() || index.is_none() { + panic!("No mint or index for a token 2022 ix") + } + let cpi_ctx = CpiContext { + program: self.token_program.to_account_info(), + accounts: TransferChecked { + from: reward_vault_info, + to: owner_reward_account_info, + mint: mint_account_info.unwrap(), + authority: self.stake_pool.to_account_info(), + }, + remaining_accounts: Vec::new(), + signer_seeds: &[stake_pool_signer_seeds!(stake_pool)], + }; + token_2022::transfer_checked( + cpi_ctx, + amount, + stake_pool.reward_pools[index.unwrap()].decimals, + ) + } else { + let cpi_ctx = CpiContext { + program: self.token_program.to_account_info(), + accounts: Transfer { + from: reward_vault_info, + to: owner_reward_account_info, + authority: self.stake_pool.to_account_info(), + }, + remaining_accounts: Vec::new(), + signer_seeds: &[stake_pool_signer_seeds!(stake_pool)], + }; + token::transfer(cpi_ctx, amount) + } } /// Iterated over reward pools to calculate amount claimable from each and @@ -63,7 +90,12 @@ impl<'info> ClaimBase<'info> { continue; } // indexes for the relevant remaining accounts - let reward_vault_account_index = remaining_accounts_index * 2; + let step = if self.token_program.key() == Token2022::id() { + 3 + } else { + 2 + }; + let reward_vault_account_index = remaining_accounts_index * step; let owner_account_index = reward_vault_account_index + 1; let len = remaining_accounts.len(); if reward_vault_account_index >= len || owner_account_index >= len { @@ -93,11 +125,18 @@ impl<'info> ClaimBase<'info> { let reward_vault_info = &remaining_accounts[reward_vault_account_index]; let owner_reward_account_info = &remaining_accounts[owner_account_index]; + let mint_account_info = if self.token_program.key() == Token2022::id() { + Some(remaining_accounts[reward_vault_account_index + 2].clone()) + } else { + None + }; self.transfer_reward_from_pool_to_owner( reward_vault_info.to_account_info(), owner_reward_account_info.to_account_info(), + mint_account_info, total_claimable, + Some(index), )?; claimed_amounts[index] = total_claimable; diff --git a/programs/spl-token-staking/src/instructions/deposit.rs b/programs/spl-token-staking/src/instructions/deposit.rs index c3e6f0d..e5acc95 100644 --- a/programs/spl-token-staking/src/instructions/deposit.rs +++ b/programs/spl-token-staking/src/instructions/deposit.rs @@ -1,5 +1,10 @@ use anchor_lang::prelude::*; -use anchor_spl::token::{self, Token, TokenAccount, Transfer}; +use anchor_spl::token::{self, Transfer}; + +use anchor_spl::token_2022::{self, Token2022, TransferChecked}; +use anchor_spl::token_interface::{ + Mint as MintInterface, TokenAccount as TokenAccountInterface, TokenInterface, +}; use crate::errors::ErrorCode; use crate::state::{u128, StakeDepositReceipt, StakePool, VoterWeightRecord}; @@ -16,13 +21,15 @@ pub struct Deposit<'info> { /// CHECK: No check needed since this account will own the StakeReceipt. pub owner: UncheckedAccount<'info>, + pub mint: InterfaceAccount<'info, MintInterface>, + /// Token Account to transfer StakePool's `mint` token from, to be deposited into the vault #[account(mut)] - pub from: Account<'info, TokenAccount>, + pub from: InterfaceAccount<'info, TokenAccountInterface>, /// Vault of the StakePool token will be transfer to #[account(mut)] - pub vault: Account<'info, TokenAccount>, + pub vault: InterfaceAccount<'info, TokenAccountInterface>, /// VoterWeightRecord which caches the total weighted stake for the owner. /// In order to allow StakePools to add Governance in the future, this @@ -45,6 +52,7 @@ pub struct Deposit<'info> { #[account( mut, has_one = vault @ ErrorCode::InvalidStakePoolVault, + has_one = mint )] pub stake_pool: AccountLoader<'info, StakePool>, @@ -62,7 +70,7 @@ pub struct Deposit<'info> { )] pub stake_deposit_receipt: Account<'info, StakeDepositReceipt>, - pub token_program: Program<'info, Token>, + pub token_program: Interface<'info, TokenInterface>, pub rent: Sysvar<'info, Rent>, pub system_program: Program<'info, System>, } @@ -70,15 +78,28 @@ pub struct Deposit<'info> { impl<'info> Deposit<'info> { /// Transfer the StakePool's `mint` from the payer's address to the StakePool vault. pub fn transfer_from_user_to_stake_vault(&self, amount: u64) -> Result<()> { - let cpi_ctx = CpiContext::new( - self.token_program.to_account_info(), - Transfer { - from: self.from.to_account_info(), - to: self.vault.to_account_info(), - authority: self.payer.to_account_info(), - }, - ); - token::transfer(cpi_ctx, amount) + if self.token_program.key() == Token2022::id() { + let cpi_ctx = CpiContext::new( + self.token_program.to_account_info(), + TransferChecked { + from: self.from.to_account_info(), + to: self.vault.to_account_info(), + mint: self.mint.to_account_info(), + authority: self.payer.to_account_info(), + }, + ); + token_2022::transfer_checked(cpi_ctx, amount, self.mint.decimals) + } else { + let cpi_ctx = CpiContext::new( + self.token_program.to_account_info(), + Transfer { + from: self.from.to_account_info(), + to: self.vault.to_account_info(), + authority: self.payer.to_account_info(), + }, + ); + token::transfer(cpi_ctx, amount) + } } } diff --git a/programs/spl-token-staking/src/instructions/initialize_stake_pool.rs b/programs/spl-token-staking/src/instructions/initialize_stake_pool.rs index 608ebbb..a852617 100644 --- a/programs/spl-token-staking/src/instructions/initialize_stake_pool.rs +++ b/programs/spl-token-staking/src/instructions/initialize_stake_pool.rs @@ -1,18 +1,13 @@ -use std::str::FromStr; - use anchor_lang::prelude::*; -use anchor_spl::token::{Mint, Token}; -use anchor_spl::token_2022::Token2022; -use anchor_spl::token_interface::{TokenAccount as TokenAccountInterface, Mint as MintInterface}; +use anchor_spl::token_interface::{ + Mint as MintInterface, TokenAccount as TokenAccountInterface, TokenInterface, +}; use crate::{ errors::ErrorCode, state::{StakePool, SCALE_FACTOR_BASE}, }; -const TOKEN: Pubkey = anchor_spl::token::spl_token::ID; -const TOKEN_2022: Pubkey = anchor_spl::token_2022::spl_token_2022::ID; - #[derive(Accounts)] #[instruction( nonce: u8, @@ -30,9 +25,6 @@ pub struct InitializeStakePool<'info> { pub authority: UncheckedAccount<'info>, /// SPL Token Mint of the underlying token to be deposited for staking - #[account( - owner = TOKEN_2022 - )] pub mint: InterfaceAccount<'info, MintInterface>, #[account( @@ -56,12 +48,11 @@ pub struct InitializeStakePool<'info> { bump, payer = payer, token::mint = mint, - token::authority = stake_pool, - owner = TOKEN_2022 + token::authority = stake_pool )] pub vault: InterfaceAccount<'info, TokenAccountInterface>, - pub token_program: Program<'info, Token2022>, + pub token_program: Interface<'info, TokenInterface>, pub rent: Sysvar<'info, Rent>, pub system_program: Program<'info, System>, } diff --git a/programs/spl-token-staking/src/instructions/withdraw.rs b/programs/spl-token-staking/src/instructions/withdraw.rs index 768bde0..0b21f32 100644 --- a/programs/spl-token-staking/src/instructions/withdraw.rs +++ b/programs/spl-token-staking/src/instructions/withdraw.rs @@ -1,5 +1,7 @@ use anchor_lang::prelude::*; -use anchor_spl::token::{self, TokenAccount, Transfer}; +use anchor_spl::token::{self, Transfer}; +use anchor_spl::token_2022::{self, Token2022, TransferChecked}; +use anchor_spl::token_interface::{Mint as MintInterface, TokenAccount as TokenAccountInterface}; use crate::{ errors::ErrorCode, @@ -16,7 +18,7 @@ pub struct Withdraw<'info> { /// Vault of the StakePool token will be transferred from #[account(mut)] - pub vault: Account<'info, TokenAccount>, + pub vault: InterfaceAccount<'info, TokenAccountInterface>, #[account( mut, @@ -34,7 +36,11 @@ pub struct Withdraw<'info> { /// Token account to transfer the previously staked token to #[account(mut)] - pub destination: Account<'info, TokenAccount>, + pub destination: InterfaceAccount<'info, TokenAccountInterface>, + + // TODO could put a has_one on this but it would fail on transfer anyways if bad. + /// Staking pool's mint + pub mint: InterfaceAccount<'info, MintInterface>, } impl<'info> Withdraw<'info> { @@ -51,19 +57,37 @@ impl<'info> Withdraw<'info> { pub fn transfer_staked_tokens_to_owner(&self) -> Result<()> { let stake_pool = self.claim_base.stake_pool.load()?; let signer_seeds: &[&[&[u8]]] = &[stake_pool_signer_seeds!(stake_pool)]; - let cpi_ctx = CpiContext::new_with_signer( - self.claim_base.token_program.to_account_info(), - Transfer { - from: self.vault.to_account_info(), - to: self.destination.to_account_info(), - authority: self.claim_base.stake_pool.to_account_info(), - }, - signer_seeds, - ); - token::transfer( - cpi_ctx, - self.claim_base.stake_deposit_receipt.deposit_amount, - ) + if self.mint.to_account_info().owner.eq(&Token2022::id()) { + let cpi_ctx = CpiContext::new_with_signer( + self.claim_base.token_program.to_account_info(), + TransferChecked { + from: self.vault.to_account_info(), + to: self.destination.to_account_info(), + mint: self.mint.to_account_info(), + authority: self.claim_base.stake_pool.to_account_info(), + }, + signer_seeds, + ); + token_2022::transfer_checked( + cpi_ctx, + self.claim_base.stake_deposit_receipt.deposit_amount, + self.mint.decimals, + ) + } else { + let cpi_ctx = CpiContext::new_with_signer( + self.claim_base.token_program.to_account_info(), + Transfer { + from: self.vault.to_account_info(), + to: self.destination.to_account_info(), + authority: self.claim_base.stake_pool.to_account_info(), + }, + signer_seeds, + ); + token::transfer( + cpi_ctx, + self.claim_base.stake_deposit_receipt.deposit_amount, + ) + } } pub fn close_stake_deposit_receipt(&self) -> Result<()> { @@ -82,7 +106,12 @@ pub fn handler<'info>(ctx: Context<'_, '_, 'info, 'info, Withdraw<'info>>) -> Re { let mut stake_pool = ctx.accounts.claim_base.stake_pool.load_mut()?; // Recalculate rewards for stake prior, so withdrawing user can receive all rewards - stake_pool.recalculate_rewards_per_effective_stake(&ctx.remaining_accounts, 2usize)?; + let step = if ctx.accounts.claim_base.token_program.key() == Token2022::id() { + 3usize + } else { + 2usize + }; + stake_pool.recalculate_rewards_per_effective_stake(&ctx.remaining_accounts, step)?; // Decrement total weighted stake for future deposit reward ownership to be calculated correctly let total_staked = stake_pool .total_weighted_stake_u128() diff --git a/programs/spl-token-staking/src/lib.rs b/programs/spl-token-staking/src/lib.rs index 9ab154d..a74993b 100644 --- a/programs/spl-token-staking/src/lib.rs +++ b/programs/spl-token-staking/src/lib.rs @@ -11,6 +11,12 @@ use crate::instructions::*; declare_id!("STAKEGztX7S1MUHxcQHieZhELCntb9Ys9BgUbeEtMu1"); +// Either token program can be used for any instruction, e.g.: +// However, it must match the mint (e.g. if mints use 2022, you must pass 2022) +// const TOKEN: Pubkey = anchor_spl::token::spl_token::ID; +// const TOKEN_2022: Pubkey = anchor_spl::token_2022::spl_token_2022::ID; +// Equivalent to {TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID} from @solana/spl-token + #[program] pub mod spl_token_staking { use super::*; diff --git a/programs/spl-token-staking/src/state.rs b/programs/spl-token-staking/src/state.rs index 3091c8b..cc7a507 100644 --- a/programs/spl-token-staking/src/state.rs +++ b/programs/spl-token-staking/src/state.rs @@ -1,5 +1,5 @@ use anchor_lang::prelude::*; -use anchor_spl::token::TokenAccount; +use anchor_spl::token_interface::TokenAccount as TokenAccountInterface; use bytemuck::{Pod, Zeroable}; use core::primitive; use jet_proc_macros::assert_size; @@ -69,7 +69,8 @@ pub struct RewardPool { pub rewards_per_effective_stake: u128, /** latest amount of tokens in the vault */ pub last_amount: u64, - _padding0: [u8; 8], + pub decimals: u8, + _padding0: [u8; 7], } impl RewardPool { @@ -77,9 +78,10 @@ impl RewardPool { self.reward_vault == Pubkey::default() } - pub fn new(reward_vault: &Pubkey) -> Self { + pub fn new(reward_vault: &Pubkey, decimals: u8) -> Self { let mut res = Self::default(); res.reward_vault = *reward_vault; + res.decimals = decimals; res } @@ -177,8 +179,9 @@ impl StakePool { if remaining_accounts_index >= remaining_accounts.len() { msg!( - "Missing at least one reward vault account. Failed at index {:?}", - remaining_accounts_index + "Missing at least one reward vault account. Failed at index {:?} but passed {:?} accounts", + remaining_accounts_index, + remaining_accounts.len() ); return err!(ErrorCode::InvalidRewardPoolVaultIndex); } @@ -188,15 +191,16 @@ impl StakePool { // indexes line up. if reward_pool.reward_vault != account_info.key() { msg!( - "expected pool: {:?} but got {:?}", + "expected pool: {:?} but got {:?} at index {:?}", reward_pool.reward_vault, - account_info.key() + account_info.key(), + remaining_accounts_index ); return err!(ErrorCode::InvalidRewardPoolVault); } - - let token_account: Account<'info, TokenAccount> = - Account::try_from(&account_info).map_err(|_| ErrorCode::InvalidRewardPoolVault)?; + let token_account: InterfaceAccount<'info, TokenAccountInterface> = + InterfaceAccount::try_from(&account_info) + .map_err(|_| ErrorCode::InvalidRewardPoolVault)?; remaining_accounts_index += reward_vault_account_offset; if reward_pool.last_amount == token_account.amount { diff --git a/tests/claim-all.ts b/tests/claim-all.ts index f4321fb..88e452a 100644 --- a/tests/claim-all.ts +++ b/tests/claim-all.ts @@ -480,16 +480,16 @@ describe("claim-all", () => { // NOTE: we must pass an array of RewardPoolVault and user token accounts // as remaining accounts - await program.methods + let claimIx = await program.methods .claimAll() .accounts({ claimBase: { owner: depositor1.publicKey, stakePool: stakePoolKey, stakeDepositReceipt: stakeReceiptKey, + tokenProgram: TOKEN_PROGRAM_ID }, }) - .signers([depositor1]) .remainingAccounts([ // reward 1 { @@ -514,7 +514,14 @@ describe("claim-all", () => { isSigner: false, }, ]) - .rpc({ skipPreflight: true }); + .instruction(); + try { + await program.provider.sendAndConfirm(new Transaction().add(claimIx), [ + depositor1, + ]); + } catch (err) { + console.log(err); + } const [depositerReward1Account, depositerReward2Account] = await Promise.all([ diff --git a/tests/decimal-overflow.ts b/tests/decimal-overflow.ts index d52d24d..d08b8b9 100644 --- a/tests/decimal-overflow.ts +++ b/tests/decimal-overflow.ts @@ -130,6 +130,7 @@ describe("decimal-overflow", () => { .accounts({ payer: depositor.publicKey, owner: depositor.publicKey, + mint: mintToBeStaked, from: mintToBeStakedAccount, stakePool: stakePoolKey, vault: vaultKey, diff --git a/tests/deposit.ts b/tests/deposit.ts index d2b1995..4f23191 100644 --- a/tests/deposit.ts +++ b/tests/deposit.ts @@ -187,6 +187,7 @@ describe("deposit", () => { .accounts({ payer: depositor.publicKey, owner: depositor.publicKey, + mint: mintToBeStaked, from: mintToBeStakedAccount, stakePool: stakePoolKey, vault: vaultKey, @@ -279,6 +280,7 @@ describe("deposit", () => { .accounts({ payer: depositor.publicKey, owner: depositor.publicKey, + mint: mintToBeStaked, from: mintToBeStakedAccount, stakePool: stakePoolKey, vault: vaultKey, @@ -370,6 +372,7 @@ describe("deposit", () => { .accounts({ payer: depositor.publicKey, owner: depositor.publicKey, + mint: mintToBeStaked, from: mintToBeStakedAccount, stakePool: stakePoolKey, vault: vaultKey, @@ -414,6 +417,7 @@ describe("deposit", () => { .accounts({ payer: depositor.publicKey, owner: depositor.publicKey, + mint: mintToBeStaked, from: mintToBeStakedAccount, stakePool: stakePoolKey, vault: vaultKey, @@ -501,6 +505,7 @@ describe("deposit", () => { .accounts({ payer: depositor.publicKey, owner: depositor.publicKey, + mint: mintToBeStaked, from: mintToBeStakedAccount, stakePool: stakePoolKey, vault: vaultKey, @@ -552,6 +557,7 @@ describe("deposit", () => { .accounts({ payer: depositor.publicKey, owner: depositor.publicKey, + mint: mintToBeStaked, from: mintToBeStakedAccount, stakePool: stakePoolKey, vault: vaultKey, @@ -611,6 +617,7 @@ describe("deposit", () => { .accounts({ payer: depositor.publicKey, owner: depositor.publicKey, + mint: mintToBeStaked, from: mintToBeStakedAccount, stakePool: stakePoolKey, vault: vaultKey, @@ -701,6 +708,7 @@ describe("deposit", () => { .accounts({ payer: depositor.publicKey, owner: owner.publicKey, + mint: mintToBeStaked, from: mintToBeStakedAccount, stakePool: stakePoolKey, vault: vaultKey, diff --git a/tests/initialize-stake-pool.ts b/tests/initialize-stake-pool.ts index 833651c..85a821a 100644 --- a/tests/initialize-stake-pool.ts +++ b/tests/initialize-stake-pool.ts @@ -103,7 +103,6 @@ describe("initialize-stake-pool", () => { mint: mintToBeStaked, vault: vaultKey, tokenProgram: TOKEN_PROGRAM_ID, - token2022Program: TOKEN_2022_PROGRAM_ID, rent: anchor.web3.SYSVAR_RENT_PUBKEY, systemProgram: anchor.web3.SystemProgram.programId, }) diff --git a/tests/token-2022/initialize-stake-pool.ts b/tests/token-2022/00_initialize-stake-pool.ts similarity index 88% rename from tests/token-2022/initialize-stake-pool.ts rename to tests/token-2022/00_initialize-stake-pool.ts index 3e644ec..e3f925f 100644 --- a/tests/token-2022/initialize-stake-pool.ts +++ b/tests/token-2022/00_initialize-stake-pool.ts @@ -1,23 +1,15 @@ import * as anchor from "@coral-xyz/anchor"; -import { SPL_TOKEN_PROGRAM_ID, splTokenProgram } from "@coral-xyz/spl-token"; +import { splTokenProgram } from "@coral-xyz/spl-token"; import { SplTokenStaking } from "../../target/types/spl_token_staking"; import { assert } from "chai"; import { mintToBeStaked } from "./hooks22"; -import { - SCALE_FACTOR_BASE, - createRegistrar, -} from "@mithraic-labs/token-staking"; -import { - TOKEN_2022_PROGRAM_ID, - TOKEN_PROGRAM_ID, - getMint, -} from "@solana/spl-token"; +import { SCALE_FACTOR_BASE } from "@mithraic-labs/token-staking"; +import { TOKEN_2022_PROGRAM_ID } from "@solana/spl-token"; import { assertBNEqual, assertKeyDefault, assertKeysEqual, } from "../genericTests"; -import { createRealm } from "../utils"; import { GOVERNANCE_PROGRAM_ID, GOVERNANCE_PROGRAM_SEED, @@ -28,8 +20,6 @@ describe("initialize-stake-pool", () => { const program = anchor.workspace .SplTokenStaking as anchor.Program; const tokenProgram = TOKEN_2022_PROGRAM_ID; - console.log("2022: " + tokenProgram); - console.log("regular: " + TOKEN_PROGRAM_ID); const tokenProgramInstance = splTokenProgram({ programId: tokenProgram }); const splGovernance = createSplGovernanceProgram( // @ts-ignore @@ -56,7 +46,7 @@ describe("initialize-stake-pool", () => { ); before(async () => { - // TODO currently governance is incompatible... + // TODO currently governance is incompatible with Token 2022... // console.log("realm"); // // create realm and registrar // const realmAuthority = program.provider.publicKey; @@ -99,9 +89,8 @@ describe("initialize-stake-pool", () => { const maxDuration = new anchor.BN(31536000); // 1 year in seconds const baseWeight = new anchor.BN(SCALE_FACTOR_BASE.toString()); const maxWeight = new anchor.BN(4 * parseInt(SCALE_FACTOR_BASE.toString())); - console.log("a"); - console.log("program " + program.programId); - await program.methods + + let initIx = await program.methods .initializeStakePool( nonce, maxWeight, @@ -114,12 +103,17 @@ describe("initialize-stake-pool", () => { stakePool: stakePoolKey, mint: mintToBeStaked, vault: vaultKey, - tokenProgram: TOKEN_2022_PROGRAM_ID, + tokenProgram: tokenProgram, rent: anchor.web3.SYSVAR_RENT_PUBKEY, systemProgram: anchor.web3.SystemProgram.programId, }) - .rpc(); - console.log("b"); + .instruction(); + await program.provider.sendAndConfirm( + new anchor.web3.Transaction().add( + initIx + ) + ); + const [vault, stakePool] = await Promise.all([ tokenProgramInstance.account.account.fetch(vaultKey), program.account.stakePool.fetch(stakePoolKey), @@ -151,5 +145,4 @@ describe("initialize-stake-pool", () => { assertBNEqual(rewardPool.lastAmount, 0); }); }); - }); diff --git a/tests/token-2022/01_deposit.ts b/tests/token-2022/01_deposit.ts new file mode 100644 index 0000000..93e1f62 --- /dev/null +++ b/tests/token-2022/01_deposit.ts @@ -0,0 +1,256 @@ +import * as anchor from "@coral-xyz/anchor"; +import { splTokenProgram } from "@coral-xyz/spl-token"; +import { assert } from "chai"; +import { + rewardMint1, + mintToBeStaked, + createDepositorSplAccounts, + TEST_MINT_DECIMALS, +} from "./hooks22"; +import { + getAssociatedTokenAddressSync, + createTransferInstruction, + createAssociatedTokenAccountInstruction, + TOKEN_2022_PROGRAM_ID, + getAccount, +} from "@solana/spl-token"; +import { + SCALE_FACTOR_BASE, + SCALE_FACTOR_BASE_BN, + addRewardPool, + calculateStakeWeight, + createRegistrar, + fetchVoterWeightRecord, + getDigitShift, + getNextUnusedStakeReceiptNonce, + initStakePool, +} from "@mithraic-labs/token-staking"; +import { assertBNEqual, assertKeysEqual } from "../genericTests"; +import { + GOVERNANCE_PROGRAM_ID, + GOVERNANCE_PROGRAM_SEED, + createSplGovernanceProgram, +} from "@mithraic-labs/spl-governance"; +import { createRealm } from "../utils"; +import { SplTokenStaking } from "../../target/types/spl_token_staking"; + +const scaleFactorBN = new anchor.BN(SCALE_FACTOR_BASE.toString()); + +describe("deposit", () => { + const program = anchor.workspace + .SplTokenStaking as anchor.Program; + // const splGovernance = createSplGovernanceProgram( + // // @ts-ignore + // program._provider.wallet, + // program.provider.connection, + // GOVERNANCE_PROGRAM_ID + // ); + const tokenProgram = TOKEN_2022_PROGRAM_ID; + const splTokenProgramInstance = splTokenProgram({ programId: tokenProgram }); + const depositor = new anchor.web3.Keypair(); + + const stakePoolNonce = 7; // TODO unique global nonce generation? + const [stakePoolKey] = anchor.web3.PublicKey.findProgramAddressSync( + [ + new anchor.BN(stakePoolNonce).toArrayLike(Buffer, "le", 1), + mintToBeStaked.toBuffer(), + program.provider.publicKey.toBuffer(), + Buffer.from("stakePool", "utf-8"), + ], + program.programId + ); + const [vaultKey] = anchor.web3.PublicKey.findProgramAddressSync( + [stakePoolKey.toBuffer(), Buffer.from("vault", "utf-8")], + program.programId + ); + const [rewardVaultKey] = anchor.web3.PublicKey.findProgramAddressSync( + [ + stakePoolKey.toBuffer(), + rewardMint1.toBuffer(), + Buffer.from("rewardVault", "utf-8"), + ], + program.programId + ); + const mintToBeStakedAccount = getAssociatedTokenAddressSync( + mintToBeStaked, + depositor.publicKey, + false, + tokenProgram + ); + const deposit1Amount = new anchor.BN(5_000_000_000); + const deposit2Amount = new anchor.BN(1_000_000_000); + const maxWeight = new anchor.BN(4 * parseInt(SCALE_FACTOR_BASE.toString())); + const minDuration = new anchor.BN(1000); + const maxDuration = new anchor.BN(4 * 31536000); + const digitShift = getDigitShift( + BigInt(maxWeight.toString()), + TEST_MINT_DECIMALS + ); + const [voterWeightRecordKey] = anchor.web3.PublicKey.findProgramAddressSync( + [ + stakePoolKey.toBuffer(), + depositor.publicKey.toBuffer(), + Buffer.from("voterWeightRecord", "utf-8"), + ], + program.programId + ); + // const realmName = "deposit-realm"; + // const [realmAddress] = anchor.web3.PublicKey.findProgramAddressSync( + // [ + // Buffer.from(GOVERNANCE_PROGRAM_SEED, "utf-8"), + // Buffer.from(realmName, "utf-8"), + // ], + // splGovernance.programId + // ); + // const communityTokenMint = mintToBeStaked; + const [registrarKey] = [anchor.web3.PublicKey.default]; + // anchor.web3.PublicKey.findProgramAddressSync( + // [ + // realmAddress.toBuffer(), + // communityTokenMint.toBuffer(), + // Buffer.from("registrar", "utf-8"), + // ], + // program.programId + // ); + // const realmAuthority = splGovernance.provider.publicKey; + + before(async () => { + // TODO currently governance is incompatible with Token 2022... + // await createRealm( + // // @ts-ignore + // splGovernance, + // realmName, + // communityTokenMint, + // realmAuthority, + // program.programId + // ); + // await createRegistrar( + // program, + // realmAddress, + // mintToBeStaked, + // GOVERNANCE_PROGRAM_ID, + // program.provider.publicKey + // ); + + // set up depositor account and stake pool account + + await createDepositorSplAccounts(program, depositor, stakePoolNonce), + await initStakePool( + program, + mintToBeStaked, + stakePoolNonce, + maxWeight, + minDuration, + maxDuration, + undefined, + registrarKey, + tokenProgram + ); + await addRewardPool( + program, + stakePoolNonce, + mintToBeStaked, + rewardMint1, + undefined, + undefined, + tokenProgram + ); + await program.methods + .createVoterWeightRecord() + .accounts({ + owner: depositor.publicKey, + registrar: null, // TODO update when governance re-added. + stakePool: stakePoolKey, + voterWeightRecord: voterWeightRecordKey, + rent: anchor.web3.SYSVAR_RENT_PUBKEY, + systemProgram: anchor.web3.SystemProgram.programId, + }) + .rpc(); + }); + + it("First Deposit (5) successful", async () => { + const nextNonce = await getNextUnusedStakeReceiptNonce( + program.provider.connection, + program.programId, + depositor.publicKey, + stakePoolKey + ); + assert.equal(nextNonce, 0); + const [stakeReceiptKey] = anchor.web3.PublicKey.findProgramAddressSync( + [ + depositor.publicKey.toBuffer(), + stakePoolKey.toBuffer(), + new anchor.BN(nextNonce).toArrayLike(Buffer, "le", 4), + Buffer.from("stakeDepositReceipt", "utf-8"), + ], + program.programId + ); + const [mintToBeStakedAccountBefore, voterWeightRecordBefore] = + await Promise.all([ + splTokenProgramInstance.account.account.fetch(mintToBeStakedAccount), + fetchVoterWeightRecord(program, voterWeightRecordKey), + ]); + + await program.methods + .deposit(nextNonce, deposit1Amount, minDuration) + .accounts({ + payer: depositor.publicKey, + owner: depositor.publicKey, + mint: mintToBeStaked, + from: mintToBeStakedAccount, + stakePool: stakePoolKey, + vault: vaultKey, + voterWeightRecord: voterWeightRecordKey, + stakeDepositReceipt: stakeReceiptKey, + tokenProgram: tokenProgram, + rent: anchor.web3.SYSVAR_RENT_PUBKEY, + systemProgram: anchor.web3.SystemProgram.programId, + }) + .signers([depositor]) + .rpc({ skipPreflight: true }); + const [ + mintToBeStakedAccountAfter, + vault, + stakeReceipt, + stakePool, + voterWeightRecord, + ] = await Promise.all([ + splTokenProgramInstance.account.account.fetch(mintToBeStakedAccount), + splTokenProgramInstance.account.account.fetch(vaultKey), + program.account.stakeDepositReceipt.fetch(stakeReceiptKey), + program.account.stakePool.fetch(stakePoolKey), + fetchVoterWeightRecord(program, voterWeightRecordKey), + ]); + const weightedStakeAmount = deposit1Amount.div( + new anchor.BN(10 ** digitShift) + ); + assertBNEqual( + voterWeightRecord.voterWeight.sub(voterWeightRecordBefore.voterWeight), + weightedStakeAmount + ); + assertBNEqual( + mintToBeStakedAccountBefore.amount.sub(deposit1Amount), + mintToBeStakedAccountAfter.amount + ); + assertBNEqual(vault.amount, deposit1Amount); + assertKeysEqual(stakeReceipt.stakePool, stakePoolKey); + assertKeysEqual(stakeReceipt.owner, depositor.publicKey); + assertKeysEqual(stakeReceipt.payer, depositor.publicKey); + assertBNEqual(stakeReceipt.depositAmount, deposit1Amount); + stakeReceipt.claimedAmounts.forEach((claimed, index) => { + assert.equal(claimed.toString(), "0", `claimed index ${index} failed`); + }); + assertBNEqual(stakeReceipt.lockupDuration, minDuration); + // May be off by 1-2 seconds + let now = Date.now() / 1000; + assert.approximately(stakeReceipt.depositTimestamp.toNumber(), now, 2); + assertBNEqual( + stakeReceipt.effectiveStake, + deposit1Amount.mul(scaleFactorBN) + ); + assertBNEqual( + stakePool.totalWeightedStake, + deposit1Amount.mul(scaleFactorBN) + ); + }); +}); diff --git a/tests/token-2022/03_claim-all.ts b/tests/token-2022/03_claim-all.ts new file mode 100644 index 0000000..d2f4539 --- /dev/null +++ b/tests/token-2022/03_claim-all.ts @@ -0,0 +1,246 @@ +import * as anchor from "@coral-xyz/anchor"; +import { splTokenProgram } from "@coral-xyz/spl-token"; +import { + createDepositorSplAccounts, + mintToBeStaked, + rewardMint1, + rewardMint2, +} from "./hooks22"; +import { + TOKEN_2022_PROGRAM_ID, + createAssociatedTokenAccountInstruction, + createBurnInstruction, + createMintToInstruction, + createTransferInstruction, + getAssociatedTokenAddressSync, +} from "@solana/spl-token"; +import { assert } from "chai"; +import { + addRewardPool, + initStakePool, + SplTokenStaking, +} from "@mithraic-labs/token-staking"; +import { deposit } from ".././utils"; +import { assertBNEqual } from ".././genericTests"; +import { Transaction } from "@solana/web3.js"; + +describe("claim-all", () => { + const program = anchor.workspace + .SplTokenStaking as anchor.Program; + const tokenProgram = TOKEN_2022_PROGRAM_ID; + const splTokenProgramInstance = splTokenProgram({ + programId: tokenProgram, + }); + const depositor1 = new anchor.web3.Keypair(); + const depositor2 = new anchor.web3.Keypair(); + const stakePoolNonce = 4; + const [stakePoolKey] = anchor.web3.PublicKey.findProgramAddressSync( + [ + new anchor.BN(stakePoolNonce).toArrayLike(Buffer, "le", 1), + mintToBeStaked.toBuffer(), + program.provider.publicKey.toBuffer(), + Buffer.from("stakePool", "utf-8"), + ], + program.programId + ); + const [rewardVaultKey] = anchor.web3.PublicKey.findProgramAddressSync( + [ + stakePoolKey.toBuffer(), + rewardMint1.toBuffer(), + Buffer.from("rewardVault", "utf-8"), + ], + program.programId + ); + const mintToBeStakedAccountKey = getAssociatedTokenAddressSync( + mintToBeStaked, + depositor1.publicKey, + undefined, + tokenProgram + ); + const depositerReward1AccKey = getAssociatedTokenAddressSync( + rewardMint1, + depositor1.publicKey, + undefined, + tokenProgram + ); + const [voterWeightRecordKey1] = anchor.web3.PublicKey.findProgramAddressSync( + [ + stakePoolKey.toBuffer(), + depositor1.publicKey.toBuffer(), + Buffer.from("voterWeightRecord", "utf-8"), + ], + program.programId + ); + const [voterWeightRecordKey2] = anchor.web3.PublicKey.findProgramAddressSync( + [ + stakePoolKey.toBuffer(), + depositor2.publicKey.toBuffer(), + Buffer.from("voterWeightRecord", "utf-8"), + ], + program.programId + ); + + before(async () => { + // set up depositor account and stake pool account + await Promise.all([ + createDepositorSplAccounts(program, depositor1, stakePoolNonce), + createDepositorSplAccounts(program, depositor2, stakePoolNonce), + initStakePool( + program, + mintToBeStaked, + stakePoolNonce, + undefined, + undefined, + undefined, + undefined, + undefined, + tokenProgram + ), + ]); + // add reward pool to the initialized stake pool + await Promise.all([ + addRewardPool( + program, + stakePoolNonce, + mintToBeStaked, + rewardMint1, + undefined, + undefined, + tokenProgram + ), + program.methods + .createVoterWeightRecord() + .accounts({ + owner: depositor1.publicKey, + registrar: null, + stakePool: stakePoolKey, + voterWeightRecord: voterWeightRecordKey1, + rent: anchor.web3.SYSVAR_RENT_PUBKEY, + systemProgram: anchor.web3.SystemProgram.programId, + }) + .rpc(), + program.methods + .createVoterWeightRecord() + .accounts({ + owner: depositor2.publicKey, + registrar: null, + stakePool: stakePoolKey, + voterWeightRecord: voterWeightRecordKey2, + rent: anchor.web3.SYSVAR_RENT_PUBKEY, + systemProgram: anchor.web3.SystemProgram.programId, + }) + .rpc(), + ]); + }); + + it("Claim all owed rewards", async () => { + const receiptNonce = 0; + const [stakeReceiptKey] = anchor.web3.PublicKey.findProgramAddressSync( + [ + depositor1.publicKey.toBuffer(), + stakePoolKey.toBuffer(), + new anchor.BN(receiptNonce).toArrayLike(Buffer, "le", 4), + Buffer.from("stakeDepositReceipt", "utf-8"), + ], + program.programId + ); + // deposit 1 token + await deposit( + program, + stakePoolNonce, + mintToBeStaked, + depositor1, + mintToBeStakedAccountKey, + new anchor.BN(1_000_000_000), + new anchor.BN(0), + receiptNonce, + voterWeightRecordKey1, + undefined, + tokenProgram + ); + const totalReward1 = 1_000_000_000; + const transferIx = createTransferInstruction( + getAssociatedTokenAddressSync( + rewardMint1, + program.provider.publicKey, + undefined, + tokenProgram + ), + rewardVaultKey, + program.provider.publicKey, + totalReward1, + [], + tokenProgram + ); + const createDepositorReward1AccountIx = + createAssociatedTokenAccountInstruction( + program.provider.publicKey, + depositerReward1AccKey, + depositor1.publicKey, + rewardMint1, + tokenProgram + ); + // transfer 1 reward token to RewardPool at index 0 + await program.provider.sendAndConfirm( + new anchor.web3.Transaction() + .add(transferIx) + .add(createDepositorReward1AccountIx) + ); + + // NOTE: we must pass an array of RewardPoolVault and user token accounts + // as remaining accounts + let ix = await program.methods + .claimAll() + .accounts({ + claimBase: { + owner: depositor1.publicKey, + stakePool: stakePoolKey, + stakeDepositReceipt: stakeReceiptKey, + tokenProgram: tokenProgram, + }, + }) + .remainingAccounts([ + { + pubkey: rewardVaultKey, + isWritable: true, + isSigner: false, + }, + { + pubkey: depositerReward1AccKey, + isWritable: true, + isSigner: false, + }, + { + pubkey: rewardMint1, + isWritable: false, + isSigner: false, + }, + ]) + .instruction(); + + try{ + await program.provider.sendAndConfirm(new Transaction().add(ix), [ + depositor1, + ]); + }catch(err){ + console.log(err); + } + + const [depositerReward1Account, stakeReceipt, stakePool] = + await Promise.all([ + splTokenProgramInstance.account.account.fetch(depositerReward1AccKey), + program.account.stakeDepositReceipt.fetch(stakeReceiptKey), + program.account.stakePool.fetch(stakePoolKey), + ]); + + assertBNEqual(depositerReward1Account.amount, totalReward1); + assertBNEqual(stakeReceipt.claimedAmounts[0], totalReward1); + assertBNEqual(stakePool.rewardPools[0].lastAmount, 0); + assertBNEqual( + stakePool.rewardPools[0].rewardsPerEffectiveStake, + totalReward1 // scale weight =1 + ); + }); + + // TODO add multiple accounts to the test the step 3 account iterator... +}); diff --git a/tests/token-2022/04_withdraw.ts b/tests/token-2022/04_withdraw.ts new file mode 100644 index 0000000..589b0b7 --- /dev/null +++ b/tests/token-2022/04_withdraw.ts @@ -0,0 +1,216 @@ +import * as anchor from "@coral-xyz/anchor"; +import { splTokenProgram } from "@coral-xyz/spl-token"; +import { + createDepositorSplAccounts, + mintToBeStaked, + rewardMint1, +} from "./hooks22"; +import { + TOKEN_2022_PROGRAM_ID, + createAssociatedTokenAccountInstruction, + createTransferInstruction, + getAssociatedTokenAddressSync, +} from "@solana/spl-token"; +import { assert } from "chai"; +import { + addRewardPool, + fetchVoterWeightRecord, + initStakePool, +} from "@mithraic-labs/token-staking"; +import { deposit } from ".././utils"; +import { assertBNEqual } from ".././genericTests"; +import { SplTokenStaking } from "../../target/types/spl_token_staking"; + +describe("withdraw", () => { + const program = anchor.workspace + .SplTokenStaking as anchor.Program; + const tokenProgram = TOKEN_2022_PROGRAM_ID; + const tokenProgramInstance = splTokenProgram({ programId: tokenProgram }); + const depositor1 = new anchor.web3.Keypair(); + const depositor2 = new anchor.web3.Keypair(); + const stakePoolNonce = 5; + const [stakePoolKey] = anchor.web3.PublicKey.findProgramAddressSync( + [ + new anchor.BN(stakePoolNonce).toArrayLike(Buffer, "le", 1), + mintToBeStaked.toBuffer(), + program.provider.publicKey.toBuffer(), + Buffer.from("stakePool", "utf-8"), + ], + program.programId + ); + const [vaultKey] = anchor.web3.PublicKey.findProgramAddressSync( + [stakePoolKey.toBuffer(), Buffer.from("vault", "utf-8")], + program.programId + ); + const [rewardVaultKey] = anchor.web3.PublicKey.findProgramAddressSync( + [ + stakePoolKey.toBuffer(), + rewardMint1.toBuffer(), + Buffer.from("rewardVault", "utf-8"), + ], + program.programId + ); + const mintToBeStakedAccountKey = getAssociatedTokenAddressSync( + mintToBeStaked, + depositor1.publicKey, + undefined, + tokenProgram + ); + const depositorReward1AccountKey = getAssociatedTokenAddressSync( + rewardMint1, + depositor1.publicKey, + undefined, + tokenProgram + ); + const [voterWeightRecordKey] = anchor.web3.PublicKey.findProgramAddressSync( + [ + stakePoolKey.toBuffer(), + depositor1.publicKey.toBuffer(), + Buffer.from("voterWeightRecord", "utf-8"), + ], + program.programId + ); + + before(async () => { + // set up depositor account and stake pool account + await Promise.all([ + createDepositorSplAccounts(program, depositor1, stakePoolNonce), + createDepositorSplAccounts(program, depositor2, stakePoolNonce), + initStakePool( + program, + mintToBeStaked, + stakePoolNonce, + undefined, + undefined, + undefined, + undefined, + undefined, + tokenProgram + ), + ]); + // add reward pool to the initialized stake pool + await Promise.all([ + addRewardPool( + program, + stakePoolNonce, + mintToBeStaked, + rewardMint1, + undefined, + undefined, + tokenProgram + ), + program.methods + .createVoterWeightRecord() + .accounts({ + owner: depositor1.publicKey, + registrar: null, + stakePool: stakePoolKey, + voterWeightRecord: voterWeightRecordKey, + rent: anchor.web3.SYSVAR_RENT_PUBKEY, + systemProgram: anchor.web3.SystemProgram.programId, + }) + .rpc(), + ]); + }); + + it("withdraw unlocked tokens", async () => { + const receiptNonce = 0; + const [stakeReceiptKey] = anchor.web3.PublicKey.findProgramAddressSync( + [ + depositor1.publicKey.toBuffer(), + stakePoolKey.toBuffer(), + new anchor.BN(receiptNonce).toArrayLike(Buffer, "le", 4), + Buffer.from("stakeDepositReceipt", "utf-8"), + ], + program.programId + ); + const [ + stakePoolBefore, + depositerMintAccountBefore, + voterWeightRecordBefore, + ] = await Promise.all([ + program.account.stakePool.fetch(stakePoolKey), + tokenProgramInstance.account.account.fetch(mintToBeStakedAccountKey), + fetchVoterWeightRecord(program, voterWeightRecordKey), + ]); + // deposit 1 token + await deposit( + program, + stakePoolNonce, + mintToBeStaked, + depositor1, + mintToBeStakedAccountKey, + new anchor.BN(1_000_000_000), + new anchor.BN(0), + receiptNonce, + voterWeightRecordKey, + undefined, + tokenProgram + ); + + await program.methods + .withdraw() + .accounts({ + claimBase: { + owner: depositor1.publicKey, + stakePool: stakePoolKey, + stakeDepositReceipt: stakeReceiptKey, + tokenProgram: tokenProgram, + }, + vault: vaultKey, + voterWeightRecord: voterWeightRecordKey, + destination: mintToBeStakedAccountKey, + mint: mintToBeStaked, + }) + .remainingAccounts([ + { + pubkey: rewardVaultKey, + isWritable: true, + isSigner: false, + }, + { + pubkey: depositorReward1AccountKey, + isWritable: true, + isSigner: false, + }, + { + pubkey: rewardMint1, + isWritable: false, + isSigner: false, + }, + ]) + .signers([depositor1]) + .rpc({ skipPreflight: true }); + + const [ + stakePoolAfter, + depositerMintAccount, + vaultAfter, + stakeDepositReceipt, + voterWeightRecordAfter, + ] = await Promise.all([ + program.account.stakePool.fetch(stakePoolKey), + tokenProgramInstance.account.account.fetch(mintToBeStakedAccountKey), + tokenProgramInstance.account.account.fetch(vaultKey), + program.provider.connection.getAccountInfo(stakeReceiptKey), + fetchVoterWeightRecord(program, voterWeightRecordKey), + ]); + assertBNEqual( + voterWeightRecordBefore.voterWeight, + voterWeightRecordAfter.voterWeight + ); + assertBNEqual( + stakePoolBefore.totalWeightedStake, + stakePoolAfter.totalWeightedStake + ); + assertBNEqual( + depositerMintAccount.amount, + depositerMintAccountBefore.amount + ); + assertBNEqual(vaultAfter.amount, 0); + assert.isNull( + stakeDepositReceipt, + "StakeDepositReceipt account not closed" + ); + }); +}); diff --git a/tests/token-2022/hooks22.ts b/tests/token-2022/hooks22.ts index f11aefe..018a64d 100644 --- a/tests/token-2022/hooks22.ts +++ b/tests/token-2022/hooks22.ts @@ -10,6 +10,7 @@ import { createMintToCheckedInstruction, createMintToInstruction, getAssociatedTokenAddressSync, + getMint, getMintLen, } from "@solana/spl-token"; @@ -221,19 +222,19 @@ export const createDepositorSplAccounts = async ( mintStake = mintToBeStaked, mintToBeStakedAmount: number | bigint = 10_000_000_000 ) => { - const [stakePoolKey] = anchor.web3.PublicKey.findProgramAddressSync( - [ - new anchor.BN(stakePoolNonce).toArrayLike(Buffer, "le", 1), - mintStake.toBuffer(), - program.provider.publicKey.toBuffer(), - Buffer.from("stakePool", "utf-8"), - ], - program.programId - ); + // const [stakePoolKey] = anchor.web3.PublicKey.findProgramAddressSync( + // [ + // new anchor.BN(stakePoolNonce).toArrayLike(Buffer, "le", 1), + // mintStake.toBuffer(), + // program.provider.publicKey.toBuffer(), + // Buffer.from("stakePool", "utf-8"), + // ], + // program.programId + // ); const mintToBeStakedAccount = getAssociatedTokenAddressSync( mintStake, depositor.publicKey, - false, + undefined, tokenProgram ); const createMintToBeStakedAccountIx = createAssociatedTokenAccountInstruction( @@ -243,23 +244,24 @@ export const createDepositorSplAccounts = async ( mintStake, tokenProgram ); + // mint 10 of StakePool's mint to provider wallet - const mintIx = createMintToInstruction( + const mintIx = createMintToCheckedInstruction( mintStake, mintToBeStakedAccount, program.provider.publicKey, mintToBeStakedAmount, - undefined, + TEST_MINT_DECIMALS, + [], tokenProgram ); const mintTx = new anchor.web3.Transaction() .add(createMintToBeStakedAccountIx) .add(mintIx); // set up depositor account - await Promise.all([ - airdropSol(program.provider.connection, depositor.publicKey, 2), - program.provider.sendAndConfirm(mintTx), - ]); + await airdropSol(program.provider.connection, depositor.publicKey, 2); + await program.provider.sendAndConfirm(mintTx); + return depositor; }; diff --git a/tests/utils.ts b/tests/utils.ts index 92bf82f..e7a2153 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -5,7 +5,11 @@ import { GOVERNANCE_PROGRAM_SEED, SPL_GOVERNANCE_IDL, } from "@mithraic-labs/spl-governance"; -import { TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from "@solana/spl-token"; +import { + TOKEN_2022_PROGRAM_ID, + TOKEN_PROGRAM_ID, + getAccount, +} from "@solana/spl-token"; /** * Stake with an existing StakePool. @@ -28,7 +32,8 @@ export const deposit = async ( duration: anchor.BN, receiptNonce: number, voterWeightRecord: anchor.Address, - rewardVaults: anchor.web3.PublicKey[] = [] + rewardVaults: anchor.web3.PublicKey[] = [], + tokenProgram: anchor.web3.PublicKey = SPL_TOKEN_PROGRAM_ID ) => { const [stakePoolKey] = anchor.web3.PublicKey.findProgramAddressSync( [ @@ -52,33 +57,31 @@ export const deposit = async ( ], program.programId ); - try { - await program.methods - .deposit(receiptNonce, amount, duration) - .accounts({ - payer: depositor.publicKey, - owner: depositor.publicKey, - from: vaultMintAccount, - stakePool: stakePoolKey, - vault: vaultKey, - voterWeightRecord, - stakeDepositReceipt: stakeReceiptKey, - tokenProgram: SPL_TOKEN_PROGRAM_ID, - rent: anchor.web3.SYSVAR_RENT_PUBKEY, - systemProgram: anchor.web3.SystemProgram.programId, - }) - .remainingAccounts( - rewardVaults.map((rewardVaultKey) => ({ - pubkey: rewardVaultKey, - isWritable: false, - isSigner: false, - })) - ) - .signers([depositor]) - .rpc(); - } catch (err) { - console.log(err); - } + + await program.methods + .deposit(receiptNonce, amount, duration) + .accounts({ + payer: depositor.publicKey, + owner: depositor.publicKey, + mint: stakePoolMint, + from: vaultMintAccount, + stakePool: stakePoolKey, + vault: vaultKey, + voterWeightRecord, + stakeDepositReceipt: stakeReceiptKey, + tokenProgram: tokenProgram, + rent: anchor.web3.SYSVAR_RENT_PUBKEY, + systemProgram: anchor.web3.SystemProgram.programId, + }) + .remainingAccounts( + rewardVaults.map((rewardVaultKey) => ({ + pubkey: rewardVaultKey, + isWritable: false, + isSigner: false, + })) + ) + .signers([depositor]) + .rpc(); }; export const createRealm = ( diff --git a/tests/withdraw.ts b/tests/withdraw.ts index 6738699..6f60cd4 100644 --- a/tests/withdraw.ts +++ b/tests/withdraw.ts @@ -135,6 +135,7 @@ describe("withdraw", () => { vault: vaultKey, voterWeightRecord: voterWeightRecordKey, destination: mintToBeStakedAccountKey, + mint: mintToBeStaked, }) .remainingAccounts([ { @@ -251,6 +252,7 @@ describe("withdraw", () => { vault: vaultKey, voterWeightRecord: voterWeightRecordKey, destination: mintToBeStakedAccountKey, + mint: mintToBeStaked, }) .remainingAccounts([ { @@ -334,6 +336,7 @@ describe("withdraw", () => { vault: vaultKey, voterWeightRecord: voterWeightRecordKey, destination: mintToBeStakedAccountKey, + mint: mintToBeStaked, }) .remainingAccounts([ { diff --git a/tests/zero-duration-pool.ts b/tests/zero-duration-pool.ts index a0337c9..e8e63e8 100644 --- a/tests/zero-duration-pool.ts +++ b/tests/zero-duration-pool.ts @@ -100,6 +100,7 @@ describe("zero-duration-pool", () => { .accounts({ payer: depositor.publicKey, owner: depositor.publicKey, + mint: mintToBeStaked, from: mintToBeStakedAccount, stakePool: stakePoolKey, vault: vaultKey,