From 49a6c03b65b1f082e9c3fad1c2a6455bf77997a8 Mon Sep 17 00:00:00 2001 From: vanitymnm Date: Tue, 25 Feb 2025 13:54:19 +0100 Subject: [PATCH 01/11] Creating data structures for LM --- .../sdk/src/state/liquidity_mining.rs | 137 ++++++++++++++++++ token-lending/sdk/src/state/obligation.rs | 25 +++- token-lending/sdk/src/state/reserve.rs | 15 +- 3 files changed, 165 insertions(+), 12 deletions(-) create mode 100644 token-lending/sdk/src/state/liquidity_mining.rs diff --git a/token-lending/sdk/src/state/liquidity_mining.rs b/token-lending/sdk/src/state/liquidity_mining.rs new file mode 100644 index 00000000000..c6c66a20083 --- /dev/null +++ b/token-lending/sdk/src/state/liquidity_mining.rs @@ -0,0 +1,137 @@ +use crate::math::Decimal; +use solana_program::pubkey::Pubkey; + +/// Determines the size of [PoolRewardManager] +const MAX_REWARDS: usize = 44; + +/// Each reserve has two managers: +/// - one for deposits +/// - one for borrows +pub struct PoolRewardManager { + /// Is updated when we change user shares in the reserve. + pub total_shares: u64, + /// Monotonically increasing time taken from clock sysvar. + pub last_update_time_secs: u64, + /// New [PoolReward] are added to the first vacant slot. + pub pool_rewards: [PoolRewardSlot; MAX_REWARDS], +} + +/// Each pool reward gets an ID which is monotonically increasing with each +/// new reward added to the pool at the particular slot. +/// +/// This helps us distinguish between two distinct rewards in the same array +/// index across time. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct PoolRewardId(pub u32); + +/// # (Un)Packing +/// This is unpacked representation. +/// When packing we use the [PoolReward] `reward_mint` to determine whether the +/// reward is vacant or not to save space. +/// +/// If the pubkey is eq to default pubkey then slot is vacant. +pub enum PoolRewardSlot { + /// New reward can be added to this slot. + Vacant { + /// Increment this ID when adding new [PoolReward]. + last_pool_reward_id: PoolRewardId, + }, + /// Reward has not been closed yet. + Occupied(PoolReward), +} + +/// Tracks rewards in a specific mint over some period of time. +pub struct PoolReward { + /// Unique ID for this slot that has never been used before, and will never + /// be used again. + pub id: PoolRewardId, + /// # (Un)Packing + /// When we pack the reward we set this to default pubkey for vacant slots. + pub vault: Pubkey, + /// Monotonically increasing time taken from clock sysvar. + pub start_time_secs: u64, + /// For how long (since start time) will this reward be releasing tokens. + pub duration_secs: u32, + /// Total token amount to distribute. + /// The token account that holds the rewards holds at least this much in + /// the beginning. + pub total_rewards: u64, + /// How many users are still tracking this reward. + /// Once this reaches zero we can close this reward. + /// There's a permission-less ix with which user rewards can be distributed + /// that's used for cranking remaining rewards. + pub num_user_reward_managers: u64, + /// Amount of rewards that have been made available to users. + /// + /// We keep adding `(total_rewards * time_passed) / (total_time)` every + /// time someone interacts with the manager + /// ([update_pool_reward_manager]). + pub allocated_rewards: Decimal, + /// We keep adding `(unlocked_rewards) / (total_shares)` every time + /// someone interacts with the manager ([update_pool_reward_manager]) + /// where + /// `unlocked_rewards = (total_rewards * time_passed) / (total_time)` + pub cumulative_rewards_per_share: Decimal, +} + +/// Tracks user's LM rewards for a specific pool (reserve.) +pub struct UserRewardManager { + /// User cannot both borrow and deposit in the same reserve. + /// This manager is unique for this reserve within the [Obligation]. + /// + /// We know whether to use [crate::state::Reserve]'s + /// `deposits_pool_reward_manager` or `borrows_pool_reward_manager` based on + /// this field. + /// + /// One optimization we could make is to link the [UserRewardManager] via + /// index which would save 32 bytes per [UserRewardManager]. + /// However, that does make the program logic more error prone. + pub reserve: Pubkey, + /// For deposits, this is the amount of collateral token user has in + /// their obligation deposit. + /// + /// For borrows, this is (borrow_amount / cumulative_borrow_rate) user + /// has in their obligation borrow. + pub share: u64, + /// Monotonically increasing time taken from clock sysvar. + pub last_update_time_secs: u64, + /// The index of each reward is important. + /// It will match the index in the [PoolRewardManager] of the reserve. + pub rewards: Vec>, +} + +/// Track user rewards for a specific [PoolReward]. +pub struct UserReward { + /// Each pool reward gets an ID which is monotonically increasing with each + /// new reward added to the pool. + pub pool_reward_id: PoolRewardId, + /// Before [UserReward.cumulative_rewards_per_share] is copied we find + /// time difference between current global rewards and last user update + /// rewards: + /// [PoolReward.cumulative_rewards_per_share] - [UserReward.cumulative_rewards_per_share] + /// + /// Then, we multiply that difference by [UserRewardManager.share] and + /// add the result to this counter. + pub earned_rewards: Decimal, + /// copied from [PoolReward.cumulative_rewards_per_share] at the time of the last update + pub cumulative_rewards_per_share: Decimal, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_fits_reserve_realloc_into_single_ix() { + const MAX_REALLOC: usize = 10 * 1024; + + let size_of_discriminant = 1; + let const_size_of_pool_manager = 8 + 8; + let required_realloc = size_of_discriminant + + const_size_of_pool_manager + + 2 * MAX_REWARDS * std::mem::size_of::(); + + println!("assert {required_realloc} <= {MAX_REALLOC}"); + assert!(required_realloc <= MAX_REALLOC); + } +} diff --git a/token-lending/sdk/src/state/obligation.rs b/token-lending/sdk/src/state/obligation.rs index 6f9f43ef18e..14af0282713 100644 --- a/token-lending/sdk/src/state/obligation.rs +++ b/token-lending/sdk/src/state/obligation.rs @@ -65,6 +65,15 @@ pub struct Obligation { pub closeable: bool, } +/// These are the two foundational user interactions in a borrow-lending protocol. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum PositionKind { + /// User is providing liquidity. + Deposit = 0, + /// User is owing liquidity. + Borrow = 1, +} + impl Obligation { /// Create a new obligation pub fn new(params: InitObligationParams) -> Self { @@ -414,13 +423,14 @@ impl ObligationLiquidity { const OBLIGATION_COLLATERAL_LEN: usize = 88; // 32 + 8 + 16 + 32 const OBLIGATION_LIQUIDITY_LEN: usize = 112; // 32 + 16 + 16 + 16 + 32 -const OBLIGATION_LEN: usize = 1300; // 1 + 8 + 1 + 32 + 32 + 16 + 16 + 16 + 16 + 64 + 1 + 1 + (88 * 1) + (112 * 9) - // @TODO: break this up by obligation / collateral / liquidity https://git.io/JOCca +const OBLIGATION_LEN_V1: usize = 1300; // 1 + 8 + 1 + 32 + 32 + 16 + 16 + 16 + 16 + 64 + 1 + 1 + (88 * 1) + (112 * 9) + // @TODO: break this up by obligation / collateral / liquidity https://git.io/JOCca impl Pack for Obligation { - const LEN: usize = OBLIGATION_LEN; + const LEN: usize = OBLIGATION_LEN_V1; + // @v2.1.0 TODO: pack vec of user reward managers fn pack_into_slice(&self, dst: &mut [u8]) { - let output = array_mut_ref![dst, 0, OBLIGATION_LEN]; + let output = array_mut_ref![dst, 0, OBLIGATION_LEN_V1]; #[allow(clippy::ptr_offset_with_cast)] let ( version, @@ -527,9 +537,10 @@ impl Pack for Obligation { } } - /// Unpacks a byte buffer into an [ObligationInfo](struct.ObligationInfo.html). + /// Unpacks a byte buffer into an [Obligation]. + // @v2.1.0 TODO: unpack vector of optional user reward managers fn unpack_from_slice(src: &[u8]) -> Result { - let input = array_ref![src, 0, OBLIGATION_LEN]; + let input = array_ref![src, 0, OBLIGATION_LEN_V1]; #[allow(clippy::ptr_offset_with_cast)] let ( version, @@ -693,7 +704,7 @@ mod test { closeable: rng.gen(), }; - let mut packed = [0u8; OBLIGATION_LEN]; + let mut packed = [0u8; OBLIGATION_LEN_V1]; Obligation::pack(obligation.clone(), &mut packed).unwrap(); let unpacked = Obligation::unpack(&packed).unwrap(); assert_eq!(obligation, unpacked); diff --git a/token-lending/sdk/src/state/reserve.rs b/token-lending/sdk/src/state/reserve.rs index 14092c277e6..2e53ba3e19d 100644 --- a/token-lending/sdk/src/state/reserve.rs +++ b/token-lending/sdk/src/state/reserve.rs @@ -1228,13 +1228,15 @@ impl IsInitialized for Reserve { } } -const RESERVE_LEN: usize = 619; // 1 + 8 + 1 + 32 + 32 + 1 + 32 + 32 + 32 + 8 + 16 + 16 + 16 + 32 + 8 + 32 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 8 + 8 + 1 + 8 + 8 + 32 + 1 + 1 + 16 + 230 +/// This is the size of the account _before_ LM feature was added. +const RESERVE_LEN_V1: usize = 619; // 1 + 8 + 1 + 32 + 32 + 1 + 32 + 32 + 32 + 8 + 16 + 16 + 16 + 32 + 8 + 32 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 8 + 8 + 1 + 8 + 8 + 32 + 1 + 1 + 16 + 230 impl Pack for Reserve { - const LEN: usize = RESERVE_LEN; + const LEN: usize = RESERVE_LEN_V1; // @TODO: break this up by reserve / liquidity / collateral / config https://git.io/JOCca + // @v2.1.0 TODO: pack deposits_pool_reward_manager and borrows_pool_reward_manager fn pack_into_slice(&self, output: &mut [u8]) { - let output = array_mut_ref![output, 0, RESERVE_LEN]; + let output = array_mut_ref![output, 0, RESERVE_LEN_V1]; #[allow(clippy::ptr_offset_with_cast)] let ( version, @@ -1422,9 +1424,12 @@ impl Pack for Reserve { pack_decimal(self.attributed_borrow_value, attributed_borrow_value); } - /// Unpacks a byte buffer into a [ReserveInfo](struct.ReserveInfo.html). + /// Unpacks a byte buffer into a [Reserve]. + // @v2.1.0 TODO: unpack deposits_pool_reward_manager and borrows_pool_reward_manager + // but default them if they are not present, this is part of the + // migration process fn unpack_from_slice(input: &[u8]) -> Result { - let input = array_ref![input, 0, RESERVE_LEN]; + let input = array_ref![input, 0, RESERVE_LEN_V1]; #[allow(clippy::ptr_offset_with_cast)] let ( version, From aa13771acaed732aa896e0845427b0827da09476 Mon Sep 17 00:00:00 2001 From: vanitymnm Date: Tue, 25 Feb 2025 13:55:17 +0100 Subject: [PATCH 02/11] Adding admin IXs for LM reward management --- token-lending/program/src/processor.rs | 75 ++ .../program/src/processor/liquidity_mining.rs | 686 ++++++++++++++++++ token-lending/sdk/src/error.rs | 3 + token-lending/sdk/src/instruction.rs | 126 +++- token-lending/sdk/src/state/mod.rs | 2 + 5 files changed, 891 insertions(+), 1 deletion(-) create mode 100644 token-lending/program/src/processor/liquidity_mining.rs diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index b92aea55911..9658fdc5bae 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -1,5 +1,7 @@ //! Program state processor +mod liquidity_mining; + use crate::state::Bonus; use crate::{ self as solend_program, @@ -202,6 +204,46 @@ pub fn process_instruction( msg!("Instruction: Donate To Reserve"); process_donate_to_reserve(program_id, liquidity_amount, accounts) } + LendingInstruction::AddPoolReward { + position_kind, + start_time_secs, + end_time_secs, + token_amount, + } => { + msg!("Instruction: Add Pool Reward"); + liquidity_mining::process_add_pool_reward( + program_id, + position_kind, + start_time_secs, + end_time_secs, + token_amount, + accounts, + ) + } + LendingInstruction::CancelPoolReward { + position_kind, + pool_reward_index, + } => { + msg!("Instruction: Cancel Pool Reward"); + liquidity_mining::process_cancel_pool_reward( + program_id, + position_kind, + pool_reward_index, + accounts, + ) + } + LendingInstruction::ClosePoolReward { + position_kind, + pool_reward_index, + } => { + msg!("Instruction: Close Pool Reward"); + liquidity_mining::process_close_pool_reward( + program_id, + position_kind, + pool_reward_index, + accounts, + ) + } } } @@ -3436,6 +3478,31 @@ fn spl_token_burn(params: TokenBurnParams<'_, '_>) -> ProgramResult { result.map_err(|_| LendingError::TokenBurnFailed.into()) } +/// Issue a spl_token `CloseAccount` instruction. +#[inline(always)] +fn spl_token_close_account(params: TokenCloseAccountParams<'_, '_>) -> ProgramResult { + let TokenCloseAccountParams { + account, + destination, + authority, + token_program, + authority_signer_seeds, + } = params; + let result = invoke_optionally_signed( + &spl_token::instruction::close_account( + token_program.key, + account.key, + destination.key, + authority.key, + &[], + )?, + &[account, destination, authority, token_program], + authority_signer_seeds, + ); + + result.map_err(|_| LendingError::TokenTransferFailed.into()) +} + fn is_cpi_call( program_id: &Pubkey, current_index: usize, @@ -3507,3 +3574,11 @@ struct TokenBurnParams<'a: 'b, 'b> { authority_signer_seeds: &'b [&'b [u8]], token_program: AccountInfo<'a>, } + +struct TokenCloseAccountParams<'a: 'b, 'b> { + account: AccountInfo<'a>, + destination: AccountInfo<'a>, + authority: AccountInfo<'a>, + authority_signer_seeds: &'b [&'b [u8]], + token_program: AccountInfo<'a>, +} diff --git a/token-lending/program/src/processor/liquidity_mining.rs b/token-lending/program/src/processor/liquidity_mining.rs new file mode 100644 index 00000000000..8747a7d9e02 --- /dev/null +++ b/token-lending/program/src/processor/liquidity_mining.rs @@ -0,0 +1,686 @@ +use crate::processor::{ + assert_rent_exempt, spl_token_close_account, spl_token_init_account, spl_token_transfer, + TokenCloseAccountParams, TokenInitializeAccountParams, TokenTransferParams, +}; +use add_pool_reward::{AddPoolRewardAccounts, AddPoolRewardParams}; +use cancel_pool_reward::{CancelPoolRewardAccounts, CancelPoolRewardParams}; +use close_pool_reward::{ClosePoolRewardAccounts, ClosePoolRewardParams}; +use solana_program::program_pack::Pack; +use solana_program::{ + account_info::{next_account_info, AccountInfo}, + clock::Clock, + entrypoint::ProgramResult, + msg, + program_error::ProgramError, + pubkey::Pubkey, + rent::Rent, + sysvar::Sysvar, +}; +use solend_sdk::{ + error::LendingError, + state::{LendingMarket, PositionKind, Reserve}, +}; +use spl_token::state::Account as TokenAccount; +use std::convert::TryInto; + +/// Cannot create a reward shorter than this. +const MIN_REWARD_PERIOD_SECS: u64 = 3_600; + +/// # Accounts +/// +/// See [add_pool_reward::AddPoolRewardAccounts::from_unchecked_iter] for a list +/// of accounts and their constraints. +/// +/// # Effects +/// +/// 1. Initializes a new reward vault account and transfers +/// `reward_token_amount` tokens from the `reward_token_source` account to +/// the new reward vault account. +/// 2. Finds an empty slot in the [Reserve]'s LM reward vector and adds it there. +/// 3. Packs all changes into account buffers. +pub(crate) fn process_add_pool_reward( + program_id: &Pubkey, + position_kind: PositionKind, + start_time_secs: u64, + end_time_secs: u64, + reward_token_amount: u64, + accounts: &[AccountInfo], +) -> ProgramResult { + let params = AddPoolRewardParams::new( + position_kind, + start_time_secs, + end_time_secs, + reward_token_amount, + )?; + + let accounts = + AddPoolRewardAccounts::from_unchecked_iter(program_id, ¶ms, &mut accounts.iter())?; + + // 1. + + spl_token_init_account(TokenInitializeAccountParams { + account: accounts.reward_token_vault_info.clone(), + mint: accounts.reward_mint_info.clone(), + owner: accounts.reward_authority_info.clone(), + rent: accounts.rent_info.clone(), + token_program: accounts.token_program_info.clone(), + })?; + let rent = &Rent::from_account_info(accounts.rent_info)?; + assert_rent_exempt(rent, accounts.reward_token_vault_info)?; + + spl_token_transfer(TokenTransferParams { + source: accounts.reward_token_source_info.clone(), + destination: accounts.reward_token_vault_info.clone(), + amount: params.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(..)"); + + // 3. + + Reserve::pack( + *accounts.reserve, + &mut accounts.reserve_info.data.borrow_mut(), + )?; + + Ok(()) +} + +/// # Accounts +/// +/// See [cancel_pool_reward::CancelPoolRewardAccounts::from_unchecked_iter] for a list +/// of accounts and their constraints. +/// +/// # Effects +/// +/// 1. Cancels any further reward emission, effectively setting end time to now. +/// 2. Transfers any unallocated rewards to the `reward_token_destination` account. +/// 3. Packs all changes into account buffers. +pub(crate) fn process_cancel_pool_reward( + program_id: &Pubkey, + position_kind: PositionKind, + pool_reward_index: u64, + accounts: &[AccountInfo], +) -> ProgramResult { + let params = CancelPoolRewardParams::new(position_kind, pool_reward_index); + + let accounts = + CancelPoolRewardAccounts::from_unchecked_iter(program_id, ¶ms, &mut accounts.iter())?; + + // 1. + + let unallocated_rewards = todo!("accounts.reserve.cancel_pool_reward(..)"); + + // 2. + + spl_token_transfer(TokenTransferParams { + source: accounts.reward_token_vault_info.clone(), + destination: accounts.reward_token_destination_info.clone(), + amount: unallocated_rewards, + authority: accounts.reward_authority_info.clone(), + authority_signer_seeds: &reward_vault_authority_seeds( + accounts.lending_market_info.key, + accounts.reserve_info.key, + accounts.reward_mint_info.key, + ), + token_program: accounts.token_program_info.clone(), + })?; + + // 3. + + Reserve::pack( + *accounts.reserve, + &mut accounts.reserve_info.data.borrow_mut(), + )?; + + Ok(()) +} + +/// # Accounts +/// +/// See [close_pool_reward::ClosePoolRewardAccounts::from_unchecked_iter] for a list +/// of accounts and their constraints. +/// +/// # Effects +/// +/// 1. Closes reward in the [Reserve] account if all users have claimed. +/// 2. Transfers any unallocated rewards to the `reward_token_destination` account. +/// 3. Closes reward vault token account. +/// 3. Packs all changes into account buffers. +pub(crate) fn process_close_pool_reward( + program_id: &Pubkey, + position_kind: PositionKind, + pool_reward_index: u64, + accounts: &[AccountInfo], +) -> ProgramResult { + let params = ClosePoolRewardParams::new(position_kind, pool_reward_index); + + let accounts = + ClosePoolRewardAccounts::from_unchecked_iter(program_id, ¶ms, &mut accounts.iter())?; + + // 1. + + let unallocated_rewards = todo!("accounts.reserve.close_pool_reward(..)"); + + // 2. + + spl_token_transfer(TokenTransferParams { + source: accounts.reward_token_vault_info.clone(), + destination: accounts.reward_token_destination_info.clone(), + amount: unallocated_rewards, + authority: accounts.reward_authority_info.clone(), + authority_signer_seeds: &reward_vault_authority_seeds( + accounts.lending_market_info.key, + accounts.reserve_info.key, + accounts.reward_mint_info.key, + ), + token_program: accounts.token_program_info.clone(), + })?; + + // 3. + + spl_token_close_account(TokenCloseAccountParams { + account: accounts.reward_token_vault_info.clone(), + destination: accounts.lending_market_owner_info.clone(), + authority: accounts.reward_authority_info.clone(), + authority_signer_seeds: &reward_vault_authority_seeds( + accounts.lending_market_info.key, + accounts.reserve_info.key, + accounts.reward_mint_info.key, + ), + token_program: accounts.token_program_info.clone(), + })?; + + // 4. + Reserve::pack( + *accounts.reserve, + &mut accounts.reserve_info.data.borrow_mut(), + )?; + + Ok(()) +} + +/// Unpacks a spl_token [TokenAccount]. +fn unpack_token_account(data: &[u8]) -> Result { + TokenAccount::unpack(data).map_err(|_| LendingError::InvalidTokenAccount) +} + +/// Derives the reward vault authority PDA address. +fn reward_vault_authority( + program_id: &Pubkey, + lending_market_key: &Pubkey, + reserve_key: &Pubkey, + reward_mint_key: &Pubkey, +) -> (Pubkey, u8) { + Pubkey::find_program_address( + &reward_vault_authority_seeds(lending_market_key, reserve_key, reward_mint_key), + program_id, + ) +} + +fn reward_vault_authority_seeds<'a, 'keys>( + lending_market_key: &'keys Pubkey, + reserve_key: &'keys Pubkey, + reward_mint_key: &'keys Pubkey, +) -> [&'keys [u8]; 4] { + [ + b"RewardVaultAuthority", + lending_market_key.as_ref(), + reserve_key.as_ref(), + reward_mint_key.as_ref(), + ] +} + +mod add_pool_reward { + use super::*; + + /// Use [Self::new] to validate the parameters. + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub(super) struct AddPoolRewardParams { + _priv: (), + pub(super) position_kind: PositionKind, + /// At least the current timestamp. + pub(super) start_time_secs: u64, + /// Larger than [MIN_REWARD_PERIOD_SECS]. + pub(super) duration_secs: u32, + /// Larger than zero. + pub(super) reward_token_amount: u64, + } + + /// Use [Self::from_unchecked_iter] to validate the accounts except for + /// * `reward_token_vault_info` + /// * `rent_info` + pub(super) struct AddPoolRewardAccounts<'a, 'info> { + _priv: (), + /// ✅ belongs to this program + /// ✅ unpacks + /// ✅ belongs to `lending_market_info` + pub(super) reserve_info: &'a AccountInfo<'info>, + pub(super) reward_mint_info: &'a AccountInfo<'info>, + /// ✅ belongs to the token program + /// ✅ owned by `lending_market_owner_info` + /// ✅ has enough tokens + /// ✅ matches `reward_mint_info` + pub(super) reward_token_source_info: &'a AccountInfo<'info>, + /// ✅ seed of `lending_market_info`, `reserve_info`, `reward_mint_info` + pub(super) reward_authority_info: &'a AccountInfo<'info>, + /// ❓ we don't yet know whether it's rent exempt + pub(super) reward_token_vault_info: &'a AccountInfo<'info>, + /// ✅ belongs to this program + /// ✅ unpacks + pub(super) 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? + pub(super) lending_market_owner_info: &'a AccountInfo<'info>, + /// ❓ we don't yet whether this is rent info + pub(super) rent_info: &'a AccountInfo<'info>, + /// ✅ matches `lending_market_info` + pub(super) token_program_info: &'a AccountInfo<'info>, + + pub(super) reserve: Box, + } + + impl AddPoolRewardParams { + pub(super) 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::PoolRewardTooShort.into()); + } + + if reward_token_amount == 0 { + msg!("Pool reward amount must be greater than zero"); + return Err(LendingError::InvalidAmount.into()); + } + + Ok(Self { + _priv: (), + + position_kind, + start_time_secs, + duration_secs, + reward_token_amount, + }) + } + } + + impl<'a, 'info> AddPoolRewardAccounts<'a, 'info> { + pub(super) fn from_unchecked_iter( + program_id: &Pubkey, + params: &AddPoolRewardParams, + iter: &mut impl Iterator>, + ) -> Result, ProgramError> { + let reserve_info = next_account_info(iter)?; + let reward_mint_info = next_account_info(iter)?; + let reward_token_source_info = next_account_info(iter)?; + let reward_authority_info = next_account_info(iter)?; + let reward_token_vault_info = next_account_info(iter)?; + let lending_market_info = next_account_info(iter)?; + let lending_market_owner_info = next_account_info(iter)?; + let rent_info = next_account_info(iter)?; + let token_program_info = next_account_info(iter)?; + + let reserve = check_pool_reward_accounts_for_admin_ixs_and_unpack_reserve( + program_id, + reserve_info, + reward_mint_info, + reward_authority_info, + lending_market_info, + lending_market_owner_info, + token_program_info, + )?; + + if reward_token_source_info.owner != token_program_info.key { + msg!("Reward token source provided must be owned by the token program"); + return Err(LendingError::InvalidTokenOwner.into()); + } + let reward_token_source = + unpack_token_account(&reward_token_source_info.data.borrow())?; + if reward_token_source.owner != *lending_market_owner_info.key { + 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()); + } + + Ok(Self { + _priv: (), + + reserve_info, + reward_mint_info, + reward_token_source_info, + reward_authority_info, + reward_token_vault_info, + lending_market_info, + lending_market_owner_info, + rent_info, + token_program_info, + + reserve, + }) + } + } +} + +mod cancel_pool_reward { + use super::*; + + pub(super) struct CancelPoolRewardParams { + _priv: (), + + position_kind: PositionKind, + pool_reward_index: u64, + } + + /// Use [Self::from_unchecked_iter] to validate the accounts. + pub(super) struct CancelPoolRewardAccounts<'a, 'info> { + _priv: (), + + /// ✅ belongs to this program + /// ✅ unpacks + /// ✅ belongs to `lending_market_info` + pub(super) reserve_info: &'a AccountInfo<'info>, + pub(super) reward_mint_info: &'a AccountInfo<'info>, + /// ✅ belongs to the token program + /// ✅ owned by `lending_market_owner_info` + /// ✅ matches `reward_mint_info` + pub(super) reward_token_destination_info: &'a AccountInfo<'info>, + /// ✅ seed of `lending_market_info`, `reserve_info`, `reward_mint_info` + pub(super) reward_authority_info: &'a AccountInfo<'info>, + /// ✅ matches reward vault pubkey stored in the [Reserve] + pub(super) reward_token_vault_info: &'a AccountInfo<'info>, + /// ✅ belongs to this program + /// ✅ unpacks + pub(super) lending_market_info: &'a AccountInfo<'info>, + /// ✅ is a signer + /// ✅ matches `lending_market_info` + pub(super) lending_market_owner_info: &'a AccountInfo<'info>, + /// ✅ matches `lending_market_info` + pub(super) token_program_info: &'a AccountInfo<'info>, + + pub(super) reserve: Box, + } + + impl<'a, 'info> CancelPoolRewardAccounts<'a, 'info> { + pub(super) fn from_unchecked_iter( + program_id: &Pubkey, + params: &CancelPoolRewardParams, + iter: &mut impl Iterator>, + ) -> Result, ProgramError> { + let reserve_info = next_account_info(iter)?; + let reward_mint_info = next_account_info(iter)?; + let reward_token_destination_info = next_account_info(iter)?; + let reward_authority_info = next_account_info(iter)?; + let reward_token_vault_info = next_account_info(iter)?; + let lending_market_info = next_account_info(iter)?; + let lending_market_owner_info = next_account_info(iter)?; + let token_program_info = next_account_info(iter)?; + + let reserve = check_pool_reward_accounts_for_admin_ixs_and_unpack_reserve( + program_id, + reserve_info, + reward_mint_info, + reward_authority_info, + lending_market_info, + lending_market_owner_info, + token_program_info, + )?; + + todo!("Check that reward_token_vault_info matches reward vault pubkey stored in [Reserve]"); + + if reward_token_destination_info.owner != token_program_info.key { + msg!("Reward token destination provided must be owned by the token program"); + return Err(LendingError::InvalidTokenOwner.into()); + } + let reward_token_destination = + unpack_token_account(&reward_token_destination_info.data.borrow())?; + if reward_token_destination.owner != *lending_market_owner_info.key { + // TBD: superfluous check? + msg!("Reward token destination owner does not match the lending market owner provided"); + return Err(LendingError::InvalidAccountInput.into()); + } + if reward_token_destination.mint != *reward_mint_info.key { + msg!("Reward token destination mint does not match the reward mint provided"); + return Err(LendingError::InvalidAccountInput.into()); + } + + Ok(Self { + _priv: (), + + reserve_info, + reward_mint_info, + reward_token_destination_info, + reward_authority_info, + reward_token_vault_info, + lending_market_info, + lending_market_owner_info, + token_program_info, + + reserve, + }) + } + } + + impl CancelPoolRewardParams { + pub(super) fn new(position_kind: PositionKind, pool_reward_index: u64) -> Self { + Self { + _priv: (), + + position_kind, + pool_reward_index, + } + } + } +} + +mod close_pool_reward { + use super::*; + + pub(super) struct ClosePoolRewardParams { + _priv: (), + + position_kind: PositionKind, + pool_reward_index: u64, + } + + /// Use [Self::from_unchecked_iter] to validate the accounts. + pub(super) struct ClosePoolRewardAccounts<'a, 'info> { + _priv: (), + + /// ✅ belongs to this program + /// ✅ unpacks + /// ✅ belongs to `lending_market_info` + pub(super) reserve_info: &'a AccountInfo<'info>, + pub(super) reward_mint_info: &'a AccountInfo<'info>, + /// ✅ belongs to the token program + /// ✅ owned by `lending_market_owner_info` + /// ✅ matches `reward_mint_info` + pub(super) reward_token_destination_info: &'a AccountInfo<'info>, + /// ✅ seed of `lending_market_info`, `reserve_info`, `reward_mint_info` + pub(super) reward_authority_info: &'a AccountInfo<'info>, + /// ✅ matches reward vault pubkey stored in the [Reserve] + pub(super) reward_token_vault_info: &'a AccountInfo<'info>, + /// ✅ belongs to this program + /// ✅ unpacks + pub(super) lending_market_info: &'a AccountInfo<'info>, + /// ✅ is a signer + /// ✅ matches `lending_market_info` + pub(super) lending_market_owner_info: &'a AccountInfo<'info>, + /// ✅ matches `lending_market_info` + pub(super) token_program_info: &'a AccountInfo<'info>, + + pub(super) reserve: Box, + } + + impl<'a, 'info> ClosePoolRewardAccounts<'a, 'info> { + pub(super) fn from_unchecked_iter( + program_id: &Pubkey, + params: &ClosePoolRewardParams, + iter: &mut impl Iterator>, + ) -> Result, ProgramError> { + let reserve_info = next_account_info(iter)?; + let reward_mint_info = next_account_info(iter)?; + let reward_token_destination_info = next_account_info(iter)?; + let reward_authority_info = next_account_info(iter)?; + let reward_token_vault_info = next_account_info(iter)?; + let lending_market_info = next_account_info(iter)?; + let lending_market_owner_info = next_account_info(iter)?; + let token_program_info = next_account_info(iter)?; + + let reserve = check_pool_reward_accounts_for_admin_ixs_and_unpack_reserve( + program_id, + reserve_info, + reward_mint_info, + reward_authority_info, + lending_market_info, + lending_market_owner_info, + token_program_info, + )?; + + todo!("Check that reward_token_vault_info matches reward vault pubkey stored in [Reserve]"); + + if reward_token_destination_info.owner != token_program_info.key { + msg!("Reward token destination provided must be owned by the token program"); + return Err(LendingError::InvalidTokenOwner.into()); + } + let reward_token_destination = + unpack_token_account(&reward_token_destination_info.data.borrow())?; + if reward_token_destination.owner != *lending_market_owner_info.key { + // TBD: superfluous check? + msg!("Reward token destination owner does not match the lending market owner provided"); + return Err(LendingError::InvalidAccountInput.into()); + } + if reward_token_destination.mint != *reward_mint_info.key { + msg!("Reward token destination mint does not match the reward mint provided"); + return Err(LendingError::InvalidAccountInput.into()); + } + + Ok(Self { + _priv: (), + + reserve_info, + reward_mint_info, + reward_token_destination_info, + reward_authority_info, + reward_token_vault_info, + lending_market_info, + lending_market_owner_info, + token_program_info, + + reserve, + }) + } + } + + impl ClosePoolRewardParams { + pub(super) fn new(position_kind: PositionKind, pool_reward_index: u64) -> Self { + Self { + _priv: (), + + position_kind, + pool_reward_index, + } + } + } +} + +/// Common checks within the admin ixs are: +/// +/// * ✅ `reserve_info` belongs to this program +/// * ✅ `reserve_info` unpacks +/// * ✅ `reserve_info` belongs to `lending_market_info` +/// * ✅ `reward_authority_info` is seed of `lending_market_info`, `reserve_info`, `reward_mint_info` +/// * ✅ `lending_market_info` belongs to this program +/// * ✅ `lending_market_info` unpacks +/// * ✅ `lending_market_owner_info` is a signer +/// * ✅ `lending_market_owner_info` matches `lending_market_info` +/// * ✅ `token_program_info` matches `lending_market_info` +/// +/// To avoid unpacking reserve twice we return it. +fn check_pool_reward_accounts_for_admin_ixs_and_unpack_reserve<'info>( + program_id: &Pubkey, + reserve_info: &AccountInfo<'info>, + reward_mint_info: &AccountInfo<'info>, + reward_authority_info: &AccountInfo<'info>, + lending_market_info: &AccountInfo<'info>, + lending_market_owner_info: &AccountInfo<'info>, + token_program_info: &AccountInfo<'info>, +) -> Result, ProgramError> { + if reserve_info.owner != program_id { + msg!("Reserve provided is not owned by the lending program"); + return Err(LendingError::InvalidAccountOwner.into()); + } + let reserve = Box::new(Reserve::unpack(&reserve_info.data.borrow())?); + + if lending_market_info.owner != program_id { + msg!("Lending market provided is not owned by the lending program"); + return Err(LendingError::InvalidAccountOwner.into()); + } + let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?; + + if reserve.lending_market != *lending_market_info.key { + msg!("Reserve lending market does not match the lending market provided"); + return Err(LendingError::InvalidAccountInput.into()); + } + + if lending_market.token_program_id != *token_program_info.key { + msg!("Lending market token program does not match the token program provided"); + return Err(LendingError::InvalidTokenProgram.into()); + } + + if lending_market.owner != *lending_market_owner_info.key { + msg!("Lending market owner does not match the lending market owner provided"); + return Err(LendingError::InvalidMarketOwner.into()); + } + if !lending_market_owner_info.is_signer { + msg!("Lending market owner provided must be a signer"); + return Err(LendingError::InvalidSigner.into()); + } + + let (expected_reward_vault_authority, _bump_seed) = reward_vault_authority( + program_id, + lending_market_info.key, + reserve_info.key, + reward_mint_info.key, + ); + if expected_reward_vault_authority != *reward_authority_info.key { + msg!("Reward vault authority does not match the expected value"); + return Err(LendingError::InvalidAccountInput.into()); + } + + Ok(reserve) +} diff --git a/token-lending/sdk/src/error.rs b/token-lending/sdk/src/error.rs index 597521cd91c..10d0c327052 100644 --- a/token-lending/sdk/src/error.rs +++ b/token-lending/sdk/src/error.rs @@ -209,6 +209,9 @@ pub enum LendingError { /// Borrow Attribution Limit Not Exceeded #[error("Borrow Attribution Limit Not Exceeded")] BorrowAttributionLimitNotExceeded, + /// Pool rewards have a hard coded minimum length in seconds. + #[error("Pool reward too short")] + PoolRewardTooShort, } impl From for ProgramError { diff --git a/token-lending/sdk/src/instruction.rs b/token-lending/sdk/src/instruction.rs index 4340458ad0f..951c74d5adf 100644 --- a/token-lending/sdk/src/instruction.rs +++ b/token-lending/sdk/src/instruction.rs @@ -1,6 +1,6 @@ //! Instruction types -use crate::state::{LendingMarketMetadata, ReserveType}; +use crate::state::{LendingMarketMetadata, PositionKind, ReserveType}; use crate::{ error::LendingError, state::{RateLimiterConfig, ReserveConfig, ReserveFees}, @@ -528,6 +528,62 @@ pub enum LendingInstruction { /// amount to donate liquidity_amount: u64, }, + + // 25 + /// AddPoolReward + /// + /// * Admin only instruction. + /// * Duration is ceiled to granularity of 1 second. + /// * Can last at most 49,710 days. + /// + /// `[writable]` Reserve account. + /// `[]` Reward mint. + /// `[writable]` Reward token account owned by signer + /// `[]` Derived reserve pool reward authority. Seed: + /// * b"RewardVaultAuthority" + /// * Lending market account pubkey + /// * Reserve account pubkey + /// `[]` Uninitialized rent-exempt account that will hold reward tokens. + /// `[]` Lending market account. + /// `[signer]` Lending market owner. + /// `[]` Rent sysvar. + /// `[]` Token program. + AddPoolReward { + /// Whether this reward applies to deposits or borrows + position_kind: PositionKind, + /// If in the past according to the Clock sysvar then started immediately. + start_time_secs: u64, + /// Must be larger than start. + end_time_secs: u64, + /// Must have at least this many tokens in the source account. + token_amount: u64, + }, + + // 26 + /// ClosePoolReward + /// + /// * Admin only instruction. + /// * Can only be called if reward period is over. + /// * Can only be called if all users claimed rewards. + ClosePoolReward { + /// Whether this reward applies to deposits or borrows + position_kind: PositionKind, + /// Identifies a reward within a reserve's deposits/borrows rewards. + pool_reward_index: u64, + }, + + // 27 + /// CancelPoolReward + /// + /// * Admin only instruction. + /// * Changed the endtime of the reward to the current time. + /// * Claims unallocated rewards to the admin signer. + CancelPoolReward { + /// Whether this reward applies to deposits or borrows + position_kind: PositionKind, + /// Identifies a reward within a reserve's deposits/borrows rewards. + pool_reward_index: u64, + }, } impl LendingInstruction { @@ -786,6 +842,46 @@ impl LendingInstruction { let (liquidity_amount, _rest) = Self::unpack_u64(rest)?; Self::DonateToReserve { liquidity_amount } } + 25 => { + let (position_kind, rest) = match Self::unpack_u8(rest)? { + (0, rest) => (PositionKind::Deposit, rest), + (1, rest) => (PositionKind::Borrow, rest), + _ => return Err(LendingError::InstructionUnpackError.into()), + }; + let (start_time_secs, rest) = Self::unpack_u64(rest)?; + let (end_time_secs, rest) = Self::unpack_u64(rest)?; + let (token_amount, _rest) = Self::unpack_u64(rest)?; + Self::AddPoolReward { + position_kind, + start_time_secs, + end_time_secs, + token_amount, + } + } + 26 => { + let (position_kind, rest) = match Self::unpack_u8(rest)? { + (0, rest) => (PositionKind::Deposit, rest), + (1, rest) => (PositionKind::Borrow, rest), + _ => return Err(LendingError::InstructionUnpackError.into()), + }; + let (pool_reward_index, _rest) = Self::unpack_u64(rest)?; + Self::ClosePoolReward { + position_kind, + pool_reward_index, + } + } + 27 => { + let (position_kind, rest) = match Self::unpack_u8(rest)? { + (0, rest) => (PositionKind::Deposit, rest), + (1, rest) => (PositionKind::Borrow, rest), + _ => return Err(LendingError::InstructionUnpackError.into()), + }; + let (pool_reward_index, _rest) = Self::unpack_u64(rest)?; + Self::CancelPoolReward { + position_kind, + pool_reward_index, + } + } _ => { msg!("Instruction cannot be unpacked"); return Err(LendingError::InstructionUnpackError.into()); @@ -1085,6 +1181,34 @@ impl LendingInstruction { buf.push(24); buf.extend_from_slice(&liquidity_amount.to_le_bytes()); } + Self::AddPoolReward { + position_kind, + start_time_secs, + end_time_secs, + token_amount, + } => { + buf.push(25); + buf.extend_from_slice(&(position_kind as u8).to_le_bytes()); + buf.extend_from_slice(&start_time_secs.to_le_bytes()); + buf.extend_from_slice(&end_time_secs.to_le_bytes()); + buf.extend_from_slice(&token_amount.to_le_bytes()); + } + Self::ClosePoolReward { + position_kind, + pool_reward_index, + } => { + buf.push(26); + buf.extend_from_slice(&(position_kind as u8).to_le_bytes()); + buf.extend_from_slice(&pool_reward_index.to_le_bytes()); + } + Self::CancelPoolReward { + position_kind, + pool_reward_index, + } => { + buf.push(27); + buf.extend_from_slice(&(position_kind as u8).to_le_bytes()); + buf.extend_from_slice(&pool_reward_index.to_le_bytes()); + } } buf } diff --git a/token-lending/sdk/src/state/mod.rs b/token-lending/sdk/src/state/mod.rs index e5b96b7cd73..c18c9793dfa 100644 --- a/token-lending/sdk/src/state/mod.rs +++ b/token-lending/sdk/src/state/mod.rs @@ -3,6 +3,7 @@ mod last_update; mod lending_market; mod lending_market_metadata; +mod liquidity_mining; mod obligation; mod rate_limiter; mod reserve; @@ -10,6 +11,7 @@ mod reserve; pub use last_update::*; pub use lending_market::*; pub use lending_market_metadata::*; +pub use liquidity_mining::*; pub use obligation::*; pub use rate_limiter::*; pub use reserve::*; From 0abe52cff07682eee46ee19906a287d86e90aba9 Mon Sep 17 00:00:00 2001 From: vanitymnm Date: Tue, 25 Feb 2025 14:06:38 +0100 Subject: [PATCH 03/11] Document accounts for admin IXs --- token-lending/sdk/src/instruction.rs | 31 +++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/token-lending/sdk/src/instruction.rs b/token-lending/sdk/src/instruction.rs index 951c74d5adf..eb2720029fb 100644 --- a/token-lending/sdk/src/instruction.rs +++ b/token-lending/sdk/src/instruction.rs @@ -543,7 +543,8 @@ pub enum LendingInstruction { /// * b"RewardVaultAuthority" /// * Lending market account pubkey /// * Reserve account pubkey - /// `[]` Uninitialized rent-exempt account that will hold reward tokens. + /// * Reward mint pubkey + /// `[writable]` Uninitialized rent-exempt account that will hold reward tokens. /// `[]` Lending market account. /// `[signer]` Lending market owner. /// `[]` Rent sysvar. @@ -565,6 +566,20 @@ pub enum LendingInstruction { /// * Admin only instruction. /// * Can only be called if reward period is over. /// * Can only be called if all users claimed rewards. + /// + /// `[writable]` Reserve account. + /// `[]` Reward mint. + /// `[writable]` Reward token account owned by signer + /// `[]` Derived reserve pool reward authority. Seed: + /// * b"RewardVaultAuthority" + /// * Lending market account pubkey + /// * Reserve account pubkey + /// * Reward mint pubkey + /// `[writable]` Reward vault token account. + /// `[]` Lending market account. + /// `[signer]` Lending market owner. + /// `[]` Rent sysvar. + /// `[]` Token program. ClosePoolReward { /// Whether this reward applies to deposits or borrows position_kind: PositionKind, @@ -578,6 +593,20 @@ pub enum LendingInstruction { /// * Admin only instruction. /// * Changed the endtime of the reward to the current time. /// * Claims unallocated rewards to the admin signer. + /// + /// `[writable]` Reserve account. + /// `[]` Reward mint. + /// `[writable]` Reward token account owned by signer + /// `[]` Derived reserve pool reward authority. Seed: + /// * b"RewardVaultAuthority" + /// * Lending market account pubkey + /// * Reserve account pubkey + /// * Reward mint pubkey + /// `[writable]` Reward vault token account. + /// `[]` Lending market account. + /// `[signer]` Lending market owner. + /// `[]` Rent sysvar. + /// `[]` Token program. CancelPoolReward { /// Whether this reward applies to deposits or borrows position_kind: PositionKind, From 4e4694bdcab538aa7e7d4793271f77b3d6a9d650 Mon Sep 17 00:00:00 2001 From: vanitymnm Date: Tue, 25 Feb 2025 14:13:15 +0100 Subject: [PATCH 04/11] Checking that token vault is empty and belogs to the token program --- .../program/src/processor/liquidity_mining.rs | 49 ++++++++++++------- 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/token-lending/program/src/processor/liquidity_mining.rs b/token-lending/program/src/processor/liquidity_mining.rs index 8747a7d9e02..cd036044219 100644 --- a/token-lending/program/src/processor/liquidity_mining.rs +++ b/token-lending/program/src/processor/liquidity_mining.rs @@ -242,7 +242,6 @@ mod add_pool_reward { /// Use [Self::new] to validate the parameters. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(super) struct AddPoolRewardParams { - _priv: (), pub(super) position_kind: PositionKind, /// At least the current timestamp. pub(super) start_time_secs: u64, @@ -250,13 +249,14 @@ mod add_pool_reward { pub(super) duration_secs: u32, /// Larger than zero. pub(super) reward_token_amount: u64, + + _priv: (), } /// Use [Self::from_unchecked_iter] to validate the accounts except for /// * `reward_token_vault_info` /// * `rent_info` pub(super) struct AddPoolRewardAccounts<'a, 'info> { - _priv: (), /// ✅ belongs to this program /// ✅ unpacks /// ✅ belongs to `lending_market_info` @@ -269,6 +269,8 @@ mod add_pool_reward { pub(super) reward_token_source_info: &'a AccountInfo<'info>, /// ✅ seed of `lending_market_info`, `reserve_info`, `reward_mint_info` pub(super) reward_authority_info: &'a AccountInfo<'info>, + /// ✅ belongs to the token program + /// ✅ has no data /// ❓ we don't yet know whether it's rent exempt pub(super) reward_token_vault_info: &'a AccountInfo<'info>, /// ✅ belongs to this program @@ -285,6 +287,8 @@ mod add_pool_reward { pub(super) token_program_info: &'a AccountInfo<'info>, pub(super) reserve: Box, + + _priv: (), } impl AddPoolRewardParams { @@ -322,12 +326,12 @@ mod add_pool_reward { } Ok(Self { - _priv: (), - position_kind, start_time_secs, duration_secs, reward_token_amount, + + _priv: (), }) } } @@ -377,9 +381,16 @@ mod add_pool_reward { return Err(LendingError::InvalidAccountInput.into()); } - Ok(Self { - _priv: (), + if reward_token_vault_info.owner != token_program_info.key { + msg!("Reward token vault provided must be owned by the token program"); + return Err(LendingError::InvalidTokenOwner.into()); + } + if !reward_token_vault_info.data.borrow().is_empty() { + msg!("Reward token vault provided must be empty"); + return Err(LendingError::InvalidAccountInput.into()); + } + Ok(Self { reserve_info, reward_mint_info, reward_token_source_info, @@ -391,6 +402,8 @@ mod add_pool_reward { token_program_info, reserve, + + _priv: (), }) } } @@ -400,16 +413,14 @@ mod cancel_pool_reward { use super::*; pub(super) struct CancelPoolRewardParams { - _priv: (), - position_kind: PositionKind, pool_reward_index: u64, + + _priv: (), } /// Use [Self::from_unchecked_iter] to validate the accounts. pub(super) struct CancelPoolRewardAccounts<'a, 'info> { - _priv: (), - /// ✅ belongs to this program /// ✅ unpacks /// ✅ belongs to `lending_market_info` @@ -433,6 +444,8 @@ mod cancel_pool_reward { pub(super) token_program_info: &'a AccountInfo<'info>, pub(super) reserve: Box, + + _priv: (), } impl<'a, 'info> CancelPoolRewardAccounts<'a, 'info> { @@ -498,10 +511,10 @@ mod cancel_pool_reward { impl CancelPoolRewardParams { pub(super) fn new(position_kind: PositionKind, pool_reward_index: u64) -> Self { Self { - _priv: (), - position_kind, pool_reward_index, + + _priv: (), } } } @@ -511,10 +524,10 @@ mod close_pool_reward { use super::*; pub(super) struct ClosePoolRewardParams { - _priv: (), - position_kind: PositionKind, pool_reward_index: u64, + + _priv: (), } /// Use [Self::from_unchecked_iter] to validate the accounts. @@ -590,8 +603,6 @@ mod close_pool_reward { } Ok(Self { - _priv: (), - reserve_info, reward_mint_info, reward_token_destination_info, @@ -602,6 +613,8 @@ mod close_pool_reward { token_program_info, reserve, + + _priv: (), }) } } @@ -609,10 +622,10 @@ mod close_pool_reward { impl ClosePoolRewardParams { pub(super) fn new(position_kind: PositionKind, pool_reward_index: u64) -> Self { Self { - _priv: (), - position_kind, pool_reward_index, + + _priv: (), } } } From 9d0c84c5d7b208731c7223b2aec4f054788d6004 Mon Sep 17 00:00:00 2001 From: vanitymnm Date: Wed, 26 Feb 2025 10:43:38 +0100 Subject: [PATCH 05/11] Note on running out of IDs --- token-lending/sdk/src/state/liquidity_mining.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/token-lending/sdk/src/state/liquidity_mining.rs b/token-lending/sdk/src/state/liquidity_mining.rs index c6c66a20083..44bc6e7db74 100644 --- a/token-lending/sdk/src/state/liquidity_mining.rs +++ b/token-lending/sdk/src/state/liquidity_mining.rs @@ -21,6 +21,15 @@ pub struct PoolRewardManager { /// /// This helps us distinguish between two distinct rewards in the same array /// index across time. +/// +/// # Wrapping +/// There are two strategies to handle wrapping: +/// 1. Consider the associated slot locked forever +/// 2. Go back to 0. +/// +/// Given that one reward lasts at least 1 hour we've got about half a +/// million years before we need to worry about wrapping in a single slot. +/// I'd call that someone elses problem. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub struct PoolRewardId(pub u32); From eff2b19ffc1846a55b531ec9686aef86de7bc337 Mon Sep 17 00:00:00 2001 From: vanitymnm Date: Wed, 26 Feb 2025 10:46:25 +0100 Subject: [PATCH 06/11] Removing libssl1.1 dependency as it can no longer be installed on CI --- ci/install-build-deps.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/ci/install-build-deps.sh b/ci/install-build-deps.sh index cd9c815df08..3962591173a 100755 --- a/ci/install-build-deps.sh +++ b/ci/install-build-deps.sh @@ -7,7 +7,6 @@ sudo apt-add-repository "deb http://apt.llvm.org/bionic/ llvm-toolchain-bionic-1 sudo apt-get update sudo apt-get install -y openssl --allow-unauthenticated sudo apt-get install -y libssl-dev --allow-unauthenticated -sudo apt-get install -y libssl1.1 --allow-unauthenticated sudo apt-get install -y libudev-dev sudo apt-get install -y binutils-dev sudo apt-get install -y libunwind-dev From 6a51385ce3a076cd110994e4d7be43fdb835a857 Mon Sep 17 00:00:00 2001 From: vanitymnm Date: Wed, 26 Feb 2025 10:57:55 +0100 Subject: [PATCH 07/11] Improving clarity of PositionKind unpacking --- .../program/src/processor/liquidity_mining.rs | 2 +- token-lending/sdk/src/instruction.rs | 37 +++++++++++-------- token-lending/sdk/src/state/obligation.rs | 12 ++++++ 3 files changed, 35 insertions(+), 16 deletions(-) diff --git a/token-lending/program/src/processor/liquidity_mining.rs b/token-lending/program/src/processor/liquidity_mining.rs index cd036044219..417a1c04cf6 100644 --- a/token-lending/program/src/processor/liquidity_mining.rs +++ b/token-lending/program/src/processor/liquidity_mining.rs @@ -223,7 +223,7 @@ fn reward_vault_authority( ) } -fn reward_vault_authority_seeds<'a, 'keys>( +fn reward_vault_authority_seeds<'keys>( lending_market_key: &'keys Pubkey, reserve_key: &'keys Pubkey, reward_mint_key: &'keys Pubkey, diff --git a/token-lending/sdk/src/instruction.rs b/token-lending/sdk/src/instruction.rs index eb2720029fb..e3f5ad7cede 100644 --- a/token-lending/sdk/src/instruction.rs +++ b/token-lending/sdk/src/instruction.rs @@ -872,11 +872,7 @@ impl LendingInstruction { Self::DonateToReserve { liquidity_amount } } 25 => { - let (position_kind, rest) = match Self::unpack_u8(rest)? { - (0, rest) => (PositionKind::Deposit, rest), - (1, rest) => (PositionKind::Borrow, rest), - _ => return Err(LendingError::InstructionUnpackError.into()), - }; + let (position_kind, rest) = Self::unpack_try_from_u8(rest)?; let (start_time_secs, rest) = Self::unpack_u64(rest)?; let (end_time_secs, rest) = Self::unpack_u64(rest)?; let (token_amount, _rest) = Self::unpack_u64(rest)?; @@ -888,11 +884,7 @@ impl LendingInstruction { } } 26 => { - let (position_kind, rest) = match Self::unpack_u8(rest)? { - (0, rest) => (PositionKind::Deposit, rest), - (1, rest) => (PositionKind::Borrow, rest), - _ => return Err(LendingError::InstructionUnpackError.into()), - }; + let (position_kind, rest) = Self::unpack_try_from_u8(rest)?; let (pool_reward_index, _rest) = Self::unpack_u64(rest)?; Self::ClosePoolReward { position_kind, @@ -900,11 +892,7 @@ impl LendingInstruction { } } 27 => { - let (position_kind, rest) = match Self::unpack_u8(rest)? { - (0, rest) => (PositionKind::Deposit, rest), - (1, rest) => (PositionKind::Borrow, rest), - _ => return Err(LendingError::InstructionUnpackError.into()), - }; + let (position_kind, rest) = Self::unpack_try_from_u8(rest)?; let (pool_reward_index, _rest) = Self::unpack_u64(rest)?; Self::CancelPoolReward { position_kind, @@ -960,6 +948,25 @@ impl LendingInstruction { Ok((value, rest)) } + fn unpack_try_from_u8(input: &[u8]) -> Result<(T, &[u8]), ProgramError> + where + T: TryFrom, + ProgramError: From<>::Error>, + { + if input.is_empty() { + msg!("u8 cannot be unpacked"); + return Err(LendingError::InstructionUnpackError.into()); + } + let (bytes, rest) = input.split_at(1); + let value = bytes + .get(..1) + .and_then(|slice| slice.try_into().ok()) + .map(u8::from_le_bytes) + .ok_or(LendingError::InstructionUnpackError)?; + + Ok((T::try_from(value)?, rest)) + } + fn unpack_bytes32(input: &[u8]) -> Result<(&[u8; 32], &[u8]), ProgramError> { if input.len() < 32 { msg!("32 bytes cannot be unpacked"); diff --git a/token-lending/sdk/src/state/obligation.rs b/token-lending/sdk/src/state/obligation.rs index 14af0282713..31620b61bcb 100644 --- a/token-lending/sdk/src/state/obligation.rs +++ b/token-lending/sdk/src/state/obligation.rs @@ -655,6 +655,18 @@ impl Pack for Obligation { } } +impl TryFrom for PositionKind { + type Error = ProgramError; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(PositionKind::Deposit), + 1 => Ok(PositionKind::Borrow), + _ => Err(LendingError::InstructionUnpackError.into()), + } + } +} + #[cfg(test)] mod test { use super::*; From fd2217342c8925e617ac1738f19cd12057999199 Mon Sep 17 00:00:00 2001 From: vanitymnm Date: Wed, 26 Feb 2025 11:13:20 +0100 Subject: [PATCH 08/11] Describing what LM is --- .../program/src/processor/liquidity_mining.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/token-lending/program/src/processor/liquidity_mining.rs b/token-lending/program/src/processor/liquidity_mining.rs index 417a1c04cf6..5c637a6856c 100644 --- a/token-lending/program/src/processor/liquidity_mining.rs +++ b/token-lending/program/src/processor/liquidity_mining.rs @@ -1,3 +1,18 @@ +//! Liquidity mining is a feature where depositors and borrowers are rewarded +//! for using the protocol. +//! The rewards are in the form of tokens that a lending market owner can attach +//! to each reserve. +//! +//! The feature is built with reference to the [Suilend][suilend-lm] +//! implementation of the same feature. +//! +//! There are three admin-only ixs: +//! - [add_pool_reward] +//! - [cancel_pool_reward] +//! - [close_pool_reward] +//! +//! [suilend-lm]: https://github.com/solendprotocol/suilend/blob/dc53150416f352053ac3acbb320ee143409c4a5d/contracts/suilend/sources/liquidity_mining.move#L2 + use crate::processor::{ assert_rent_exempt, spl_token_close_account, spl_token_init_account, spl_token_transfer, TokenCloseAccountParams, TokenInitializeAccountParams, TokenTransferParams, From 8ee16b03ad0128585c688aec52be17c8f7d05d41 Mon Sep 17 00:00:00 2001 From: vanity mnm Date: Wed, 26 Feb 2025 11:15:57 +0100 Subject: [PATCH 09/11] Improving documentation and wording --- token-lending/sdk/src/state/liquidity_mining.rs | 5 +++-- token-lending/sdk/src/state/obligation.rs | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/token-lending/sdk/src/state/liquidity_mining.rs b/token-lending/sdk/src/state/liquidity_mining.rs index 44bc6e7db74..c4a73849ebc 100644 --- a/token-lending/sdk/src/state/liquidity_mining.rs +++ b/token-lending/sdk/src/state/liquidity_mining.rs @@ -2,6 +2,7 @@ use crate::math::Decimal; use solana_program::pubkey::Pubkey; /// Determines the size of [PoolRewardManager] +/// TODO: This should be configured when we're dealing with migrations later but we should aim for 50. const MAX_REWARDS: usize = 44; /// Each reserve has two managers: @@ -27,9 +28,9 @@ pub struct PoolRewardManager { /// 1. Consider the associated slot locked forever /// 2. Go back to 0. /// -/// Given that one reward lasts at least 1 hour we've got about half a +/// Given that one reward lasts at least 1 hour we've got at least half a /// million years before we need to worry about wrapping in a single slot. -/// I'd call that someone elses problem. +/// I'd call that someone else's problem. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub struct PoolRewardId(pub u32); diff --git a/token-lending/sdk/src/state/obligation.rs b/token-lending/sdk/src/state/obligation.rs index 14af0282713..e17bcfa2f21 100644 --- a/token-lending/sdk/src/state/obligation.rs +++ b/token-lending/sdk/src/state/obligation.rs @@ -423,6 +423,7 @@ impl ObligationLiquidity { const OBLIGATION_COLLATERAL_LEN: usize = 88; // 32 + 8 + 16 + 32 const OBLIGATION_LIQUIDITY_LEN: usize = 112; // 32 + 16 + 16 + 16 + 32 +/// This is the size of the account _before_ LM feature was added. const OBLIGATION_LEN_V1: usize = 1300; // 1 + 8 + 1 + 32 + 32 + 16 + 16 + 16 + 16 + 64 + 1 + 1 + (88 * 1) + (112 * 9) // @TODO: break this up by obligation / collateral / liquidity https://git.io/JOCca impl Pack for Obligation { From 7a4b404d0bb01d906a676db081ced54e57aa47e1 Mon Sep 17 00:00:00 2001 From: vanitymnm Date: Wed, 26 Feb 2025 11:18:01 +0100 Subject: [PATCH 10/11] Removing duplicate code --- token-lending/sdk/src/instruction.rs | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/token-lending/sdk/src/instruction.rs b/token-lending/sdk/src/instruction.rs index e3f5ad7cede..de75c22a66e 100644 --- a/token-lending/sdk/src/instruction.rs +++ b/token-lending/sdk/src/instruction.rs @@ -953,18 +953,8 @@ impl LendingInstruction { T: TryFrom, ProgramError: From<>::Error>, { - if input.is_empty() { - msg!("u8 cannot be unpacked"); - return Err(LendingError::InstructionUnpackError.into()); - } - let (bytes, rest) = input.split_at(1); - let value = bytes - .get(..1) - .and_then(|slice| slice.try_into().ok()) - .map(u8::from_le_bytes) - .ok_or(LendingError::InstructionUnpackError)?; - - Ok((T::try_from(value)?, rest)) + let (byte, rest) = Self::unpack_u8(input)?; + Ok((T::try_from(byte)?, rest)) } fn unpack_bytes32(input: &[u8]) -> Result<(&[u8; 32], &[u8]), ProgramError> { From 7833ca233bbe14c181d5f4e763399eca6b0ed1e6 Mon Sep 17 00:00:00 2001 From: vanitymnm Date: Fri, 28 Feb 2025 11:27:11 +0100 Subject: [PATCH 11/11] Fixing error names --- token-lending/program/src/processor.rs | 2 +- token-lending/program/src/processor/liquidity_mining.rs | 2 +- token-lending/sdk/src/error.rs | 7 ++++++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 9658fdc5bae..470558d4f38 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -3500,7 +3500,7 @@ fn spl_token_close_account(params: TokenCloseAccountParams<'_, '_>) -> ProgramRe authority_signer_seeds, ); - result.map_err(|_| LendingError::TokenTransferFailed.into()) + result.map_err(|_| LendingError::CloseTokenAccountFailed.into()) } fn is_cpi_call( diff --git a/token-lending/program/src/processor/liquidity_mining.rs b/token-lending/program/src/processor/liquidity_mining.rs index 5c637a6856c..76defa6c5e6 100644 --- a/token-lending/program/src/processor/liquidity_mining.rs +++ b/token-lending/program/src/processor/liquidity_mining.rs @@ -332,7 +332,7 @@ mod add_pool_reward { }; 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::PoolRewardTooShort.into()); + return Err(LendingError::PoolRewardPeriodTooShort.into()); } if reward_token_amount == 0 { diff --git a/token-lending/sdk/src/error.rs b/token-lending/sdk/src/error.rs index 10d0c327052..e5f6302199c 100644 --- a/token-lending/sdk/src/error.rs +++ b/token-lending/sdk/src/error.rs @@ -211,7 +211,12 @@ pub enum LendingError { BorrowAttributionLimitNotExceeded, /// Pool rewards have a hard coded minimum length in seconds. #[error("Pool reward too short")] - PoolRewardTooShort, + PoolRewardPeriodTooShort, + + // 60 + /// Cannot close token account + #[error("Cannot close token account")] + CloseTokenAccountFailed, } impl From for ProgramError {