Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand All @@ -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>,
Expand All @@ -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>,
Expand All @@ -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, &params, &mut accounts.iter())?;
let mut accounts =
AddPoolRewardAccounts::from_unchecked_iter(program_id, &mut accounts.iter())?;

// 1.

Expand All @@ -118,15 +100,24 @@ 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(),
})?;

// 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.

Expand All @@ -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<Self, ProgramError> {
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<Item = &'a AccountInfo<'info>>,
) -> Result<AddPoolRewardAccounts<'a, 'info>, ProgramError> {
let reserve_info = next_account_info(iter)?;
Expand Down Expand Up @@ -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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
3 changes: 3 additions & 0 deletions token-lending/sdk/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<LendingError> for ProgramError {
Expand Down
76 changes: 75 additions & 1 deletion token-lending/sdk/src/state/liquidity_mining.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(
Expand Down
11 changes: 11 additions & 0 deletions token-lending/sdk/src/state/reserve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading