From b91a4e16993ce3d38cb10f3bf09b0f6d0a2a2d94 Mon Sep 17 00:00:00 2001 From: Kyrylo Stepanov Date: Tue, 3 Sep 2024 15:04:06 +0300 Subject: [PATCH 1/7] add slash instruction && tests for it --- programs/rewards/src/instruction.rs | 35 ++++++++++ programs/rewards/src/instructions/mod.rs | 7 ++ .../rewards/src/instructions/penalties/mod.rs | 10 +-- .../src/instructions/penalties/slash.rs | 34 ++++++++++ .../rewards/tests/rewards/penalties/mod.rs | 1 + .../rewards/tests/rewards/penalties/slash.rs | 64 +++++++++++++++++++ programs/rewards/tests/rewards/utils.rs | 24 +++++++ 7 files changed, 170 insertions(+), 5 deletions(-) create mode 100644 programs/rewards/src/instructions/penalties/slash.rs create mode 100644 programs/rewards/tests/rewards/penalties/slash.rs diff --git a/programs/rewards/src/instruction.rs b/programs/rewards/src/instruction.rs index 4d2b21bf..241d7b98 100644 --- a/programs/rewards/src/instruction.rs +++ b/programs/rewards/src/instruction.rs @@ -167,6 +167,16 @@ pub enum RewardsInstruction { restrict_batch_minting_until_ts: u64, mining_owner: Pubkey, }, + + #[account(0, signer, name = "deposit_authority", desc = "The address of the Staking program's Registrar, which is PDA and is responsible for signing CPIs")] + #[account(1, name = "reward_pool", desc = "The address of the reward pool")] + #[account(2, writable, name = "mining", desc = "The address of the mining account which belongs to the user and stores info about user's rewards")] + Slash { + /// Amount to withdraw + amount: u64, + /// Specifies the owner of the Mining Account + mining_owner: Pubkey, + }, } /// Creates 'InitializePool' instruction. @@ -522,3 +532,28 @@ pub fn restrict_batch_minting( accounts, ) } + +#[allow(clippy::too_many_arguments)] +pub fn slash( + program_id: &Pubkey, + deposit_authority: &Pubkey, + reward_pool: &Pubkey, + mining: &Pubkey, + mining_owner: &Pubkey, + amount: u64, +) -> Instruction { + let accounts = vec![ + AccountMeta::new_readonly(*deposit_authority, true), + AccountMeta::new(*reward_pool, false), + AccountMeta::new(*mining, false), + ]; + + Instruction::new_with_borsh( + *program_id, + &RewardsInstruction::Slash { + amount, + mining_owner: *mining_owner, + }, + accounts, + ) +} diff --git a/programs/rewards/src/instructions/mod.rs b/programs/rewards/src/instructions/mod.rs index 5e29f51c..e5c662d2 100644 --- a/programs/rewards/src/instructions/mod.rs +++ b/programs/rewards/src/instructions/mod.rs @@ -138,5 +138,12 @@ pub fn process_instruction<'a>( &mining_owner, ) } + RewardsInstruction::Slash { + amount, + mining_owner, + } => { + msg!("RewardsInstruction: Slash"); + process_slash(program_id, accounts, &mining_owner, amount) + } } } diff --git a/programs/rewards/src/instructions/penalties/mod.rs b/programs/rewards/src/instructions/penalties/mod.rs index dfd511cc..462fb411 100644 --- a/programs/rewards/src/instructions/penalties/mod.rs +++ b/programs/rewards/src/instructions/penalties/mod.rs @@ -1,9 +1,9 @@ -pub(crate) use allow_tokenflow::*; -pub(crate) use restrict_tokenflow::*; - mod allow_tokenflow; +mod restrict_batch_minting; mod restrict_tokenflow; +mod slash; +pub(crate) use allow_tokenflow::*; pub(crate) use restrict_batch_minting::*; - -mod restrict_batch_minting; +pub(crate) use restrict_tokenflow::*; +pub(crate) use slash::*; diff --git a/programs/rewards/src/instructions/penalties/slash.rs b/programs/rewards/src/instructions/penalties/slash.rs new file mode 100644 index 00000000..266d9f42 --- /dev/null +++ b/programs/rewards/src/instructions/penalties/slash.rs @@ -0,0 +1,34 @@ +use crate::{asserts::assert_and_get_pool_and_mining, utils::AccountLoader}; +use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, pubkey::Pubkey}; + +pub fn process_slash<'a>( + program_id: &Pubkey, + accounts: &'a [AccountInfo<'a>], + mining_owner: &Pubkey, + amount: u64, +) -> ProgramResult { + let account_info_iter = &mut accounts.iter().enumerate(); + + let deposit_authority = AccountLoader::next_signer(account_info_iter)?; + let reward_pool = AccountLoader::next_with_owner(account_info_iter, program_id)?; + let mining = AccountLoader::next_with_owner(account_info_iter, program_id)?; + + let reward_pool_data = &mut reward_pool.data.borrow_mut(); + let mining_data = &mut mining.data.borrow_mut(); + + let (mut wrapped_reward_pool, mut wrapped_mining) = assert_and_get_pool_and_mining( + program_id, + mining_owner, + mining, + reward_pool, + deposit_authority, + reward_pool_data, + mining_data, + )?; + + wrapped_mining.refresh_rewards(&wrapped_reward_pool.cumulative_index)?; + + wrapped_reward_pool.withdraw(&mut wrapped_mining, amount, None)?; + + Ok(()) +} diff --git a/programs/rewards/tests/rewards/penalties/mod.rs b/programs/rewards/tests/rewards/penalties/mod.rs index 47f1c151..114780a6 100644 --- a/programs/rewards/tests/rewards/penalties/mod.rs +++ b/programs/rewards/tests/rewards/penalties/mod.rs @@ -1,2 +1,3 @@ mod restrict_batch_minting; +mod slash; mod tokenflow_restrictions; diff --git a/programs/rewards/tests/rewards/penalties/slash.rs b/programs/rewards/tests/rewards/penalties/slash.rs new file mode 100644 index 00000000..d49c1c80 --- /dev/null +++ b/programs/rewards/tests/rewards/penalties/slash.rs @@ -0,0 +1,64 @@ +use crate::utils::*; +use mplx_rewards::{ + state::{WrappedMining, WrappedRewardPool}, + utils::LockupPeriod, +}; +use solana_program::pubkey::Pubkey; +use solana_program_test::*; +use solana_sdk::{signature::Keypair, signer::Signer}; +use std::borrow::BorrowMut; + +async fn setup() -> (ProgramTestContext, TestRewards, Pubkey, Pubkey) { + let test = ProgramTest::new("mplx_rewards", mplx_rewards::ID, None); + let mut context = test.start_with_context().await; + + let owner = &context.payer.pubkey(); + + let mint = Keypair::new(); + create_mint(&mut context, &mint, owner).await.unwrap(); + + let test_rewards = TestRewards::new(mint.pubkey()); + test_rewards.initialize_pool(&mut context).await.unwrap(); + + let user = Keypair::new(); + let user_mining = test_rewards.initialize_mining(&mut context, &user).await; + + (context, test_rewards, user.pubkey(), user_mining) +} + +#[tokio::test] +async fn success() { + let (mut context, test_rewards, user, mining) = setup().await; + + let lockup_period = LockupPeriod::ThreeMonths; + test_rewards + .deposit_mining( + &mut context, + &mining, + 100, + lockup_period, + &user, + &mining, + &user, + ) + .await + .unwrap(); + + test_rewards + .slash(&mut context, &mining, &user, 30) + .await + .unwrap(); + + let mut reward_pool_account = + get_account(&mut context, &test_rewards.reward_pool.pubkey()).await; + let reward_pool_data = &mut reward_pool_account.data.borrow_mut(); + let wrapped_reward_pool = WrappedRewardPool::from_bytes_mut(reward_pool_data).unwrap(); + let reward_pool = wrapped_reward_pool.pool; + + assert_eq!(reward_pool.total_share, 170); + + let mut mining_account = get_account(&mut context, &mining).await; + let mining_data = &mut mining_account.data.borrow_mut(); + let mining = WrappedMining::from_bytes_mut(mining_data).unwrap(); + assert_eq!(mining.mining.share, 170); +} diff --git a/programs/rewards/tests/rewards/utils.rs b/programs/rewards/tests/rewards/utils.rs index b734d728..bb1abff0 100644 --- a/programs/rewards/tests/rewards/utils.rs +++ b/programs/rewards/tests/rewards/utils.rs @@ -181,6 +181,30 @@ impl TestRewards { context.banks_client.process_transaction(tx).await } + pub async fn slash( + &self, + context: &mut ProgramTestContext, + mining_account: &Pubkey, + owner: &Pubkey, + amount: u64, + ) -> BanksClientResult<()> { + let tx = Transaction::new_signed_with_payer( + &[mplx_rewards::instruction::slash( + &mplx_rewards::id(), + &self.deposit_authority.pubkey(), + &self.reward_pool.pubkey(), + mining_account, + owner, + amount, + )], + Some(&context.payer.pubkey()), + &[&context.payer, &self.deposit_authority], + context.last_blockhash, + ); + + context.banks_client.process_transaction(tx).await + } + pub async fn withdraw_mining( &self, context: &mut ProgramTestContext, From b6f3eb889e2a65ea79a74c6feb8cae3f52b37ac5 Mon Sep 17 00:00:00 2001 From: Kyrylo Stepanov Date: Tue, 3 Sep 2024 17:48:02 +0300 Subject: [PATCH 2/7] update slashing instruction to correctly work with stakes that were partially slashed --- programs/rewards/src/instruction.rs | 17 +- programs/rewards/src/instructions/mod.rs | 13 +- .../src/instructions/penalties/slash.rs | 13 +- programs/rewards/src/state/reward_pool.rs | 31 +++ .../tests/rewards/fixtures/mplx_rewards.so | Bin 246320 -> 253808 bytes .../rewards/tests/rewards/penalties/slash.rs | 201 +++++++++++++++++- programs/rewards/tests/rewards/utils.rs | 12 +- 7 files changed, 261 insertions(+), 26 deletions(-) diff --git a/programs/rewards/src/instruction.rs b/programs/rewards/src/instruction.rs index 241d7b98..20e888bf 100644 --- a/programs/rewards/src/instruction.rs +++ b/programs/rewards/src/instruction.rs @@ -172,10 +172,13 @@ pub enum RewardsInstruction { #[account(1, name = "reward_pool", desc = "The address of the reward pool")] #[account(2, writable, name = "mining", desc = "The address of the mining account which belongs to the user and stores info about user's rewards")] Slash { - /// Amount to withdraw - amount: u64, - /// Specifies the owner of the Mining Account mining_owner: Pubkey, + // number of tokens that had been slashed + slash_amount_in_native: u64, + // weighted stake part for the slashed number of tokens multiplied by the period + slash_amount_multiplied_by_period: u64, + // None if it's Flex period, because it's already expired + stake_expiration_date: Option, }, } @@ -540,7 +543,9 @@ pub fn slash( reward_pool: &Pubkey, mining: &Pubkey, mining_owner: &Pubkey, - amount: u64, + slash_amount_in_native: u64, + slash_amount_multiplied_by_period: u64, + stake_expiration_date: Option, ) -> Instruction { let accounts = vec![ AccountMeta::new_readonly(*deposit_authority, true), @@ -551,8 +556,10 @@ pub fn slash( Instruction::new_with_borsh( *program_id, &RewardsInstruction::Slash { - amount, mining_owner: *mining_owner, + slash_amount_in_native, + slash_amount_multiplied_by_period, + stake_expiration_date, }, accounts, ) diff --git a/programs/rewards/src/instructions/mod.rs b/programs/rewards/src/instructions/mod.rs index e5c662d2..dd77c803 100644 --- a/programs/rewards/src/instructions/mod.rs +++ b/programs/rewards/src/instructions/mod.rs @@ -139,11 +139,20 @@ pub fn process_instruction<'a>( ) } RewardsInstruction::Slash { - amount, mining_owner, + slash_amount_in_native, + slash_amount_multiplied_by_period, + stake_expiration_date, } => { msg!("RewardsInstruction: Slash"); - process_slash(program_id, accounts, &mining_owner, amount) + process_slash( + program_id, + accounts, + &mining_owner, + slash_amount_in_native, + slash_amount_multiplied_by_period, + stake_expiration_date, + ) } } } diff --git a/programs/rewards/src/instructions/penalties/slash.rs b/programs/rewards/src/instructions/penalties/slash.rs index 266d9f42..e1a09001 100644 --- a/programs/rewards/src/instructions/penalties/slash.rs +++ b/programs/rewards/src/instructions/penalties/slash.rs @@ -5,7 +5,9 @@ pub fn process_slash<'a>( program_id: &Pubkey, accounts: &'a [AccountInfo<'a>], mining_owner: &Pubkey, - amount: u64, + slash_amount_in_native: u64, + slash_amount_multiplied_by_period: u64, + stake_expiration_date: Option, ) -> ProgramResult { let account_info_iter = &mut accounts.iter().enumerate(); @@ -26,9 +28,12 @@ pub fn process_slash<'a>( mining_data, )?; - wrapped_mining.refresh_rewards(&wrapped_reward_pool.cumulative_index)?; - - wrapped_reward_pool.withdraw(&mut wrapped_mining, amount, None)?; + wrapped_reward_pool.slash( + &mut wrapped_mining, + slash_amount_in_native, + slash_amount_multiplied_by_period, + stake_expiration_date, + )?; Ok(()) } diff --git a/programs/rewards/src/state/reward_pool.rs b/programs/rewards/src/state/reward_pool.rs index 2a537b2f..3fba3bcf 100644 --- a/programs/rewards/src/state/reward_pool.rs +++ b/programs/rewards/src/state/reward_pool.rs @@ -286,6 +286,37 @@ impl<'a> WrappedRewardPool<'a> { Ok(()) } + /// Process slash for specified number of tokens + pub fn slash( + &mut self, + mining: &mut WrappedMining, + slash_amount_in_native: u64, + slash_amount_multiplied_by_period: u64, + stake_expiration_date: Option, + ) -> ProgramResult { + self.withdraw(mining, slash_amount_multiplied_by_period, None)?; + + if stake_expiration_date.is_some() { + let stake_expiration_date = stake_expiration_date.unwrap(); + let beginning_of_the_stake_expiration_date = + stake_expiration_date - (stake_expiration_date % SECONDS_PER_DAY); + let diff_by_expiration_date = + slash_amount_multiplied_by_period.safe_sub(slash_amount_in_native)?; + let diff_record = mining + .weighted_stake_diffs + .get_mut(&beginning_of_the_stake_expiration_date) + .ok_or(MplxRewardsError::NoWeightedStakeModifiersAtADate)?; + *diff_record = diff_record.safe_sub(diff_by_expiration_date)?; + + let diff_record = self + .weighted_stake_diffs + .get_mut(&beginning_of_the_stake_expiration_date) + .ok_or(MplxRewardsError::NoWeightedStakeModifiersAtADate)?; + *diff_record = diff_record.safe_sub(diff_by_expiration_date)?; + } + Ok(()) + } + /// Process extend stake #[allow(clippy::too_many_arguments)] pub fn extend( diff --git a/programs/rewards/tests/rewards/fixtures/mplx_rewards.so b/programs/rewards/tests/rewards/fixtures/mplx_rewards.so index b3fae7fd88e62d1fd8e31b5c6fe74d59cafaf1e7..1add0af023ea8b04df05cd80d31fa917bb1f059b 100755 GIT binary patch delta 72733 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 (ProgramTestContext, TestRewards, Pubkey, Pubkey) { @@ -27,25 +28,67 @@ async fn setup() -> (ProgramTestContext, TestRewards, Pubkey, Pubkey) { } #[tokio::test] -async fn success() { - let (mut context, test_rewards, user, mining) = setup().await; +async fn one_stake_for_a_date() { + let (mut context, test_rewards, user, mining_addr) = setup().await; - let lockup_period = LockupPeriod::ThreeMonths; + let stake_expiration_date = context + .banks_client + .get_sysvar::() + .await + .unwrap() + .unix_timestamp as u64 + + SECONDS_PER_DAY * 30 * 6; + let stake_expiration_date = stake_expiration_date - stake_expiration_date % SECONDS_PER_DAY; + + let lockup_period = LockupPeriod::SixMonths; test_rewards .deposit_mining( &mut context, - &mining, - 100, + &mining_addr, + 150, lockup_period, &user, - &mining, + &mining_addr, &user, ) .await .unwrap(); + // just test both pool and mining states are correct + let mut reward_pool_account = + get_account(&mut context, &test_rewards.reward_pool.pubkey()).await; + let reward_pool_data = &mut reward_pool_account.data.borrow_mut(); + let wrapped_reward_pool = WrappedRewardPool::from_bytes_mut(reward_pool_data).unwrap(); + let reward_pool = wrapped_reward_pool.pool; + assert_eq!(reward_pool.total_share, 600); + assert_eq!( + *wrapped_reward_pool + .weighted_stake_diffs + .get(&stake_expiration_date) + .unwrap(), + 450 + ); + let mut mining_account = get_account(&mut context, &mining_addr).await; + let mining_data = &mut mining_account.data.borrow_mut(); + let mining = WrappedMining::from_bytes_mut(mining_data).unwrap(); + assert_eq!(mining.mining.share, 600); + assert_eq!( + *mining + .weighted_stake_diffs + .get(&stake_expiration_date) + .unwrap(), + 450 + ); + test_rewards - .slash(&mut context, &mining, &user, 30) + .slash( + &mut context, + &mining_addr, + &user, + 50, + 200, + Some(stake_expiration_date), + ) .await .unwrap(); @@ -54,11 +97,147 @@ async fn success() { let reward_pool_data = &mut reward_pool_account.data.borrow_mut(); let wrapped_reward_pool = WrappedRewardPool::from_bytes_mut(reward_pool_data).unwrap(); let reward_pool = wrapped_reward_pool.pool; + assert_eq!(reward_pool.total_share, 400); + assert_eq!( + *wrapped_reward_pool + .weighted_stake_diffs + .get(&stake_expiration_date) + .unwrap(), + 300 + ); + let mut mining_account = get_account(&mut context, &mining_addr).await; + let mining_data = &mut mining_account.data.borrow_mut(); + let mining = WrappedMining::from_bytes_mut(mining_data).unwrap(); + assert_eq!(mining.mining.share, 400); + assert_eq!( + *mining + .weighted_stake_diffs + .get(&stake_expiration_date) + .unwrap(), + 300 + ); +} + +#[tokio::test] +async fn multiple_stakes_for_a_date_but_one_slashed() { + let (mut context, test_rewards, user, mining_addr) = setup().await; - assert_eq!(reward_pool.total_share, 170); + let stake_expiration_date = context + .banks_client + .get_sysvar::() + .await + .unwrap() + .unix_timestamp as u64 + + SECONDS_PER_DAY * 365; + let stake_expiration_date = stake_expiration_date - stake_expiration_date % SECONDS_PER_DAY; + + let lockup_period = LockupPeriod::OneYear; + test_rewards + .deposit_mining( + &mut context, + &mining_addr, + 200, // 200 x6 + lockup_period, + &user, + &mining_addr, + &user, + ) + .await + .unwrap(); - let mut mining_account = get_account(&mut context, &mining).await; + advance_clock_by_ts(&mut context, (SECONDS_PER_DAY * 185).try_into().unwrap()).await; + let lockup_period = LockupPeriod::SixMonths; + test_rewards + .deposit_mining( + &mut context, + &mining_addr, + 150, // 150 x4 + lockup_period, + &user, + &mining_addr, + &user, + ) + .await + .unwrap(); + + let lockup_period = LockupPeriod::Flex; + test_rewards + .deposit_mining( + &mut context, + &mining_addr, + 100, // 100 x1 + lockup_period, + &user, + &mining_addr, + &user, + ) + .await + .unwrap(); + + // weighted stake = 150*4 + 200*6 + 100*1 = 1900 + // diff = 1900 - 150 - 200 - 100 = 1450 + + // just test both pool and mining states are correct + let mut reward_pool_account = + get_account(&mut context, &test_rewards.reward_pool.pubkey()).await; + let reward_pool_data = &mut reward_pool_account.data.borrow_mut(); + let wrapped_reward_pool = WrappedRewardPool::from_bytes_mut(reward_pool_data).unwrap(); + let reward_pool = wrapped_reward_pool.pool; + assert_eq!(reward_pool.total_share, 1900); + assert_eq!( + *wrapped_reward_pool + .weighted_stake_diffs + .get(&stake_expiration_date) + .unwrap(), + 1450 + ); + + let mut mining_account = get_account(&mut context, &mining_addr).await; + let mining_data = &mut mining_account.data.borrow_mut(); + let mining = WrappedMining::from_bytes_mut(mining_data).unwrap(); + assert_eq!(mining.mining.share, 1900); + assert_eq!( + *mining + .weighted_stake_diffs + .get(&stake_expiration_date) + .unwrap(), + 1450 + ); + + test_rewards + .slash( + &mut context, + &mining_addr, + &user, + 50, + 200, + Some(stake_expiration_date), + ) + .await + .unwrap(); + + let mut reward_pool_account = + get_account(&mut context, &test_rewards.reward_pool.pubkey()).await; + let reward_pool_data = &mut reward_pool_account.data.borrow_mut(); + let wrapped_reward_pool = WrappedRewardPool::from_bytes_mut(reward_pool_data).unwrap(); + let reward_pool = wrapped_reward_pool.pool; + assert_eq!(reward_pool.total_share, 1700); + assert_eq!( + *wrapped_reward_pool + .weighted_stake_diffs + .get(&stake_expiration_date) + .unwrap(), + 1300 + ); + let mut mining_account = get_account(&mut context, &mining_addr).await; let mining_data = &mut mining_account.data.borrow_mut(); let mining = WrappedMining::from_bytes_mut(mining_data).unwrap(); - assert_eq!(mining.mining.share, 170); + assert_eq!(mining.mining.share, 1700); + assert_eq!( + *mining + .weighted_stake_diffs + .get(&stake_expiration_date) + .unwrap(), + 1300 + ); } diff --git a/programs/rewards/tests/rewards/utils.rs b/programs/rewards/tests/rewards/utils.rs index bb1abff0..7693d1e4 100644 --- a/programs/rewards/tests/rewards/utils.rs +++ b/programs/rewards/tests/rewards/utils.rs @@ -185,8 +185,10 @@ impl TestRewards { &self, context: &mut ProgramTestContext, mining_account: &Pubkey, - owner: &Pubkey, - amount: u64, + mining_owner: &Pubkey, + slash_amount_in_native: u64, + slash_amount_multiplied_by_period: u64, + stake_expiration_date: Option, ) -> BanksClientResult<()> { let tx = Transaction::new_signed_with_payer( &[mplx_rewards::instruction::slash( @@ -194,8 +196,10 @@ impl TestRewards { &self.deposit_authority.pubkey(), &self.reward_pool.pubkey(), mining_account, - owner, - amount, + mining_owner, + slash_amount_in_native, + slash_amount_multiplied_by_period, + stake_expiration_date, )], Some(&context.payer.pubkey()), &[&context.payer, &self.deposit_authority], From 53599f401e909e0cf2c7213a3770ac94937bff21 Mon Sep 17 00:00:00 2001 From: Kyrylo Stepanov Date: Wed, 4 Sep 2024 14:48:07 +0300 Subject: [PATCH 3/7] small improvements to the deposit_mining() logic --- programs/rewards/src/instruction.rs | 2 +- programs/rewards/src/state/reward_pool.rs | 27 +++++++++++++++-------- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/programs/rewards/src/instruction.rs b/programs/rewards/src/instruction.rs index 20e888bf..403dded7 100644 --- a/programs/rewards/src/instruction.rs +++ b/programs/rewards/src/instruction.rs @@ -169,7 +169,7 @@ pub enum RewardsInstruction { }, #[account(0, signer, name = "deposit_authority", desc = "The address of the Staking program's Registrar, which is PDA and is responsible for signing CPIs")] - #[account(1, name = "reward_pool", desc = "The address of the reward pool")] + #[account(1, writable, name = "reward_pool", desc = "The address of the reward pool")] #[account(2, writable, name = "mining", desc = "The address of the mining account which belongs to the user and stores info about user's rewards")] Slash { mining_owner: Pubkey, diff --git a/programs/rewards/src/state/reward_pool.rs b/programs/rewards/src/state/reward_pool.rs index 3fba3bcf..f8a3e887 100644 --- a/programs/rewards/src/state/reward_pool.rs +++ b/programs/rewards/src/state/reward_pool.rs @@ -218,8 +218,9 @@ impl<'a> WrappedRewardPool<'a> { self.pool.total_share = self.pool.total_share.safe_add(weighted_stake)?; mining.mining.share = mining.mining.share.safe_add(weighted_stake)?; - let stake_expiraion_date = lockup_period.end_timestamp(get_curr_unix_ts())?; - let modifier = if let Some(modifier) = self.weighted_stake_diffs.get(&stake_expiraion_date) + let stake_expiration_date = lockup_period.end_timestamp(get_curr_unix_ts())?; + + let modifier = if let Some(modifier) = self.weighted_stake_diffs.get(&stake_expiration_date) { *modifier } else { @@ -227,18 +228,24 @@ impl<'a> WrappedRewardPool<'a> { }; self.weighted_stake_diffs.insert( - lockup_period.end_timestamp(get_curr_unix_ts())?, + stake_expiration_date, modifier.safe_add(weighted_stake_diff)?, ); - let date_to_insert = &lockup_period.end_timestamp(get_curr_unix_ts())?; - if mining.weighted_stake_diffs.get(date_to_insert).is_some() { - let modifier = mining.weighted_stake_diffs.get_mut(date_to_insert).unwrap(); + if mining + .weighted_stake_diffs + .get(&stake_expiration_date) + .is_some() + { + let modifier = mining + .weighted_stake_diffs + .get_mut(&stake_expiration_date) + .unwrap(); *modifier = modifier.safe_add(weighted_stake_diff)?; } else { mining .weighted_stake_diffs - .insert(*date_to_insert, weighted_stake_diff); + .insert(stake_expiration_date, weighted_stake_diff); } if let Some(delegate_mining_acc) = delegate_mining { @@ -296,12 +303,13 @@ impl<'a> WrappedRewardPool<'a> { ) -> ProgramResult { self.withdraw(mining, slash_amount_multiplied_by_period, None)?; - if stake_expiration_date.is_some() { - let stake_expiration_date = stake_expiration_date.unwrap(); + if let Some(stake_expiration_date) = stake_expiration_date { let beginning_of_the_stake_expiration_date = stake_expiration_date - (stake_expiration_date % SECONDS_PER_DAY); + let diff_by_expiration_date = slash_amount_multiplied_by_period.safe_sub(slash_amount_in_native)?; + let diff_record = mining .weighted_stake_diffs .get_mut(&beginning_of_the_stake_expiration_date) @@ -314,6 +322,7 @@ impl<'a> WrappedRewardPool<'a> { .ok_or(MplxRewardsError::NoWeightedStakeModifiersAtADate)?; *diff_record = diff_record.safe_sub(diff_by_expiration_date)?; } + Ok(()) } From d6bcb79ddb4014d604946c91db885f44f98f1e68 Mon Sep 17 00:00:00 2001 From: Kyrylo Stepanov Date: Wed, 4 Sep 2024 19:57:48 +0300 Subject: [PATCH 4/7] Add decrease rewards penalty instruction --- programs/rewards/src/error.rs | 6 + programs/rewards/src/instruction.rs | 33 ++++++ programs/rewards/src/instructions/mod.rs | 12 ++ .../penalties/decrease_rewards.rs | 32 ++++++ .../rewards/src/instructions/penalties/mod.rs | 2 + programs/rewards/src/state/mining.rs | 103 +++++++++++++++++- programs/rewards/tests/rewards/utils.rs | 25 +++++ 7 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 programs/rewards/src/instructions/penalties/decrease_rewards.rs diff --git a/programs/rewards/src/error.rs b/programs/rewards/src/error.rs index d4035aad..7ff8583c 100644 --- a/programs/rewards/src/error.rs +++ b/programs/rewards/src/error.rs @@ -109,6 +109,12 @@ pub enum MplxRewardsError { /// Withdrawal is restricted while claiming is restricted #[error("Withdrawal is restricted while claiming is restricted")] WithdrawalRestricted, + + /// 25 + #[error( + "Rewards: Penalty is not apliable becase it's bigger than the mining's weighted stake" + )] + DecreaseRewardsTooBig, } impl PrintProgramError for MplxRewardsError { diff --git a/programs/rewards/src/instruction.rs b/programs/rewards/src/instruction.rs index 403dded7..a02d7246 100644 --- a/programs/rewards/src/instruction.rs +++ b/programs/rewards/src/instruction.rs @@ -180,6 +180,15 @@ pub enum RewardsInstruction { // None if it's Flex period, because it's already expired stake_expiration_date: Option, }, + + #[account(0, signer, name = "deposit_authority", desc = "The address of the Staking program's Registrar, which is PDA and is responsible for signing CPIs")] + #[account(1, writable, name = "reward_pool", desc = "The address of the reward pool")] + #[account(2, writable, name = "mining", desc = "The address of the mining account which belongs to the user and stores info about user's rewards")] + DecreaseRewards { + mining_owner: Pubkey, + // The number by which weighted stake should be decreased + decreased_weighted_stake_number: u64, + }, } /// Creates 'InitializePool' instruction. @@ -564,3 +573,27 @@ pub fn slash( accounts, ) } + +pub fn decrease_rewards( + program_id: &Pubkey, + deposit_authority: &Pubkey, + reward_pool: &Pubkey, + mining: &Pubkey, + mining_owner: &Pubkey, + decreased_weighted_stake_number: u64, +) -> Instruction { + let accounts = vec![ + AccountMeta::new_readonly(*deposit_authority, true), + AccountMeta::new(*reward_pool, false), + AccountMeta::new(*mining, false), + ]; + + Instruction::new_with_borsh( + *program_id, + &RewardsInstruction::DecreaseRewards { + mining_owner: *mining_owner, + decreased_weighted_stake_number, + }, + accounts, + ) +} diff --git a/programs/rewards/src/instructions/mod.rs b/programs/rewards/src/instructions/mod.rs index dd77c803..145b52bf 100644 --- a/programs/rewards/src/instructions/mod.rs +++ b/programs/rewards/src/instructions/mod.rs @@ -154,5 +154,17 @@ pub fn process_instruction<'a>( stake_expiration_date, ) } + RewardsInstruction::DecreaseRewards { + mining_owner, + decreased_weighted_stake_number, + } => { + msg!("RewardsInstruction: DecreaseRewards"); + process_decrease_rewards( + program_id, + accounts, + &mining_owner, + decreased_weighted_stake_number, + ) + } } } diff --git a/programs/rewards/src/instructions/penalties/decrease_rewards.rs b/programs/rewards/src/instructions/penalties/decrease_rewards.rs new file mode 100644 index 00000000..bed43577 --- /dev/null +++ b/programs/rewards/src/instructions/penalties/decrease_rewards.rs @@ -0,0 +1,32 @@ +use crate::{asserts::assert_and_get_pool_and_mining, utils::AccountLoader}; +use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, pubkey::Pubkey}; + +pub fn process_decrease_rewards<'a>( + program_id: &Pubkey, + accounts: &'a [AccountInfo<'a>], + mining_owner: &Pubkey, + decreased_weighted_stake_number: u64, +) -> ProgramResult { + let account_info_iter = &mut accounts.iter().enumerate(); + + let deposit_authority = AccountLoader::next_signer(account_info_iter)?; + let reward_pool = AccountLoader::next_with_owner(account_info_iter, program_id)?; + let mining = AccountLoader::next_with_owner(account_info_iter, program_id)?; + + let reward_pool_data = &mut reward_pool.data.borrow_mut(); + let mining_data = &mut mining.data.borrow_mut(); + + let (_, mut wrapped_mining) = assert_and_get_pool_and_mining( + program_id, + mining_owner, + mining, + reward_pool, + deposit_authority, + reward_pool_data, + mining_data, + )?; + + wrapped_mining.decrease_rewards(decreased_weighted_stake_number)?; + + Ok(()) +} diff --git a/programs/rewards/src/instructions/penalties/mod.rs b/programs/rewards/src/instructions/penalties/mod.rs index 462fb411..2c0ec928 100644 --- a/programs/rewards/src/instructions/penalties/mod.rs +++ b/programs/rewards/src/instructions/penalties/mod.rs @@ -1,9 +1,11 @@ mod allow_tokenflow; +mod decrease_rewards; mod restrict_batch_minting; mod restrict_tokenflow; mod slash; pub(crate) use allow_tokenflow::*; +pub(crate) use decrease_rewards::*; pub(crate) use restrict_batch_minting::*; pub(crate) use restrict_tokenflow::*; pub(crate) use slash::*; diff --git a/programs/rewards/src/state/mining.rs b/programs/rewards/src/state/mining.rs index 87323126..6c6fee11 100644 --- a/programs/rewards/src/state/mining.rs +++ b/programs/rewards/src/state/mining.rs @@ -81,6 +81,35 @@ impl<'a> WrappedMining<'a> { Ok(()) } + + /// Decrease rewards + pub fn decrease_rewards(&mut self, mut decreased_weighted_stake_number: u64) -> ProgramResult { + if decreased_weighted_stake_number == 0 { + return Ok(()); + } + + if decreased_weighted_stake_number > self.mining.share { + return Err(MplxRewardsError::DecreaseRewardsTooBig.into()); + } + + self.mining.share = self + .mining + .share + .safe_sub(decreased_weighted_stake_number)?; + + for (_, stake_diff) in self.weighted_stake_diffs.iter_mut().rev() { + if stake_diff > &mut decreased_weighted_stake_number { + *stake_diff = stake_diff.safe_sub(decreased_weighted_stake_number)?; + break; + } else { + decreased_weighted_stake_number = + decreased_weighted_stake_number.safe_sub(*stake_diff)?; + *stake_diff = 0; + } + } + + Ok(()) + } } #[repr(C)] @@ -257,8 +286,11 @@ impl<'a> WrappedImmutableMining<'a> { }) } } + +#[allow(unused_imports)] mod test { - #[allow(unused_imports)] + use super::*; + #[test] fn test_wrapped_immutable_mining_is_same_size_as_wrapped_mining() { assert_eq!( @@ -306,4 +338,73 @@ mod test { ); assert_eq!(wrapped_immutable_mining.mining.bump, bump); } + + #[test] + fn slighly_decrease_rewards() { + let mut wrapped_mining = super::WrappedMining { + mining: &mut super::Mining { + share: 3600, + ..Default::default() + }, + weighted_stake_diffs: &mut Default::default(), + }; + // three stakes: + // - 500 x4 (six months) + // - 700 x2 (three months) + // - 200 x1 (flex) + wrapped_mining.weighted_stake_diffs.insert(365, 1500); + wrapped_mining.weighted_stake_diffs.insert(180, 700); + + wrapped_mining.decrease_rewards(300).unwrap(); + + assert_eq!(wrapped_mining.mining.share, 3300); + assert_eq!(wrapped_mining.weighted_stake_diffs.get(&365), Some(&1200)); + assert_eq!(wrapped_mining.weighted_stake_diffs.get(&180), Some(&700)); + } + + #[test] + fn moderate_decrease_rewards() { + let mut wrapped_mining = super::WrappedMining { + mining: &mut super::Mining { + share: 3600, + ..Default::default() + }, + weighted_stake_diffs: &mut Default::default(), + }; + // three stakes: + // - 500 x4 (six months) + // - 700 x2 (three months) + // - 200 x1 (flex) + wrapped_mining.weighted_stake_diffs.insert(365, 1500); + wrapped_mining.weighted_stake_diffs.insert(180, 700); + + wrapped_mining.decrease_rewards(2200).unwrap(); + + assert_eq!(wrapped_mining.mining.share, 1400); + assert_eq!(wrapped_mining.weighted_stake_diffs.get(&365), Some(&0)); + assert_eq!(wrapped_mining.weighted_stake_diffs.get(&180), Some(&0)); + } + + #[test] + fn severe_decrease_rewards() { + let mut wrapped_mining = super::WrappedMining { + mining: &mut super::Mining { + share: 3600, + ..Default::default() + }, + weighted_stake_diffs: &mut Default::default(), + }; + // three stakes: + // - 500 x4 (six months) + // - 700 x2 (three months) + // - 200 x1 (flex) + wrapped_mining.weighted_stake_diffs.insert(365, 1500); + wrapped_mining.weighted_stake_diffs.insert(180, 700); + + wrapped_mining.decrease_rewards(3500).unwrap(); + + assert_eq!(wrapped_mining.mining.share, 100); + assert_eq!(wrapped_mining.weighted_stake_diffs.get(&365), Some(&0)); + assert_eq!(wrapped_mining.weighted_stake_diffs.get(&180), Some(&0)); + } } diff --git a/programs/rewards/tests/rewards/utils.rs b/programs/rewards/tests/rewards/utils.rs index 7693d1e4..ce17d858 100644 --- a/programs/rewards/tests/rewards/utils.rs +++ b/programs/rewards/tests/rewards/utils.rs @@ -368,6 +368,31 @@ impl TestRewards { context.banks_client.process_transaction(tx).await } + #[allow(dead_code)] + pub async fn decrease_rewards( + &self, + context: &mut ProgramTestContext, + mining_account: &Pubkey, + mining_owner: &Pubkey, + decreased_weighted_stake_number: u64, + ) -> BanksClientResult<()> { + let tx = Transaction::new_signed_with_payer( + &[mplx_rewards::instruction::decrease_rewards( + &mplx_rewards::id(), + &self.deposit_authority.pubkey(), + &self.reward_pool.pubkey(), + mining_account, + mining_owner, + decreased_weighted_stake_number, + )], + Some(&context.payer.pubkey()), + &[&context.payer, &self.deposit_authority], + context.last_blockhash, + ); + + context.banks_client.process_transaction(tx).await + } + pub async fn restrict_tokenflow( &self, context: &mut ProgramTestContext, From 08f2676f5ed7bf74caf3d90691d8a97ca5ad9ae9 Mon Sep 17 00:00:00 2001 From: Kyrylo Stepanov Date: Fri, 6 Sep 2024 15:01:43 +0300 Subject: [PATCH 5/7] add clarifying comments --- programs/rewards/src/state/mining.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/programs/rewards/src/state/mining.rs b/programs/rewards/src/state/mining.rs index 6c6fee11..3bb588b0 100644 --- a/programs/rewards/src/state/mining.rs +++ b/programs/rewards/src/state/mining.rs @@ -92,13 +92,17 @@ impl<'a> WrappedMining<'a> { return Err(MplxRewardsError::DecreaseRewardsTooBig.into()); } + // apply penalty to the weighted stake self.mining.share = self .mining .share .safe_sub(decreased_weighted_stake_number)?; + // going through the weighted stake diffs backwards + // and decreasing the modifiers accordingly to the decreased share number. + // otherwise moddifier might decrease the share more then needed, even to negative value. for (_, stake_diff) in self.weighted_stake_diffs.iter_mut().rev() { - if stake_diff > &mut decreased_weighted_stake_number { + if stake_diff >= &mut decreased_weighted_stake_number { *stake_diff = stake_diff.safe_sub(decreased_weighted_stake_number)?; break; } else { From 247cdcc5dab8e2cfeaddaa161b6710bfad5c05b7 Mon Sep 17 00:00:00 2001 From: Kyrylo Stepanov Date: Mon, 9 Sep 2024 19:54:07 +0300 Subject: [PATCH 6/7] one more test --- .../rewards/tests/rewards/penalties/slash.rs | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/programs/rewards/tests/rewards/penalties/slash.rs b/programs/rewards/tests/rewards/penalties/slash.rs index 1f113dd4..9be0cea0 100644 --- a/programs/rewards/tests/rewards/penalties/slash.rs +++ b/programs/rewards/tests/rewards/penalties/slash.rs @@ -118,6 +118,97 @@ async fn one_stake_for_a_date() { ); } +#[tokio::test] +async fn another_one_stake_for_a_date() { + let (mut context, test_rewards, user, mining_addr) = setup().await; + + let stake_expiration_date = context + .banks_client + .get_sysvar::() + .await + .unwrap() + .unix_timestamp as u64 + + SECONDS_PER_DAY * 30 * 3; + let stake_expiration_date = stake_expiration_date - stake_expiration_date % SECONDS_PER_DAY; + + let lockup_period = LockupPeriod::ThreeMonths; + test_rewards + .deposit_mining( + &mut context, + &mining_addr, + 10_000, + lockup_period, + &user, + &mining_addr, + &user, + ) + .await + .unwrap(); + + // just test both pool and mining states are correct + let mut reward_pool_account = + get_account(&mut context, &test_rewards.reward_pool.pubkey()).await; + let reward_pool_data = &mut reward_pool_account.data.borrow_mut(); + let wrapped_reward_pool = WrappedRewardPool::from_bytes_mut(reward_pool_data).unwrap(); + let reward_pool = wrapped_reward_pool.pool; + assert_eq!(reward_pool.total_share, 20_000); + assert_eq!( + *wrapped_reward_pool + .weighted_stake_diffs + .get(&stake_expiration_date) + .unwrap(), + 10_000 + ); + let mut mining_account = get_account(&mut context, &mining_addr).await; + let mining_data = &mut mining_account.data.borrow_mut(); + let mining = WrappedMining::from_bytes_mut(mining_data).unwrap(); + assert_eq!(mining.mining.share, 20_000); + assert_eq!( + *mining + .weighted_stake_diffs + .get(&stake_expiration_date) + .unwrap(), + 10_000 + ); + + test_rewards + .slash( + &mut context, + &mining_addr, + &user, + 5_000, + 10_000, + Some(stake_expiration_date), + ) + .await + .unwrap(); + + let mut reward_pool_account = + get_account(&mut context, &test_rewards.reward_pool.pubkey()).await; + let reward_pool_data = &mut reward_pool_account.data.borrow_mut(); + let wrapped_reward_pool = WrappedRewardPool::from_bytes_mut(reward_pool_data).unwrap(); + let reward_pool = wrapped_reward_pool.pool; + assert_eq!(reward_pool.total_share, 10_000); + assert_eq!( + *wrapped_reward_pool + .weighted_stake_diffs + .get(&stake_expiration_date) + .unwrap(), + 5_000 + ); + let mut mining_account = get_account(&mut context, &mining_addr).await; + let mining_data = &mut mining_account.data.borrow_mut(); + let mining = WrappedMining::from_bytes_mut(mining_data).unwrap(); + assert_eq!(mining.mining.share, 10_000); + assert_eq!( + *mining + .weighted_stake_diffs + .get(&stake_expiration_date) + .unwrap(), + 5_000 + ); +} + #[tokio::test] async fn multiple_stakes_for_a_date_but_one_slashed() { let (mut context, test_rewards, user, mining_addr) = setup().await; From da55a6eeaf71f5f0f1adb6c992517954bc1dcc62 Mon Sep 17 00:00:00 2001 From: Kyrylo Stepanov Date: Fri, 13 Sep 2024 17:19:51 +0300 Subject: [PATCH 7/7] remove redundant ixs, errors, account size, etc --- programs/rewards/src/error.rs | 44 +-- programs/rewards/src/instruction.rs | 91 ----- programs/rewards/src/instructions/claim.rs | 5 - programs/rewards/src/instructions/mod.rs | 20 -- .../instructions/penalties/allow_tokenflow.rs | 31 -- .../rewards/src/instructions/penalties/mod.rs | 6 - .../penalties/restrict_batch_minting.rs | 32 -- .../penalties/restrict_tokenflow.rs | 31 -- .../src/instructions/withdraw_mining.rs | 5 - programs/rewards/src/state/mining.rs | 44 +-- .../rewards/tests/rewards/penalties/mod.rs | 2 - .../penalties/restrict_batch_minting.rs | 58 ---- .../penalties/tokenflow_restrictions.rs | 312 ------------------ programs/rewards/tests/rewards/utils.rs | 68 ---- 14 files changed, 14 insertions(+), 735 deletions(-) delete mode 100644 programs/rewards/src/instructions/penalties/allow_tokenflow.rs delete mode 100644 programs/rewards/src/instructions/penalties/restrict_batch_minting.rs delete mode 100644 programs/rewards/src/instructions/penalties/restrict_tokenflow.rs delete mode 100644 programs/rewards/tests/rewards/penalties/restrict_batch_minting.rs delete mode 100644 programs/rewards/tests/rewards/penalties/tokenflow_restrictions.rs diff --git a/programs/rewards/src/error.rs b/programs/rewards/src/error.rs index 7ff8583c..c2bf1d7a 100644 --- a/programs/rewards/src/error.rs +++ b/programs/rewards/src/error.rs @@ -41,76 +41,52 @@ pub enum MplxRewardsError { #[error("Rewards: distribution_ends_at date is lower than current date")] InvalidPrimitiveTypesConversion, - /// 11 + /// 6 /// Impossible to close accounts while it has unclaimed rewards #[error("Rewards: unclaimed rewards must be claimed")] RewardsMustBeClaimed, - /// 12 + /// 7 /// No need to transfer zero amount of rewards. #[error("Rewards: rewards amount must be positive")] RewardsMustBeGreaterThanZero, - /// 13 - /// Delegate lack of tokens - #[error("Rewards: Delegate must have at least 15_000_000 of own weighted stake")] - InsufficientWeightedStake, - - /// 14 + /// 8 /// Stake from others must be zero #[error("Rewards: Stake from others must be zero")] StakeFromOthersMustBeZero, - /// 15 + /// 9 /// No need to transfer zero amount of rewards. #[error("No changes at the date in weighted stake modifiers while they're expected")] NoWeightedStakeModifiersAtADate, - /// 16 + /// 10 /// To change a delegate, the new delegate must differ from the current one #[error("Passed delegates are the same")] DelegatesAreTheSame, - /// 17 + /// 11 /// Getting pointer to the data of the zero-copy account has failed #[error("Getting pointer to the data of the zero-copy account has failed")] RetreivingZeroCopyAccountFailire, - /// 18 + /// 12 /// Account is already initialized #[error("Account is already initialized")] AlreadyInitialized, - /// 19 + /// 13 /// Incorrect mining address. #[error("Invalid mining")] InvalidMining, - /// 20 + /// 14 /// Failed to derive PDA. #[error("Failed to derive PDA")] DerivationError, - /// 21 - #[error("Mining already restricted")] - MiningAlreadyRestricted, - - /// 22 - /// Mining is not restricted - #[error("Mining is not restricted")] - MiningNotRestricted, - - /// 23 - /// Claiming is restricted - #[error("Claiming is restricted")] - ClaimingRestricted, - - /// 24 - /// Withdrawal is restricted while claiming is restricted - #[error("Withdrawal is restricted while claiming is restricted")] - WithdrawalRestricted, - - /// 25 + /// 15 #[error( "Rewards: Penalty is not apliable becase it's bigger than the mining's weighted stake" )] diff --git a/programs/rewards/src/instruction.rs b/programs/rewards/src/instruction.rs index a02d7246..07c51f9e 100644 --- a/programs/rewards/src/instruction.rs +++ b/programs/rewards/src/instruction.rs @@ -145,29 +145,6 @@ pub enum RewardsInstruction { new_delegate: Pubkey, }, - /// Prevents the mining account from rewards withdrawing - #[account(0, signer, name = "deposit_authority", desc = "The address of the Staking program's Registrar, which is PDA and is responsible for signing CPIs")] - #[account(1, name = "reward_pool", desc = "The address of the reward pool")] - #[account(2, writable, name = "mining", desc = "The address of the mining account which belongs to the user and stores info about user's rewards")] - RestrictTokenFlow { - mining_owner: Pubkey, - }, - - #[account(0, signer, name = "deposit_authority", desc = "The address of the Staking program's Registrar, which is PDA and is responsible for signing CPIs")] - #[account(1, name = "reward_pool", desc = "The address of the reward pool")] - #[account(2, writable, name = "mining", desc = "The address of the mining account which belongs to the user and stores info about user's rewards")] - AllowTokenFlow { - mining_owner: Pubkey, - }, - - #[account(0, signer, name = "deposit_authority", desc = "The address of the Staking program's Registrar, which is PDA and is responsible for signing CPIs")] - #[account(1, name = "reward_pool", desc = "The address of the reward pool")] - #[account(2, writable, name = "mining", desc = "The address of the mining account which belongs to the user and stores info about user's rewards")] - RestrictBatchMinting { - restrict_batch_minting_until_ts: u64, - mining_owner: Pubkey, - }, - #[account(0, signer, name = "deposit_authority", desc = "The address of the Staking program's Registrar, which is PDA and is responsible for signing CPIs")] #[account(1, writable, name = "reward_pool", desc = "The address of the reward pool")] #[account(2, writable, name = "mining", desc = "The address of the mining account which belongs to the user and stores info about user's rewards")] @@ -477,74 +454,6 @@ pub fn change_delegate( ) } -pub fn restrict_tokenflow( - program_id: &Pubkey, - deposit_authority: &Pubkey, - reward_pool: &Pubkey, - mining: &Pubkey, - mining_owner: &Pubkey, -) -> Instruction { - let accounts = vec![ - AccountMeta::new_readonly(*deposit_authority, true), - AccountMeta::new_readonly(*reward_pool, false), - AccountMeta::new(*mining, false), - ]; - - Instruction::new_with_borsh( - *program_id, - &RewardsInstruction::RestrictTokenFlow { - mining_owner: *mining_owner, - }, - accounts, - ) -} - -pub fn allow_tokenflow( - program_id: &Pubkey, - deposit_authority: &Pubkey, - reward_pool: &Pubkey, - mining: &Pubkey, - mining_owner: &Pubkey, -) -> Instruction { - let accounts = vec![ - AccountMeta::new_readonly(*deposit_authority, true), - AccountMeta::new_readonly(*reward_pool, false), - AccountMeta::new(*mining, false), - ]; - - Instruction::new_with_borsh( - *program_id, - &RewardsInstruction::AllowTokenFlow { - mining_owner: *mining_owner, - }, - accounts, - ) -} - -pub fn restrict_batch_minting( - program_id: &Pubkey, - deposit_authority: &Pubkey, - reward_pool: &Pubkey, - mining: &Pubkey, - mining_owner: &Pubkey, - restrict_batch_minting_until_ts: u64, -) -> Instruction { - let accounts = vec![ - AccountMeta::new_readonly(*deposit_authority, true), - AccountMeta::new_readonly(*reward_pool, false), - AccountMeta::new(*mining, false), - ]; - - Instruction::new_with_borsh( - *program_id, - &RewardsInstruction::RestrictBatchMinting { - restrict_batch_minting_until_ts, - mining_owner: *mining_owner, - }, - accounts, - ) -} - #[allow(clippy::too_many_arguments)] pub fn slash( program_id: &Pubkey, diff --git a/programs/rewards/src/instructions/claim.rs b/programs/rewards/src/instructions/claim.rs index c84432ef..d409f5a6 100644 --- a/programs/rewards/src/instructions/claim.rs +++ b/programs/rewards/src/instructions/claim.rs @@ -1,6 +1,5 @@ use crate::{ asserts::{assert_account_key, assert_account_owner}, - error::MplxRewardsError, state::{WrappedMining, WrappedRewardPool}, utils::{spl_transfer, AccountLoader}, }; @@ -43,10 +42,6 @@ pub fn process_claim<'a>(program_id: &Pubkey, accounts: &'a [AccountInfo<'a>]) - let mining_data = &mut mining.data.borrow_mut(); let mut wrapped_mining = WrappedMining::from_bytes_mut(mining_data)?; - if wrapped_mining.mining.is_tokenflow_restricted() { - return Err(MplxRewardsError::ClaimingRestricted.into()); - } - assert_account_owner(reward_pool, program_id)?; assert_account_key(mining_owner, &wrapped_mining.mining.owner)?; assert_account_key(reward_pool, &wrapped_mining.mining.reward_pool)?; diff --git a/programs/rewards/src/instructions/mod.rs b/programs/rewards/src/instructions/mod.rs index 145b52bf..1296f6f4 100644 --- a/programs/rewards/src/instructions/mod.rs +++ b/programs/rewards/src/instructions/mod.rs @@ -118,26 +118,6 @@ pub fn process_instruction<'a>( msg!("RewardsInstruction: ChangeDelegate"); process_change_delegate(program_id, accounts, staked_amount, &new_delegate) } - RewardsInstruction::RestrictTokenFlow { mining_owner } => { - msg!("RewardsInstruction: RestrictClaiming"); - process_restrict_tokenflow(program_id, accounts, &mining_owner) - } - RewardsInstruction::AllowTokenFlow { mining_owner } => { - msg!("RewardsInstruction: AllowClaiming"); - process_allow_tokenflow(program_id, accounts, &mining_owner) - } - RewardsInstruction::RestrictBatchMinting { - restrict_batch_minting_until_ts, - mining_owner, - } => { - msg!("RewardsInstruction: RestrictBatchMinting"); - process_restrict_batch_minting( - program_id, - accounts, - restrict_batch_minting_until_ts, - &mining_owner, - ) - } RewardsInstruction::Slash { mining_owner, slash_amount_in_native, diff --git a/programs/rewards/src/instructions/penalties/allow_tokenflow.rs b/programs/rewards/src/instructions/penalties/allow_tokenflow.rs deleted file mode 100644 index 6f17c4ce..00000000 --- a/programs/rewards/src/instructions/penalties/allow_tokenflow.rs +++ /dev/null @@ -1,31 +0,0 @@ -use crate::{asserts::assert_and_get_pool_and_mining, utils::AccountLoader}; -use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, pubkey::Pubkey}; - -pub fn process_allow_tokenflow<'a>( - program_id: &Pubkey, - accounts: &'a [AccountInfo<'a>], - mining_owner: &Pubkey, -) -> ProgramResult { - let account_info_iter = &mut accounts.iter().enumerate(); - - let deposit_authority = AccountLoader::next_signer(account_info_iter)?; - let reward_pool = AccountLoader::next_with_owner(account_info_iter, program_id)?; - let mining = AccountLoader::next_with_owner(account_info_iter, program_id)?; - - let reward_pool_data = &mut reward_pool.data.borrow_mut(); - let mining_data = &mut mining.data.borrow_mut(); - - let (_, wrapped_mining) = assert_and_get_pool_and_mining( - program_id, - mining_owner, - mining, - reward_pool, - deposit_authority, - reward_pool_data, - mining_data, - )?; - - wrapped_mining.mining.allow_tokenflow()?; - - Ok(()) -} diff --git a/programs/rewards/src/instructions/penalties/mod.rs b/programs/rewards/src/instructions/penalties/mod.rs index 2c0ec928..6908ad0c 100644 --- a/programs/rewards/src/instructions/penalties/mod.rs +++ b/programs/rewards/src/instructions/penalties/mod.rs @@ -1,11 +1,5 @@ -mod allow_tokenflow; mod decrease_rewards; -mod restrict_batch_minting; -mod restrict_tokenflow; mod slash; -pub(crate) use allow_tokenflow::*; pub(crate) use decrease_rewards::*; -pub(crate) use restrict_batch_minting::*; -pub(crate) use restrict_tokenflow::*; pub(crate) use slash::*; diff --git a/programs/rewards/src/instructions/penalties/restrict_batch_minting.rs b/programs/rewards/src/instructions/penalties/restrict_batch_minting.rs deleted file mode 100644 index 38a3b797..00000000 --- a/programs/rewards/src/instructions/penalties/restrict_batch_minting.rs +++ /dev/null @@ -1,32 +0,0 @@ -use crate::{asserts::assert_and_get_pool_and_mining, utils::AccountLoader}; -use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, pubkey::Pubkey}; - -pub fn process_restrict_batch_minting<'a>( - program_id: &Pubkey, - accounts: &'a [AccountInfo<'a>], - restrict_batch_minting_until_ts: u64, - mining_owner: &Pubkey, -) -> ProgramResult { - let account_info_iter = &mut accounts.iter().enumerate(); - - let deposit_authority = AccountLoader::next_signer(account_info_iter)?; - let reward_pool = AccountLoader::next_with_owner(account_info_iter, program_id)?; - let mining = AccountLoader::next_with_owner(account_info_iter, program_id)?; - - let reward_pool_data = &mut reward_pool.data.borrow_mut(); - let mining_data = &mut mining.data.borrow_mut(); - - let (_, wrapped_mining) = assert_and_get_pool_and_mining( - program_id, - mining_owner, - mining, - reward_pool, - deposit_authority, - reward_pool_data, - mining_data, - )?; - - wrapped_mining.mining.batch_minting_restricted_until = restrict_batch_minting_until_ts; - - Ok(()) -} diff --git a/programs/rewards/src/instructions/penalties/restrict_tokenflow.rs b/programs/rewards/src/instructions/penalties/restrict_tokenflow.rs deleted file mode 100644 index 70c02ae0..00000000 --- a/programs/rewards/src/instructions/penalties/restrict_tokenflow.rs +++ /dev/null @@ -1,31 +0,0 @@ -use crate::{asserts::assert_and_get_pool_and_mining, utils::AccountLoader}; -use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, pubkey::Pubkey}; - -pub fn process_restrict_tokenflow<'a>( - program_id: &Pubkey, - accounts: &'a [AccountInfo<'a>], - mining_owner: &Pubkey, -) -> ProgramResult { - let account_info_iter = &mut accounts.iter().enumerate(); - - let deposit_authority = AccountLoader::next_signer(account_info_iter)?; - let reward_pool = AccountLoader::next_with_owner(account_info_iter, program_id)?; - let mining = AccountLoader::next_with_owner(account_info_iter, program_id)?; - - let reward_pool_data = &mut reward_pool.data.borrow_mut(); - let mining_data = &mut mining.data.borrow_mut(); - - let (_, wrapped_mining) = assert_and_get_pool_and_mining( - program_id, - mining_owner, - mining, - reward_pool, - deposit_authority, - reward_pool_data, - mining_data, - )?; - - wrapped_mining.mining.restrict_tokenflow()?; - - Ok(()) -} diff --git a/programs/rewards/src/instructions/withdraw_mining.rs b/programs/rewards/src/instructions/withdraw_mining.rs index 5a0e5470..d9f88993 100644 --- a/programs/rewards/src/instructions/withdraw_mining.rs +++ b/programs/rewards/src/instructions/withdraw_mining.rs @@ -1,6 +1,5 @@ use crate::{ asserts::assert_and_get_pool_and_mining, - error::MplxRewardsError, utils::{get_delegate_mining, AccountLoader}, }; @@ -34,10 +33,6 @@ pub fn process_withdraw_mining<'a>( mining_data, )?; - if wrapped_mining.mining.is_tokenflow_restricted() { - return Err(MplxRewardsError::WithdrawalRestricted.into()); - } - let delegate_mining = get_delegate_mining(delegate_mining, mining)?; if let Some(delegate_mining) = delegate_mining { verify_delegate_mining_address(program_id, delegate_mining, delegate, reward_pool.key)? diff --git a/programs/rewards/src/state/mining.rs b/programs/rewards/src/state/mining.rs index 3bb588b0..dac43c52 100644 --- a/programs/rewards/src/state/mining.rs +++ b/programs/rewards/src/state/mining.rs @@ -1,6 +1,6 @@ use crate::{error::MplxRewardsError, state::PRECISION}; -use crate::utils::{get_curr_unix_ts, SafeArithmeticOperations}; +use crate::utils::SafeArithmeticOperations; use bytemuck::{Pod, Zeroable}; use shank::ShankAccount; use sokoban::{NodeAllocatorMap, ZeroCopy}; @@ -33,12 +33,6 @@ pub struct WrappedImmutableMining<'a> { } pub const ACCOUNT_TYPE_BYTE: usize = 0; -const IS_TOKENFLOW_RESTRICTED_MASK: u8 = 1 << 0; - -/// That byte represents the set of applicable penalties. The structure is follows: -/// - 0: tokenflow -/// - 1-7: reserved -pub const PENALTIES_BYTE: usize = 1; impl<'a> WrappedMining<'a> { pub const LEN: usize = @@ -137,17 +131,13 @@ pub struct Mining { pub unclaimed_rewards: u64, /// This field sums up each time somebody stakes to that account as a delegate. pub stake_from_others: u64, - /// The date when batch minting is restricted until. - pub batch_minting_restricted_until: u64, /// Bump of the mining account pub bump: u8, /// Account type - Mining. This discriminator should exist in order to prevent /// shenanigans with customly modified accounts and their fields. /// 0: account type - /// 1: claim is restricted - /// 2: penalties bitmap - /// 3-15: unused - pub data: [u8; 15], + /// 1-7: unused + pub data: [u8; 7], } impl ZeroCopy for Mining {} @@ -160,7 +150,7 @@ impl Mining { pub fn initialize(reward_pool: Pubkey, owner: Pubkey, bump: u8) -> Mining { let account_type = AccountType::Mining.into(); - let mut data = [0; 15]; + let mut data = [0; 7]; data[ACCOUNT_TYPE_BYTE] = account_type; Mining { @@ -241,32 +231,6 @@ impl Mining { Ok(()) } - - pub fn restrict_tokenflow(&mut self) -> ProgramResult { - if self.is_tokenflow_restricted() { - Err(MplxRewardsError::MiningAlreadyRestricted.into()) - } else { - self.data[PENALTIES_BYTE] |= IS_TOKENFLOW_RESTRICTED_MASK; - Ok(()) - } - } - - pub fn allow_tokenflow(&mut self) -> ProgramResult { - if !self.is_tokenflow_restricted() { - Err(MplxRewardsError::MiningAlreadyRestricted.into()) - } else { - self.data[PENALTIES_BYTE] &= !(IS_TOKENFLOW_RESTRICTED_MASK); - Ok(()) - } - } - - pub fn is_tokenflow_restricted(&self) -> bool { - self.data[PENALTIES_BYTE] & IS_TOKENFLOW_RESTRICTED_MASK > 0 - } - - pub fn is_batch_minting_restricted(&self) -> bool { - self.batch_minting_restricted_until > get_curr_unix_ts() - } } impl IsInitialized for Mining { diff --git a/programs/rewards/tests/rewards/penalties/mod.rs b/programs/rewards/tests/rewards/penalties/mod.rs index 114780a6..530fe9f5 100644 --- a/programs/rewards/tests/rewards/penalties/mod.rs +++ b/programs/rewards/tests/rewards/penalties/mod.rs @@ -1,3 +1 @@ -mod restrict_batch_minting; mod slash; -mod tokenflow_restrictions; diff --git a/programs/rewards/tests/rewards/penalties/restrict_batch_minting.rs b/programs/rewards/tests/rewards/penalties/restrict_batch_minting.rs deleted file mode 100644 index eaeb7db8..00000000 --- a/programs/rewards/tests/rewards/penalties/restrict_batch_minting.rs +++ /dev/null @@ -1,58 +0,0 @@ -use crate::utils::*; -use mplx_rewards::state::WrappedImmutableMining; -use solana_program::pubkey::Pubkey; -use solana_program_test::*; -use solana_sdk::{signature::Keypair, signer::Signer}; -use std::borrow::Borrow; - -async fn setup() -> (ProgramTestContext, TestRewards, Pubkey) { - let test = ProgramTest::new("mplx_rewards", mplx_rewards::ID, None); - let mut context = test.start_with_context().await; - - let owner = &context.payer.pubkey(); - - let mint = Keypair::new(); - create_mint(&mut context, &mint, owner).await.unwrap(); - - let test_rewards = TestRewards::new(mint.pubkey()); - test_rewards.initialize_pool(&mut context).await.unwrap(); - - // mint token for fill_authority aka wallet who will fill the vault with tokens - let rewarder = Keypair::new(); - create_token_account( - &mut context, - &rewarder, - &test_rewards.token_mint_pubkey, - &test_rewards.fill_authority.pubkey(), - 0, - ) - .await - .unwrap(); - mint_tokens( - &mut context, - &test_rewards.token_mint_pubkey, - &rewarder.pubkey(), - 1_000_000, - ) - .await - .unwrap(); - - (context, test_rewards, rewarder.pubkey()) -} - -#[tokio::test] -async fn batch_minting_restricted() { - let (mut context, test_rewards, _) = setup().await; - - let (user_a, _, user_mining_a) = create_end_user(&mut context, &test_rewards).await; - - test_rewards - .restrict_batch_minting(&mut context, &user_mining_a, 100, &user_a.pubkey()) - .await - .unwrap(); - - let mining_account = get_account(&mut context, &user_mining_a).await; - let mining_data = &mining_account.data.borrow(); - let mining = WrappedImmutableMining::from_bytes(mining_data).unwrap(); - assert_eq!(mining.mining.batch_minting_restricted_until, 100); -} diff --git a/programs/rewards/tests/rewards/penalties/tokenflow_restrictions.rs b/programs/rewards/tests/rewards/penalties/tokenflow_restrictions.rs deleted file mode 100644 index 380ed9ab..00000000 --- a/programs/rewards/tests/rewards/penalties/tokenflow_restrictions.rs +++ /dev/null @@ -1,312 +0,0 @@ -use crate::utils::*; -use assert_custom_on_chain_error::AssertCustomOnChainErr; -use mplx_rewards::{ - state::{WrappedMining, WrappedRewardPool}, - utils::LockupPeriod, -}; -use solana_program::{program_pack::Pack, pubkey::Pubkey}; -use solana_program_test::*; -use solana_sdk::{clock::SECONDS_PER_DAY, signature::Keypair, signer::Signer}; -use spl_token::state::Account; -use std::borrow::{Borrow, BorrowMut}; - -async fn setup() -> (ProgramTestContext, TestRewards, Pubkey) { - let test = ProgramTest::new("mplx_rewards", mplx_rewards::ID, None); - let mut context = test.start_with_context().await; - - let owner = &context.payer.pubkey(); - - let mint = Keypair::new(); - create_mint(&mut context, &mint, owner).await.unwrap(); - - let test_rewards = TestRewards::new(mint.pubkey()); - test_rewards.initialize_pool(&mut context).await.unwrap(); - - // mint token for fill_authority aka wallet who will fill the vault with tokens - let rewarder = Keypair::new(); - create_token_account( - &mut context, - &rewarder, - &test_rewards.token_mint_pubkey, - &test_rewards.fill_authority.pubkey(), - 0, - ) - .await - .unwrap(); - mint_tokens( - &mut context, - &test_rewards.token_mint_pubkey, - &rewarder.pubkey(), - 1_000_000, - ) - .await - .unwrap(); - - (context, test_rewards, rewarder.pubkey()) -} - -#[tokio::test] -async fn claim_restricted() { - let (mut context, test_rewards, rewarder) = setup().await; - - let (user_a, user_rewards_a, user_mining_a) = - create_end_user(&mut context, &test_rewards).await; - test_rewards - .deposit_mining( - &mut context, - &user_mining_a, - 100, - LockupPeriod::ThreeMonths, - &user_a.pubkey(), - &user_mining_a, - &user_a.pubkey(), - ) - .await - .unwrap(); - - // fill vault with tokens - let distribution_ends_at = context - .banks_client - .get_sysvar::() - .await - .unwrap() - .unix_timestamp as u64 - + SECONDS_PER_DAY; - - test_rewards - .fill_vault(&mut context, &rewarder, 100, distribution_ends_at) - .await - .unwrap(); - - test_rewards.distribute_rewards(&mut context).await.unwrap(); - - // restrict claiming - test_rewards - .restrict_tokenflow(&mut context, &user_mining_a, &user_a.pubkey()) - .await - .unwrap(); - - test_rewards - .claim( - &mut context, - &user_a, - &user_mining_a, - &user_rewards_a.pubkey(), - ) - .await - .assert_on_chain_err(mplx_rewards::error::MplxRewardsError::ClaimingRestricted); -} - -#[tokio::test] -async fn claim_allowed() { - let (mut context, test_rewards, rewarder) = setup().await; - - let (user_a, user_rewards_a, user_mining_a) = - create_end_user(&mut context, &test_rewards).await; - test_rewards - .deposit_mining( - &mut context, - &user_mining_a, - 100, - LockupPeriod::ThreeMonths, - &user_a.pubkey(), - &user_mining_a, - &user_a.pubkey(), - ) - .await - .unwrap(); - - // fill vault with tokens - let distribution_ends_at = context - .banks_client - .get_sysvar::() - .await - .unwrap() - .unix_timestamp as u64 - + SECONDS_PER_DAY; - - test_rewards - .fill_vault(&mut context, &rewarder, 100, distribution_ends_at) - .await - .unwrap(); - - test_rewards.distribute_rewards(&mut context).await.unwrap(); - - // restrict claiming - test_rewards - .restrict_tokenflow(&mut context, &user_mining_a, &user_a.pubkey()) - .await - .unwrap(); - - test_rewards - .claim( - &mut context, - &user_a, - &user_mining_a, - &user_rewards_a.pubkey(), - ) - .await - .assert_on_chain_err(mplx_rewards::error::MplxRewardsError::ClaimingRestricted); - - // allow claiming - test_rewards - .allow_tokenflow(&mut context, &user_mining_a, &user_a.pubkey()) - .await - .unwrap(); - - // MUST TO ADVANCE TO AVOID CACHING - advance_clock_by_ts(&mut context, 2).await; - - test_rewards - .claim( - &mut context, - &user_a, - &user_mining_a, - &user_rewards_a.pubkey(), - ) - .await - .unwrap(); - - let user_reward_account_a = get_account(&mut context, &user_rewards_a.pubkey()).await; - let user_rewards_a = Account::unpack(user_reward_account_a.data.borrow()).unwrap(); - - assert_eq!(user_rewards_a.amount, 100); -} - -#[tokio::test] -async fn withdraw_restricted() { - let (mut context, test_rewards, rewarder) = setup().await; - - let (user_a, _, user_mining_a) = create_end_user(&mut context, &test_rewards).await; - test_rewards - .deposit_mining( - &mut context, - &user_mining_a, - 100, - LockupPeriod::ThreeMonths, - &user_a.pubkey(), - &user_mining_a, - &user_a.pubkey(), - ) - .await - .unwrap(); - - // fill vault with tokens - let distribution_ends_at = context - .banks_client - .get_sysvar::() - .await - .unwrap() - .unix_timestamp as u64 - + SECONDS_PER_DAY; - - test_rewards - .fill_vault(&mut context, &rewarder, 100, distribution_ends_at) - .await - .unwrap(); - - test_rewards.distribute_rewards(&mut context).await.unwrap(); - - // restrict claiming - test_rewards - .restrict_tokenflow(&mut context, &user_mining_a, &user_a.pubkey()) - .await - .unwrap(); - - test_rewards - .withdraw_mining( - &mut context, - &user_mining_a, - &user_mining_a, - 100, - &user_a.pubkey(), - &user_a.pubkey(), - ) - .await - .assert_on_chain_err(mplx_rewards::error::MplxRewardsError::WithdrawalRestricted); -} - -#[tokio::test] -async fn withdraw_allowed() { - let (mut context, test_rewards, rewarder) = setup().await; - - let (user_a, _, user_mining_a) = create_end_user(&mut context, &test_rewards).await; - test_rewards - .deposit_mining( - &mut context, - &user_mining_a, - 100, - LockupPeriod::ThreeMonths, - &user_a.pubkey(), - &user_mining_a, - &user_a.pubkey(), - ) - .await - .unwrap(); - - // fill vault with tokens - let distribution_ends_at = context - .banks_client - .get_sysvar::() - .await - .unwrap() - .unix_timestamp as u64 - + SECONDS_PER_DAY; - - test_rewards - .fill_vault(&mut context, &rewarder, 100, distribution_ends_at) - .await - .unwrap(); - - test_rewards.distribute_rewards(&mut context).await.unwrap(); - - // restrict claiming - test_rewards - .restrict_tokenflow(&mut context, &user_mining_a, &user_a.pubkey()) - .await - .unwrap(); - - test_rewards - .withdraw_mining( - &mut context, - &user_mining_a, - &user_mining_a, - 100, - &user_a.pubkey(), - &user_a.pubkey(), - ) - .await - .assert_on_chain_err(mplx_rewards::error::MplxRewardsError::WithdrawalRestricted); - - // prevent caching - advance_clock_by_ts(&mut context, (distribution_ends_at + 1).try_into().unwrap()).await; - test_rewards - .allow_tokenflow(&mut context, &user_mining_a, &user_a.pubkey()) - .await - .unwrap(); - - test_rewards - .withdraw_mining( - &mut context, - &user_mining_a, - &user_mining_a, - 100, - &user_a.pubkey(), - &user_a.pubkey(), - ) - .await - .unwrap(); - - let mut reward_pool_account = - get_account(&mut context, &test_rewards.reward_pool.pubkey()).await; - let reward_pool_data = &mut reward_pool_account.data.borrow_mut(); - let wrapped_reward_pool = WrappedRewardPool::from_bytes_mut(reward_pool_data).unwrap(); - let reward_pool = wrapped_reward_pool.pool; - - assert_eq!(reward_pool.total_share, 0); - - let mut mining_account = get_account(&mut context, &user_mining_a).await; - let mining_data = &mut mining_account.data.borrow_mut(); - let wrapped_mining = WrappedMining::from_bytes_mut(mining_data).unwrap(); - assert_eq!(wrapped_mining.mining.share, 0); -} diff --git a/programs/rewards/tests/rewards/utils.rs b/programs/rewards/tests/rewards/utils.rs index ce17d858..c8bf56a4 100644 --- a/programs/rewards/tests/rewards/utils.rs +++ b/programs/rewards/tests/rewards/utils.rs @@ -392,74 +392,6 @@ impl TestRewards { context.banks_client.process_transaction(tx).await } - - pub async fn restrict_tokenflow( - &self, - context: &mut ProgramTestContext, - mining_account: &Pubkey, - mining_owner: &Pubkey, - ) -> BanksClientResult<()> { - let tx = Transaction::new_signed_with_payer( - &[mplx_rewards::instruction::restrict_tokenflow( - &mplx_rewards::id(), - &self.deposit_authority.pubkey(), - &self.reward_pool.pubkey(), - mining_account, - mining_owner, - )], - Some(&context.payer.pubkey()), - &[&context.payer, &self.deposit_authority], - context.last_blockhash, - ); - - context.banks_client.process_transaction(tx).await - } - - pub async fn allow_tokenflow( - &self, - context: &mut ProgramTestContext, - mining_account: &Pubkey, - mining_owner: &Pubkey, - ) -> BanksClientResult<()> { - let tx = Transaction::new_signed_with_payer( - &[mplx_rewards::instruction::allow_tokenflow( - &mplx_rewards::id(), - &self.deposit_authority.pubkey(), - &self.reward_pool.pubkey(), - mining_account, - mining_owner, - )], - Some(&context.payer.pubkey()), - &[&context.payer, &self.deposit_authority], - context.last_blockhash, - ); - - context.banks_client.process_transaction(tx).await - } - - pub async fn restrict_batch_minting( - &self, - context: &mut ProgramTestContext, - mining_account: &Pubkey, - restrict_batch_minting_until_ts: u64, - mining_owner: &Pubkey, - ) -> BanksClientResult<()> { - let tx = Transaction::new_signed_with_payer( - &[mplx_rewards::instruction::restrict_batch_minting( - &mplx_rewards::id(), - &self.deposit_authority.pubkey(), - &self.reward_pool.pubkey(), - mining_account, - mining_owner, - restrict_batch_minting_until_ts, - )], - Some(&context.payer.pubkey()), - &[&context.payer, &self.deposit_authority], - context.last_blockhash, - ); - - context.banks_client.process_transaction(tx).await - } } pub async fn create_token_account(