From 406806db8e72431e19f7048ec1c66f9f171346a5 Mon Sep 17 00:00:00 2001 From: Kyrylo Stepanov Date: Wed, 10 Jul 2024 16:46:16 +0300 Subject: [PATCH 01/14] initial design --- programs/rewards/src/error.rs | 5 ++++ .../src/instructions/deposit_mining.rs | 24 +++++++++++++++++-- .../rewards/src/instructions/extend_stake.rs | 23 +++++++++++++++++- programs/rewards/src/state/mining.rs | 8 +++---- programs/rewards/src/state/reward_pool.rs | 22 ++++++++++++++++- 5 files changed, 73 insertions(+), 9 deletions(-) diff --git a/programs/rewards/src/error.rs b/programs/rewards/src/error.rs index 1b79a049..d34c5768 100644 --- a/programs/rewards/src/error.rs +++ b/programs/rewards/src/error.rs @@ -75,6 +75,11 @@ pub enum MplxRewardsError { /// 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, } impl PrintProgramError for MplxRewardsError { diff --git a/programs/rewards/src/instructions/deposit_mining.rs b/programs/rewards/src/instructions/deposit_mining.rs index 06282513..ed7b0a46 100644 --- a/programs/rewards/src/instructions/deposit_mining.rs +++ b/programs/rewards/src/instructions/deposit_mining.rs @@ -1,6 +1,7 @@ use crate::{ asserts::assert_account_key, - state::{Mining, RewardPool}, + error::MplxRewardsError, + state::{Mining, RewardPool, DELEGATE_MINIMAL_OWNED_WEIGHTED_STAKE}, utils::{AccountLoader, LockupPeriod}, }; use solana_program::{ @@ -13,6 +14,7 @@ pub struct DepositMiningContext<'a, 'b> { reward_pool: &'a AccountInfo<'b>, mining: &'a AccountInfo<'b>, deposit_authority: &'a AccountInfo<'b>, + delegate: &'a AccountInfo<'b>, } impl<'a, 'b> DepositMiningContext<'a, 'b> { @@ -26,11 +28,13 @@ impl<'a, 'b> DepositMiningContext<'a, 'b> { let reward_pool = AccountLoader::next_with_owner(account_info_iter, program_id)?; let mining = AccountLoader::next_with_owner(account_info_iter, program_id)?; let deposit_authority = AccountLoader::next_signer(account_info_iter)?; + let delegate = AccountLoader::next_with_owner(account_info_iter, program_id)?; Ok(DepositMiningContext { reward_pool, mining, deposit_authority, + delegate, }) } @@ -68,7 +72,23 @@ impl<'a, 'b> DepositMiningContext<'a, 'b> { } } - reward_pool.deposit(&mut mining, amount, lockup_period)?; + let mut delegate_mining = if mining.owner != *self.delegate.key { + let delegate_mining = Mining::unpack(&self.delegate.data.borrow())?; + if delegate_mining + .share + .checked_sub(delegate_mining.stake_from_others) + .ok_or(MplxRewardsError::MathOverflow)? + < DELEGATE_MINIMAL_OWNED_WEIGHTED_STAKE + { + return Err(MplxRewardsError::InsufficientWeightedStake.into()); + } + + Some(delegate_mining) + } else { + None + }; + + reward_pool.deposit(&mut mining, amount, lockup_period, delegate_mining.as_mut())?; RewardPool::pack(reward_pool, *self.reward_pool.data.borrow_mut())?; Mining::pack(mining, *self.mining.data.borrow_mut())?; diff --git a/programs/rewards/src/instructions/extend_stake.rs b/programs/rewards/src/instructions/extend_stake.rs index cd4b3096..b8c465c4 100644 --- a/programs/rewards/src/instructions/extend_stake.rs +++ b/programs/rewards/src/instructions/extend_stake.rs @@ -1,6 +1,7 @@ use crate::{ asserts::assert_account_key, - state::{Mining, RewardPool}, + error::MplxRewardsError, + state::{Mining, RewardPool, DELEGATE_MINIMAL_OWNED_WEIGHTED_STAKE}, utils::{AccountLoader, LockupPeriod}, }; use solana_program::{ @@ -13,6 +14,7 @@ pub struct ExtendStakeContext<'a, 'b> { reward_pool: &'a AccountInfo<'b>, mining: &'a AccountInfo<'b>, deposit_authority: &'a AccountInfo<'b>, + delegate: &'a AccountInfo<'b>, } impl<'a, 'b> ExtendStakeContext<'a, 'b> { @@ -26,11 +28,13 @@ impl<'a, 'b> ExtendStakeContext<'a, 'b> { let reward_pool = AccountLoader::next_with_owner(account_info_iter, program_id)?; let mining = AccountLoader::next_with_owner(account_info_iter, program_id)?; let deposit_authority = AccountLoader::next_signer(account_info_iter)?; + let delegate = AccountLoader::next_with_owner(account_info_iter, program_id)?; Ok(ExtendStakeContext { reward_pool, mining, deposit_authority, + delegate, }) } @@ -73,6 +77,22 @@ impl<'a, 'b> ExtendStakeContext<'a, 'b> { } } + let mut delegate_mining = if mining.owner != *self.delegate.key { + let delegate_mining = Mining::unpack(&self.delegate.data.borrow())?; + if delegate_mining + .share + .checked_sub(delegate_mining.stake_from_others) + .ok_or(MplxRewardsError::MathOverflow)? + < DELEGATE_MINIMAL_OWNED_WEIGHTED_STAKE + { + return Err(MplxRewardsError::InsufficientWeightedStake.into()); + } + + Some(delegate_mining) + } else { + None + }; + reward_pool.extend( &mut mining, old_lockup_period, @@ -80,6 +100,7 @@ impl<'a, 'b> ExtendStakeContext<'a, 'b> { deposit_start_ts, base_amount, additional_amount, + delegate_mining.as_mut(), )?; RewardPool::pack(reward_pool, *self.reward_pool.data.borrow_mut())?; diff --git a/programs/rewards/src/state/mining.rs b/programs/rewards/src/state/mining.rs index e4e731a8..448f17c1 100644 --- a/programs/rewards/src/state/mining.rs +++ b/programs/rewards/src/state/mining.rs @@ -37,8 +37,8 @@ pub struct Mining { /// That "index" points at the moment when the last reward has been recieved. Also, /// it' s responsible for weighted_stake changes and, therefore, rewards calculations. pub index: RewardIndex, - /// Delegate address where tokens will be staked in "delegated staking model". - pub delegate: Pubkey, + /// This field sums up each time somebody stakes to that account as a delegate. + pub stake_from_others: u64, } impl Mining { @@ -48,10 +48,8 @@ impl Mining { account_type: AccountType::Mining, reward_pool, bump, - share: 0, owner, - index: RewardIndex::default(), - delegate: owner, + ..Default::default() } } diff --git a/programs/rewards/src/state/reward_pool.rs b/programs/rewards/src/state/reward_pool.rs index d6a0f346..d91dd6bd 100644 --- a/programs/rewards/src/state/reward_pool.rs +++ b/programs/rewards/src/state/reward_pool.rs @@ -18,6 +18,7 @@ use solana_program::{ /// Precision for index calculation pub const PRECISION: u128 = 10_000_000_000_000_000; +pub const DELEGATE_MINIMAL_OWNED_WEIGHTED_STAKE: u64 = 15_000_000; /// Reward pool #[derive(Debug, BorshDeserialize, BorshSerialize, BorshSchema, Default)] @@ -106,6 +107,7 @@ impl RewardPool { mining: &mut Mining, amount: u64, lockup_period: LockupPeriod, + delegate_mining: Option<&mut Mining>, ) -> ProgramResult { mining.refresh_rewards(&self.calculator)?; @@ -152,6 +154,18 @@ impl RewardPool { .checked_add(weighted_stake_diff) .ok_or(MplxRewardsError::MathOverflow)?; + if let Some(delegate_mining) = delegate_mining { + delegate_mining + .stake_from_others + .checked_add(amount) + .ok_or(MplxRewardsError::MathOverflow)?; // delegate_mining.stake_from_others += amount; + + self.total_share = self + .total_share + .checked_add(amount) + .ok_or(MplxRewardsError::MathOverflow)?; + } + Ok(()) } @@ -187,6 +201,7 @@ impl RewardPool { deposit_start_ts: u64, base_amount: u64, additional_amount: u64, + delegate_mining: Option<&mut Mining>, ) -> ProgramResult { mining.refresh_rewards(&self.calculator)?; @@ -254,7 +269,12 @@ impl RewardPool { let amount_to_restake = base_amount .checked_add(additional_amount) .ok_or(MplxRewardsError::MathOverflow)?; - self.deposit(mining, amount_to_restake, new_lockup_period)?; + self.deposit( + mining, + amount_to_restake, + new_lockup_period, + delegate_mining, + )?; Ok(()) } From af50ccea1dc64e91e34c8c83dfca198a8634eca1 Mon Sep 17 00:00:00 2001 From: Kyrylo Stepanov Date: Wed, 10 Jul 2024 17:36:25 +0300 Subject: [PATCH 02/14] fix tests for delegated initial design --- programs/rewards/src/instruction.rs | 6 +++++ .../src/instructions/deposit_mining.rs | 5 ++-- .../rewards/src/instructions/extend_stake.rs | 5 ++-- programs/rewards/tests/rewards/claim.rs | 20 ++++++++++++++ .../rewards/tests/rewards/close_mining.rs | 1 + .../rewards/tests/rewards/deposit_mining.rs | 18 +++++++++++-- .../tests/rewards/distribute_rewards.rs | 2 ++ .../rewards/tests/rewards/extend_stake.rs | 26 +++++++++++++++++-- programs/rewards/tests/rewards/fill_vault.rs | 1 + programs/rewards/tests/rewards/utils.rs | 4 +++ .../rewards/tests/rewards/withdraw_mining.rs | 11 ++++++-- 11 files changed, 87 insertions(+), 12 deletions(-) diff --git a/programs/rewards/src/instruction.rs b/programs/rewards/src/instruction.rs index 2a242a2e..73ebbda8 100644 --- a/programs/rewards/src/instruction.rs +++ b/programs/rewards/src/instruction.rs @@ -58,6 +58,7 @@ pub enum RewardsInstruction { #[account(1, writable, name = "mining", desc = "The address of the mining account which belongs to the user and stores info about user's rewards")] #[account(2, name = "reward_mint", desc = "The address of the reward mint")] #[account(3, signer, name = "deposit_authority", desc = "The address of the Staking program's Registrar, which is PDA and is responsible for signing CPIs")] + #[account(4, signer, name = "delegate", desc = "The address of Mining Account that might be used as a delegate in delegated staking model")] DepositMining { /// Amount to deposit amount: u64, @@ -94,6 +95,7 @@ pub enum RewardsInstruction { #[account(1, writable, name = "mining", desc = "The address of the mining account which belongs to the user and stores info about user's rewards")] #[account(2, name = "reward_mint", desc = "The address of the reward mint")] #[account(3, signer, name = "deposit_authority", desc = "The address of the Staking program's Registrar, which is PDA and is responsible for signing CPIs")] + #[account(4, signer, name = "delegate", desc = "The address of Mining Account that might be used as a delegate in delegated staking model")] ExtendStake { /// Lockup period before restaking. Actually it's only needed /// for Flex to AnyPeriod edge case @@ -222,6 +224,7 @@ pub fn deposit_mining( reward_pool: &Pubkey, mining: &Pubkey, deposit_authority: &Pubkey, + delegate: &Pubkey, amount: u64, lockup_period: LockupPeriod, owner: &Pubkey, @@ -230,6 +233,7 @@ pub fn deposit_mining( AccountMeta::new(*reward_pool, false), AccountMeta::new(*mining, false), AccountMeta::new_readonly(*deposit_authority, true), + AccountMeta::new(*delegate, false), ]; Instruction::new_with_borsh( @@ -301,6 +305,7 @@ pub fn extend_stake( reward_pool: &Pubkey, mining: &Pubkey, deposit_authority: &Pubkey, + delegate: &Pubkey, old_lockup_period: LockupPeriod, new_lockup_period: LockupPeriod, deposit_start_ts: u64, @@ -312,6 +317,7 @@ pub fn extend_stake( AccountMeta::new(*reward_pool, false), AccountMeta::new(*mining, false), AccountMeta::new_readonly(*deposit_authority, true), + AccountMeta::new(*delegate, false), ]; Instruction::new_with_borsh( diff --git a/programs/rewards/src/instructions/deposit_mining.rs b/programs/rewards/src/instructions/deposit_mining.rs index ed7b0a46..5bcb4039 100644 --- a/programs/rewards/src/instructions/deposit_mining.rs +++ b/programs/rewards/src/instructions/deposit_mining.rs @@ -72,12 +72,11 @@ impl<'a, 'b> DepositMiningContext<'a, 'b> { } } - let mut delegate_mining = if mining.owner != *self.delegate.key { + let mut delegate_mining = if self.mining.key != self.delegate.key { let delegate_mining = Mining::unpack(&self.delegate.data.borrow())?; if delegate_mining .share - .checked_sub(delegate_mining.stake_from_others) - .ok_or(MplxRewardsError::MathOverflow)? + .saturating_sub(delegate_mining.stake_from_others) < DELEGATE_MINIMAL_OWNED_WEIGHTED_STAKE { return Err(MplxRewardsError::InsufficientWeightedStake.into()); diff --git a/programs/rewards/src/instructions/extend_stake.rs b/programs/rewards/src/instructions/extend_stake.rs index b8c465c4..f1481580 100644 --- a/programs/rewards/src/instructions/extend_stake.rs +++ b/programs/rewards/src/instructions/extend_stake.rs @@ -77,12 +77,11 @@ impl<'a, 'b> ExtendStakeContext<'a, 'b> { } } - let mut delegate_mining = if mining.owner != *self.delegate.key { + let mut delegate_mining = if self.mining.key != self.delegate.key { let delegate_mining = Mining::unpack(&self.delegate.data.borrow())?; if delegate_mining .share - .checked_sub(delegate_mining.stake_from_others) - .ok_or(MplxRewardsError::MathOverflow)? + .saturating_sub(delegate_mining.stake_from_others) < DELEGATE_MINIMAL_OWNED_WEIGHTED_STAKE { return Err(MplxRewardsError::InsufficientWeightedStake.into()); diff --git a/programs/rewards/tests/rewards/claim.rs b/programs/rewards/tests/rewards/claim.rs index 3841c055..bc098896 100644 --- a/programs/rewards/tests/rewards/claim.rs +++ b/programs/rewards/tests/rewards/claim.rs @@ -58,6 +58,7 @@ async fn with_two_users() { 100, LockupPeriod::ThreeMonths, &user_a.pubkey(), + &user_mining_a, ) .await .unwrap(); @@ -71,6 +72,7 @@ async fn with_two_users() { 100, LockupPeriod::ThreeMonths, &user_b.pubkey(), + &user_mining_b, ) .await .unwrap(); @@ -136,6 +138,7 @@ async fn flex_vs_three_months() { 100, LockupPeriod::ThreeMonths, &user_a.pubkey(), + &user_mining_a, ) .await .unwrap(); @@ -151,6 +154,7 @@ async fn flex_vs_three_months() { 100, LockupPeriod::ThreeMonths, &user_b.pubkey(), + &user_mining_b, ) .await .unwrap(); @@ -216,6 +220,7 @@ async fn multiple_consequantial_distributions_for_two_users() { 100, LockupPeriod::ThreeMonths, &user_a.pubkey(), + &user_mining_a, ) .await .unwrap(); @@ -229,6 +234,7 @@ async fn multiple_consequantial_distributions_for_two_users() { 100, LockupPeriod::OneYear, &user_b.pubkey(), + &user_mining_b, ) .await .unwrap(); @@ -309,6 +315,7 @@ async fn rewards_after_distribution_are_unclaimable() { 100, LockupPeriod::ThreeMonths, &user_a.pubkey(), + &user_mining_a, ) .await .unwrap(); @@ -363,6 +370,7 @@ async fn rewards_after_distribution_are_unclaimable() { 100, LockupPeriod::OneYear, &user_b.pubkey(), + &user_mining_b, ) .await .unwrap(); @@ -398,6 +406,7 @@ async fn switch_to_flex_is_correct() { 100, LockupPeriod::ThreeMonths, &user_a.pubkey(), + &user_mining_a, ) .await .unwrap(); @@ -411,6 +420,7 @@ async fn switch_to_flex_is_correct() { 100, LockupPeriod::OneYear, &user_b.pubkey(), + &user_mining_b, ) .await .unwrap(); @@ -475,6 +485,7 @@ async fn two_deposits_vs_one() { 100, LockupPeriod::OneYear, &user_a.pubkey(), + &user_mining_a, ) .await .unwrap(); @@ -488,6 +499,7 @@ async fn two_deposits_vs_one() { 50, LockupPeriod::OneYear, &user_b.pubkey(), + &user_mining_b, ) .await .unwrap(); @@ -501,6 +513,7 @@ async fn two_deposits_vs_one() { 50, LockupPeriod::OneYear, &user_b.pubkey(), + &user_mining_b, ) .await .unwrap(); @@ -562,6 +575,7 @@ async fn claim_tokens_after_deposit_expiration() { 100, LockupPeriod::OneYear, &user_a.pubkey(), + &user_mining_a, ) .await .unwrap(); @@ -575,6 +589,7 @@ async fn claim_tokens_after_deposit_expiration() { 300, LockupPeriod::ThreeMonths, &user_b.pubkey(), + &user_mining_b, ) .await .unwrap(); @@ -639,6 +654,7 @@ async fn claim_after_withdraw_is_correct() { 100, LockupPeriod::OneYear, &user_a.pubkey(), + &user_mining_a, ) .await .unwrap(); @@ -651,6 +667,7 @@ async fn claim_after_withdraw_is_correct() { 50, LockupPeriod::OneYear, &user_b.pubkey(), + &user_mining_b, ) .await .unwrap(); @@ -662,6 +679,7 @@ async fn claim_after_withdraw_is_correct() { 150, LockupPeriod::ThreeMonths, &user_b.pubkey(), + &user_mining_b, ) .await .unwrap(); @@ -789,6 +807,7 @@ async fn with_two_users_with_flex() { 100, LockupPeriod::Flex, &user_a.pubkey(), + &user_mining_a, ) .await .unwrap(); @@ -802,6 +821,7 @@ async fn with_two_users_with_flex() { 100, LockupPeriod::Flex, &user_b.pubkey(), + &user_mining_b, ) .await .unwrap(); diff --git a/programs/rewards/tests/rewards/close_mining.rs b/programs/rewards/tests/rewards/close_mining.rs index e44fd574..9213f146 100644 --- a/programs/rewards/tests/rewards/close_mining.rs +++ b/programs/rewards/tests/rewards/close_mining.rs @@ -50,6 +50,7 @@ async fn success() { 100, LockupPeriod::ThreeMonths, &mining_owner.pubkey(), + &mining, ) .await .unwrap(); diff --git a/programs/rewards/tests/rewards/deposit_mining.rs b/programs/rewards/tests/rewards/deposit_mining.rs index eb08a861..0d627c6a 100644 --- a/programs/rewards/tests/rewards/deposit_mining.rs +++ b/programs/rewards/tests/rewards/deposit_mining.rs @@ -42,7 +42,14 @@ async fn success() { let (mut context, test_rewards, user, mining) = setup().await; test_rewards - .deposit_mining(&mut context, &mining, 100, LockupPeriod::ThreeMonths, &user) + .deposit_mining( + &mut context, + &mining, + 100, + LockupPeriod::ThreeMonths, + &user, + &mining, + ) .await .unwrap(); @@ -61,7 +68,14 @@ async fn success_with_flex() { let (mut context, test_rewards, user, mining) = setup().await; test_rewards - .deposit_mining(&mut context, &mining, 100, LockupPeriod::Flex, &user) + .deposit_mining( + &mut context, + &mining, + 100, + LockupPeriod::Flex, + &user, + &mining, + ) .await .unwrap(); diff --git a/programs/rewards/tests/rewards/distribute_rewards.rs b/programs/rewards/tests/rewards/distribute_rewards.rs index ddb15b29..6d960527 100644 --- a/programs/rewards/tests/rewards/distribute_rewards.rs +++ b/programs/rewards/tests/rewards/distribute_rewards.rs @@ -55,6 +55,7 @@ async fn happy_path() { 100, LockupPeriod::ThreeMonths, &user.pubkey(), + &user_mining_addr, ) .await .unwrap(); @@ -101,6 +102,7 @@ async fn happy_path_with_flex() { 100, LockupPeriod::Flex, &user.pubkey(), + &user_mining_addr, ) .await .unwrap(); diff --git a/programs/rewards/tests/rewards/extend_stake.rs b/programs/rewards/tests/rewards/extend_stake.rs index 3a5f0b94..2784af79 100644 --- a/programs/rewards/tests/rewards/extend_stake.rs +++ b/programs/rewards/tests/rewards/extend_stake.rs @@ -50,7 +50,14 @@ async fn restake_before_its_expired() { let new_lockup_period = LockupPeriod::ThreeMonths; test_rewards - .deposit_mining(&mut context, &mining, 100, old_lockup_period, &mining_owner) + .deposit_mining( + &mut context, + &mining, + 100, + old_lockup_period, + &mining_owner, + &mining, + ) .await .unwrap(); @@ -62,6 +69,7 @@ async fn restake_before_its_expired() { .extend_stake( &mut context, &mining, + &mining, old_lockup_period, new_lockup_period, deposit_start_ts, @@ -108,6 +116,7 @@ async fn restake_for_another_period_after_old_is_expired() { 100, LockupPeriod::ThreeMonths, &mining_owner, + &mining, ) .await .unwrap(); @@ -123,6 +132,7 @@ async fn restake_for_another_period_after_old_is_expired() { .extend_stake( &mut context, &mining, + &mining, old_lockup_period, new_lockup_period, deposit_start_ts, @@ -158,7 +168,14 @@ async fn just_prolong_without_adding_tokes() { let new_lockup_period = LockupPeriod::ThreeMonths; test_rewards - .deposit_mining(&mut context, &mining, 100, old_lockup_period, &mining_owner) + .deposit_mining( + &mut context, + &mining, + 100, + old_lockup_period, + &mining_owner, + &mining, + ) .await .unwrap(); @@ -170,6 +187,7 @@ async fn just_prolong_without_adding_tokes() { .extend_stake( &mut context, &mining, + &mining, old_lockup_period, new_lockup_period, deposit_start_ts, @@ -216,6 +234,7 @@ async fn restake_after_its_expired_with_no_additional_tokens() { 100, LockupPeriod::ThreeMonths, &mining_owner, + &mining, ) .await .unwrap(); @@ -231,6 +250,7 @@ async fn restake_after_its_expired_with_no_additional_tokens() { .extend_stake( &mut context, &mining, + &mining, old_lockup_period, new_lockup_period, deposit_start_ts, @@ -271,6 +291,7 @@ async fn restake_in_expiration_day() { 100, LockupPeriod::ThreeMonths, &mining_owner, + &mining, ) .await .unwrap(); @@ -286,6 +307,7 @@ async fn restake_in_expiration_day() { .extend_stake( &mut context, &mining, + &mining, old_lockup_period, new_lockup_period, deposit_start_ts, diff --git a/programs/rewards/tests/rewards/fill_vault.rs b/programs/rewards/tests/rewards/fill_vault.rs index b110a17b..0b4733e1 100644 --- a/programs/rewards/tests/rewards/fill_vault.rs +++ b/programs/rewards/tests/rewards/fill_vault.rs @@ -36,6 +36,7 @@ async fn setup() -> (ProgramTestContext, TestRewards) { 100, LockupPeriod::ThreeMonths, &user.pubkey(), + &user_mining, ) .await .unwrap(); diff --git a/programs/rewards/tests/rewards/utils.rs b/programs/rewards/tests/rewards/utils.rs index fb0f4475..bb877f65 100644 --- a/programs/rewards/tests/rewards/utils.rs +++ b/programs/rewards/tests/rewards/utils.rs @@ -117,6 +117,7 @@ impl TestRewards { amount: u64, lockup_period: LockupPeriod, owner: &Pubkey, + delegate: &Pubkey, ) -> BanksClientResult<()> { let tx = Transaction::new_signed_with_payer( &[mplx_rewards::instruction::deposit_mining( @@ -124,6 +125,7 @@ impl TestRewards { &self.reward_pool, mining_account, &self.deposit_authority.pubkey(), + delegate, amount, lockup_period, owner, @@ -235,6 +237,7 @@ impl TestRewards { &self, context: &mut ProgramTestContext, mining_account: &Pubkey, + delegate: &Pubkey, old_lockup_period: LockupPeriod, new_lockup_period: LockupPeriod, deposit_start_ts: u64, @@ -248,6 +251,7 @@ impl TestRewards { &self.reward_pool, mining_account, &self.deposit_authority.pubkey(), + delegate, old_lockup_period, new_lockup_period, deposit_start_ts, diff --git a/programs/rewards/tests/rewards/withdraw_mining.rs b/programs/rewards/tests/rewards/withdraw_mining.rs index 53cca26a..2a6e08f1 100644 --- a/programs/rewards/tests/rewards/withdraw_mining.rs +++ b/programs/rewards/tests/rewards/withdraw_mining.rs @@ -41,7 +41,7 @@ async fn success() { let lockup_period = LockupPeriod::ThreeMonths; test_rewards - .deposit_mining(&mut context, &mining, 100, lockup_period, &user) + .deposit_mining(&mut context, &mining, 100, lockup_period, &user, &mining) .await .unwrap(); @@ -66,7 +66,14 @@ async fn success_with_5kkk_after_expiring() { let lockup_period = LockupPeriod::ThreeMonths; test_rewards - .deposit_mining(&mut context, &mining, 5000000000, lockup_period, &user) + .deposit_mining( + &mut context, + &mining, + 5000000000, + lockup_period, + &user, + &mining, + ) .await .unwrap(); From ddd256fef6147303d013c2b92e1e03f6d445515b Mon Sep 17 00:00:00 2001 From: Kyrylo Stepanov Date: Thu, 11 Jul 2024 18:33:08 +0300 Subject: [PATCH 03/14] Extend extend_stake instruction with delegate parameter Extend deposit_mining instruction with delegate parameter --- programs/rewards/src/instruction.rs | 12 ++--- .../rewards/src/instructions/close_mining.rs | 8 ++- .../src/instructions/deposit_mining.rs | 27 +++++----- .../rewards/src/instructions/extend_stake.rs | 27 +++++----- programs/rewards/src/utils.rs | 54 +++++++++++++++++++ 5 files changed, 87 insertions(+), 41 deletions(-) diff --git a/programs/rewards/src/instruction.rs b/programs/rewards/src/instruction.rs index 73ebbda8..be28b30a 100644 --- a/programs/rewards/src/instruction.rs +++ b/programs/rewards/src/instruction.rs @@ -58,7 +58,7 @@ pub enum RewardsInstruction { #[account(1, writable, name = "mining", desc = "The address of the mining account which belongs to the user and stores info about user's rewards")] #[account(2, name = "reward_mint", desc = "The address of the reward mint")] #[account(3, signer, name = "deposit_authority", desc = "The address of the Staking program's Registrar, which is PDA and is responsible for signing CPIs")] - #[account(4, signer, name = "delegate", desc = "The address of Mining Account that might be used as a delegate in delegated staking model")] + #[account(4, name = "delegate_mining", desc = "The address of Mining Account that might be used as a delegate in delegated staking model")] DepositMining { /// Amount to deposit amount: u64, @@ -95,7 +95,7 @@ pub enum RewardsInstruction { #[account(1, writable, name = "mining", desc = "The address of the mining account which belongs to the user and stores info about user's rewards")] #[account(2, name = "reward_mint", desc = "The address of the reward mint")] #[account(3, signer, name = "deposit_authority", desc = "The address of the Staking program's Registrar, which is PDA and is responsible for signing CPIs")] - #[account(4, signer, name = "delegate", desc = "The address of Mining Account that might be used as a delegate in delegated staking model")] + #[account(4, name = "delegate_mining", desc = "The address of Mining Account that might be used as a delegate in delegated staking model")] ExtendStake { /// Lockup period before restaking. Actually it's only needed /// for Flex to AnyPeriod edge case @@ -224,7 +224,7 @@ pub fn deposit_mining( reward_pool: &Pubkey, mining: &Pubkey, deposit_authority: &Pubkey, - delegate: &Pubkey, + delegate_mining: &Pubkey, amount: u64, lockup_period: LockupPeriod, owner: &Pubkey, @@ -233,7 +233,7 @@ pub fn deposit_mining( AccountMeta::new(*reward_pool, false), AccountMeta::new(*mining, false), AccountMeta::new_readonly(*deposit_authority, true), - AccountMeta::new(*delegate, false), + AccountMeta::new(*delegate_mining, false), ]; Instruction::new_with_borsh( @@ -305,7 +305,7 @@ pub fn extend_stake( reward_pool: &Pubkey, mining: &Pubkey, deposit_authority: &Pubkey, - delegate: &Pubkey, + delegate_mining: &Pubkey, old_lockup_period: LockupPeriod, new_lockup_period: LockupPeriod, deposit_start_ts: u64, @@ -317,7 +317,7 @@ pub fn extend_stake( AccountMeta::new(*reward_pool, false), AccountMeta::new(*mining, false), AccountMeta::new_readonly(*deposit_authority, true), - AccountMeta::new(*delegate, false), + AccountMeta::new(*delegate_mining, false), ]; Instruction::new_with_borsh( diff --git a/programs/rewards/src/instructions/close_mining.rs b/programs/rewards/src/instructions/close_mining.rs index 76cfeefe..99c29cbd 100644 --- a/programs/rewards/src/instructions/close_mining.rs +++ b/programs/rewards/src/instructions/close_mining.rs @@ -2,7 +2,7 @@ use crate::{ asserts::assert_account_key, error::MplxRewardsError, state::{Mining, RewardPool}, - utils::AccountLoader, + utils::{AccountLoader, SafeArithmeticOperations}, }; use solana_program::{ account_info::AccountInfo, entrypoint::ProgramResult, program_error::ProgramError, @@ -56,11 +56,9 @@ impl<'a, 'b> CloseMiningContext<'a, 'b> { let dest_starting_lamports = self.target_account.lamports(); - **self.target_account.lamports.borrow_mut() = dest_starting_lamports - .checked_add(self.mining.lamports()) - .ok_or(MplxRewardsError::MathOverflow)?; + **self.target_account.lamports.borrow_mut() = + dest_starting_lamports.safe_add(self.mining.lamports())?; **self.mining.lamports.borrow_mut() = 0; - let mut source_data = self.mining.data.borrow_mut(); source_data.fill(0); diff --git a/programs/rewards/src/instructions/deposit_mining.rs b/programs/rewards/src/instructions/deposit_mining.rs index 5bcb4039..89d3c20b 100644 --- a/programs/rewards/src/instructions/deposit_mining.rs +++ b/programs/rewards/src/instructions/deposit_mining.rs @@ -2,7 +2,7 @@ use crate::{ asserts::assert_account_key, error::MplxRewardsError, state::{Mining, RewardPool, DELEGATE_MINIMAL_OWNED_WEIGHTED_STAKE}, - utils::{AccountLoader, LockupPeriod}, + utils::{find_mining_program_address, AccountLoader, LockupPeriod}, }; use solana_program::{ account_info::AccountInfo, entrypoint::ProgramResult, msg, program_error::ProgramError, @@ -14,7 +14,7 @@ pub struct DepositMiningContext<'a, 'b> { reward_pool: &'a AccountInfo<'b>, mining: &'a AccountInfo<'b>, deposit_authority: &'a AccountInfo<'b>, - delegate: &'a AccountInfo<'b>, + delegate_mining: &'a AccountInfo<'b>, } impl<'a, 'b> DepositMiningContext<'a, 'b> { @@ -28,13 +28,13 @@ impl<'a, 'b> DepositMiningContext<'a, 'b> { let reward_pool = AccountLoader::next_with_owner(account_info_iter, program_id)?; let mining = AccountLoader::next_with_owner(account_info_iter, program_id)?; let deposit_authority = AccountLoader::next_signer(account_info_iter)?; - let delegate = AccountLoader::next_with_owner(account_info_iter, program_id)?; + let delegate_mining = AccountLoader::next_with_owner(account_info_iter, program_id)?; Ok(DepositMiningContext { reward_pool, mining, deposit_authority, - delegate, + delegate_mining, }) } @@ -50,15 +50,8 @@ impl<'a, 'b> DepositMiningContext<'a, 'b> { let mut mining = Mining::unpack(&self.mining.data.borrow())?; { - let mining_pubkey = Pubkey::create_program_address( - &[ - b"mining".as_ref(), - mining_owner.as_ref(), - self.reward_pool.key.as_ref(), - &[mining.bump], - ], - program_id, - )?; + let (mining_pubkey, _) = + find_mining_program_address(program_id, mining_owner, self.reward_pool.key); assert_account_key(self.mining, &mining_pubkey)?; assert_account_key(self.deposit_authority, &reward_pool.deposit_authority)?; assert_account_key(self.reward_pool, &mining.reward_pool)?; @@ -72,8 +65,8 @@ impl<'a, 'b> DepositMiningContext<'a, 'b> { } } - let mut delegate_mining = if self.mining.key != self.delegate.key { - let delegate_mining = Mining::unpack(&self.delegate.data.borrow())?; + let mut delegate_mining = if self.mining.key != self.delegate_mining.key { + let delegate_mining = Mining::unpack(&self.delegate_mining.data.borrow())?; if delegate_mining .share .saturating_sub(delegate_mining.stake_from_others) @@ -92,6 +85,10 @@ impl<'a, 'b> DepositMiningContext<'a, 'b> { RewardPool::pack(reward_pool, *self.reward_pool.data.borrow_mut())?; Mining::pack(mining, *self.mining.data.borrow_mut())?; + if let Some(delegate_mining) = delegate_mining { + Mining::pack(delegate_mining, *self.delegate_mining.data.borrow_mut())?; + } + Ok(()) } } diff --git a/programs/rewards/src/instructions/extend_stake.rs b/programs/rewards/src/instructions/extend_stake.rs index f1481580..00927db8 100644 --- a/programs/rewards/src/instructions/extend_stake.rs +++ b/programs/rewards/src/instructions/extend_stake.rs @@ -2,7 +2,7 @@ use crate::{ asserts::assert_account_key, error::MplxRewardsError, state::{Mining, RewardPool, DELEGATE_MINIMAL_OWNED_WEIGHTED_STAKE}, - utils::{AccountLoader, LockupPeriod}, + utils::{find_mining_program_address, AccountLoader, LockupPeriod}, }; use solana_program::{ account_info::AccountInfo, clock::SECONDS_PER_DAY, entrypoint::ProgramResult, msg, @@ -14,7 +14,7 @@ pub struct ExtendStakeContext<'a, 'b> { reward_pool: &'a AccountInfo<'b>, mining: &'a AccountInfo<'b>, deposit_authority: &'a AccountInfo<'b>, - delegate: &'a AccountInfo<'b>, + delegate_mining: &'a AccountInfo<'b>, } impl<'a, 'b> ExtendStakeContext<'a, 'b> { @@ -28,13 +28,13 @@ impl<'a, 'b> ExtendStakeContext<'a, 'b> { let reward_pool = AccountLoader::next_with_owner(account_info_iter, program_id)?; let mining = AccountLoader::next_with_owner(account_info_iter, program_id)?; let deposit_authority = AccountLoader::next_signer(account_info_iter)?; - let delegate = AccountLoader::next_with_owner(account_info_iter, program_id)?; + let delegate_mining = AccountLoader::next_with_owner(account_info_iter, program_id)?; Ok(ExtendStakeContext { reward_pool, mining, deposit_authority, - delegate, + delegate_mining, }) } @@ -55,15 +55,8 @@ impl<'a, 'b> ExtendStakeContext<'a, 'b> { let deposit_start_ts = deposit_start_ts - (deposit_start_ts % SECONDS_PER_DAY); { - let mining_pubkey = Pubkey::create_program_address( - &[ - b"mining".as_ref(), - mining_owner.as_ref(), - self.reward_pool.key.as_ref(), - &[mining.bump], - ], - program_id, - )?; + let (mining_pubkey, _) = + find_mining_program_address(program_id, mining_owner, self.reward_pool.key); assert_account_key(self.mining, &mining_pubkey)?; assert_account_key(self.deposit_authority, &reward_pool.deposit_authority)?; assert_account_key(self.reward_pool, &mining.reward_pool)?; @@ -77,8 +70,8 @@ impl<'a, 'b> ExtendStakeContext<'a, 'b> { } } - let mut delegate_mining = if self.mining.key != self.delegate.key { - let delegate_mining = Mining::unpack(&self.delegate.data.borrow())?; + let mut delegate_mining = if self.mining.key != self.delegate_mining.key { + let delegate_mining = Mining::unpack(&self.delegate_mining.data.borrow())?; if delegate_mining .share .saturating_sub(delegate_mining.stake_from_others) @@ -105,6 +98,10 @@ impl<'a, 'b> ExtendStakeContext<'a, 'b> { RewardPool::pack(reward_pool, *self.reward_pool.data.borrow_mut())?; Mining::pack(mining, *self.mining.data.borrow_mut())?; + if let Some(delegate_mining) = delegate_mining { + Mining::pack(delegate_mining, *self.delegate_mining.data.borrow_mut())?; + } + Ok(()) } } diff --git a/programs/rewards/src/utils.rs b/programs/rewards/src/utils.rs index bff6523b..19df56ca 100644 --- a/programs/rewards/src/utils.rs +++ b/programs/rewards/src/utils.rs @@ -297,3 +297,57 @@ pub fn get_curr_unix_ts() -> u64 { // in unix means the date is earlier than 1970y Clock::get().unwrap().unix_timestamp as u64 } + +pub(crate) trait SafeArithmeticOperations +where + Self: std::marker::Sized, +{ + fn safe_sub(&self, amount: Self) -> Result; + fn safe_add(&self, amount: Self) -> Result; + fn safe_mul(&self, amount: Self) -> Result; + fn safe_div(&self, amount: Self) -> Result; +} + +impl SafeArithmeticOperations for u64 { + fn safe_sub(&self, amount: u64) -> Result { + self.checked_sub(amount) + .ok_or(MplxRewardsError::MathOverflow) + } + + fn safe_add(&self, amount: u64) -> Result { + self.checked_add(amount) + .ok_or(MplxRewardsError::MathOverflow) + } + + fn safe_mul(&self, amount: u64) -> Result { + self.checked_mul(amount) + .ok_or(MplxRewardsError::MathOverflow) + } + + fn safe_div(&self, amount: u64) -> Result { + self.checked_div(amount) + .ok_or(MplxRewardsError::MathOverflow) + } +} + +impl SafeArithmeticOperations for u128 { + fn safe_sub(&self, amount: u128) -> Result { + self.checked_sub(amount) + .ok_or(MplxRewardsError::MathOverflow) + } + + fn safe_add(&self, amount: u128) -> Result { + self.checked_add(amount) + .ok_or(MplxRewardsError::MathOverflow) + } + + fn safe_mul(&self, amount: u128) -> Result { + self.checked_mul(amount) + .ok_or(MplxRewardsError::MathOverflow) + } + + fn safe_div(&self, amount: u128) -> Result { + self.checked_div(amount) + .ok_or(MplxRewardsError::MathOverflow) + } +} From f5f69bc13b4a6f6fa13be84618ae23a54b40dedc Mon Sep 17 00:00:00 2001 From: Kyrylo Stepanov Date: Thu, 11 Jul 2024 18:34:31 +0300 Subject: [PATCH 04/14] Refactor and make arithmetic operations more readable --- .../rewards/src/instructions/fill_vault.rs | 11 +- .../src/instructions/withdraw_mining.rs | 13 +- programs/rewards/src/state/mining.rs | 18 +-- programs/rewards/src/state/reward_pool.rs | 135 +++++++----------- 4 files changed, 62 insertions(+), 115 deletions(-) diff --git a/programs/rewards/src/instructions/fill_vault.rs b/programs/rewards/src/instructions/fill_vault.rs index 49e81148..86a945de 100644 --- a/programs/rewards/src/instructions/fill_vault.rs +++ b/programs/rewards/src/instructions/fill_vault.rs @@ -2,7 +2,7 @@ use crate::{ asserts::assert_account_key, error::MplxRewardsError, state::RewardPool, - utils::{get_curr_unix_ts, spl_transfer, AccountLoader}, + utils::{get_curr_unix_ts, spl_transfer, AccountLoader, SafeArithmeticOperations}, }; use solana_program::{ account_info::AccountInfo, clock::SECONDS_PER_DAY, entrypoint::ProgramResult, @@ -81,20 +81,17 @@ impl<'a, 'b> FillVaultContext<'a, 'b> { } let days_diff = distribution_ends_at_day_start - .checked_sub(reward_pool.calculator.distribution_ends_at) - .ok_or(MplxRewardsError::MathOverflow)?; + .safe_sub(reward_pool.calculator.distribution_ends_at)?; reward_pool.calculator.distribution_ends_at = reward_pool .calculator .distribution_ends_at - .checked_add(days_diff) - .ok_or(MplxRewardsError::MathOverflow)?; + .safe_add(days_diff)?; reward_pool.calculator.tokens_available_for_distribution = reward_pool .calculator .tokens_available_for_distribution - .checked_add(rewards) - .ok_or(MplxRewardsError::MathOverflow)?; + .safe_add(rewards)?; } spl_transfer( diff --git a/programs/rewards/src/instructions/withdraw_mining.rs b/programs/rewards/src/instructions/withdraw_mining.rs index 16246e1c..603ce3c6 100644 --- a/programs/rewards/src/instructions/withdraw_mining.rs +++ b/programs/rewards/src/instructions/withdraw_mining.rs @@ -1,7 +1,7 @@ use crate::{ asserts::assert_account_key, state::{Mining, RewardPool}, - utils::AccountLoader, + utils::{find_mining_program_address, AccountLoader}, }; use solana_program::{ @@ -45,15 +45,8 @@ impl<'a, 'b> WithdrawMiningContext<'a, 'b> { let mut reward_pool = RewardPool::unpack(&self.reward_pool.data.borrow())?; let mut mining = Mining::unpack(&self.mining.data.borrow())?; - let mining_pubkey = Pubkey::create_program_address( - &[ - b"mining".as_ref(), - mining_owner.as_ref(), - self.reward_pool.key.as_ref(), - &[mining.bump], - ], - program_id, - )?; + let (mining_pubkey, _) = + find_mining_program_address(program_id, mining_owner, self.reward_pool.key); assert_account_key(self.mining, &mining_pubkey)?; assert_account_key(self.deposit_authority, &reward_pool.deposit_authority)?; assert_account_key(self.reward_pool, &mining.reward_pool)?; diff --git a/programs/rewards/src/state/mining.rs b/programs/rewards/src/state/mining.rs index 448f17c1..a6fa42a9 100644 --- a/programs/rewards/src/state/mining.rs +++ b/programs/rewards/src/state/mining.rs @@ -142,9 +142,7 @@ impl RewardIndex { &mut self.index_with_precision, )?; - total_share = total_share - .checked_sub(*modifier_diff) - .ok_or(MplxRewardsError::MathOverflow)?; + total_share = total_share.safe_sub(*modifier_diff)?; } // +1 because we don't need beginning_of_the_day self.weighted_stake_diffs = self @@ -171,19 +169,14 @@ impl RewardIndex { let rewards = u64::try_from( vault_index_for_date - .checked_sub(*index_with_precision) - .ok_or(MplxRewardsError::MathOverflow)? - .checked_mul(u128::from(total_share)) - .ok_or(MplxRewardsError::MathOverflow)? - .checked_div(PRECISION) - .ok_or(MplxRewardsError::MathOverflow)?, + .safe_sub(*index_with_precision)? + .safe_mul(u128::from(total_share))? + .safe_div(PRECISION)?, ) .map_err(|_| MplxRewardsError::InvalidPrimitiveTypesConversion)?; if rewards > 0 { - *unclaimed_rewards = unclaimed_rewards - .checked_add(rewards) - .ok_or(MplxRewardsError::MathOverflow)?; + *unclaimed_rewards = (*unclaimed_rewards).safe_add(rewards)?; } *index_with_precision = *vault_index_for_date; @@ -197,3 +190,4 @@ impl IsInitialized for Mining { self.account_type == AccountType::Mining } } +use crate::utils::SafeArithmeticOperations; diff --git a/programs/rewards/src/state/reward_pool.rs b/programs/rewards/src/state/reward_pool.rs index d91dd6bd..8522291f 100644 --- a/programs/rewards/src/state/reward_pool.rs +++ b/programs/rewards/src/state/reward_pool.rs @@ -3,7 +3,7 @@ use std::collections::BTreeMap; use crate::{ error::MplxRewardsError, state::{AccountType, Mining}, - utils::{get_curr_unix_ts, LockupPeriod}, + utils::{get_curr_unix_ts, LockupPeriod, SafeArithmeticOperations}, }; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; use solana_program::{ @@ -95,8 +95,7 @@ impl RewardPool { self.calculator.tokens_available_for_distribution = self .calculator .tokens_available_for_distribution - .checked_sub(rewards) - .ok_or(MplxRewardsError::MathOverflow)?; + .safe_sub(rewards)?; Ok(()) } @@ -112,58 +111,36 @@ impl RewardPool { mining.refresh_rewards(&self.calculator)?; // regular weighted stake which will be used in rewards distribution - let weighted_stake = amount - .checked_mul(lockup_period.multiplier()) - .ok_or(MplxRewardsError::MathOverflow)?; + let weighted_stake = amount.safe_mul(lockup_period.multiplier())?; // shows how weighted stake will change at the end of the staking period // weighted_stake_diff = weighted_stake - (amount * flex_multiplier) - let weighted_stake_diff = weighted_stake - .checked_sub( - amount - .checked_mul(LockupPeriod::Flex.multiplier()) - .ok_or(MplxRewardsError::MathOverflow)?, - ) - .ok_or(MplxRewardsError::MathOverflow)?; + let weighted_stake_diff = + weighted_stake.safe_sub(amount.safe_mul(LockupPeriod::Flex.multiplier())?)?; - self.total_share = self - .total_share - .checked_add(weighted_stake) - .ok_or(MplxRewardsError::MathOverflow)?; - - mining.share = mining - .share - .checked_add(weighted_stake) - .ok_or(MplxRewardsError::MathOverflow)?; + self.total_share = self.total_share.safe_add(weighted_stake)?; + mining.share = mining.share.safe_add(weighted_stake)?; let modifier = self .calculator .weighted_stake_diffs .entry(lockup_period.end_timestamp(get_curr_unix_ts())?) .or_default(); - *modifier = modifier - .checked_add(weighted_stake_diff) - .ok_or(MplxRewardsError::MathOverflow)?; + *modifier = modifier.safe_add(weighted_stake_diff)?; let modifier = mining .index .weighted_stake_diffs .entry(lockup_period.end_timestamp(get_curr_unix_ts())?) .or_default(); - *modifier = modifier - .checked_add(weighted_stake_diff) - .ok_or(MplxRewardsError::MathOverflow)?; + + *modifier = (*modifier).safe_add(weighted_stake_diff)?; if let Some(delegate_mining) = delegate_mining { - delegate_mining - .stake_from_others - .checked_add(amount) - .ok_or(MplxRewardsError::MathOverflow)?; // delegate_mining.stake_from_others += amount; + delegate_mining.stake_from_others = + delegate_mining.stake_from_others.safe_add(amount)?; - self.total_share = self - .total_share - .checked_add(amount) - .ok_or(MplxRewardsError::MathOverflow)?; + self.total_share = self.total_share.safe_add(amount)?; } Ok(()) @@ -173,14 +150,8 @@ impl RewardPool { pub fn withdraw(&mut self, mining: &mut Mining, amount: u64) -> ProgramResult { mining.refresh_rewards(&self.calculator)?; - self.total_share = self - .total_share - .checked_sub(amount) - .ok_or(MplxRewardsError::MathOverflow)?; - mining.share = mining - .share - .checked_sub(amount) - .ok_or(MplxRewardsError::MathOverflow)?; + self.total_share = self.total_share.safe_sub(amount)?; + mining.share = mining.share.safe_sub(amount)?; let curr_ts = Clock::get().unwrap().unix_timestamp as u64; let beginning_of_the_day = curr_ts - (curr_ts % SECONDS_PER_DAY); @@ -193,6 +164,7 @@ impl RewardPool { } /// Process extend stake + #[allow(clippy::too_many_arguments)] pub fn extend( &mut self, mining: &mut Mining, @@ -214,22 +186,19 @@ impl RewardPool { }; // curr_part_of_weighted_stake_for_flex = old_base_amount * flex_multipler - let curr_part_of_weighted_stake_for_flex = base_amount - .checked_mul(LockupPeriod::Flex.multiplier()) - .ok_or(MplxRewardsError::MathOverflow)?; + let curr_part_of_weighted_stake_for_flex = + base_amount.safe_mul(LockupPeriod::Flex.multiplier())?; // if current date is lower than stake expiration date, we need to // remove stake modifier from the date of expiration if curr_ts < deposit_old_expiration_ts { - // current_part_of_weighted_stake = - let curr_part_of_weighted_stake = base_amount - .checked_mul(old_lockup_period.multiplier()) - .ok_or(MplxRewardsError::MathOverflow)?; + // current_part_of_weighted_stake = base_amount * lockup_period_multiplier + let curr_part_of_weighted_stake = + base_amount.safe_mul(old_lockup_period.multiplier())?; // weighted_stake_modifier_to_remove = old_base_amount * lockup_period_multiplier - amount_times_flex - let weighted_stake_diff = curr_part_of_weighted_stake - .checked_sub(curr_part_of_weighted_stake_for_flex) - .ok_or(MplxRewardsError::MathOverflow)?; + let weighted_stake_diff = + curr_part_of_weighted_stake.safe_sub(curr_part_of_weighted_stake_for_flex)?; self.calculator .weighted_stake_diffs @@ -243,32 +212,35 @@ impl RewardPool { .and_modify(|modifier| *modifier -= weighted_stake_diff); // also, we need to reduce staking power because we want to extend stake from "scratch" - mining.share = mining - .share - .checked_sub(curr_part_of_weighted_stake) - .ok_or(MplxRewardsError::MathOverflow)?; + mining.share = mining.share.safe_sub(curr_part_of_weighted_stake)?; - self.total_share = self - .total_share - .checked_sub(curr_part_of_weighted_stake) - .ok_or(MplxRewardsError::MathOverflow)?; + self.total_share = self.total_share.safe_sub(curr_part_of_weighted_stake)?; } else { // otherwise, we want to substract flex multiplier, becase deposit has expired already mining.share = mining .share - .checked_sub(curr_part_of_weighted_stake_for_flex) - .ok_or(MplxRewardsError::MathOverflow)?; + .safe_sub(curr_part_of_weighted_stake_for_flex)?; self.total_share = self .total_share - .checked_sub(curr_part_of_weighted_stake_for_flex) - .ok_or(MplxRewardsError::MathOverflow)?; + .safe_sub(curr_part_of_weighted_stake_for_flex)?; } // do actions like it's a regular deposit - let amount_to_restake = base_amount - .checked_add(additional_amount) - .ok_or(MplxRewardsError::MathOverflow)?; + let amount_to_restake = base_amount.safe_add(additional_amount)?; + + // Refactored delegate_mining update + let delegate_mining = match delegate_mining { + Some(dm) => { + if dm.stake_from_others > 0 { + dm.stake_from_others = dm.stake_from_others.saturating_sub(base_amount); + self.total_share = self.total_share.safe_sub(base_amount)?; + } + Some(dm) + } + None => None, + }; + self.deposit( mining, amount_to_restake, @@ -346,9 +318,7 @@ impl RewardCalculator { break; } - total_share = total_share - .checked_sub(*modifier) - .ok_or(MplxRewardsError::MathOverflow)?; + total_share = total_share.safe_sub(*modifier)?; } // drop keys because they have been already consumed and no longer needed // +1 because we don't need beginning_of_the_day @@ -367,14 +337,10 @@ impl RewardCalculator { date_to_process: u64, ) -> ProgramResult { let index = PRECISION - .checked_mul(u128::from(rewards)) - .ok_or(MplxRewardsError::MathOverflow)? - .checked_div(u128::from(total_share)) - .ok_or(MplxRewardsError::MathOverflow)?; + .safe_mul(u128::from(rewards))? + .safe_div(u128::from(total_share))?; - let latest_index = index_with_precision - .checked_add(index) - .ok_or(MplxRewardsError::MathOverflow)?; + let latest_index = index_with_precision.safe_add(index)?; cumulative_index.insert(date_to_process, latest_index); *index_with_precision = latest_index; @@ -394,13 +360,10 @@ impl RewardCalculator { // ((tokens_available_for_distribution * precision) / days_left) / precision Ok(u64::try_from( - ((u128::from(self.tokens_available_for_distribution)) - .checked_mul(PRECISION) - .ok_or(MplxRewardsError::MathOverflow)? - .checked_div(distribution_days_left) - .ok_or(MplxRewardsError::MathOverflow)?) - .checked_div(PRECISION) - .ok_or(MplxRewardsError::MathOverflow)?, + (u128::from(self.tokens_available_for_distribution)) + .safe_mul(PRECISION)? + .safe_div(distribution_days_left)? + .safe_div(PRECISION)?, ) .map_err(|_| MplxRewardsError::InvalidPrimitiveTypesConversion)?) } From 70a7bb8ab111ffca754d2e21168f321d99923e6e Mon Sep 17 00:00:00 2001 From: Kyrylo Stepanov Date: Thu, 11 Jul 2024 18:34:51 +0300 Subject: [PATCH 05/14] Add tests for delegated staking --- .../rewards/tests/rewards/deposit_mining.rs | 51 ++++++++++++ .../rewards/tests/rewards/extend_stake.rs | 82 +++++++++++++++++++ programs/rewards/tests/rewards/utils.rs | 8 +- 3 files changed, 137 insertions(+), 4 deletions(-) diff --git a/programs/rewards/tests/rewards/deposit_mining.rs b/programs/rewards/tests/rewards/deposit_mining.rs index 0d627c6a..dbcf7f4f 100644 --- a/programs/rewards/tests/rewards/deposit_mining.rs +++ b/programs/rewards/tests/rewards/deposit_mining.rs @@ -88,3 +88,54 @@ async fn success_with_flex() { let mining = Mining::unpack(mining_account.data.borrow()).unwrap(); assert_eq!(mining.share, 100); } + +#[tokio::test] +async fn delegating_success() { + let (mut context, test_rewards, user, mining) = setup().await; + + let delegate = Keypair::new(); + let delegate_mining = test_rewards + .initialize_mining(&mut context, &delegate.pubkey()) + .await; + test_rewards + .deposit_mining( + &mut context, + &delegate_mining, + 3_000_000, // 18_000_000 of weighted stake + LockupPeriod::OneYear, + &delegate.pubkey(), + &delegate_mining, + ) + .await + .unwrap(); + let delegate_mining_account = get_account(&mut context, &delegate_mining).await; + let d_mining = Mining::unpack(delegate_mining_account.data.borrow()).unwrap(); + assert_eq!(d_mining.share, 18_000_000); + assert_eq!(d_mining.stake_from_others, 0); + + test_rewards + .deposit_mining( + &mut context, + &mining, + 100, + LockupPeriod::Flex, + &user, + &delegate_mining, + ) + .await + .unwrap(); + + let delegate_mining_account = get_account(&mut context, &delegate_mining).await; + let d_mining = Mining::unpack(delegate_mining_account.data.borrow()).unwrap(); + assert_eq!(d_mining.share, 18_000_000); + assert_eq!(d_mining.stake_from_others, 100); + + let reward_pool_account = get_account(&mut context, &test_rewards.reward_pool).await; + let reward_pool = RewardPool::unpack(reward_pool_account.data.borrow()).unwrap(); + + assert_eq!(reward_pool.total_share, 18_000_200); + + let mining_account = get_account(&mut context, &mining).await; + let mining = Mining::unpack(mining_account.data.borrow()).unwrap(); + assert_eq!(mining.share, 100); +} diff --git a/programs/rewards/tests/rewards/extend_stake.rs b/programs/rewards/tests/rewards/extend_stake.rs index 2784af79..141d495c 100644 --- a/programs/rewards/tests/rewards/extend_stake.rs +++ b/programs/rewards/tests/rewards/extend_stake.rs @@ -328,6 +328,88 @@ async fn restake_in_expiration_day() { check_weighted_stake(&mut context, mining, 200).await; } +#[tokio::test] +async fn prolong_with_delegate() { + let (mut context, test_rewards, mining_owner, mining) = setup().await; + + let delegate = Keypair::new(); + let delegate_mining = test_rewards + .initialize_mining(&mut context, &delegate.pubkey()) + .await; + test_rewards + .deposit_mining( + &mut context, + &delegate_mining, + 3_000_000, // 18_000_000 of weighted stake + LockupPeriod::OneYear, + &delegate.pubkey(), + &delegate_mining, + ) + .await + .unwrap(); + let delegate_mining_account = get_account(&mut context, &delegate_mining).await; + let d_mining = Mining::unpack(delegate_mining_account.data.borrow()).unwrap(); + assert_eq!(d_mining.share, 18_000_000); + assert_eq!(d_mining.stake_from_others, 0); + + let deposit_start_ts = context + .banks_client + .get_sysvar::() + .await + .unwrap() + .unix_timestamp as u64; + let base_amount = 100; + let additional_amount = 0; + let old_lockup_period = LockupPeriod::ThreeMonths; + let new_lockup_period = LockupPeriod::ThreeMonths; + + test_rewards + .deposit_mining( + &mut context, + &mining, + 100, + old_lockup_period, + &mining_owner, + &mining, + ) + .await + .unwrap(); + + // advance for ten days + let curr_ts = + advance_clock_by_ts(&mut context, (10 * SECONDS_PER_DAY).try_into().unwrap()).await; + + test_rewards + .extend_stake( + &mut context, + &mining, + &delegate_mining, + old_lockup_period, + new_lockup_period, + deposit_start_ts, + base_amount, + additional_amount, + &mining_owner, + ) + .await + .unwrap(); + + // new expiration date modifier added + let beginning_of_the_old_expiration_day = LockupPeriod::ThreeMonths + .end_timestamp(deposit_start_ts - (deposit_start_ts % SECONDS_PER_DAY)) + .unwrap(); + check_modifier_at_a_day(&mut context, mining, 0, beginning_of_the_old_expiration_day).await; + + // new expiration date modifier added + let beginning_of_the_expiration_day = LockupPeriod::ThreeMonths + .end_timestamp(curr_ts as u64) + .unwrap(); + check_modifier_at_a_day(&mut context, mining, 100, beginning_of_the_expiration_day).await; + + // and power is multiplied twice + check_weighted_stake(&mut context, mining, 200).await; +} + pub async fn check_weighted_stake( context: &mut ProgramTestContext, mining_account: Pubkey, diff --git a/programs/rewards/tests/rewards/utils.rs b/programs/rewards/tests/rewards/utils.rs index bb877f65..125c7294 100644 --- a/programs/rewards/tests/rewards/utils.rs +++ b/programs/rewards/tests/rewards/utils.rs @@ -117,7 +117,7 @@ impl TestRewards { amount: u64, lockup_period: LockupPeriod, owner: &Pubkey, - delegate: &Pubkey, + delegate_mining: &Pubkey, ) -> BanksClientResult<()> { let tx = Transaction::new_signed_with_payer( &[mplx_rewards::instruction::deposit_mining( @@ -125,7 +125,7 @@ impl TestRewards { &self.reward_pool, mining_account, &self.deposit_authority.pubkey(), - delegate, + delegate_mining, amount, lockup_period, owner, @@ -237,7 +237,7 @@ impl TestRewards { &self, context: &mut ProgramTestContext, mining_account: &Pubkey, - delegate: &Pubkey, + delegate_mining: &Pubkey, old_lockup_period: LockupPeriod, new_lockup_period: LockupPeriod, deposit_start_ts: u64, @@ -251,7 +251,7 @@ impl TestRewards { &self.reward_pool, mining_account, &self.deposit_authority.pubkey(), - delegate, + delegate_mining, old_lockup_period, new_lockup_period, deposit_start_ts, From 12def29c921eafb56090e80d703b9bc3f5b2ace4 Mon Sep 17 00:00:00 2001 From: Kyrylo Stepanov Date: Fri, 12 Jul 2024 12:08:59 +0300 Subject: [PATCH 06/14] minor fix --- programs/rewards/src/instructions/initialize_pool.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/programs/rewards/src/instructions/initialize_pool.rs b/programs/rewards/src/instructions/initialize_pool.rs index 24358023..4a6b97ed 100644 --- a/programs/rewards/src/instructions/initialize_pool.rs +++ b/programs/rewards/src/instructions/initialize_pool.rs @@ -61,7 +61,7 @@ impl<'a, 'b> InitializePoolContext<'a, 'b> { assert_uninitialized(self.reward_vault)?; let (reward_pool_pubkey, pool_bump) = - find_reward_pool_program_address(program_id, &self.deposit_authority.key); + find_reward_pool_program_address(program_id, self.deposit_authority.key); assert_account_key(self.reward_pool, &reward_pool_pubkey)?; let reward_pool_seeds = &[ From 4fef4983fe43cda9179d5a3d771b25898171d0f7 Mon Sep 17 00:00:00 2001 From: Matiukhin Vlad <87382371+rwwwx@users.noreply.github.com> Date: Wed, 10 Jul 2024 15:56:28 +0300 Subject: [PATCH 07/14] [MTG-237] ci cd (#17) fixed CI/CD, added README.md and cargo audit --- .cargo/audit.toml | 10 ++++ .github/workflows/ci-cargo-audit.yml | 40 +++++++++++++ .github/workflows/ci-lint-test.yml | 76 +++++++++++++++++++++++++ README.md | 18 ++++++ clients/rust/tests/create.rs | 9 +-- programs/rewards/tests/rewards/utils.rs | 2 +- 6 files changed, 147 insertions(+), 8 deletions(-) create mode 100644 .cargo/audit.toml create mode 100644 .github/workflows/ci-cargo-audit.yml create mode 100644 .github/workflows/ci-lint-test.yml create mode 100644 README.md diff --git a/.cargo/audit.toml b/.cargo/audit.toml new file mode 100644 index 00000000..3725528f --- /dev/null +++ b/.cargo/audit.toml @@ -0,0 +1,10 @@ +[advisories] +# Ignore unfixable vulnerabilities for now. In new solana version +# it will be fixed. +ignore = ["RUSTSEC-2024-0344", "RUSTSEC-2022-0093"] + +[output] +deny = [] # exit on error if unmaintained dependencies are found +format = "terminal" # "terminal" (human-readable report) or "json" +quiet = true # Only print information on error +show_tree = true # Show inverse dependency trees along with advisories (default: true) diff --git a/.github/workflows/ci-cargo-audit.yml b/.github/workflows/ci-cargo-audit.yml new file mode 100644 index 00000000..1b115ff3 --- /dev/null +++ b/.github/workflows/ci-cargo-audit.yml @@ -0,0 +1,40 @@ +name: Cargo Audit + +on: + push: + branches: + - main + pull_request: + + # Allowing manual runs with ability to choose branch + workflow_dispatch: + + # Optimisation option by targeting direct paths to only scan when there are changes to dependencies in the push/PR + # push: + # paths: + # - 'Cargo.toml' + # - 'Cargo.lock' + # pull_request: + # paths: + # - 'Cargo.toml' + # - 'Cargo.lock' + + # Example of running scheduled scans at 6AM UTC every Monday to regularly check for vulnerable dependencies + # schedule: + # - cron: '0 6 * * 1' + +jobs: + Cargo-audit: + name: Cargo Vulnerability Scanner + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Install Cargo Audit + uses: actions-rs/install@v0.1 + with: + crate: cargo-audit + version: latest + + - name: Run Cargo Audit + run: cargo audit -c always diff --git a/.github/workflows/ci-lint-test.yml b/.github/workflows/ci-lint-test.yml new file mode 100644 index 00000000..415521bc --- /dev/null +++ b/.github/workflows/ci-lint-test.yml @@ -0,0 +1,76 @@ +name: Lint and Test + +on: + push: + branches: + - main + pull_request: + +env: + CARGO_TERM_COLOR: always + SOLANA_VERSION: "1.18.9" + RUST_TOOLCHAIN: "1.78.0" + +defaults: + run: + working-directory: ./ + +jobs: + lint: + name: Linter + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Install Rust nightly + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: ${{ env.RUST_TOOLCHAIN }} + components: rustfmt, clippy + cache: true + + - name: Run fmt + run: cargo fmt -- --check + + - name: Run clippy + run: cargo clippy --all-targets --all-features --workspace --exclude rewards -- -D warnings + + tests: + name: Tests + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Install Linux dependencies + run: sudo apt-get update && sudo apt-get install -y pkg-config build-essential libudev-dev + + - name: Install Rust nightly + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: ${{ env.RUST_TOOLCHAIN }} + cache: true + + - name: Cache Solana binaries + uses: actions/cache@v2 + with: + path: ~/.cache/solana + key: ${{ runner.os }}-${{ env.RUST_TOOLCHAIN }} + + - name: Install Solana + run: | + sh -c "$(curl -sSfL https://release.solana.com/v${{ env.SOLANA_VERSION }}/install)" + echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH + export PATH="$HOME/.local/share/solana/install/active_release/bin:$PATH" + solana --version + echo "Generating keypair..." + solana-keygen new -o "$HOME/.config/solana/id.json" --no-passphrase --silent + + - name: Switch toolchain + run: | + rustup override set ${{ env.RUST_TOOLCHAIN }} + solana-install init ${{ env.SOLANA_VERSION }} + + - name: Run tests + run: cargo test-bpf diff --git a/README.md b/README.md new file mode 100644 index 00000000..6484b11b --- /dev/null +++ b/README.md @@ -0,0 +1,18 @@ +# Development Setup + +## Rust +* Built and developed using - Rust stable(`rustc 1.78.0`) or higher. +* If you are working on both `staking` and `rewards` contracts use:
+ `rustup override set ` to get rid of manual version switching. + +## Solana +* Built and developed using - Solana version `1.18.9` or higher. +* To switch Solana version use - `solana-install init `. + +## Build and Test +* To build contract use `cargo build-bpf`. +* Run Rust based tests use - `cargo test-bpf`. + +## Formating and Linting +* Run `cargo clippy --all-targets --all-features --workspace -- -D warnings` before pushing your changes. +* Run `cargo +nightly fmt` before pushing your changes. diff --git a/clients/rust/tests/create.rs b/clients/rust/tests/create.rs index 50e0f5b5..39fe02bc 100644 --- a/clients/rust/tests/create.rs +++ b/clients/rust/tests/create.rs @@ -1,12 +1,7 @@ #![cfg(feature = "test-sbf")] -use borsh::BorshDeserialize; // use rewards::{accounts::MyAccount, instructions::CreateBuilder}; -use solana_program_test::{tokio, ProgramTest}; -use solana_sdk::{ - signature::{Keypair, Signer}, - transaction::Transaction, -}; +use solana_program_test::tokio; #[tokio::test] async fn create() { @@ -16,7 +11,7 @@ async fn create() { // Given a new keypair. - let address = Keypair::new(); + // let address = Keypair::new(); // let ix = CreateBuilder::new() // .address(address.pubkey()) diff --git a/programs/rewards/tests/rewards/utils.rs b/programs/rewards/tests/rewards/utils.rs index 125c7294..5d0b9d8c 100644 --- a/programs/rewards/tests/rewards/utils.rs +++ b/programs/rewards/tests/rewards/utils.rs @@ -284,7 +284,7 @@ impl TestRewards { &self.reward_pool, )], Some(&context.payer.pubkey()), - &[&context.payer, &self.deposit_authority, &mining_owner], + &[&context.payer, &self.deposit_authority, mining_owner], context.last_blockhash, ); From 2980d249409d4b672bfe6f2cc0400f6e955f765c Mon Sep 17 00:00:00 2001 From: Kyrylo Stepanov Date: Fri, 12 Jul 2024 14:15:04 +0300 Subject: [PATCH 08/14] Change ordering for PartialOrd and Ord for LockupPeriod --- programs/rewards/src/utils.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/programs/rewards/src/utils.rs b/programs/rewards/src/utils.rs index 19df56ca..70f2e271 100644 --- a/programs/rewards/src/utils.rs +++ b/programs/rewards/src/utils.rs @@ -236,19 +236,18 @@ impl AccountLoader { } /// LockupPeriod is used to define the time during which the lockup will recieve full reward -#[repr(u8)] -#[derive(BorshDeserialize, BorshSerialize, Debug, PartialEq, Eq, Clone, Copy)] +#[derive(BorshDeserialize, BorshSerialize, Debug, PartialEq, Eq, Clone, Copy, PartialOrd, Ord)] pub enum LockupPeriod { /// Unreachable option None, + /// Unlimited lockup period. + Flex, /// Three months ThreeMonths, /// SixMonths SixMonths, /// OneYear OneYear, - /// Unlimited lockup period. - Flex, } impl LockupPeriod { From 4451a66b9f0b6b1452ca057d5d361fe1ef5fc8ca Mon Sep 17 00:00:00 2001 From: Kyrylo Stepanov Date: Fri, 12 Jul 2024 14:48:10 +0300 Subject: [PATCH 09/14] refactor --- programs/rewards/src/asserts.rs | 27 +++++++++++++++++-- .../src/instructions/deposit_mining.rs | 22 +++------------ .../rewards/src/instructions/extend_stake.rs | 21 +++------------ 3 files changed, 33 insertions(+), 37 deletions(-) diff --git a/programs/rewards/src/asserts.rs b/programs/rewards/src/asserts.rs index 7724ddf7..47e1e797 100644 --- a/programs/rewards/src/asserts.rs +++ b/programs/rewards/src/asserts.rs @@ -1,10 +1,13 @@ //! Asserts for account verifications use solana_program::{ account_info::AccountInfo, entrypoint::ProgramResult, msg, program_error::ProgramError, - pubkey::Pubkey, rent::Rent, sysvar::Sysvar, + program_pack::Pack, pubkey::Pubkey, rent::Rent, sysvar::Sysvar, }; -use crate::error::MplxRewardsError; +use crate::{ + error::MplxRewardsError, + state::{Mining, DELEGATE_MINIMAL_OWNED_WEIGHTED_STAKE}, +}; /// Assert signer. pub fn assert_signer(account: &AccountInfo) -> ProgramResult { @@ -80,3 +83,23 @@ pub fn assert_non_zero_amount(amount: u64) -> ProgramResult { Ok(()) } + +pub fn verify_delegate_mining_requirements( + delegate_mining: &AccountInfo, + mining: &AccountInfo, +) -> Result, ProgramError> { + if mining.key != delegate_mining.key { + let delegate_mining = Mining::unpack(&delegate_mining.data.borrow())?; + if delegate_mining + .share + .saturating_sub(delegate_mining.stake_from_others) + < DELEGATE_MINIMAL_OWNED_WEIGHTED_STAKE + { + return Err(MplxRewardsError::InsufficientWeightedStake.into()); + } + + Ok(Some(delegate_mining)) + } else { + Ok(None) + } +} diff --git a/programs/rewards/src/instructions/deposit_mining.rs b/programs/rewards/src/instructions/deposit_mining.rs index 89d3c20b..1e4cc4fa 100644 --- a/programs/rewards/src/instructions/deposit_mining.rs +++ b/programs/rewards/src/instructions/deposit_mining.rs @@ -1,7 +1,6 @@ use crate::{ - asserts::assert_account_key, - error::MplxRewardsError, - state::{Mining, RewardPool, DELEGATE_MINIMAL_OWNED_WEIGHTED_STAKE}, + asserts::{assert_account_key, verify_delegate_mining_requirements}, + state::{Mining, RewardPool}, utils::{find_mining_program_address, AccountLoader, LockupPeriod}, }; use solana_program::{ @@ -65,21 +64,8 @@ impl<'a, 'b> DepositMiningContext<'a, 'b> { } } - let mut delegate_mining = if self.mining.key != self.delegate_mining.key { - let delegate_mining = Mining::unpack(&self.delegate_mining.data.borrow())?; - if delegate_mining - .share - .saturating_sub(delegate_mining.stake_from_others) - < DELEGATE_MINIMAL_OWNED_WEIGHTED_STAKE - { - return Err(MplxRewardsError::InsufficientWeightedStake.into()); - } - - Some(delegate_mining) - } else { - None - }; - + let mut delegate_mining = + verify_delegate_mining_requirements(self.delegate_mining, self.mining)?; reward_pool.deposit(&mut mining, amount, lockup_period, delegate_mining.as_mut())?; RewardPool::pack(reward_pool, *self.reward_pool.data.borrow_mut())?; diff --git a/programs/rewards/src/instructions/extend_stake.rs b/programs/rewards/src/instructions/extend_stake.rs index 00927db8..cae17bc4 100644 --- a/programs/rewards/src/instructions/extend_stake.rs +++ b/programs/rewards/src/instructions/extend_stake.rs @@ -1,7 +1,6 @@ use crate::{ - asserts::assert_account_key, - error::MplxRewardsError, - state::{Mining, RewardPool, DELEGATE_MINIMAL_OWNED_WEIGHTED_STAKE}, + asserts::{assert_account_key, verify_delegate_mining_requirements}, + state::{Mining, RewardPool}, utils::{find_mining_program_address, AccountLoader, LockupPeriod}, }; use solana_program::{ @@ -70,20 +69,8 @@ impl<'a, 'b> ExtendStakeContext<'a, 'b> { } } - let mut delegate_mining = if self.mining.key != self.delegate_mining.key { - let delegate_mining = Mining::unpack(&self.delegate_mining.data.borrow())?; - if delegate_mining - .share - .saturating_sub(delegate_mining.stake_from_others) - < DELEGATE_MINIMAL_OWNED_WEIGHTED_STAKE - { - return Err(MplxRewardsError::InsufficientWeightedStake.into()); - } - - Some(delegate_mining) - } else { - None - }; + let mut delegate_mining = + verify_delegate_mining_requirements(self.delegate_mining, self.mining)?; reward_pool.extend( &mut mining, From dd3d777cc5e5aa3b0d7a9396275b074f04a3a8c3 Mon Sep 17 00:00:00 2001 From: Kyrylo Stepanov Date: Fri, 12 Jul 2024 17:26:34 +0300 Subject: [PATCH 10/14] refactor asserts --- programs/rewards/src/asserts.rs | 13 +++++++++++++ .../rewards/src/instructions/deposit_mining.rs | 13 +++---------- programs/rewards/src/instructions/extend_stake.rs | 13 +++---------- .../rewards/src/instructions/withdraw_mining.rs | 14 ++++---------- 4 files changed, 23 insertions(+), 30 deletions(-) diff --git a/programs/rewards/src/asserts.rs b/programs/rewards/src/asserts.rs index 47e1e797..94ef4d26 100644 --- a/programs/rewards/src/asserts.rs +++ b/programs/rewards/src/asserts.rs @@ -84,6 +84,19 @@ pub fn assert_non_zero_amount(amount: u64) -> ProgramResult { Ok(()) } +pub fn assert_pubkey_eq(given: &Pubkey, expected: &Pubkey) -> ProgramResult { + if given == expected { + Ok(()) + } else { + msg!( + "Assert account error. Got {} Expected {}", + *given, + *expected + ); + Err(ProgramError::InvalidArgument) + } +} + pub fn verify_delegate_mining_requirements( delegate_mining: &AccountInfo, mining: &AccountInfo, diff --git a/programs/rewards/src/instructions/deposit_mining.rs b/programs/rewards/src/instructions/deposit_mining.rs index 1e4cc4fa..8bb7f3da 100644 --- a/programs/rewards/src/instructions/deposit_mining.rs +++ b/programs/rewards/src/instructions/deposit_mining.rs @@ -1,10 +1,10 @@ use crate::{ - asserts::{assert_account_key, verify_delegate_mining_requirements}, + asserts::{assert_account_key, assert_pubkey_eq, verify_delegate_mining_requirements}, state::{Mining, RewardPool}, utils::{find_mining_program_address, AccountLoader, LockupPeriod}, }; use solana_program::{ - account_info::AccountInfo, entrypoint::ProgramResult, msg, program_error::ProgramError, + account_info::AccountInfo, entrypoint::ProgramResult, program_error::ProgramError, program_pack::Pack, pubkey::Pubkey, }; @@ -54,14 +54,7 @@ impl<'a, 'b> DepositMiningContext<'a, 'b> { assert_account_key(self.mining, &mining_pubkey)?; assert_account_key(self.deposit_authority, &reward_pool.deposit_authority)?; assert_account_key(self.reward_pool, &mining.reward_pool)?; - if mining_owner != &mining.owner { - msg!( - "Assert account error. Got {} Expected {}", - *mining_owner, - mining.owner - ); - return Err(ProgramError::InvalidArgument); - } + assert_pubkey_eq(&mining.owner, mining_owner)?; } let mut delegate_mining = diff --git a/programs/rewards/src/instructions/extend_stake.rs b/programs/rewards/src/instructions/extend_stake.rs index cae17bc4..e754d265 100644 --- a/programs/rewards/src/instructions/extend_stake.rs +++ b/programs/rewards/src/instructions/extend_stake.rs @@ -1,10 +1,10 @@ use crate::{ - asserts::{assert_account_key, verify_delegate_mining_requirements}, + asserts::{assert_account_key, assert_pubkey_eq, verify_delegate_mining_requirements}, state::{Mining, RewardPool}, utils::{find_mining_program_address, AccountLoader, LockupPeriod}, }; use solana_program::{ - account_info::AccountInfo, clock::SECONDS_PER_DAY, entrypoint::ProgramResult, msg, + account_info::AccountInfo, clock::SECONDS_PER_DAY, entrypoint::ProgramResult, program_error::ProgramError, program_pack::Pack, pubkey::Pubkey, }; @@ -59,14 +59,7 @@ impl<'a, 'b> ExtendStakeContext<'a, 'b> { assert_account_key(self.mining, &mining_pubkey)?; assert_account_key(self.deposit_authority, &reward_pool.deposit_authority)?; assert_account_key(self.reward_pool, &mining.reward_pool)?; - if mining_owner != &mining.owner { - msg!( - "Assert account error. Got {} Expected {}", - *mining_owner, - mining.owner - ); - return Err(ProgramError::InvalidArgument); - } + assert_pubkey_eq(&mining.owner, mining_owner)?; } let mut delegate_mining = diff --git a/programs/rewards/src/instructions/withdraw_mining.rs b/programs/rewards/src/instructions/withdraw_mining.rs index 603ce3c6..99230786 100644 --- a/programs/rewards/src/instructions/withdraw_mining.rs +++ b/programs/rewards/src/instructions/withdraw_mining.rs @@ -1,11 +1,11 @@ use crate::{ - asserts::assert_account_key, + asserts::{assert_account_key, assert_pubkey_eq}, state::{Mining, RewardPool}, utils::{find_mining_program_address, AccountLoader}, }; use solana_program::{ - account_info::AccountInfo, entrypoint::ProgramResult, msg, program_error::ProgramError, + account_info::AccountInfo, entrypoint::ProgramResult, program_error::ProgramError, program_pack::Pack, pubkey::Pubkey, }; @@ -50,14 +50,8 @@ impl<'a, 'b> WithdrawMiningContext<'a, 'b> { assert_account_key(self.mining, &mining_pubkey)?; assert_account_key(self.deposit_authority, &reward_pool.deposit_authority)?; assert_account_key(self.reward_pool, &mining.reward_pool)?; - if mining_owner != &mining.owner { - msg!( - "Assert account error. Got {} Expected {}", - *mining_owner, - mining.owner - ); - return Err(ProgramError::InvalidArgument); - } + assert_pubkey_eq(&mining.owner, mining_owner)?; + reward_pool.withdraw(&mut mining, amount)?; RewardPool::pack(reward_pool, *self.reward_pool.data.borrow_mut())?; From 1486cba7d43428cc84f8b30b724aa582131dd469 Mon Sep 17 00:00:00 2001 From: Kyrylo Stepanov Date: Fri, 12 Jul 2024 22:30:18 +0300 Subject: [PATCH 11/14] remove redundant substraction from delegated stake check --- programs/rewards/src/asserts.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/programs/rewards/src/asserts.rs b/programs/rewards/src/asserts.rs index 94ef4d26..04b1149f 100644 --- a/programs/rewards/src/asserts.rs +++ b/programs/rewards/src/asserts.rs @@ -103,11 +103,7 @@ pub fn verify_delegate_mining_requirements( ) -> Result, ProgramError> { if mining.key != delegate_mining.key { let delegate_mining = Mining::unpack(&delegate_mining.data.borrow())?; - if delegate_mining - .share - .saturating_sub(delegate_mining.stake_from_others) - < DELEGATE_MINIMAL_OWNED_WEIGHTED_STAKE - { + if delegate_mining.share < DELEGATE_MINIMAL_OWNED_WEIGHTED_STAKE { return Err(MplxRewardsError::InsufficientWeightedStake.into()); } From 33700a65aff6c08f085a0e8f287a1f88d298a3cb Mon Sep 17 00:00:00 2001 From: Kyrylo Stepanov Date: Fri, 12 Jul 2024 22:40:36 +0300 Subject: [PATCH 12/14] Forbid mining account from closing if it has stake_from_others --- programs/rewards/src/error.rs | 5 ++ .../rewards/src/instructions/close_mining.rs | 4 ++ .../rewards/tests/rewards/close_mining.rs | 72 ++++++++++++++++++- 3 files changed, 79 insertions(+), 2 deletions(-) diff --git a/programs/rewards/src/error.rs b/programs/rewards/src/error.rs index d34c5768..52ea45ef 100644 --- a/programs/rewards/src/error.rs +++ b/programs/rewards/src/error.rs @@ -80,6 +80,11 @@ pub enum MplxRewardsError { /// Delegate lack of tokens #[error("Rewards: Delegate must have at least 15_000_000 of own weighted stake")] InsufficientWeightedStake, + + /// 14 + /// Stake from others must be zero + #[error("Rewards: Stake from others must be zero")] + StakeFromOthersMustBeZero, } impl PrintProgramError for MplxRewardsError { diff --git a/programs/rewards/src/instructions/close_mining.rs b/programs/rewards/src/instructions/close_mining.rs index 99c29cbd..dabe4143 100644 --- a/programs/rewards/src/instructions/close_mining.rs +++ b/programs/rewards/src/instructions/close_mining.rs @@ -50,6 +50,10 @@ impl<'a, 'b> CloseMiningContext<'a, 'b> { let mining = Mining::unpack(&self.mining.data.borrow())?; assert_account_key(self.mining_owner, &mining.owner)?; + if mining.stake_from_others > 0 { + return Err(MplxRewardsError::StakeFromOthersMustBeZero.into()); + } + if mining.index.unclaimed_rewards != 0 { return Err(MplxRewardsError::RewardsMustBeClaimed.into()); } diff --git a/programs/rewards/tests/rewards/close_mining.rs b/programs/rewards/tests/rewards/close_mining.rs index 9213f146..ee10f440 100644 --- a/programs/rewards/tests/rewards/close_mining.rs +++ b/programs/rewards/tests/rewards/close_mining.rs @@ -1,8 +1,13 @@ +use std::borrow::Borrow; + use crate::utils::*; -use mplx_rewards::utils::LockupPeriod; +use mplx_rewards::{error::MplxRewardsError, state::Mining, utils::LockupPeriod}; use solana_program::pubkey::Pubkey; use solana_program_test::*; -use solana_sdk::{signature::Keypair, signer::Signer}; +use solana_sdk::{ + instruction::InstructionError, program_pack::Pack, signature::Keypair, signer::Signer, + transaction::TransactionError, +}; async fn setup() -> (ProgramTestContext, TestRewards, Keypair, Pubkey) { let test = ProgramTest::new( @@ -66,3 +71,66 @@ async fn success() { let mining_owner = get_account(&mut context, &mining_owner.pubkey()).await; assert!(mining_owner.lamports > 0); } + +#[tokio::test] +async fn forbing_closing_if_stake_from_others_is_not_zero() { + let (mut context, test_rewards, mining_owner, mining) = setup().await; + + let delegate = Keypair::new(); + let delegate_mining = test_rewards + .initialize_mining(&mut context, &delegate.pubkey()) + .await; + test_rewards + .deposit_mining( + &mut context, + &delegate_mining, + 3_000_000, // 18_000_000 of weighted stake + LockupPeriod::OneYear, + &delegate.pubkey(), + &delegate_mining, + ) + .await + .unwrap(); + let delegate_mining_account = get_account(&mut context, &delegate_mining).await; + let d_mining = Mining::unpack(delegate_mining_account.data.borrow()).unwrap(); + assert_eq!(d_mining.share, 18_000_000); + assert_eq!(d_mining.stake_from_others, 0); + + let mining_owner_before = context + .banks_client + .get_account(mining_owner.pubkey()) + .await + .unwrap(); + assert_eq!(None, mining_owner_before); + + test_rewards + .deposit_mining( + &mut context, + &mining, + 100, + LockupPeriod::ThreeMonths, + &mining_owner.pubkey(), + &delegate_mining, + ) + .await + .unwrap(); + + let res = test_rewards + .close_mining( + &mut context, + &delegate_mining, + &delegate, + &delegate.pubkey(), + ) + .await; + + match res { + Err(BanksClientError::TransactionError(TransactionError::InstructionError( + _, + InstructionError::Custom(code), + ))) => { + assert_eq!(code, MplxRewardsError::StakeFromOthersMustBeZero as u32); + } + _ => unreachable!(), + } +} From 17900a81b77c5d4dfdf23ee7f6599c60de21a2cd Mon Sep 17 00:00:00 2001 From: Kyrylo Stepanov Date: Fri, 12 Jul 2024 22:47:59 +0300 Subject: [PATCH 13/14] remove useless comment --- programs/rewards/src/state/reward_pool.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/programs/rewards/src/state/reward_pool.rs b/programs/rewards/src/state/reward_pool.rs index 8522291f..e0d4d1af 100644 --- a/programs/rewards/src/state/reward_pool.rs +++ b/programs/rewards/src/state/reward_pool.rs @@ -229,7 +229,6 @@ impl RewardPool { // do actions like it's a regular deposit let amount_to_restake = base_amount.safe_add(additional_amount)?; - // Refactored delegate_mining update let delegate_mining = match delegate_mining { Some(dm) => { if dm.stake_from_others > 0 { From 3cdbd8926855dcd47475d2bd242e49fd1cb7af6f Mon Sep 17 00:00:00 2001 From: Kyrylo Stepanov Date: Tue, 16 Jul 2024 15:12:04 +0300 Subject: [PATCH 14/14] add delegate_mining field to the withdraw_mining && remove verification for weighted stake since it's done on the staking contract --- programs/rewards/src/asserts.rs | 12 +++------- programs/rewards/src/instruction.rs | 3 +++ .../src/instructions/deposit_mining.rs | 5 ++--- .../rewards/src/instructions/extend_stake.rs | 5 ++--- .../src/instructions/withdraw_mining.rs | 13 +++++++++-- programs/rewards/src/state/reward_pool.rs | 22 ++++++++++++++----- programs/rewards/tests/rewards/claim.rs | 8 ++++++- .../rewards/tests/rewards/extend_stake.rs | 22 ++++++++++++++++--- programs/rewards/tests/rewards/utils.rs | 2 ++ .../rewards/tests/rewards/withdraw_mining.rs | 4 ++-- 10 files changed, 67 insertions(+), 29 deletions(-) diff --git a/programs/rewards/src/asserts.rs b/programs/rewards/src/asserts.rs index 04b1149f..882fa18c 100644 --- a/programs/rewards/src/asserts.rs +++ b/programs/rewards/src/asserts.rs @@ -4,10 +4,7 @@ use solana_program::{ program_pack::Pack, pubkey::Pubkey, rent::Rent, sysvar::Sysvar, }; -use crate::{ - error::MplxRewardsError, - state::{Mining, DELEGATE_MINIMAL_OWNED_WEIGHTED_STAKE}, -}; +use crate::{error::MplxRewardsError, state::Mining}; /// Assert signer. pub fn assert_signer(account: &AccountInfo) -> ProgramResult { @@ -97,18 +94,15 @@ pub fn assert_pubkey_eq(given: &Pubkey, expected: &Pubkey) -> ProgramResult { } } -pub fn verify_delegate_mining_requirements( +pub fn get_delegate_mining( delegate_mining: &AccountInfo, mining: &AccountInfo, ) -> Result, ProgramError> { if mining.key != delegate_mining.key { let delegate_mining = Mining::unpack(&delegate_mining.data.borrow())?; - if delegate_mining.share < DELEGATE_MINIMAL_OWNED_WEIGHTED_STAKE { - return Err(MplxRewardsError::InsufficientWeightedStake.into()); - } - Ok(Some(delegate_mining)) } else { + // None means delegate_mining is the same as mining Ok(None) } } diff --git a/programs/rewards/src/instruction.rs b/programs/rewards/src/instruction.rs index be28b30a..eb2d25fa 100644 --- a/programs/rewards/src/instruction.rs +++ b/programs/rewards/src/instruction.rs @@ -72,6 +72,7 @@ pub enum RewardsInstruction { #[account(0, writable, name = "reward_pool", desc = "The address of the reward pool")] #[account(1, writable, name = "mining", desc = "The address of the mining account which belongs to the user and stores info about user's rewards")] #[account(2, signer, name = "deposit_authority", desc = "The address of the Staking program's Registrar, which is PDA and is responsible for signing CPIs")] + #[account(3, name = "delegate_mining", desc = "The address of Mining Account that might be used as a delegate in delegated staking model")] WithdrawMining { /// Amount to withdraw amount: u64, @@ -253,6 +254,7 @@ pub fn withdraw_mining( reward_pool: &Pubkey, mining: &Pubkey, deposit_authority: &Pubkey, + delegate_mining: &Pubkey, amount: u64, owner: &Pubkey, ) -> Instruction { @@ -260,6 +262,7 @@ pub fn withdraw_mining( AccountMeta::new(*reward_pool, false), AccountMeta::new(*mining, false), AccountMeta::new_readonly(*deposit_authority, true), + AccountMeta::new(*delegate_mining, false), ]; Instruction::new_with_borsh( diff --git a/programs/rewards/src/instructions/deposit_mining.rs b/programs/rewards/src/instructions/deposit_mining.rs index 8bb7f3da..b50ddca6 100644 --- a/programs/rewards/src/instructions/deposit_mining.rs +++ b/programs/rewards/src/instructions/deposit_mining.rs @@ -1,5 +1,5 @@ use crate::{ - asserts::{assert_account_key, assert_pubkey_eq, verify_delegate_mining_requirements}, + asserts::{assert_account_key, assert_pubkey_eq, get_delegate_mining}, state::{Mining, RewardPool}, utils::{find_mining_program_address, AccountLoader, LockupPeriod}, }; @@ -57,8 +57,7 @@ impl<'a, 'b> DepositMiningContext<'a, 'b> { assert_pubkey_eq(&mining.owner, mining_owner)?; } - let mut delegate_mining = - verify_delegate_mining_requirements(self.delegate_mining, self.mining)?; + let mut delegate_mining = get_delegate_mining(self.delegate_mining, self.mining)?; reward_pool.deposit(&mut mining, amount, lockup_period, delegate_mining.as_mut())?; RewardPool::pack(reward_pool, *self.reward_pool.data.borrow_mut())?; diff --git a/programs/rewards/src/instructions/extend_stake.rs b/programs/rewards/src/instructions/extend_stake.rs index e754d265..da89ad5f 100644 --- a/programs/rewards/src/instructions/extend_stake.rs +++ b/programs/rewards/src/instructions/extend_stake.rs @@ -1,5 +1,5 @@ use crate::{ - asserts::{assert_account_key, assert_pubkey_eq, verify_delegate_mining_requirements}, + asserts::{assert_account_key, assert_pubkey_eq, get_delegate_mining}, state::{Mining, RewardPool}, utils::{find_mining_program_address, AccountLoader, LockupPeriod}, }; @@ -62,8 +62,7 @@ impl<'a, 'b> ExtendStakeContext<'a, 'b> { assert_pubkey_eq(&mining.owner, mining_owner)?; } - let mut delegate_mining = - verify_delegate_mining_requirements(self.delegate_mining, self.mining)?; + let mut delegate_mining = get_delegate_mining(self.delegate_mining, self.mining)?; reward_pool.extend( &mut mining, diff --git a/programs/rewards/src/instructions/withdraw_mining.rs b/programs/rewards/src/instructions/withdraw_mining.rs index 99230786..5e3d41a9 100644 --- a/programs/rewards/src/instructions/withdraw_mining.rs +++ b/programs/rewards/src/instructions/withdraw_mining.rs @@ -1,5 +1,5 @@ use crate::{ - asserts::{assert_account_key, assert_pubkey_eq}, + asserts::{assert_account_key, assert_pubkey_eq, get_delegate_mining}, state::{Mining, RewardPool}, utils::{find_mining_program_address, AccountLoader}, }; @@ -14,6 +14,7 @@ pub struct WithdrawMiningContext<'a, 'b> { reward_pool: &'a AccountInfo<'b>, mining: &'a AccountInfo<'b>, deposit_authority: &'a AccountInfo<'b>, + delegate_mining: &'a AccountInfo<'b>, } impl<'a, 'b> WithdrawMiningContext<'a, 'b> { @@ -27,11 +28,13 @@ impl<'a, 'b> WithdrawMiningContext<'a, 'b> { let reward_pool = AccountLoader::next_with_owner(account_info_iter, program_id)?; let mining = AccountLoader::next_with_owner(account_info_iter, program_id)?; let deposit_authority = AccountLoader::next_signer(account_info_iter)?; + let delegate_mining = AccountLoader::next_with_owner(account_info_iter, program_id)?; Ok(WithdrawMiningContext { reward_pool, mining, deposit_authority, + delegate_mining, }) } @@ -52,11 +55,17 @@ impl<'a, 'b> WithdrawMiningContext<'a, 'b> { assert_account_key(self.reward_pool, &mining.reward_pool)?; assert_pubkey_eq(&mining.owner, mining_owner)?; - reward_pool.withdraw(&mut mining, amount)?; + let mut delegate_mining = get_delegate_mining(self.delegate_mining, self.mining)?; + + reward_pool.withdraw(&mut mining, amount, delegate_mining.as_mut())?; RewardPool::pack(reward_pool, *self.reward_pool.data.borrow_mut())?; Mining::pack(mining, *self.mining.data.borrow_mut())?; + if let Some(delegate_mining) = delegate_mining { + Mining::pack(delegate_mining, *self.delegate_mining.data.borrow_mut())?; + } + Ok(()) } } diff --git a/programs/rewards/src/state/reward_pool.rs b/programs/rewards/src/state/reward_pool.rs index e0d4d1af..52f5464f 100644 --- a/programs/rewards/src/state/reward_pool.rs +++ b/programs/rewards/src/state/reward_pool.rs @@ -18,7 +18,6 @@ use solana_program::{ /// Precision for index calculation pub const PRECISION: u128 = 10_000_000_000_000_000; -pub const DELEGATE_MINIMAL_OWNED_WEIGHTED_STAKE: u64 = 15_000_000; /// Reward pool #[derive(Debug, BorshDeserialize, BorshSerialize, BorshSchema, Default)] @@ -147,7 +146,12 @@ impl RewardPool { } /// Process withdraw - pub fn withdraw(&mut self, mining: &mut Mining, amount: u64) -> ProgramResult { + pub fn withdraw( + &mut self, + mining: &mut Mining, + amount: u64, + delegate_mining: Option<&mut Mining>, + ) -> ProgramResult { mining.refresh_rewards(&self.calculator)?; self.total_share = self.total_share.safe_sub(amount)?; @@ -160,6 +164,13 @@ impl RewardPool { .consume_old_modifiers(beginning_of_the_day, self.total_share)?; self.total_share = reward_pool_share; + if let Some(delegate_mining) = delegate_mining { + delegate_mining.stake_from_others = + delegate_mining.stake_from_others.safe_sub(amount)?; + + self.total_share = self.total_share.safe_sub(amount)?; + } + Ok(()) } @@ -231,10 +242,9 @@ impl RewardPool { let delegate_mining = match delegate_mining { Some(dm) => { - if dm.stake_from_others > 0 { - dm.stake_from_others = dm.stake_from_others.saturating_sub(base_amount); - self.total_share = self.total_share.safe_sub(base_amount)?; - } + dm.stake_from_others = dm.stake_from_others.safe_sub(base_amount)?; + self.total_share = self.total_share.safe_sub(base_amount)?; + Some(dm) } None => None, diff --git a/programs/rewards/tests/rewards/claim.rs b/programs/rewards/tests/rewards/claim.rs index bc098896..569a63ac 100644 --- a/programs/rewards/tests/rewards/claim.rs +++ b/programs/rewards/tests/rewards/claim.rs @@ -756,7 +756,13 @@ async fn claim_after_withdraw_is_correct() { .await; test_rewards - .withdraw_mining(&mut context, &user_mining_b, 150, &user_b.pubkey()) + .withdraw_mining( + &mut context, + &user_mining_b, + &user_mining_b, + 150, + &user_b.pubkey(), + ) .await .unwrap(); diff --git a/programs/rewards/tests/rewards/extend_stake.rs b/programs/rewards/tests/rewards/extend_stake.rs index 141d495c..884a6b23 100644 --- a/programs/rewards/tests/rewards/extend_stake.rs +++ b/programs/rewards/tests/rewards/extend_stake.rs @@ -1,5 +1,8 @@ use crate::utils::*; -use mplx_rewards::{state::Mining, utils::LockupPeriod}; +use mplx_rewards::{ + state::{Mining, RewardPool}, + utils::LockupPeriod, +}; use solana_program::pubkey::Pubkey; use solana_program_test::*; use solana_sdk::{clock::SECONDS_PER_DAY, program_pack::Pack, signature::Keypair, signer::Signer}; @@ -367,13 +370,26 @@ async fn prolong_with_delegate() { .deposit_mining( &mut context, &mining, - 100, + base_amount, old_lockup_period, &mining_owner, - &mining, + &delegate_mining, ) .await .unwrap(); + let mining_account = get_account(&mut context, &mining).await; + let mining_unpacked = Mining::unpack(mining_account.data.borrow()).unwrap(); + assert_eq!(mining_unpacked.share, 200); + assert_eq!(mining_unpacked.stake_from_others, 0); + + let delegate_mining_account = get_account(&mut context, &delegate_mining).await; + let d_mining = Mining::unpack(delegate_mining_account.data.borrow()).unwrap(); + assert_eq!(d_mining.share, 18_000_000); + assert_eq!(d_mining.stake_from_others, 100); + + let reward_pool_acc = get_account(&mut context, &test_rewards.reward_pool).await; + let reward_pool_unpacked = RewardPool::unpack(reward_pool_acc.data.borrow()).unwrap(); + assert_eq!(reward_pool_unpacked.total_share, 18_000_300); // advance for ten days let curr_ts = diff --git a/programs/rewards/tests/rewards/utils.rs b/programs/rewards/tests/rewards/utils.rs index 5d0b9d8c..6b3a5d13 100644 --- a/programs/rewards/tests/rewards/utils.rs +++ b/programs/rewards/tests/rewards/utils.rs @@ -142,6 +142,7 @@ impl TestRewards { &self, context: &mut ProgramTestContext, mining_account: &Pubkey, + delegate_mining: &Pubkey, amount: u64, owner: &Pubkey, ) -> BanksClientResult<()> { @@ -151,6 +152,7 @@ impl TestRewards { &self.reward_pool, mining_account, &self.deposit_authority.pubkey(), + delegate_mining, amount, owner, )], diff --git a/programs/rewards/tests/rewards/withdraw_mining.rs b/programs/rewards/tests/rewards/withdraw_mining.rs index 2a6e08f1..4627f853 100644 --- a/programs/rewards/tests/rewards/withdraw_mining.rs +++ b/programs/rewards/tests/rewards/withdraw_mining.rs @@ -46,7 +46,7 @@ async fn success() { .unwrap(); test_rewards - .withdraw_mining(&mut context, &mining, 30, &user) + .withdraw_mining(&mut context, &mining, &mining, 30, &user) .await .unwrap(); @@ -80,7 +80,7 @@ async fn success_with_5kkk_after_expiring() { advance_clock_by_ts(&mut context, (100 * SECONDS_PER_DAY).try_into().unwrap()).await; test_rewards - .withdraw_mining(&mut context, &mining, 5000000000, &user) + .withdraw_mining(&mut context, &mining, &mining, 5000000000, &user) .await .unwrap();