From 79b7a6fc649b2535837e6ee19a377f5daa08b653 Mon Sep 17 00:00:00 2001 From: Dana Date: Fri, 24 Sep 2021 12:25:57 -0400 Subject: [PATCH] wip --- programs/reward-pool/src/lib.rs | 327 +++++++++++++++++++++++--------- tests/reward-pool.js | 217 ++++++++++++--------- tests/user.js | 72 ++++++- tests/utils.js | 64 +++++-- 4 files changed, 475 insertions(+), 205 deletions(-) diff --git a/programs/reward-pool/src/lib.rs b/programs/reward-pool/src/lib.rs index cab257d..a56678e 100644 --- a/programs/reward-pool/src/lib.rs +++ b/programs/reward-pool/src/lib.rs @@ -1,17 +1,36 @@ +use crate::constants::*; use anchor_lang::prelude::*; -use anchor_lang::solana_program::{sysvar, program_option::COption}; +use anchor_lang::solana_program::{sysvar, clock, program_option::COption}; use anchor_spl::token::{self, TokenAccount, Token, Mint}; use std::convert::Into; use std::convert::TryInto; -declare_id!("SrWDHBuK1WAP2T5SB7vfm5kSyww8hbyCjj5zRivdEWY"); +#[cfg(not(feature = "local-testing"))] +declare_id!("UNKNOWN" fail build ); +#[cfg(feature = "local-testing")] +declare_id!("SRWdZfXVSH7usoNVGAMBMpTnRf4PDQWRCtd3ZLUYDsP"); + + +#[cfg(not(feature = "local-testing"))] +mod constants { + pub const X_STEP_TOKEN_MINT_PUBKEY: &str = "xStpgUCss9piqeFUk2iLVcvJEGhAdJxJQuwLkXP555G"; + pub const MIN_DURATION: u64 = 1440; +} + +#[cfg(feature = "local-testing")] +mod constants { + pub const X_STEP_TOKEN_MINT_PUBKEY: &str = "xsTPvEj7rELYcqe2D1k3M5zRe85xWWFK3x1SWDN5qPY"; + pub const MIN_DURATION: u64 = 1; +} + +const PRECISION: u128 = u64::MAX as u128; pub fn update_rewards( pool: &mut Account, user: Option<&mut Box>>, - clock: &Clock, total_staked: u64, ) -> Result<()> { + let clock = clock::Clock::get().unwrap(); let last_time_reward_applicable = last_time_reward_applicable(pool.reward_duration_end, clock.unix_timestamp); @@ -23,13 +42,15 @@ pub fn update_rewards( pool.reward_a_rate, ); - pool.reward_b_per_token_stored = reward_per_token( - total_staked, - pool.reward_b_per_token_stored, - last_time_reward_applicable, - pool.last_update_time, - pool.reward_b_rate, - ); + if pool.reward_a_vault.key() != pool.reward_b_vault.key() { + pool.reward_b_per_token_stored = reward_per_token( + total_staked, + pool.reward_b_per_token_stored, + last_time_reward_applicable, + pool.last_update_time, + pool.reward_b_rate, + ); + } pool.last_update_time = last_time_reward_applicable; @@ -58,8 +79,6 @@ pub fn last_time_reward_applicable(reward_duration_end: u64, unix_timestamp: i64 return std::cmp::min(unix_timestamp.try_into().unwrap(), reward_duration_end); } -const PRECISION: u128 = u64::MAX as u128; - pub fn reward_per_token( total_staked: u64, reward_per_token_stored: u128, @@ -116,11 +135,28 @@ pub mod reward_pool { pool_nonce: u8, reward_duration: u64, ) -> Result<()> { + + if reward_duration < MIN_DURATION { + return Err(ErrorCode::DurationTooShort.into()); + } + + //xstep lockup + let cpi_ctx = CpiContext::new( + ctx.accounts.token_program.to_account_info(), + token::Transfer { + from: ctx.accounts.x_token_depositor.to_account_info(), + to: ctx.accounts.x_token_pool_vault.to_account_info(), + authority: ctx.accounts.x_token_deposit_authority.to_account_info(), + }, + ); + token::transfer(cpi_ctx, 10_000_000_000_000)?; + let pool = &mut ctx.accounts.pool; pool.authority = ctx.accounts.authority.key(); pool.nonce = pool_nonce; pool.paused = false; + pool.x_token_pool_vault = ctx.accounts.x_token_pool_vault.key(); pool.staking_mint = ctx.accounts.staking_mint.key(); pool.staking_vault = ctx.accounts.staking_vault.key(); pool.reward_a_mint = ctx.accounts.reward_a_mint.key(); @@ -156,12 +192,65 @@ pub mod reward_pool { Ok(()) } - pub fn pause(ctx: Context, paused: bool) -> Result<()> { - ctx.accounts.pool.paused = paused; + pub fn pause(ctx: Context) -> Result<()> { + let pool = &mut ctx.accounts.pool; + pool.paused = true; + + //xstep refund + let seeds = &[ + pool.to_account_info().key.as_ref(), + &[pool.nonce], + ]; + let pool_signer = &[&seeds[..]]; + let cpi_ctx = CpiContext::new_with_signer( + ctx.accounts.token_program.to_account_info(), + token::Transfer { + from: ctx.accounts.x_token_pool_vault.to_account_info(), + to: ctx.accounts.x_token_receiver.to_account_info(), + authority: ctx.accounts.pool_signer.to_account_info(), + }, + pool_signer, + ); + + token::transfer(cpi_ctx, ctx.accounts.x_token_pool_vault.amount)?; + + let cpi_ctx = CpiContext::new_with_signer( + ctx.accounts.token_program.to_account_info(), + token::CloseAccount { + account: ctx.accounts.x_token_pool_vault.to_account_info(), + destination: ctx.accounts.authority.to_account_info(), + authority: ctx.accounts.pool_signer.to_account_info(), + }, + pool_signer, + ); + token::close_account(cpi_ctx)?; + + pool.x_token_pool_vault = Pubkey::default(); + + Ok(()) + } + + pub fn unpause(ctx: Context) -> Result<()> { + let pool = &mut ctx.accounts.pool; + pool.paused = false; + + //the prior token vault was closed when pausing + pool.x_token_pool_vault = ctx.accounts.x_token_pool_vault.key(); + + //xstep lockup + let cpi_ctx = CpiContext::new( + ctx.accounts.token_program.to_account_info(), + token::Transfer { + from: ctx.accounts.x_token_depositor.to_account_info(), + to: ctx.accounts.x_token_pool_vault.to_account_info(), + authority: ctx.accounts.x_token_deposit_authority.to_account_info(), + }, + ); + token::transfer(cpi_ctx, 10_000_000_000_000)?; + Ok(()) } - #[access_control(is_unpaused(&ctx.accounts.pool))] pub fn stake(ctx: Context, amount: u64) -> Result<()> { if amount == 0 { return Err(ErrorCode::AmountMustBeGreaterThanZero.into()); @@ -173,7 +262,6 @@ pub mod reward_pool { update_rewards( &mut ctx.accounts.pool, user_opt, - &ctx.accounts.clock, total_staked, ) .unwrap(); @@ -202,16 +290,18 @@ pub mod reward_pool { } let total_staked = ctx.accounts.staking_vault.amount; + + if ctx.accounts.user.balance_staked < spt_amount { + return Err(ErrorCode::InsufficientFundUnstake.into()); + } let user_opt = Some(&mut ctx.accounts.user); update_rewards( &mut ctx.accounts.pool, user_opt, - &ctx.accounts.clock, total_staked, ) .unwrap(); - ctx.accounts.user.balance_staked = ctx.accounts.user.balance_staked.checked_sub(spt_amount).unwrap(); // Transfer tokens from the pool vault to user vault. @@ -237,21 +327,24 @@ pub mod reward_pool { Ok(()) } - #[access_control(is_unpaused(&ctx.accounts.pool))] pub fn fund(ctx: Context, amount_a: u64, amount_b: u64) -> Result<()> { + //if vault a and b are the same, we just use a + if amount_b > 0 && ctx.accounts.reward_a_vault.key() == ctx.accounts.reward_b_vault.key() { + return Err(ErrorCode::SingleStakeTokenBCannotBeFunded.into()); + } + let pool = &mut ctx.accounts.pool; let total_staked = ctx.accounts.staking_vault.amount; update_rewards( pool, None, - &ctx.accounts.clock, total_staked, ) .unwrap(); - let current_time = ctx.accounts.clock.unix_timestamp.try_into().unwrap(); + let current_time = clock::Clock::get().unwrap().unix_timestamp.try_into().unwrap(); let reward_period_end = pool.reward_duration_end; if current_time >= reward_period_end { @@ -315,7 +408,6 @@ pub mod reward_pool { update_rewards( &mut ctx.accounts.pool, user_opt, - &ctx.accounts.clock, total_staked, ) .unwrap(); @@ -460,42 +552,44 @@ pub mod reward_pool { &[signer_seeds], )?; - //close token b vault - let ix = spl_token::instruction::transfer( - &spl_token::ID, - ctx.accounts.reward_b_vault.to_account_info().key, - ctx.accounts.reward_b_refundee.to_account_info().key, - ctx.accounts.pool_signer.key, - &[ctx.accounts.pool_signer.key], - ctx.accounts.reward_b_vault.amount, - )?; - solana_program::program::invoke_signed( - &ix, - &[ - ctx.accounts.token_program.to_account_info(), - ctx.accounts.reward_b_vault.to_account_info(), - ctx.accounts.reward_b_refundee.to_account_info(), - ctx.accounts.pool_signer.clone(), - ], - &[signer_seeds], - )?; - let ix = spl_token::instruction::close_account( - &spl_token::ID, - ctx.accounts.reward_b_vault.to_account_info().key, - ctx.accounts.refundee.key, - ctx.accounts.pool_signer.key, - &[ctx.accounts.pool_signer.key], - )?; - solana_program::program::invoke_signed( - &ix, - &[ - ctx.accounts.token_program.to_account_info(), - ctx.accounts.reward_b_vault.to_account_info(), - ctx.accounts.refundee.clone(), - ctx.accounts.pool_signer.clone(), - ], - &[signer_seeds], - )?; + if ctx.accounts.reward_a_vault.key() != ctx.accounts.reward_b_vault.key() { + //close token b vault + let ix = spl_token::instruction::transfer( + &spl_token::ID, + ctx.accounts.reward_b_vault.to_account_info().key, + ctx.accounts.reward_b_refundee.to_account_info().key, + ctx.accounts.pool_signer.key, + &[ctx.accounts.pool_signer.key], + ctx.accounts.reward_b_vault.amount, + )?; + solana_program::program::invoke_signed( + &ix, + &[ + ctx.accounts.token_program.to_account_info(), + ctx.accounts.reward_b_vault.to_account_info(), + ctx.accounts.reward_b_refundee.to_account_info(), + ctx.accounts.pool_signer.clone(), + ], + &[signer_seeds], + )?; + let ix = spl_token::instruction::close_account( + &spl_token::ID, + ctx.accounts.reward_b_vault.to_account_info().key, + ctx.accounts.refundee.key, + ctx.accounts.pool_signer.key, + &[ctx.accounts.pool_signer.key], + )?; + solana_program::program::invoke_signed( + &ix, + &[ + ctx.accounts.token_program.to_account_info(), + ctx.accounts.reward_b_vault.to_account_info(), + ctx.accounts.refundee.clone(), + ctx.accounts.pool_signer.clone(), + ], + &[signer_seeds], + )?; + } Ok(()) } @@ -504,7 +598,20 @@ pub mod reward_pool { #[derive(Accounts)] #[instruction(pool_nonce: u8)] pub struct InitializePool<'info> { - authority: Signer<'info>, + authority: AccountInfo<'info>, + + #[account( + mut, + constraint = x_token_pool_vault.mint == X_STEP_TOKEN_MINT_PUBKEY.parse::().unwrap(), + constraint = x_token_pool_vault.owner == pool_signer.key(), + )] + x_token_pool_vault: Box>, + #[account( + mut, + constraint = x_token_depositor.mint == X_STEP_TOKEN_MINT_PUBKEY.parse::().unwrap() + )] + x_token_depositor: Box>, + x_token_deposit_authority: Signer<'info>, staking_mint: Box>, #[account( @@ -512,7 +619,7 @@ pub struct InitializePool<'info> { constraint = staking_vault.owner == pool_signer.key(), //strangely, spl maintains this on owner reassignment for non-native accounts //we don't want to be given an account that someone else could close when empty - //because in our pool close operation we want to assert it is still open + //because in our "pool close" operation we want to assert it is still open constraint = staking_vault.close_authority == COption::None, )] staking_vault: Box>, @@ -521,7 +628,7 @@ pub struct InitializePool<'info> { #[account( constraint = reward_a_vault.mint == reward_a_mint.key(), constraint = reward_a_vault.owner == pool_signer.key(), - constraint = staking_vault.close_authority == COption::None, + constraint = reward_a_vault.close_authority == COption::None, )] reward_a_vault: Box>, @@ -529,7 +636,7 @@ pub struct InitializePool<'info> { #[account( constraint = reward_b_vault.mint == reward_b_mint.key(), constraint = reward_b_vault.owner == pool_signer.key(), - constraint = staking_vault.close_authority == COption::None, + constraint = reward_b_vault.close_authority == COption::None, )] reward_b_vault: Box>, @@ -545,13 +652,18 @@ pub struct InitializePool<'info> { zero, )] pool: Box>, + + token_program: Program<'info, Token>, } #[derive(Accounts)] #[instruction(nonce: u8)] pub struct CreateUser<'info> { // Stake instance. - #[account(mut)] + #[account( + mut, + constraint = !pool.paused, + )] pool: Box>, // Member. #[account( @@ -571,12 +683,63 @@ pub struct CreateUser<'info> { #[derive(Accounts)] pub struct Pause<'info> { + #[account(mut)] + x_token_pool_vault: Box>, + #[account(mut)] + x_token_receiver: Box>, + + #[account( + mut, + has_one = authority, + has_one = x_token_pool_vault, + constraint = !pool.paused, + constraint = pool.reward_duration_end < clock::Clock::get().unwrap().unix_timestamp.try_into().unwrap(), + //constraint = pool.reward_duration_end > 0, + )] + pool: Box>, + authority: Signer<'info>, + + #[account( + seeds = [ + pool.to_account_info().key.as_ref() + ], + bump = pool.nonce, + )] + pool_signer: AccountInfo<'info>, + token_program: Program<'info, Token>, +} + +#[derive(Accounts)] +pub struct Unpause<'info> { + #[account( + mut, + constraint = x_token_pool_vault.mint == X_STEP_TOKEN_MINT_PUBKEY.parse::().unwrap(), + constraint = x_token_pool_vault.owner == pool_signer.key(), + )] + x_token_pool_vault: Box>, + #[account( + mut, + constraint = x_token_depositor.mint == X_STEP_TOKEN_MINT_PUBKEY.parse::().unwrap() + )] + x_token_depositor: Box>, + x_token_deposit_authority: Signer<'info>, + #[account( mut, - has_one = authority + has_one = authority, + constraint = pool.paused, )] pool: Box>, authority: Signer<'info>, + + #[account( + seeds = [ + pool.to_account_info().key.as_ref() + ], + bump = pool.nonce, + )] + pool_signer: AccountInfo<'info>, + token_program: Program<'info, Token>, } #[derive(Accounts)] @@ -585,6 +748,7 @@ pub struct Stake<'info> { #[account( mut, has_one = staking_vault, + constraint = !pool.paused, )] pool: Box>, #[account( @@ -621,7 +785,6 @@ pub struct Stake<'info> { pool_signer: AccountInfo<'info>, // Misc. - clock: Sysvar<'info, Clock>, token_program: Program<'info, Token>, } @@ -635,6 +798,7 @@ pub struct Fund<'info> { has_one = reward_b_vault, //require signed funder auth - otherwise constant micro fund could hold funds hostage constraint = pool.authority == *funder.to_account_info().key, + constraint = !pool.paused, )] pool: Box>, #[account(mut)] @@ -660,7 +824,6 @@ pub struct Fund<'info> { pool_signer: AccountInfo<'info>, // Misc. - clock: Sysvar<'info, Clock>, token_program: Program<'info, Token>, } @@ -709,7 +872,6 @@ pub struct ClaimReward<'info> { pool_signer: AccountInfo<'info>, // Misc. - clock: Sysvar<'info, Clock>, token_program: Program<'info, Token>, } @@ -754,6 +916,8 @@ pub struct ClosePool<'info> { has_one = staking_vault, has_one = reward_a_vault, has_one = reward_b_vault, + constraint = pool.paused, + constraint = pool.reward_duration_end > 0, constraint = pool.reward_duration_end < sysvar::clock::Clock::get().unwrap().unix_timestamp.try_into().unwrap(), constraint = pool.user_stake_count == 0, )] @@ -786,6 +950,8 @@ pub struct Pool { pub nonce: u8, /// Paused state of the program pub paused: bool, + /// The vault holding users' xSTEP + pub x_token_pool_vault: Pubkey, /// Mint of the token that can be staked. pub staking_mint: Pubkey, /// Vault to store staked tokens. @@ -837,31 +1003,16 @@ pub struct User { pub nonce: u8, } -fn is_unpaused<'info>(pool: &Account<'info, Pool>) -> Result<()> { - if pool.paused { - return Err(ErrorCode::PoolPaused.into()); - } - Ok(()) -} - #[error] pub enum ErrorCode { - #[msg("The pool is paused.")] - PoolPaused, - #[msg("The nonce given doesn't derive a valid program address.")] - InvalidNonce, - #[msg("User signer doesn't match the derived address.")] - InvalidUserSigner, - #[msg("An unknown error has occured.")] - Unknown, - #[msg("Invalid config supplied.")] - InvalidConfig, - #[msg("Please specify the correct authority for this program.")] - InvalidProgramAuthority, #[msg("Insufficient funds to unstake.")] InsufficientFundUnstake, #[msg("Amount must be greater than zero.")] AmountMustBeGreaterThanZero, - #[msg("Program already initialized.")] - ProgramAlreadyInitialized, + #[msg("Reward B cannot be funded - pool is single stake.")] + SingleStakeTokenBCannotBeFunded, + #[msg("Pool is paused.")] + PoolPaused, + #[msg("Duration cannot be shorter than one day.")] + DurationTooShort, } diff --git a/tests/reward-pool.js b/tests/reward-pool.js index 07a9723..bfa10a3 100644 --- a/tests/reward-pool.js +++ b/tests/reward-pool.js @@ -5,6 +5,7 @@ const { TOKEN_PROGRAM_ID, Token } = require("@solana/spl-token"); const TokenInstructions = require("@project-serum/serum").TokenInstructions; const utils = require("./utils"); const { User, claimForUsers } = require("./user"); +const fs = require('fs'); let program = anchor.workspace.RewardPool; @@ -29,8 +30,12 @@ setProvider(provider); describe('Multiuser Reward Pool', () => { const rewardDuration = new anchor.BN(10); - const rewardDuration2 = new anchor.BN(20); + const rewardDuration2 = new anchor.BN(30); + const rewardDuration3 = new anchor.BN(20); + let xMintKey; + let xMintObject; + let xMintPubkey; let users; let users2; let funders; @@ -41,8 +46,16 @@ describe('Multiuser Reward Pool', () => { let stakingMint2; let poolKeypair = anchor.web3.Keypair.generate(); let poolKeypair2 = anchor.web3.Keypair.generate(); + let poolKeypair3 = anchor.web3.Keypair.generate(); it("Initialize mints", async () => { + //this is the xstep token + //test xstep token hardcoded in program, mint authority is itself + rawdata = fs.readFileSync('tests/keys/TESTING-xsTPvEj7rELYcqe2D1k3M5zRe85xWWFK3x1SWDN5qPY.json'); + keyData = JSON.parse(rawdata); + xMintKey = anchor.web3.Keypair.fromSecretKey(new Uint8Array(keyData)); + xMintPubkey = xMintKey.publicKey; + xMintObject = await utils.createMintFromPriv(xMintKey, provider, provider.wallet.publicKey, null, 9, TOKEN_PROGRAM_ID); setProvider(envProvider); //these mints are ecosystem mints not owned //by funder or user @@ -51,39 +64,49 @@ describe('Multiuser Reward Pool', () => { mintC = await utils.createMint(provider, 3); stakingMint = await utils.createMint(provider, 9); stakingMint2 = await utils.createMint(provider, 5); + stakingMint3 = await utils.createMint(provider, 2); }); it("Initialize users", async () => { users = [1, 2, 3, 4, 5].map(a => new User(a)); users2 = [11, 12].map(a => new User(a)); await Promise.all( - users.map(a => a.init(10_000_000_000, stakingMint.publicKey, 5_000_000_000, mintA.publicKey, 0, mintB.publicKey, 0)) + users.map(a => a.init(10_000_000_000, xMintPubkey, 0, stakingMint.publicKey, 5_000_000_000, mintA.publicKey, 0, mintB.publicKey, 0)) .concat( - users2.map(a => a.init(10_000_000_000, stakingMint2.publicKey, 500_000, mintB.publicKey, 0, mintC.publicKey, 0)) + users2.map(a => a.init(10_000_000_000, xMintPubkey, 0, stakingMint2.publicKey, 500_000, mintB.publicKey, 0, mintC.publicKey, 0)) ) ); }) it("Initialize funders", async () => { - funders = [0, 10].map(a => new User(a)); + funders = [0, 10, 20].map(a => new User(a)); await Promise.all([ - funders[0].init(10_000_000_000, stakingMint.publicKey, 0, mintA.publicKey, 100_000_000_000, mintB.publicKey, 200_000_000_000), - funders[1].init(10_000_000_000, stakingMint2.publicKey, 0, mintB.publicKey, 10_000_000_000, mintC.publicKey, 10_000), + funders[0].init(10_000_000_000, xMintPubkey, 9_999_999_999_999, stakingMint.publicKey, 0, mintA.publicKey, 100_000_000_000, mintB.publicKey, 200_000_000_000), + funders[1].init(10_000_000_000, xMintPubkey, 10_000_000_000_000, stakingMint2.publicKey, 0, mintB.publicKey, 10_000_000_000, mintC.publicKey, 10_000), + funders[2].init(10_000_000_000, xMintPubkey, 10_000_000_000_000, stakingMint3.publicKey, 0, mintB.publicKey, 10_000_000_000, mintC.publicKey, 1), ]); }); it("Creates a pool", async () => { - await funders[0].initializePool(poolKeypair, rewardDuration); + try { + await funders[0].initializePool(poolKeypair, rewardDuration, false); + assert.fail("did not fail for lack of xSTEP"); + } catch (e) { } + + //give just ONE more xSTEP + xMintObject.mintTo(funders[0].xTokenPubkey, envProvider.wallet.payer, [], 1) + await funders[0].initializePool(poolKeypair, rewardDuration, false); //second funder tries to create with same pubkey try { - await funders[1].initializePool(poolKeypair, rewardDuration2); + await funders[1].initializePool(poolKeypair, rewardDuration2, false); assert.fail("did not fail to create dupe pool"); } catch (e) { } - await funders[1].initializePool(poolKeypair2, rewardDuration2); + await funders[1].initializePool(poolKeypair2, rewardDuration2, false); + await funders[2].initializePool(poolKeypair3, rewardDuration3, true); }); - + it('Users create staking accounts', async () => { let pool = funders[0].poolPubkey; let pool2 = funders[1].poolPubkey; @@ -134,65 +157,6 @@ describe('Multiuser Reward Pool', () => { } catch (e) { } }); - it('User tries to pause the pool using own pubkey', async () => { - try { - await users[0].pausePool(true, null); - assert.fail("did not fail on user pause pool"); - } catch (e) { } - }); - - it('User tries to pause the pool using funder pubkey but unsigned', async () => { - try { - await users[0].pausePool(true, funders[0].provider.wallet.publicKey); - assert.fail("did not fail on user pause pool"); - } catch (e) { } - }); - - it('Funder pauses the pool', async () => { - await funders[0].pausePool(true, null); - }); - - it('Funder pauses the paused pool', async () => { - await funders[0].pausePool(true, null); - }); - - it('User tries to stake some tokens in paused pool', async () => { - try { - await users[3].stakeTokens(100_000); - assert.fail("did not fail on user staking in paused pool"); - } catch (e) { } - }); - - it('User tries to unpause the pool using own pubkey', async () => { - try { - await users[0].pausePool(false, null); - assert.fail("did not fail on user pause pool"); - } catch (e) { } - }); - - it('User tries to unpause the pool using funder pubkey but unsigned', async () => { - try { - await users[0].pausePool(false, funders[0].provider.wallet.publicKey); - assert.fail("did not fail on user pause pool"); - } catch (e) { } - }); - - it('User unstakes some tokens in paused pool', async () => { - await users[2].unstakeTokens(250_000_000); - }); - - it('Funder unpauses the pool', async () => { - await funders[0].pausePool(false, null); - }); - - it('Funder unpauses the unpaused pool', async () => { - await funders[0].pausePool(false, null); - }); - - it('User stakes some tokens in unpaused pool', async () => { - await users[2].stakeTokens(250_000_000); - }); - it('Users try to unstake when they have none', async () => { try { await users[3].unstakeTokens(1); @@ -206,18 +170,19 @@ describe('Multiuser Reward Pool', () => { assert.fail("did not fail on user unstaking when more than they have"); } catch (e) { } }); - - //delete me - it('Pool 2 stakers', async () => { - await users2[0].stakeTokens(250_000); - }); //now is still users stakes: 2_000_000_000, 2_000_000_000, 500_000_000, 0, 0 it('Funder funds the pool', async () => { - //10 second duration - await funders[0].fund(1_000_000_000, 2_000_000_000); - //30 second duration - await funders[1].fund(1_000_000_000, 1_000); //with decimals, this is 1 of each + //10 second duration + await funders[0].fund(1_000_000_000, 2_000_000_000); + //30 second duration + await funders[1].fund(1_000_000_000, 1_000); //with decimals, this is 1 of each + + try { + await funders[2].fund(1_000_000_000, 1); + assert.fail("single stake pool should fail if funded token b"); + } catch (e) { } + await funders[2].fund(1_000_000_000, 0); }); it('waits', async () => { @@ -260,7 +225,7 @@ describe('Multiuser Reward Pool', () => { }); it('waits', async () => { - await wait(2); //pool 1 @ -3, pool 2 @ 16 + await wait(2); //pool 1 @ -3, pool 2 @ 17 }); it('Pool 2 stake only stakes halfway through duration', async () => { @@ -273,23 +238,13 @@ describe('Multiuser Reward Pool', () => { assert.fail("did not fail closing active staking account"); } catch (e) { } }); - - it('Users claim after end of fund', async () => { - await claimForUsers(users); - assert.strictEqual(0.444444, await getTokenBalance(users[0].mintAPubkey)); - assert.strictEqual(0.888889, await getTokenBalance(users[0].mintBPubkey)); - assert.strictEqual(0.444444, await getTokenBalance(users[1].mintAPubkey)); - assert.strictEqual(0.888889, await getTokenBalance(users[1].mintBPubkey)); - assert.strictEqual(0.111111, await getTokenBalance(users[2].mintAPubkey)); - assert.strictEqual(0.222222, await getTokenBalance(users[2].mintBPubkey)); - }); - + it('Funder funds the pool again', async () => { await funders[0].fund(1_000_000_000, 1_000_000_000); }); it('waits', async () => { - await wait(4); //pool 1 @ 6, pool 2 @ 12 + await wait(4); //pool 1 @ 6, pool 2 @ 13 }); it('Funder funds the pool during emissions', async () => { @@ -301,7 +256,7 @@ describe('Multiuser Reward Pool', () => { }); it('waits', async () => { - await wait(7); //pool 1 @ 3, pool 2 @ 5 + await wait(5); //pool 1 @ 5, pool 2 @ 7 }); let oldValA; @@ -318,7 +273,7 @@ describe('Multiuser Reward Pool', () => { }); it('waits', async () => { - await wait(5); //pool 1 @ -1, pool 2 @ 0 + await wait(7); //pool 1 @ -2, pool 2 @ 0 }); let newValA; @@ -423,6 +378,77 @@ describe('Multiuser Reward Pool', () => { assert(u2B > u1B/12); }); + it('User tries to pause the pool using own pubkey', async () => { + try { + await users[0].pausePool(true, null); + assert.fail("did not fail on user pause pool"); + } catch (e) { } + }); + + it('User tries to pause the pool using funder pubkey but unsigned', async () => { + try { + await users[0].pausePool(true, funders[0].provider.wallet.publicKey); + assert.fail("did not fail on user pause pool"); + } catch (e) { } + }); + + it('Funder pauses the pool', async () => { + assert.strictEqual(0, await getTokenBalance(funders[0].xTokenPubkey)); + await funders[0].pausePool(null); + //assert xtoken refunded + assert.strictEqual(10_000, await getTokenBalance(funders[0].xTokenPubkey)); + }); + + it('Funder pauses the paused pool', async () => { + try { + await funders[0].pausePool(null); + assert.fail("did not fail on pausing paused pool"); + } catch (e) { } + }); + + it('User tries to stake some tokens in paused pool', async () => { + try { + await users[3].stakeTokens(100_000); + assert.fail("did not fail on user staking in paused pool"); + } catch (e) { } + }); + + it('User tries to unpause the pool using own pubkey', async () => { + try { + await users[0].unpausePool(null); + assert.fail("did not fail on user pause pool"); + } catch (e) { } + }); + + it('User tries to unpause the pool using funder pubkey but unsigned', async () => { + try { + await users[0].unpausePool(funders[0].provider.wallet.publicKey); + assert.fail("did not fail on user pause pool"); + } catch (e) { } + }); + + it('User unstakes some tokens in paused pool', async () => { + await users[2].unstakeTokens(250_000_000); + }); + + it('Funder unpauses the pool', async () => { + assert.strictEqual(10_000, await getTokenBalance(funders[0].xTokenPubkey)); + await funders[0].unpausePool(null); + //assert xtoken spent + assert.strictEqual(0, await getTokenBalance(funders[0].xTokenPubkey)); + }); + + it('Funder unpauses the unpaused pool', async () => { + try { + await funders[0].unpausePool(null); + assert.fail("did not fail on pausing paused pool"); + } catch (e) { } + }); + + it('User stakes some tokens in unpaused pool', async () => { + await users[2].stakeTokens(250_000_000); + }); + it("Tries to close a pool with active user", async () => { try { await funders[1].closePool(); @@ -437,6 +463,11 @@ describe('Multiuser Reward Pool', () => { }); it("Pool 2 closes", async () => { + try { + await funders[1].closePool(); + throw "funder was able to close pool without pausing first?!" + } catch { } + await funders[1].pausePool(); await funders[1].closePool(); let pool = await provider.connection.getAccountInfo(funders[1].admin.poolKeypair.publicKey); let sv = await provider.connection.getAccountInfo(funders[1].admin.stakingMintVault); diff --git a/tests/user.js b/tests/user.js index b8a7a8b..d769b46 100644 --- a/tests/user.js +++ b/tests/user.js @@ -21,7 +21,7 @@ async function claimForUsers(users) { class User { constructor(a) { this.id = a; } - async init(initialLamports, stakingMint, initialStaking, mintA, initialA, mintB, initialB) { + async init(initialLamports, xTokenMint, initialXToken, stakingMint, initialStaking, mintA, initialA, mintB, initialB) { this.keypair = new anchor.web3.Keypair(); this.pubkey = this.keypair.publicKey; @@ -33,6 +33,8 @@ class User { this.program = new anchor.Program(program.idl, program.programId, this.provider); this.initialLamports = initialLamports; + this.xTokenMintObject = new Token(this.provider.connection, xTokenMint, TOKEN_PROGRAM_ID, this.provider.wallet.payer); + this.initialXToken = initialXToken; this.stakingMintObject = new Token(this.provider.connection, stakingMint, TOKEN_PROGRAM_ID, this.provider.wallet.payer); this.initialStaking = initialStaking; this.mintAObject = new Token(this.provider.connection, mintA, TOKEN_PROGRAM_ID, this.provider.wallet.payer); @@ -45,6 +47,10 @@ class User { this.userNonce = null; this.lpPubkey = null; + this.xTokenPubkey = await this.xTokenMintObject.createAssociatedTokenAccount(this.pubkey); + if (initialXToken > 0) { + await this.xTokenMintObject.mintTo(this.xTokenPubkey, envProvider.wallet.payer, [], initialXToken); + } this.stakingPubkey = await this.stakingMintObject.createAssociatedTokenAccount(this.pubkey); if (initialStaking > 0) { await this.stakingMintObject.mintTo(this.stakingPubkey, envProvider.wallet.payer, [], initialStaking); @@ -59,7 +65,7 @@ class User { } } - async initializePool(poolKeypair, rewardDuration) { + async initializePool(poolKeypair, rewardDuration, singleStake) { const [ _poolSigner, _nonce, @@ -70,6 +76,7 @@ class User { let poolSigner = _poolSigner; let poolNonce = _nonce; + let xTokenPoolVault = await this.xTokenMintObject.createAccount(poolSigner); let stakingMintVault = await this.stakingMintObject.createAccount(poolSigner); let mintAVault = await this.mintAObject.createAccount(poolSigner); let mintBVault = await this.mintBObject.createAccount(poolSigner); @@ -79,6 +86,7 @@ class User { poolKeypair, poolSigner, poolNonce, + xTokenPoolVault, stakingMintVault, mintAVault, mintBVault @@ -89,14 +97,18 @@ class User { { accounts: { authority: this.provider.wallet.publicKey, + xTokenDepositAuthority: this.provider.wallet.publicKey, + xTokenPoolVault: xTokenPoolVault, + xTokenDepositor: this.xTokenPubkey, stakingMint: this.stakingMintObject.publicKey, stakingVault: stakingMintVault, rewardAMint: this.mintAObject.publicKey, rewardAVault: mintAVault, - rewardBMint: this.mintBObject.publicKey, - rewardBVault: mintBVault, + rewardBMint: singleStake ? this.mintAObject.publicKey : this.mintBObject.publicKey, + rewardBVault: singleStake ? mintAVault : mintBVault, poolSigner: poolSigner, pool: this.poolPubkey, + tokenProgram: TOKEN_PROGRAM_ID, }, signers: [poolKeypair], instructions: [ @@ -113,6 +125,7 @@ class User { async createUserStakingAccount(poolPubkey) { this.poolPubkey = poolPubkey; + const [ _userPubkey, _userNonce, ] = await anchor.web3.PublicKey.findProgramAddress( @@ -167,15 +180,60 @@ class User { ); } - async pausePool(isPaused, authority) { + async pausePool(authority) { + let poolObject = await this.program.account.pool.fetch(this.poolPubkey); + + const [ + _poolSigner, + _nonce, + ] = await anchor.web3.PublicKey.findProgramAddress( + [this.poolPubkey.toBuffer()], + this.program.programId + ); + let poolSigner = _poolSigner; + await this.program.rpc.pause( - isPaused, { accounts: { + xTokenPoolVault: poolObject.xTokenPoolVault, + xTokenReceiver: this.xTokenPubkey, pool: this.poolPubkey, authority: authority ?? this.provider.wallet.publicKey, + poolSigner: poolSigner, + tokenProgram: TOKEN_PROGRAM_ID, }, - }); + } + ); + } + + async unpausePool(authority) { + let poolObject = await this.program.account.pool.fetch(this.poolPubkey); + + const [ + _poolSigner, + _nonce, + ] = await anchor.web3.PublicKey.findProgramAddress( + [this.poolPubkey.toBuffer()], + this.program.programId + ); + let poolSigner = _poolSigner; + + let xTokenPoolVault = await this.xTokenMintObject.createAccount(poolSigner); + this.admin.xTokenPoolVault = xTokenPoolVault; + + await this.program.rpc.unpause( + { + accounts: { + xTokenPoolVault: xTokenPoolVault, + xTokenDepositor: this.xTokenPubkey, + xTokenDepositAuthority: this.provider.wallet.publicKey, + pool: this.poolPubkey, + authority: authority ?? this.provider.wallet.publicKey, + poolSigner: poolSigner, + tokenProgram: TOKEN_PROGRAM_ID, + }, + } + ); } async unstakeTokens(amount) { diff --git a/tests/utils.js b/tests/utils.js index 4a99455..d1ab1cd 100644 --- a/tests/utils.js +++ b/tests/utils.js @@ -1,6 +1,6 @@ const anchor = require("@project-serum/anchor"); const TokenInstructions = require("@project-serum/serum").TokenInstructions; -const { TOKEN_PROGRAM_ID, Token } = require("@solana/spl-token"); +const { TOKEN_PROGRAM_ID, Token, MintLayout } = require("@solana/spl-token"); async function initializeProgram(program, provider, authMintPubkey) { const [ _configPubkey, _nonce] = await anchor.web3.PublicKey.findProgramAddress([Buffer.from("config")], program.programId); @@ -38,6 +38,51 @@ async function createMintAndVault(provider, vaultOwner, decimals) { return [mint, vault]; } +async function createMintFromPriv( + mintAccount, + provider, + mintAuthority, + freezeAuthority, + decimals, + programId, +) { + const token = new Token( + provider.connection, + mintAccount.publicKey, + programId, + provider.wallet.payer, + ); + + // Allocate memory for the account + const balanceNeeded = await Token.getMinBalanceRentForExemptMint( + provider.connection, + ); + + const transaction = new anchor.web3.Transaction(); + transaction.add( + anchor.web3.SystemProgram.createAccount({ + fromPubkey: provider.wallet.payer.publicKey, + newAccountPubkey: mintAccount.publicKey, + lamports: balanceNeeded, + space: MintLayout.span, + programId, + }), + ); + + transaction.add( + Token.createInitMintInstruction( + programId, + mintAccount.publicKey, + decimals, + mintAuthority, + freezeAuthority, + ), + ); + + await provider.send(transaction, [mintAccount]); + return token; +} + async function mintToAccount( provider, mint, @@ -76,25 +121,10 @@ async function sendLamports( await provider.send(tx); } -async function createMintToAccountInstrs( - mint, - destination, - amount, - mintAuthority -) { -return [ - TokenInstructions.mintTo({ - mint, - destination: destination, - amount: amount, - mintAuthority: mintAuthority, - }), -]; -} - module.exports = { mintToAccount, createMintAndVault, + createMintFromPriv, createMint, sendLamports, initializeProgram,