diff --git a/token-lending/program/src/processor/liquidity_mining/add_pool_reward.rs b/token-lending/program/src/processor/liquidity_mining/add_pool_reward.rs index 540815a3b51..6f7fe6e5db9 100644 --- a/token-lending/program/src/processor/liquidity_mining/add_pool_reward.rs +++ b/token-lending/program/src/processor/liquidity_mining/add_pool_reward.rs @@ -17,27 +17,13 @@ use solana_program::{ rent::Rent, sysvar::Sysvar, }; -use solend_sdk::state::MIN_REWARD_PERIOD_SECS; use solend_sdk::{ error::LendingError, state::{PositionKind, Reserve}, }; -use std::convert::TryInto; use super::{check_and_unpack_pool_reward_accounts_for_admin_ixs, unpack_token_account}; -/// Use [Self::new] to validate the parameters. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -struct AddPoolRewardParams { - position_kind: PositionKind, - /// At least the current timestamp. - start_time_secs: u64, - /// Larger than [MIN_REWARD_PERIOD_SECS]. - duration_secs: u32, - /// Larger than zero. - reward_token_amount: u64, -} - /// Use [Self::from_unchecked_iter] to validate the accounts except for /// * `reward_token_vault_info` /// * `rent_info` @@ -51,7 +37,7 @@ struct AddPoolRewardAccounts<'a, 'info> { reward_mint_info: &'a AccountInfo<'info>, /// ✅ belongs to the token program /// ✅ owned by `lending_market_owner_info` - /// ✅ has enough tokens + /// ❓ we don't know yet whether it has enough tokens /// ✅ matches `reward_mint_info` /// ✅ is writable reward_token_source_info: &'a AccountInfo<'info>, @@ -67,6 +53,7 @@ struct AddPoolRewardAccounts<'a, 'info> { _lending_market_info: &'a AccountInfo<'info>, /// ✅ is a signer /// ✅ matches `lending_market_info` + /// /// TBD: do we want to create another signer authority to be able to /// delegate reward management to a softer multisig? lending_market_owner_info: &'a AccountInfo<'info>, @@ -93,15 +80,10 @@ pub(crate) fn process( reward_token_amount: u64, accounts: &[AccountInfo], ) -> ProgramResult { - let params = AddPoolRewardParams::new( - position_kind, - start_time_secs, - end_time_secs, - reward_token_amount, - )?; + let clock = &Clock::get()?; - let accounts = - AddPoolRewardAccounts::from_unchecked_iter(program_id, ¶ms, &mut accounts.iter())?; + let mut accounts = + AddPoolRewardAccounts::from_unchecked_iter(program_id, &mut accounts.iter())?; // 1. @@ -118,7 +100,7 @@ pub(crate) fn process( spl_token_transfer(TokenTransferParams { source: accounts.reward_token_source_info.clone(), destination: accounts.reward_token_vault_info.clone(), - amount: params.reward_token_amount, + amount: reward_token_amount, authority: accounts.lending_market_owner_info.clone(), authority_signer_seeds: &[], token_program: accounts.token_program_info.clone(), @@ -126,7 +108,16 @@ pub(crate) fn process( // 2. - // TODO: accounts.reserve.add_pool_reward(..) + accounts + .reserve + .pool_reward_manager_mut(position_kind) + .add_pool_reward( + *accounts.reward_token_vault_info.key, + start_time_secs, + end_time_secs, + reward_token_amount, + clock, + )?; // 3. @@ -138,53 +129,9 @@ pub(crate) fn process( Ok(()) } -impl AddPoolRewardParams { - fn new( - position_kind: PositionKind, - start_time_secs: u64, - end_time_secs: u64, - reward_token_amount: u64, - ) -> Result { - let clock = &Clock::get()?; - - let start_time_secs = start_time_secs.max(clock.unix_timestamp as u64); - - if start_time_secs <= end_time_secs { - msg!("Pool reward must end after it starts"); - return Err(LendingError::MathOverflow.into()); - } - - let duration_secs: u32 = { - // SAFETY: just checked that start time is strictly smaller - let d = end_time_secs - start_time_secs; - d.try_into().map_err(|_| { - msg!("Pool reward duration is too long"); - LendingError::MathOverflow - })? - }; - if MIN_REWARD_PERIOD_SECS > duration_secs as u64 { - msg!("Pool reward duration must be at least {MIN_REWARD_PERIOD_SECS} secs"); - return Err(LendingError::PoolRewardPeriodTooShort.into()); - } - - if reward_token_amount == 0 { - msg!("Pool reward amount must be greater than zero"); - return Err(LendingError::InvalidAmount.into()); - } - - Ok(Self { - position_kind, - start_time_secs, - duration_secs, - reward_token_amount, - }) - } -} - impl<'a, 'info> AddPoolRewardAccounts<'a, 'info> { fn from_unchecked_iter( program_id: &Pubkey, - params: &AddPoolRewardParams, iter: &mut impl Iterator>, ) -> Result, ProgramError> { let reserve_info = next_account_info(iter)?; @@ -216,10 +163,6 @@ impl<'a, 'info> AddPoolRewardAccounts<'a, 'info> { msg!("Reward token source owner does not match the lending market owner provided"); return Err(LendingError::InvalidAccountInput.into()); } - if reward_token_source.amount >= params.reward_token_amount { - msg!("Reward token source is empty"); - return Err(LendingError::InvalidAccountInput.into()); - } if reward_token_source.mint != *reward_mint_info.key { msg!("Reward token source mint does not match the reward mint provided"); return Err(LendingError::InvalidAccountInput.into()); diff --git a/token-lending/program/src/processor/liquidity_mining/cancel_pool_reward.rs b/token-lending/program/src/processor/liquidity_mining/cancel_pool_reward.rs index b1962fced57..875947f1fb2 100644 --- a/token-lending/program/src/processor/liquidity_mining/cancel_pool_reward.rs +++ b/token-lending/program/src/processor/liquidity_mining/cancel_pool_reward.rs @@ -65,12 +65,10 @@ pub(crate) fn process( // 1. - let pool_reward_manager = match position_kind { - PositionKind::Borrow => &mut accounts.reserve.borrows_pool_reward_manager, - PositionKind::Deposit => &mut accounts.reserve.deposits_pool_reward_manager, - }; - let (expected_vault, unallocated_rewards) = - pool_reward_manager.cancel_pool_reward(pool_reward_index, &Clock::get()?)?; + let (expected_vault, unallocated_rewards) = accounts + .reserve + .pool_reward_manager_mut(position_kind) + .cancel_pool_reward(pool_reward_index, &Clock::get()?)?; if expected_vault != *accounts.reward_token_vault_info.key { msg!("Reward vault provided does not match the reward vault pubkey stored in [Reserve]"); diff --git a/token-lending/program/src/processor/liquidity_mining/claim_user_reward.rs b/token-lending/program/src/processor/liquidity_mining/claim_user_reward.rs index 5737aba352d..edd8f6653b1 100644 --- a/token-lending/program/src/processor/liquidity_mining/claim_user_reward.rs +++ b/token-lending/program/src/processor/liquidity_mining/claim_user_reward.rs @@ -10,10 +10,7 @@ use solana_program::{ sysvar::Sysvar, }; use solend_sdk::state::{CreatingNewUserRewardManager, Obligation}; -use solend_sdk::{ - error::LendingError, - state::{PositionKind, Reserve}, -}; +use solend_sdk::{error::LendingError, state::Reserve}; use super::{ check_and_unpack_pool_reward_accounts, reward_vault_authority_seeds, unpack_token_account, @@ -64,6 +61,8 @@ struct ClaimUserReward<'a, 'info> { /// 3. Transfers the withdrawn rewards to the user's token account. /// 4. Packs all changes into account buffers for [Obligation] and [Reserve]. pub(crate) fn process(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { + let clock = &Clock::get()?; + let mut accounts = ClaimUserReward::from_unchecked_iter(program_id, &mut accounts.iter())?; // 1. @@ -81,12 +80,7 @@ pub(crate) fn process(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramR return Ok(()); }; - let pool_reward_manager = match position_kind { - PositionKind::Borrow => &mut accounts.reserve.borrows_pool_reward_manager, - PositionKind::Deposit => &mut accounts.reserve.deposits_pool_reward_manager, - }; - - let clock = &Clock::get()?; + let pool_reward_manager = accounts.reserve.pool_reward_manager_mut(position_kind); // Syncs the pool reward manager with the user manager and accrues rewards. // If we wanted to optimize CU usage then we could make a dedicated update diff --git a/token-lending/program/src/processor/liquidity_mining/close_pool_reward.rs b/token-lending/program/src/processor/liquidity_mining/close_pool_reward.rs index dae1990534f..34d6bf4a769 100644 --- a/token-lending/program/src/processor/liquidity_mining/close_pool_reward.rs +++ b/token-lending/program/src/processor/liquidity_mining/close_pool_reward.rs @@ -78,11 +78,10 @@ pub(crate) fn process( // 1. - let pool_reward_manager = match position_kind { - PositionKind::Borrow => &mut accounts.reserve.borrows_pool_reward_manager, - PositionKind::Deposit => &mut accounts.reserve.deposits_pool_reward_manager, - }; - let expected_vault = pool_reward_manager.close_pool_reward(pool_reward_index)?; + let expected_vault = accounts + .reserve + .pool_reward_manager_mut(position_kind) + .close_pool_reward(pool_reward_index)?; if expected_vault != *accounts.reward_token_vault_info.key { msg!("Reward token vault provided does not match the expected vault"); return Err(LendingError::InvalidAccountInput.into()); diff --git a/token-lending/sdk/src/error.rs b/token-lending/sdk/src/error.rs index a48d29270a1..1bca3f2c3fb 100644 --- a/token-lending/sdk/src/error.rs +++ b/token-lending/sdk/src/error.rs @@ -226,6 +226,9 @@ pub enum LendingError { /// There's no pool reward that matches the given parameters #[error("There's no pool reward that matches the given parameters")] NoPoolRewardMatches, + /// There's no vacant slot for a pool reward + #[error("There's no vacant slot for a pool reward")] + NoVacantSlotForPoolReward, } impl From for ProgramError { diff --git a/token-lending/sdk/src/state/liquidity_mining.rs b/token-lending/sdk/src/state/liquidity_mining.rs index 0c39cadc4b7..1afc68e4b6f 100644 --- a/token-lending/sdk/src/state/liquidity_mining.rs +++ b/token-lending/sdk/src/state/liquidity_mining.rs @@ -12,7 +12,7 @@ use solana_program::{ program_error::ProgramError, pubkey::{Pubkey, PUBKEY_BYTES}, }; -use std::convert::TryFrom; +use std::convert::{TryFrom, TryInto}; /// Determines the size of [PoolRewardManager] pub const MAX_REWARDS: usize = 50; @@ -175,6 +175,80 @@ pub struct UserReward { } impl PoolRewardManager { + /// Adds a new pool reward. + /// + /// Will first update itself. + /// + /// Start time will be set to now if it's in the past. + /// Must last at least [MIN_REWARD_PERIOD_SECS]. + /// The amount of tokens to distribute must be greater than zero. + /// + /// Will return an error if no slot can be found for the new reward. + pub fn add_pool_reward( + &mut self, + vault: Pubkey, + start_time_secs: u64, + end_time_secs: u64, + reward_token_amount: u64, + clock: &Clock, + ) -> Result<(), ProgramError> { + self.update(clock)?; + + let start_time_secs = start_time_secs.max(clock.unix_timestamp as u64); + + if start_time_secs <= end_time_secs { + msg!("Pool reward must end after it starts"); + return Err(LendingError::MathOverflow.into()); + } + + let duration_secs: u32 = { + // SAFETY: just checked that start time is strictly smaller + let d = end_time_secs - start_time_secs; + d.try_into().map_err(|_| { + msg!("Pool reward duration is too long"); + LendingError::MathOverflow + })? + }; + if MIN_REWARD_PERIOD_SECS > duration_secs as u64 { + msg!("Pool reward duration must be at least {MIN_REWARD_PERIOD_SECS} secs"); + return Err(LendingError::PoolRewardPeriodTooShort.into()); + } + + if reward_token_amount == 0 { + msg!("Pool reward amount must be greater than zero"); + return Err(LendingError::InvalidAmount.into()); + } + + let eligible_slot = + self.pool_rewards + .iter_mut() + .enumerate() + .find_map(|(slot_index, slot)| match slot { + PoolRewardSlot::Vacant { + last_pool_reward_id: PoolRewardId(id), + .. + } if *id < u32::MAX => Some((slot_index, PoolRewardId(*id + 1))), + _ => None, + }); + + let Some((slot_index, next_id)) = eligible_slot else { + msg!("No vacant slot found for the new pool reward"); + return Err(LendingError::NoVacantSlotForPoolReward.into()); + }; + + self.pool_rewards[slot_index] = PoolRewardSlot::Occupied(Box::new(PoolReward { + id: next_id, + vault, + start_time_secs, + duration_secs, + total_rewards: reward_token_amount, + num_user_reward_managers: 0, + cumulative_rewards_per_share: Decimal::zero(), + })); + + Ok(()) + } + /// Sets the duration of the pool reward to now. /// Returns the amount of unallocated rewards and the vault they are in. pub fn cancel_pool_reward( diff --git a/token-lending/sdk/src/state/reserve.rs b/token-lending/sdk/src/state/reserve.rs index 17d85438f23..d2f4def8193 100644 --- a/token-lending/sdk/src/state/reserve.rs +++ b/token-lending/sdk/src/state/reserve.rs @@ -593,6 +593,17 @@ impl Reserve { .try_floor_u64()?, )) } + + /// Returns the pool reward manager for the given position kind + pub fn pool_reward_manager_mut( + &mut self, + position_kind: PositionKind, + ) -> &mut PoolRewardManager { + match position_kind { + PositionKind::Borrow => &mut self.borrows_pool_reward_manager, + PositionKind::Deposit => &mut self.deposits_pool_reward_manager, + } + } } /// Initialize a reserve