From 49a6c03b65b1f082e9c3fad1c2a6455bf77997a8 Mon Sep 17 00:00:00 2001 From: vanitymnm Date: Tue, 25 Feb 2025 13:54:19 +0100 Subject: [PATCH 01/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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 96372285f0ae2133dfa0cb82a194bec902ed3144 Mon Sep 17 00:00:00 2001 From: vanitymnm Date: Fri, 28 Feb 2025 11:16:49 +0100 Subject: [PATCH 11/30] Adding math for reward accrual --- .../program/src/processor/liquidity_mining.rs | 5 +- .../sdk/src/state/liquidity_mining.rs | 190 +++++++++++++++++- 2 files changed, 188 insertions(+), 7 deletions(-) diff --git a/token-lending/program/src/processor/liquidity_mining.rs b/token-lending/program/src/processor/liquidity_mining.rs index 5c637a6856c..1cb65d8b54a 100644 --- a/token-lending/program/src/processor/liquidity_mining.rs +++ b/token-lending/program/src/processor/liquidity_mining.rs @@ -38,9 +38,6 @@ use solend_sdk::{ 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 @@ -252,6 +249,8 @@ fn reward_vault_authority_seeds<'keys>( } mod add_pool_reward { + use solend_sdk::state::MIN_REWARD_PERIOD_SECS; + use super::*; /// Use [Self::new] to validate the parameters. diff --git a/token-lending/sdk/src/state/liquidity_mining.rs b/token-lending/sdk/src/state/liquidity_mining.rs index c4a73849ebc..53b4ddf94f9 100644 --- a/token-lending/sdk/src/state/liquidity_mining.rs +++ b/token-lending/sdk/src/state/liquidity_mining.rs @@ -1,5 +1,11 @@ -use crate::math::Decimal; -use solana_program::pubkey::Pubkey; +use crate::{ + error::LendingError, + math::{Decimal, TryAdd, TryDiv, TryMul, TrySub}, +}; +use solana_program::{clock::Clock, program_error::ProgramError, pubkey::Pubkey}; + +/// Cannot create a reward shorter than this. +pub const MIN_REWARD_PERIOD_SECS: u64 = 3_600; /// Determines the size of [PoolRewardManager] /// TODO: This should be configured when we're dealing with migrations later but we should aim for 50. @@ -28,8 +34,8 @@ 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 at least half a -/// million years before we need to worry about wrapping in a single slot. +/// Given that one reward lasts at [MIN_REWARD_PERIOD_SECS] 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 else's problem. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub struct PoolRewardId(pub u32); @@ -127,8 +133,154 @@ pub struct UserReward { pub cumulative_rewards_per_share: Decimal, } +impl PoolRewardManager { + /// Should be updated before any interaction with rewards. + fn update(&mut self, clock: &Clock) -> Result<(), ProgramError> { + let curr_unix_timestamp_secs = clock.unix_timestamp as u64; + + if self.last_update_time_secs >= curr_unix_timestamp_secs { + return Ok(()); + } + + if self.total_shares == 0 { + self.last_update_time_secs = curr_unix_timestamp_secs; + return Ok(()); + } + + let last_update_time_secs = self.last_update_time_secs; + + // get rewards that started already and did not finish yet + let running_rewards = self + .pool_rewards + .iter_mut() + .filter_map(|r| match r { + PoolRewardSlot::Occupied(reward) => Some(reward), + _ => None, + }) + .filter(|r| curr_unix_timestamp_secs > r.start_time_secs) + .filter(|r| last_update_time_secs < (r.start_time_secs + r.duration_secs as u64)); + + for reward in running_rewards { + let end_time_secs = reward.start_time_secs + reward.duration_secs as u64; + let time_passed_secs = curr_unix_timestamp_secs + .min(end_time_secs) + .checked_sub(reward.start_time_secs.max(last_update_time_secs)) + .ok_or(LendingError::MathOverflow)?; + + // We assert that a reward has been running for at least [MIN_REWARD_PERIOD_SECS]. + // This won't error on division by zero. + let unlocked_rewards = Decimal::from(reward.total_rewards) + .try_mul(Decimal::from(time_passed_secs))? + .try_div(Decimal::from(end_time_secs - reward.start_time_secs))?; + + reward.allocated_rewards = reward.allocated_rewards.try_add(unlocked_rewards)?; + + reward.cumulative_rewards_per_share = reward + .cumulative_rewards_per_share + .try_add(unlocked_rewards.try_div(Decimal::from(self.total_shares))?)?; + } + + self.last_update_time_secs = curr_unix_timestamp_secs; + + Ok(()) + } +} + +enum CreatingNewRewardManager { + Yes, + No, +} + +impl UserRewardManager { + /// Should be updated before any interaction with rewards. + /// + /// # Assumption + /// Invoker has checked that this [PoolRewardManager] matches the + /// [UserRewardManager]. + fn update( + &mut self, + pool_reward_manager: &mut PoolRewardManager, + clock: &Clock, + creating_new_reward_manager: CreatingNewRewardManager, + ) -> Result<(), ProgramError> { + pool_reward_manager.update(clock)?; + + let curr_unix_timestamp_secs = clock.unix_timestamp as u64; + + if matches!(creating_new_reward_manager, CreatingNewRewardManager::No) + && curr_unix_timestamp_secs == self.last_update_time_secs + { + return Ok(()); + } + + self.rewards + .resize_with(pool_reward_manager.pool_rewards.len(), || None); + + for (reward_index, pool_reward) in pool_reward_manager.pool_rewards.iter_mut().enumerate() { + let PoolRewardSlot::Occupied(pool_reward) = pool_reward else { + // no reward to track + continue; + }; + + let end_time_secs = pool_reward.start_time_secs + pool_reward.duration_secs as u64; + + match self.rewards.get_mut(reward_index) { + None => unreachable!("We've just resized the rewards."), + Some(None) if self.last_update_time_secs > end_time_secs => { + // reward period ended, skip + } + Some(None) => { + // user did not yet start accruing rewards + + let new_user_reward = UserReward { + pool_reward_id: pool_reward.id, + cumulative_rewards_per_share: pool_reward.cumulative_rewards_per_share, + earned_rewards: if self.last_update_time_secs <= pool_reward.start_time_secs + { + pool_reward + .cumulative_rewards_per_share + .try_mul(Decimal::from(self.share))? + } else { + debug_assert!(matches!( + creating_new_reward_manager, + CreatingNewRewardManager::Yes + )); + Decimal::zero() + }, + }; + + self.rewards[reward_index] = Some(new_user_reward); + + pool_reward.num_user_reward_managers += 1; + } + Some(Some(user_reward)) => { + // user is already accruing rewards, add the difference + + let new_reward_amount = pool_reward + .cumulative_rewards_per_share + .try_sub(user_reward.cumulative_rewards_per_share)? + .try_mul(Decimal::from(self.share))?; + + user_reward.earned_rewards = + user_reward.earned_rewards.try_add(new_reward_amount)?; + + user_reward.cumulative_rewards_per_share = + pool_reward.cumulative_rewards_per_share; + } + } + } + + self.last_update_time_secs = curr_unix_timestamp_secs; + + Ok(()) + } +} + #[cfg(test)] mod tests { + //! TODO: Rewrite these tests from their Suilend counterparts. + //! TODO: Calculate test coverage and add tests for missing branches. + use super::*; #[test] @@ -144,4 +296,34 @@ mod tests { println!("assert {required_realloc} <= {MAX_REALLOC}"); assert!(required_realloc <= MAX_REALLOC); } + + #[test] + fn it_tests_pool_reward_manager_basic() { + // TODO: rewrite Suilend "test_pool_reward_manager_basic" test + } + + #[test] + fn it_tests_pool_reward_manager_multiple_rewards() { + // TODO: rewrite Suilend "test_pool_reward_manager_multiple_rewards" + } + + #[test] + fn it_tests_pool_reward_zero_share() { + // TODO: rewrite Suilend "test_pool_reward_manager_zero_share" + } + + #[test] + fn it_tests_pool_reward_manager_auto_farm() { + // TODO: rewrite Suilend "test_pool_reward_manager_auto_farm" + } + + #[test] + fn it_tests_add_too_many_pool_rewards() { + // TODO: rewrite Suilend "test_add_too_many_pool_rewards" + } + + #[test] + fn it_tests_pool_reward_manager_cancel_and_close_regression() { + // TODO: rewrite Suilend "test_pool_reward_manager_cancel_and_close_regression" + } } From ef72f92fdd317719f825425be1bba489ad4f97d8 Mon Sep 17 00:00:00 2001 From: vanitymnm Date: Fri, 28 Feb 2025 11:24:02 +0100 Subject: [PATCH 12/30] Improving comments wording --- .../sdk/src/state/liquidity_mining.rs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/token-lending/sdk/src/state/liquidity_mining.rs b/token-lending/sdk/src/state/liquidity_mining.rs index 53b4ddf94f9..860c0b24e4f 100644 --- a/token-lending/sdk/src/state/liquidity_mining.rs +++ b/token-lending/sdk/src/state/liquidity_mining.rs @@ -167,8 +167,8 @@ impl PoolRewardManager { .checked_sub(reward.start_time_secs.max(last_update_time_secs)) .ok_or(LendingError::MathOverflow)?; - // We assert that a reward has been running for at least [MIN_REWARD_PERIOD_SECS]. - // This won't error on division by zero. + // When adding a reward we assert that a reward lasts for at least [MIN_REWARD_PERIOD_SECS]. + // Hence this won't error on overflow nor on division by zero. let unlocked_rewards = Decimal::from(reward.total_rewards) .try_mul(Decimal::from(time_passed_secs))? .try_div(Decimal::from(end_time_secs - reward.start_time_secs))?; @@ -186,7 +186,8 @@ impl PoolRewardManager { } } -enum CreatingNewRewardManager { +enum CreatingNewUserRewardManager { + /// If we are creating a [UserRewardManager] then we want to populate it. Yes, No, } @@ -201,14 +202,16 @@ impl UserRewardManager { &mut self, pool_reward_manager: &mut PoolRewardManager, clock: &Clock, - creating_new_reward_manager: CreatingNewRewardManager, + creating_new_reward_manager: CreatingNewUserRewardManager, ) -> Result<(), ProgramError> { pool_reward_manager.update(clock)?; let curr_unix_timestamp_secs = clock.unix_timestamp as u64; - if matches!(creating_new_reward_manager, CreatingNewRewardManager::No) - && curr_unix_timestamp_secs == self.last_update_time_secs + if matches!( + creating_new_reward_manager, + CreatingNewUserRewardManager::No + ) && curr_unix_timestamp_secs == self.last_update_time_secs { return Ok(()); } @@ -243,12 +246,13 @@ impl UserRewardManager { } else { debug_assert!(matches!( creating_new_reward_manager, - CreatingNewRewardManager::Yes + CreatingNewUserRewardManager::Yes )); Decimal::zero() }, }; + // we resized this vector to match the pool rewards self.rewards[reward_index] = Some(new_user_reward); pool_reward.num_user_reward_managers += 1; From 7833ca233bbe14c181d5f4e763399eca6b0ed1e6 Mon Sep 17 00:00:00 2001 From: vanitymnm Date: Fri, 28 Feb 2025 11:27:11 +0100 Subject: [PATCH 13/30] 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 { From d8901cce582f55aac44f051f284b2d37d4c9372a Mon Sep 17 00:00:00 2001 From: vanitymnm Date: Wed, 5 Mar 2025 12:58:47 +0100 Subject: [PATCH 14/30] Merging stash --- .../sdk/src/state/liquidity_mining.rs | 57 ++++- token-lending/sdk/src/state/reserve.rs | 226 ++++++++++-------- 2 files changed, 169 insertions(+), 114 deletions(-) diff --git a/token-lending/sdk/src/state/liquidity_mining.rs b/token-lending/sdk/src/state/liquidity_mining.rs index 860c0b24e4f..a348452394f 100644 --- a/token-lending/sdk/src/state/liquidity_mining.rs +++ b/token-lending/sdk/src/state/liquidity_mining.rs @@ -2,18 +2,19 @@ use crate::{ error::LendingError, math::{Decimal, TryAdd, TryDiv, TryMul, TrySub}, }; +use solana_program::program_pack::{Pack, Sealed}; use solana_program::{clock::Clock, program_error::ProgramError, pubkey::Pubkey}; -/// Cannot create a reward shorter than this. -pub const MIN_REWARD_PERIOD_SECS: u64 = 3_600; - /// 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; +const MAX_REWARDS: usize = 50; +/// Cannot create a reward shorter than this. +pub const MIN_REWARD_PERIOD_SECS: u64 = 3_600; /// Each reserve has two managers: /// - one for deposits /// - one for borrows +#[derive(Clone, Debug, PartialEq)] pub struct PoolRewardManager { /// Is updated when we change user shares in the reserve. pub total_shares: u64, @@ -46,6 +47,7 @@ pub struct PoolRewardId(pub u32); /// reward is vacant or not to save space. /// /// If the pubkey is eq to default pubkey then slot is vacant. +#[derive(Clone, Debug, PartialEq)] pub enum PoolRewardSlot { /// New reward can be added to this slot. Vacant { @@ -57,6 +59,16 @@ pub enum PoolRewardSlot { } /// Tracks rewards in a specific mint over some period of time. +/// +/// # Reward cancellation +/// In Suilend we also store the amount of rewards that have been made available +/// to users already. +/// We keep adding `(total_rewards * time_passed) / (total_time)` every +/// time someone interacts with the manager. +/// This value is used to transfer the unallocated rewards to the admin. +/// However, this can be calculated dynamically which avoids storing extra +/// [Decimal] on each [PoolReward]. +#[derive(Clone, Debug, Default, PartialEq)] pub struct PoolReward { /// Unique ID for this slot that has never been used before, and will never /// be used again. @@ -77,12 +89,6 @@ pub struct PoolReward { /// 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 @@ -173,8 +179,6 @@ impl PoolRewardManager { .try_mul(Decimal::from(time_passed_secs))? .try_div(Decimal::from(end_time_secs - reward.start_time_secs))?; - reward.allocated_rewards = reward.allocated_rewards.try_add(unlocked_rewards)?; - reward.cumulative_rewards_per_share = reward .cumulative_rewards_per_share .try_add(unlocked_rewards.try_div(Decimal::from(self.total_shares))?)?; @@ -280,6 +284,35 @@ impl UserRewardManager { } } +impl PoolReward { + const LEN: usize = std::mem::size_of::(); +} + +impl Default for PoolRewardManager { + fn default() -> Self { + Self { + total_shares: 0, + last_update_time_secs: 0, + pool_rewards: std::array::from_fn(|_| PoolRewardSlot::default()), + } + } +} + +impl Default for PoolRewardSlot { + fn default() -> Self { + Self::Vacant { + last_pool_reward_id: PoolRewardId(0), + } + } +} + +impl Sealed for PoolRewardManager {} + +impl Pack for PoolRewardManager { + /// total_shares + last_update_time_secs + pool_rewards. + const LEN: usize = 8 + 8 + MAX_REWARDS * PoolReward::LEN; +} + #[cfg(test)] mod tests { //! TODO: Rewrite these tests from their Suilend counterparts. diff --git a/token-lending/sdk/src/state/reserve.rs b/token-lending/sdk/src/state/reserve.rs index 2e53ba3e19d..0febc9606b4 100644 --- a/token-lending/sdk/src/state/reserve.rs +++ b/token-lending/sdk/src/state/reserve.rs @@ -60,6 +60,14 @@ pub struct Reserve { pub rate_limiter: RateLimiter, /// Attributed borrows in USD pub attributed_borrow_value: Decimal, + /// Contains liquidity mining rewards for deposits. + /// + /// Added @v2.1.0 + pub deposits_pool_reward_manager: PoolRewardManager, + /// Contains liquidity mining rewards for borrows. + /// + /// Added @v2.1.0 + pub borrows_pool_reward_manager: PoolRewardManager, } impl Reserve { @@ -1229,14 +1237,18 @@ impl IsInitialized for Reserve { } /// 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 +const RESERVE_LEN_V2_0_2: 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 _after_ LM feature was added. +const RESERVE_LEN_v2_1_0: usize = RESERVE_LEN_V2_0_2 + PoolRewardManager::LEN * 2; + impl Pack for Reserve { - const LEN: usize = RESERVE_LEN_V1; + const LEN: usize = RESERVE_LEN_V2_0_2; // @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 + // @v2.1.0 TODO: add discriminator fn pack_into_slice(&self, output: &mut [u8]) { - let output = array_mut_ref![output, 0, RESERVE_LEN_V1]; + let output = array_mut_ref![output, 0, RESERVE_LEN_V2_0_2]; #[allow(clippy::ptr_offset_with_cast)] let ( version, @@ -1429,7 +1441,7 @@ impl Pack for Reserve { // 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_V1]; + let input_v2_0_2 = array_ref![input, 0, RESERVE_LEN_V2_0_2]; #[allow(clippy::ptr_offset_with_cast)] let ( version, @@ -1481,7 +1493,7 @@ impl Pack for Reserve { config_attributed_borrow_limit_close, _padding, ) = array_refs![ - input, + input_v2_0_2, 1, 8, 1, @@ -1553,110 +1565,118 @@ impl Pack for Reserve { u8::from_le_bytes(*config_max_liquidation_threshold), ); - Ok(Self { - version, - last_update: LastUpdate { - slot: u64::from_le_bytes(*last_update_slot), - stale: unpack_bool(last_update_stale)?, + let last_update = LastUpdate { + slot: u64::from_le_bytes(*last_update_slot), + stale: unpack_bool(last_update_stale)?, + }; + + let liquidity = ReserveLiquidity { + mint_pubkey: Pubkey::new_from_array(*liquidity_mint_pubkey), + mint_decimals: u8::from_le_bytes(*liquidity_mint_decimals), + supply_pubkey: Pubkey::new_from_array(*liquidity_supply_pubkey), + pyth_oracle_pubkey: Pubkey::new_from_array(*liquidity_pyth_oracle_pubkey), + switchboard_oracle_pubkey: Pubkey::new_from_array(*liquidity_switchboard_oracle_pubkey), + available_amount: u64::from_le_bytes(*liquidity_available_amount), + borrowed_amount_wads: unpack_decimal(liquidity_borrowed_amount_wads), + cumulative_borrow_rate_wads: unpack_decimal(liquidity_cumulative_borrow_rate_wads), + accumulated_protocol_fees_wads: unpack_decimal( + liquidity_accumulated_protocol_fees_wads, + ), + market_price: unpack_decimal(liquidity_market_price), + smoothed_market_price: unpack_decimal(liquidity_smoothed_market_price), + extra_market_price: match liquidity_extra_market_price_flag[0] { + 0 => None, + 1 => Some(unpack_decimal(liquidity_extra_market_price)), + _ => { + msg!("Invalid extra market price flag"); + return Err(ProgramError::InvalidAccountData); + } }, - lending_market: Pubkey::new_from_array(*lending_market), - liquidity: ReserveLiquidity { - mint_pubkey: Pubkey::new_from_array(*liquidity_mint_pubkey), - mint_decimals: u8::from_le_bytes(*liquidity_mint_decimals), - supply_pubkey: Pubkey::new_from_array(*liquidity_supply_pubkey), - pyth_oracle_pubkey: Pubkey::new_from_array(*liquidity_pyth_oracle_pubkey), - switchboard_oracle_pubkey: Pubkey::new_from_array( - *liquidity_switchboard_oracle_pubkey, - ), - available_amount: u64::from_le_bytes(*liquidity_available_amount), - borrowed_amount_wads: unpack_decimal(liquidity_borrowed_amount_wads), - cumulative_borrow_rate_wads: unpack_decimal(liquidity_cumulative_borrow_rate_wads), - accumulated_protocol_fees_wads: unpack_decimal( - liquidity_accumulated_protocol_fees_wads, - ), - market_price: unpack_decimal(liquidity_market_price), - smoothed_market_price: unpack_decimal(liquidity_smoothed_market_price), - extra_market_price: match liquidity_extra_market_price_flag[0] { - 0 => None, - 1 => Some(unpack_decimal(liquidity_extra_market_price)), - _ => { - msg!("Invalid extra market price flag"); - return Err(ProgramError::InvalidAccountData); - } - }, + }; + + let collateral = ReserveCollateral { + mint_pubkey: Pubkey::new_from_array(*collateral_mint_pubkey), + mint_total_supply: u64::from_le_bytes(*collateral_mint_total_supply), + supply_pubkey: Pubkey::new_from_array(*collateral_supply_pubkey), + }; + + let config = ReserveConfig { + optimal_utilization_rate, + max_utilization_rate: max( + optimal_utilization_rate, + u8::from_le_bytes(*config_max_utilization_rate), + ), + loan_to_value_ratio: u8::from_le_bytes(*config_loan_to_value_ratio), + liquidation_bonus, + max_liquidation_bonus, + liquidation_threshold, + max_liquidation_threshold, + min_borrow_rate: u8::from_le_bytes(*config_min_borrow_rate), + optimal_borrow_rate: u8::from_le_bytes(*config_optimal_borrow_rate), + max_borrow_rate, + super_max_borrow_rate: max( + max_borrow_rate as u64, + u64::from_le_bytes(*config_super_max_borrow_rate), + ), + fees: ReserveFees { + borrow_fee_wad: u64::from_le_bytes(*config_fees_borrow_fee_wad), + flash_loan_fee_wad: u64::from_le_bytes(*config_fees_flash_loan_fee_wad), + host_fee_percentage: u8::from_le_bytes(*config_fees_host_fee_percentage), + }, + deposit_limit: u64::from_le_bytes(*config_deposit_limit), + borrow_limit: u64::from_le_bytes(*config_borrow_limit), + fee_receiver: Pubkey::new_from_array(*config_fee_receiver), + protocol_liquidation_fee: min( + u8::from_le_bytes(*config_protocol_liquidation_fee), + // the behaviour of this variable changed in v2.0.2 and now represents a + // fraction of the total liquidation value that the protocol receives as + // a bonus. Prior to v2.0.2, this variable used to represent a percentage of of + // the liquidator's bonus that would be sent to the protocol. For safety, we + // cap the value here to MAX_PROTOCOL_LIQUIDATION_FEE_DECA_BPS. + MAX_PROTOCOL_LIQUIDATION_FEE_DECA_BPS, + ), + protocol_take_rate: u8::from_le_bytes(*config_protocol_take_rate), + added_borrow_weight_bps: u64::from_le_bytes(*config_added_borrow_weight_bps), + reserve_type: ReserveType::from_u8(config_asset_type[0]).unwrap(), + scaled_price_offset_bps: i64::from_le_bytes(*config_scaled_price_offset_bps), + extra_oracle_pubkey: if config_extra_oracle_pubkey == &[0; 32] { + None + } else { + Some(Pubkey::new_from_array(*config_extra_oracle_pubkey)) }, - collateral: ReserveCollateral { - mint_pubkey: Pubkey::new_from_array(*collateral_mint_pubkey), - mint_total_supply: u64::from_le_bytes(*collateral_mint_total_supply), - supply_pubkey: Pubkey::new_from_array(*collateral_supply_pubkey), + // this field is added in v2.0.3 and we will never set it to zero. only time it'll + // the following two fields are added in v2.0.3 and we will never set it to zero. only time they will + // be zero is when we upgrade from v2.0.2 to v2.0.3. in that case, the correct + // thing to do is set the value to u64::MAX. + attributed_borrow_limit_open: { + let value = u64::from_le_bytes(*config_attributed_borrow_limit_open); + if value == 0 { + u64::MAX + } else { + value + } }, - config: ReserveConfig { - optimal_utilization_rate, - max_utilization_rate: max( - optimal_utilization_rate, - u8::from_le_bytes(*config_max_utilization_rate), - ), - loan_to_value_ratio: u8::from_le_bytes(*config_loan_to_value_ratio), - liquidation_bonus, - max_liquidation_bonus, - liquidation_threshold, - max_liquidation_threshold, - min_borrow_rate: u8::from_le_bytes(*config_min_borrow_rate), - optimal_borrow_rate: u8::from_le_bytes(*config_optimal_borrow_rate), - max_borrow_rate, - super_max_borrow_rate: max( - max_borrow_rate as u64, - u64::from_le_bytes(*config_super_max_borrow_rate), - ), - fees: ReserveFees { - borrow_fee_wad: u64::from_le_bytes(*config_fees_borrow_fee_wad), - flash_loan_fee_wad: u64::from_le_bytes(*config_fees_flash_loan_fee_wad), - host_fee_percentage: u8::from_le_bytes(*config_fees_host_fee_percentage), - }, - deposit_limit: u64::from_le_bytes(*config_deposit_limit), - borrow_limit: u64::from_le_bytes(*config_borrow_limit), - fee_receiver: Pubkey::new_from_array(*config_fee_receiver), - protocol_liquidation_fee: min( - u8::from_le_bytes(*config_protocol_liquidation_fee), - // the behaviour of this variable changed in v2.0.2 and now represents a - // fraction of the total liquidation value that the protocol receives as - // a bonus. Prior to v2.0.2, this variable used to represent a percentage of of - // the liquidator's bonus that would be sent to the protocol. For safety, we - // cap the value here to MAX_PROTOCOL_LIQUIDATION_FEE_DECA_BPS. - MAX_PROTOCOL_LIQUIDATION_FEE_DECA_BPS, - ), - protocol_take_rate: u8::from_le_bytes(*config_protocol_take_rate), - added_borrow_weight_bps: u64::from_le_bytes(*config_added_borrow_weight_bps), - reserve_type: ReserveType::from_u8(config_asset_type[0]).unwrap(), - scaled_price_offset_bps: i64::from_le_bytes(*config_scaled_price_offset_bps), - extra_oracle_pubkey: if config_extra_oracle_pubkey == &[0; 32] { - None + attributed_borrow_limit_close: { + let value = u64::from_le_bytes(*config_attributed_borrow_limit_close); + if value == 0 { + u64::MAX } else { - Some(Pubkey::new_from_array(*config_extra_oracle_pubkey)) - }, - // this field is added in v2.0.3 and we will never set it to zero. only time it'll - // the following two fields are added in v2.0.3 and we will never set it to zero. only time they will - // be zero is when we upgrade from v2.0.2 to v2.0.3. in that case, the correct - // thing to do is set the value to u64::MAX. - attributed_borrow_limit_open: { - let value = u64::from_le_bytes(*config_attributed_borrow_limit_open); - if value == 0 { - u64::MAX - } else { - value - } - }, - attributed_borrow_limit_close: { - let value = u64::from_le_bytes(*config_attributed_borrow_limit_close); - if value == 0 { - u64::MAX - } else { - value - } - }, + value + } }, + }; + + Ok(Self { + version, + last_update, + lending_market: Pubkey::new_from_array(*lending_market), + liquidity, + collateral, + config, rate_limiter: RateLimiter::unpack_from_slice(rate_limiter)?, attributed_borrow_value: unpack_decimal(attributed_borrow_value), + borrows_pool_reward_manager: Default::default(), // TODO + deposits_pool_reward_manager: Default::default(), // TODO }) } } @@ -1750,6 +1770,8 @@ mod test { }, rate_limiter: rand_rate_limiter(), attributed_borrow_value: rand_decimal(), + borrows_pool_reward_manager: Default::default(), // TODO + deposits_pool_reward_manager: Default::default(), // TODO }; let mut packed = [0u8; Reserve::LEN]; From b9808d78efa189bd5266e241c156be8b788f3fb2 Mon Sep 17 00:00:00 2001 From: vanitymnm Date: Wed, 5 Mar 2025 14:19:03 +0100 Subject: [PATCH 15/30] Packing and unpacking reward managers --- .../sdk/src/state/liquidity_mining.rs | 161 +++++++++++++++++- token-lending/sdk/src/state/reserve.rs | 49 ++++-- 2 files changed, 189 insertions(+), 21 deletions(-) diff --git a/token-lending/sdk/src/state/liquidity_mining.rs b/token-lending/sdk/src/state/liquidity_mining.rs index a348452394f..4da52a3bd42 100644 --- a/token-lending/sdk/src/state/liquidity_mining.rs +++ b/token-lending/sdk/src/state/liquidity_mining.rs @@ -1,13 +1,21 @@ use crate::{ error::LendingError, math::{Decimal, TryAdd, TryDiv, TryMul, TrySub}, + state::unpack_decimal, }; +use arrayref::{array_mut_ref, array_ref, array_refs, mut_array_refs}; use solana_program::program_pack::{Pack, Sealed}; -use solana_program::{clock::Clock, program_error::ProgramError, pubkey::Pubkey}; +use solana_program::{ + clock::Clock, + program_error::ProgramError, + pubkey::{Pubkey, PUBKEY_BYTES}, +}; + +use super::pack_decimal; /// 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 = 50; + /// Cannot create a reward shorter than this. pub const MIN_REWARD_PERIOD_SECS: u64 = 3_600; @@ -93,6 +101,9 @@ pub struct PoolReward { /// someone interacts with the manager ([update_pool_reward_manager]) /// where /// `unlocked_rewards = (total_rewards * time_passed) / (total_time)` + /// + /// # (Un)Packing + /// We only store 16 most significant digits. pub cumulative_rewards_per_share: Decimal, } @@ -285,6 +296,10 @@ impl UserRewardManager { } impl PoolReward { + const LEN: usize = PoolRewardId::LEN + PUBKEY_BYTES + 8 + 4 + 8 + 8 + 16; +} + +impl PoolRewardId { const LEN: usize = std::mem::size_of::(); } @@ -311,6 +326,141 @@ impl Sealed for PoolRewardManager {} impl Pack for PoolRewardManager { /// total_shares + last_update_time_secs + pool_rewards. const LEN: usize = 8 + 8 + MAX_REWARDS * PoolReward::LEN; + + fn pack_into_slice(&self, output: &mut [u8]) { + output[0..8].copy_from_slice(&self.total_shares.to_le_bytes()); + output[8..16].copy_from_slice(&self.last_update_time_secs.to_le_bytes()); + + for (index, pool_reward_slot) in self.pool_rewards.iter().enumerate() { + let offset = 16 + index * PoolReward::LEN; + let raw_pool_reward = array_mut_ref![output, offset, PoolReward::LEN]; + + let ( + dst_id, + dst_vault, + dst_start_time_secs, + dst_duration_secs, + dst_total_rewards, + dst_num_user_reward_managers, + dst_cumulative_rewards_per_share_wads, + ) = mut_array_refs![ + raw_pool_reward, + PoolRewardId::LEN, + PUBKEY_BYTES, + 8, // start_time_secs + 4, // duration_secs + 8, // total_rewards + 8, // num_user_reward_managers + 16 // cumulative_rewards_per_share + ]; + + let ( + id, + vault, + start_time_secs, + duration_secs, + total_rewards, + num_user_reward_managers, + cumulative_rewards_per_share, + ) = match pool_reward_slot { + PoolRewardSlot::Vacant { + last_pool_reward_id, + } => ( + last_pool_reward_id, + Pubkey::default(), + 0u64, + 0u32, + 0u64, + 0u64, + Decimal::zero(), + ), + PoolRewardSlot::Occupied(PoolReward { + id, + vault, + start_time_secs, + duration_secs, + total_rewards, + num_user_reward_managers, + cumulative_rewards_per_share, + }) => ( + id, + *vault, + *start_time_secs, + *duration_secs, + *total_rewards, + *num_user_reward_managers, + *cumulative_rewards_per_share, + ), + }; + + dst_id.copy_from_slice(&id.0.to_le_bytes()); + dst_vault.copy_from_slice(vault.as_ref()); + *dst_start_time_secs = start_time_secs.to_le_bytes(); + *dst_duration_secs = duration_secs.to_le_bytes(); + *dst_total_rewards = total_rewards.to_le_bytes(); + *dst_num_user_reward_managers = num_user_reward_managers.to_le_bytes(); + pack_decimal( + cumulative_rewards_per_share, + dst_cumulative_rewards_per_share_wads, + ); + } + } + + fn unpack_from_slice(input: &[u8]) -> Result { + let mut pool_reward_manager = PoolRewardManager { + total_shares: u64::from_le_bytes(*array_ref![input, 0, 8]), + last_update_time_secs: u64::from_le_bytes(*array_ref![input, 8, 8]), + ..Default::default() + }; + + for index in 0..MAX_REWARDS { + let offset = 8 + 8 + index * PoolReward::LEN; + let raw_pool_reward = array_ref![input, offset, PoolReward::LEN]; + + let ( + src_id, + src_vault, + src_start_time_secs, + src_duration_secs, + src_total_rewards, + src_num_user_reward_managers, + src_cumulative_rewards_per_share_wads, + ) = array_refs![ + raw_pool_reward, + PoolRewardId::LEN, + PUBKEY_BYTES, + 8, // start_time_secs + 4, // duration_secs + 8, // total_rewards + 8, // num_user_reward_managers + 16 // cumulative_rewards_per_share + ]; + + let vault = Pubkey::new_from_array(*src_vault); + let pool_reward_id = PoolRewardId(u32::from_le_bytes(*src_id)); + + // SAFETY: ok to assign because we know the index is less than length + pool_reward_manager.pool_rewards[index] = if vault == Pubkey::default() { + PoolRewardSlot::Vacant { + last_pool_reward_id: pool_reward_id, + } + } else { + PoolRewardSlot::Occupied(PoolReward { + id: pool_reward_id, + vault, + start_time_secs: u64::from_le_bytes(*src_start_time_secs), + duration_secs: u32::from_le_bytes(*src_duration_secs), + total_rewards: u64::from_le_bytes(*src_total_rewards), + num_user_reward_managers: u64::from_le_bytes(*src_num_user_reward_managers), + cumulative_rewards_per_share: unpack_decimal( + src_cumulative_rewards_per_share_wads, + ), + }) + }; + } + + Ok(pool_reward_manager) + } } #[cfg(test)] @@ -325,12 +475,7 @@ mod tests { 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}"); + let required_realloc = size_of_discriminant * PoolRewardManager::LEN; assert!(required_realloc <= MAX_REALLOC); } diff --git a/token-lending/sdk/src/state/reserve.rs b/token-lending/sdk/src/state/reserve.rs index 0febc9606b4..5687bc27648 100644 --- a/token-lending/sdk/src/state/reserve.rs +++ b/token-lending/sdk/src/state/reserve.rs @@ -60,14 +60,14 @@ pub struct Reserve { pub rate_limiter: RateLimiter, /// Attributed borrows in USD pub attributed_borrow_value: Decimal, - /// Contains liquidity mining rewards for deposits. - /// - /// Added @v2.1.0 - pub deposits_pool_reward_manager: PoolRewardManager, /// Contains liquidity mining rewards for borrows. /// /// Added @v2.1.0 pub borrows_pool_reward_manager: PoolRewardManager, + /// Contains liquidity mining rewards for deposits. + /// + /// Added @v2.1.0 + pub deposits_pool_reward_manager: PoolRewardManager, } impl Reserve { @@ -1239,16 +1239,16 @@ impl IsInitialized for Reserve { /// This is the size of the account _before_ LM feature was added. const RESERVE_LEN_V2_0_2: 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 _after_ LM feature was added. -const RESERVE_LEN_v2_1_0: usize = RESERVE_LEN_V2_0_2 + PoolRewardManager::LEN * 2; +const RESERVE_LEN_V2_1_0: usize = RESERVE_LEN_V2_0_2 + PoolRewardManager::LEN * 2; impl Pack for Reserve { - const LEN: usize = RESERVE_LEN_V2_0_2; + const LEN: usize = RESERVE_LEN_V2_1_0; // @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 // @v2.1.0 TODO: add discriminator fn pack_into_slice(&self, output: &mut [u8]) { - let output = array_mut_ref![output, 0, RESERVE_LEN_V2_0_2]; + let output = array_mut_ref![output, 0, Reserve::LEN]; #[allow(clippy::ptr_offset_with_cast)] let ( version, @@ -1298,7 +1298,9 @@ impl Pack for Reserve { attributed_borrow_value, config_attributed_borrow_limit_open, config_attributed_borrow_limit_close, - _padding, + _padding, // TODO: use some of this for discriminator + output_for_borrows_pool_reward_manager, + output_for_deposits_pool_reward_manager, ) = mut_array_refs![ output, 1, @@ -1348,7 +1350,9 @@ impl Pack for Reserve { 16, 8, 8, - 49 + 49, + PoolRewardManager::LEN, + PoolRewardManager::LEN ]; // reserve @@ -1434,6 +1438,16 @@ impl Pack for Reserve { self.config.attributed_borrow_limit_close.to_le_bytes(); pack_decimal(self.attributed_borrow_value, attributed_borrow_value); + + Pack::pack_into_slice( + &self.borrows_pool_reward_manager, + output_for_borrows_pool_reward_manager, + ); + + Pack::pack_into_slice( + &self.deposits_pool_reward_manager, + output_for_deposits_pool_reward_manager, + ); } /// Unpacks a byte buffer into a [Reserve]. @@ -1441,7 +1455,7 @@ impl Pack for Reserve { // but default them if they are not present, this is part of the // migration process fn unpack_from_slice(input: &[u8]) -> Result { - let input_v2_0_2 = array_ref![input, 0, RESERVE_LEN_V2_0_2]; + let input_v2_0_2 = array_ref![input, 0, RESERVE_LEN_V2_1_0]; #[allow(clippy::ptr_offset_with_cast)] let ( version, @@ -1492,6 +1506,8 @@ impl Pack for Reserve { config_attributed_borrow_limit_open, config_attributed_borrow_limit_close, _padding, + input_for_borrows_pool_reward_manager, + input_for_deposits_pool_reward_manager, ) = array_refs![ input_v2_0_2, 1, @@ -1541,7 +1557,9 @@ impl Pack for Reserve { 16, 8, 8, - 49 + 49, + PoolRewardManager::LEN, + PoolRewardManager::LEN ]; let version = u8::from_le_bytes(*version); @@ -1666,6 +1684,11 @@ impl Pack for Reserve { }, }; + let borrows_pool_reward_manager = + PoolRewardManager::unpack_from_slice(input_for_borrows_pool_reward_manager)?; + let deposits_pool_reward_manager = + PoolRewardManager::unpack_from_slice(input_for_deposits_pool_reward_manager)?; + Ok(Self { version, last_update, @@ -1675,8 +1698,8 @@ impl Pack for Reserve { config, rate_limiter: RateLimiter::unpack_from_slice(rate_limiter)?, attributed_borrow_value: unpack_decimal(attributed_borrow_value), - borrows_pool_reward_manager: Default::default(), // TODO - deposits_pool_reward_manager: Default::default(), // TODO + borrows_pool_reward_manager, + deposits_pool_reward_manager, }) } } From 341a6a9889f25c7b71caf5a5f17bde9b960fb0be Mon Sep 17 00:00:00 2001 From: vanitymnm Date: Fri, 7 Mar 2025 13:03:21 +0100 Subject: [PATCH 16/30] Adding packing & unpacking tests --- Cargo.lock | 19 +++---- Cargo.toml | 5 +- token-lending/sdk/Cargo.toml | 8 +-- .../sdk/src/state/liquidity_mining.rs | 52 +++++++++++++++++++ token-lending/sdk/src/state/reserve.rs | 10 ++-- 5 files changed, 69 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 63624df360b..88adf94e7f6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -780,18 +780,18 @@ dependencies = [ [[package]] name = "bit-set" -version = "0.5.3" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" dependencies = [ "bit-vec", ] [[package]] name = "bit-vec" -version = "0.6.3" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] name = "bitflags" @@ -2444,12 +2444,6 @@ version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" -[[package]] -name = "libm" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" - [[package]] name = "libredox" version = "0.1.3" @@ -2900,7 +2894,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", - "libm", ] [[package]] @@ -3368,9 +3361,9 @@ dependencies = [ [[package]] name = "proptest" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4c2511913b88df1637da85cc8d96ec8e43a3f8bb8ccb71ee1ac240d6f3df58d" +checksum = "14cae93065090804185d3b75f0bf93b8eeda30c7a9b4a33d3bdb3988d6229e50" dependencies = [ "bit-set", "bit-vec", diff --git a/Cargo.toml b/Cargo.toml index 7daa3e7968f..a28824d2685 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,8 +3,9 @@ members = [ "token-lending/cli", "token-lending/program", "token-lending/sdk", - "token-lending/brick" -, "token-lending/oracles"] + "token-lending/brick", + "token-lending/oracles", +] [profile.dev] split-debuginfo = "unpacked" diff --git a/token-lending/sdk/Cargo.toml b/token-lending/sdk/Cargo.toml index f969a318766..267ed2e09b9 100644 --- a/token-lending/sdk/Cargo.toml +++ b/token-lending/sdk/Cargo.toml @@ -14,7 +14,7 @@ bytemuck = "1.5.1" num-derive = "0.3" num-traits = "0.2" solana-program = ">=1.9" -spl-token = { version = "3.2.0", features=["no-entrypoint"] } +spl-token = { version = "3.2.0", features = ["no-entrypoint"] } static_assertions = "1.1.0" thiserror = "1.0" uint = "=0.9.1" @@ -23,11 +23,11 @@ uint = "=0.9.1" assert_matches = "1.5.0" base64 = "0.13" log = "0.4.14" -proptest = "1.0" -solana-sdk = ">=1.9" +proptest = "1.6" +rand = "0.8.5" serde = ">=1.0.140" serde_yaml = "0.8" -rand = "0.8.5" +solana-sdk = ">=1.9" [lib] crate-type = ["cdylib", "lib"] diff --git a/token-lending/sdk/src/state/liquidity_mining.rs b/token-lending/sdk/src/state/liquidity_mining.rs index 4da52a3bd42..b9d76b22aed 100644 --- a/token-lending/sdk/src/state/liquidity_mining.rs +++ b/token-lending/sdk/src/state/liquidity_mining.rs @@ -468,7 +468,31 @@ mod tests { //! TODO: Rewrite these tests from their Suilend counterparts. //! TODO: Calculate test coverage and add tests for missing branches. + use rand::Rng; + use super::*; + use proptest::prelude::*; + + fn pool_reward_manager_strategy() -> impl Strategy { + (0..100u32).prop_perturb(|_, mut rng| PoolRewardManager::new_rand(&mut rng)) + } + + proptest! { + #[test] + fn it_packs_and_unpacks(pool_reward_manager in pool_reward_manager_strategy()) { + let mut packed = vec![0u8; PoolRewardManager::LEN]; + Pack::pack_into_slice(&pool_reward_manager, &mut packed); + let unpacked = PoolRewardManager::unpack_from_slice(&packed).unwrap(); + prop_assert_eq!(pool_reward_manager, unpacked); + } + } + + #[test] + fn it_unpacks_empty_bytes_as_default() { + let packed = vec![0u8; PoolRewardManager::LEN]; + let unpacked = PoolRewardManager::unpack_from_slice(&packed).unwrap(); + assert_eq!(unpacked, PoolRewardManager::default()); + } #[test] fn it_fits_reserve_realloc_into_single_ix() { @@ -508,4 +532,32 @@ mod tests { fn it_tests_pool_reward_manager_cancel_and_close_regression() { // TODO: rewrite Suilend "test_pool_reward_manager_cancel_and_close_regression" } + + impl PoolRewardManager { + pub(crate) fn new_rand(rng: &mut impl Rng) -> Self { + Self { + total_shares: rng.gen(), + last_update_time_secs: rng.gen(), + pool_rewards: std::array::from_fn(|_| { + let is_vacant = rng.gen_bool(0.5); + + if is_vacant { + PoolRewardSlot::Vacant { + last_pool_reward_id: PoolRewardId(rng.gen()), + } + } else { + PoolRewardSlot::Occupied(PoolReward { + id: PoolRewardId(rng.gen()), + vault: Pubkey::new_unique(), + start_time_secs: rng.gen(), + duration_secs: rng.gen(), + total_rewards: rng.gen(), + cumulative_rewards_per_share: Decimal::from_scaled_val(rng.gen()), + num_user_reward_managers: rng.gen(), + }) + } + }), + } + } + } } diff --git a/token-lending/sdk/src/state/reserve.rs b/token-lending/sdk/src/state/reserve.rs index 5687bc27648..887d901ccc7 100644 --- a/token-lending/sdk/src/state/reserve.rs +++ b/token-lending/sdk/src/state/reserve.rs @@ -1245,7 +1245,7 @@ impl Pack for Reserve { const LEN: usize = RESERVE_LEN_V2_1_0; // @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 + // @v2.1.0: packs deposits_pool_reward_manager and borrows_pool_reward_manager // @v2.1.0 TODO: add discriminator fn pack_into_slice(&self, output: &mut [u8]) { let output = array_mut_ref![output, 0, Reserve::LEN]; @@ -1451,9 +1451,7 @@ impl Pack for Reserve { } /// 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 + // @v2.1.0 unpacks deposits_pool_reward_manager and borrows_pool_reward_manager fn unpack_from_slice(input: &[u8]) -> Result { let input_v2_0_2 = array_ref![input, 0, RESERVE_LEN_V2_1_0]; #[allow(clippy::ptr_offset_with_cast)] @@ -1793,8 +1791,8 @@ mod test { }, rate_limiter: rand_rate_limiter(), attributed_borrow_value: rand_decimal(), - borrows_pool_reward_manager: Default::default(), // TODO - deposits_pool_reward_manager: Default::default(), // TODO + borrows_pool_reward_manager: PoolRewardManager::new_rand(&mut rng), + deposits_pool_reward_manager: PoolRewardManager::new_rand(&mut rng), }; let mut packed = [0u8; Reserve::LEN]; From 965ca13656c7fb3fd906a699d8f9c4585382bf83 Mon Sep 17 00:00:00 2001 From: vanitymnm Date: Fri, 7 Mar 2025 15:23:13 +0100 Subject: [PATCH 17/30] Adding an ix to migrate reserve --- token-lending/program/src/processor.rs | 6 + .../program/src/processor/liquidity_mining.rs | 119 ++++++++++++++++++ token-lending/sdk/src/instruction.rs | 12 ++ .../sdk/src/state/liquidity_mining.rs | 55 ++++---- token-lending/sdk/src/state/reserve.rs | 30 +++-- 5 files changed, 186 insertions(+), 36 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 470558d4f38..4006289dc76 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -244,6 +244,12 @@ pub fn process_instruction( accounts, ) } + + // temporary ix for upgrade + LendingInstruction::UpgradeReserveToV2_1_0 => { + msg!("Instruction: Upgrade Reserve to v2.1.0"); + liquidity_mining::upgrade_reserve(program_id, accounts) + } } } diff --git a/token-lending/program/src/processor/liquidity_mining.rs b/token-lending/program/src/processor/liquidity_mining.rs index ed8b274ed06..dcdbaf11cc6 100644 --- a/token-lending/program/src/processor/liquidity_mining.rs +++ b/token-lending/program/src/processor/liquidity_mining.rs @@ -37,6 +37,7 @@ use solend_sdk::{ }; use spl_token::state::Account as TokenAccount; use std::convert::TryInto; +use upgrade_reserve::UpgradeReserveAccounts; /// # Accounts /// @@ -217,6 +218,73 @@ pub(crate) fn process_close_pool_reward( Ok(()) } +/// Temporary ix to upgrade a reserve to LM feature added in @v2.0.2. +/// Fails if reserve was not sized as @v2.0.2. +/// +/// # Accounts +/// +/// See [upgrade_reserve::UpgradeReserveAccounts::from_unchecked_iter] for a list +/// of accounts and their constraints. +/// +/// # Effects +/// +/// 1. Takes payer's lamports and pays for the rent increase. +/// 2. Reallocates the reserve account to the latest size. +/// 3. Repacks the reserve account. +pub(crate) fn upgrade_reserve(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { + let accounts = UpgradeReserveAccounts::from_unchecked_iter(program_id, &mut accounts.iter())?; + + // + // 1. + // + + let current_rent = accounts.reserve_info.lamports(); + let new_rent = Rent::get()?.minimum_balance(Reserve::LEN); + + if current_rent < new_rent { + // this will always be the case unless Solana goes UBI + let extra_rent = new_rent - current_rent; + + let mut payer_lamports = accounts.payer.try_borrow_mut_lamports()?; + + if **payer_lamports < extra_rent { + msg!("Payer does not have enough lamports to cover the rent increase"); + return Err(ProgramError::InsufficientFunds); + } + + **payer_lamports -= extra_rent; + accounts + .reserve_info + .try_borrow_mut_lamports()? + .checked_add(extra_rent) + .ok_or(LendingError::MathOverflow)?; + } + + // + // 2. + // + + // From the [AccountInfo::realloc] docs: + // + // > Memory used to grow is already zero-initialized upon program entrypoint + // > and re-zeroing it wastes compute units. If within the same call a program + // > reallocs from larger to smaller and back to larger again the new space + // > could contain stale data. Pass true for zero_init in this case, + // > otherwise compute units will be wasted re-zero-initializing. + let zero_init = false; + accounts.reserve_info.realloc(Reserve::LEN, zero_init)?; + + // + // 3. + // + + // sanity checks that pack and unpack reserves ok + let reserve = Reserve::unpack(&accounts.reserve_info.data.borrow())?; + Reserve::pack(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) @@ -645,6 +713,57 @@ mod close_pool_reward { } } +mod upgrade_reserve { + use solend_sdk::state::RESERVE_LEN_V2_0_2; + + use super::*; + + pub(super) struct UpgradeReserveAccounts<'a, 'info> { + /// The pool fella who pays for this. + /// + /// ✅ is a signer + pub(super) payer: &'a AccountInfo<'info>, + /// Reserve sized as v2.0.2. + /// + /// ✅ belongs to this program + /// ✅ is sized [RESERVE_LEN_V2_0_2], ie. for sure [Reserve] account + pub(super) reserve_info: &'a AccountInfo<'info>, + + _priv: (), + } + + impl<'a, 'info> UpgradeReserveAccounts<'a, 'info> { + pub(super) fn from_unchecked_iter( + program_id: &Pubkey, + iter: &mut impl Iterator>, + ) -> Result, ProgramError> { + let payer = next_account_info(iter)?; + let reserve_info = next_account_info(iter)?; + + if !payer.is_signer { + msg!("Payer provided must be a signer"); + return Err(LendingError::InvalidSigner.into()); + } + + if reserve_info.owner != program_id { + msg!("Reserve provided must be owned by the lending program"); + return Err(LendingError::InvalidAccountOwner.into()); + } + + if reserve_info.data_len() != RESERVE_LEN_V2_0_2 { + msg!("Reserve provided must be sized as v2.0.2"); + return Err(LendingError::InvalidAccountInput.into()); + } + + Ok(Self { + payer, + reserve_info, + _priv: (), + }) + } + } +} + /// Common checks within the admin ixs are: /// /// * ✅ `reserve_info` belongs to this program diff --git a/token-lending/sdk/src/instruction.rs b/token-lending/sdk/src/instruction.rs index de75c22a66e..043b9812104 100644 --- a/token-lending/sdk/src/instruction.rs +++ b/token-lending/sdk/src/instruction.rs @@ -613,6 +613,14 @@ pub enum LendingInstruction { /// Identifies a reward within a reserve's deposits/borrows rewards. pool_reward_index: u64, }, + + // 255 + /// UpgradeReserveToV2_1_0 + /// + /// Temporary ix which upgrades reserves from @2.0.2 to @2.1.0 with + /// liquidity mining feature. + /// Once all reserves are upgraded this ix is not necessary any more. + UpgradeReserveToV2_1_0, } impl LendingInstruction { @@ -899,6 +907,7 @@ impl LendingInstruction { pool_reward_index, } } + 255 => Self::UpgradeReserveToV2_1_0, _ => { msg!("Instruction cannot be unpacked"); return Err(LendingError::InstructionUnpackError.into()); @@ -1235,6 +1244,9 @@ impl LendingInstruction { buf.extend_from_slice(&(position_kind as u8).to_le_bytes()); buf.extend_from_slice(&pool_reward_index.to_le_bytes()); } + Self::UpgradeReserveToV2_1_0 => { + buf.push(255); + } } buf } diff --git a/token-lending/sdk/src/state/liquidity_mining.rs b/token-lending/sdk/src/state/liquidity_mining.rs index b9d76b22aed..7b5d81ce4ba 100644 --- a/token-lending/sdk/src/state/liquidity_mining.rs +++ b/token-lending/sdk/src/state/liquidity_mining.rs @@ -63,7 +63,9 @@ pub enum PoolRewardSlot { last_pool_reward_id: PoolRewardId, }, /// Reward has not been closed yet. - Occupied(PoolReward), + /// + /// We box the [PoolReward] to avoid stack overflow. + Occupied(Box), } /// Tracks rewards in a specific mint over some period of time. @@ -366,7 +368,7 @@ impl Pack for PoolRewardManager { PoolRewardSlot::Vacant { last_pool_reward_id, } => ( - last_pool_reward_id, + *last_pool_reward_id, Pubkey::default(), 0u64, 0u32, @@ -374,22 +376,14 @@ impl Pack for PoolRewardManager { 0u64, Decimal::zero(), ), - PoolRewardSlot::Occupied(PoolReward { - id, - vault, - start_time_secs, - duration_secs, - total_rewards, - num_user_reward_managers, - cumulative_rewards_per_share, - }) => ( - id, - *vault, - *start_time_secs, - *duration_secs, - *total_rewards, - *num_user_reward_managers, - *cumulative_rewards_per_share, + PoolRewardSlot::Occupied(pool_reward) => ( + pool_reward.id, + pool_reward.vault, + pool_reward.start_time_secs, + pool_reward.duration_secs, + pool_reward.total_rewards, + pool_reward.num_user_reward_managers, + pool_reward.cumulative_rewards_per_share, ), }; @@ -445,7 +439,7 @@ impl Pack for PoolRewardManager { last_pool_reward_id: pool_reward_id, } } else { - PoolRewardSlot::Occupied(PoolReward { + PoolRewardSlot::Occupied(Box::new(PoolReward { id: pool_reward_id, vault, start_time_secs: u64::from_le_bytes(*src_start_time_secs), @@ -455,7 +449,7 @@ impl Pack for PoolRewardManager { cumulative_rewards_per_share: unpack_decimal( src_cumulative_rewards_per_share_wads, ), - }) + })) }; } @@ -468,10 +462,9 @@ mod tests { //! TODO: Rewrite these tests from their Suilend counterparts. //! TODO: Calculate test coverage and add tests for missing branches. - use rand::Rng; - use super::*; use proptest::prelude::*; + use rand::Rng; fn pool_reward_manager_strategy() -> impl Strategy { (0..100u32).prop_perturb(|_, mut rng| PoolRewardManager::new_rand(&mut rng)) @@ -492,11 +485,23 @@ mod tests { let packed = vec![0u8; PoolRewardManager::LEN]; let unpacked = PoolRewardManager::unpack_from_slice(&packed).unwrap(); assert_eq!(unpacked, PoolRewardManager::default()); + + // sanity check that everything starts at 0 + let all_rewards_are_empty = unpacked.pool_rewards.iter().all(|pool_reward| { + matches!( + pool_reward, + PoolRewardSlot::Vacant { + last_pool_reward_id: PoolRewardId(0) + } + ) + }); + + assert!(all_rewards_are_empty); } #[test] fn it_fits_reserve_realloc_into_single_ix() { - const MAX_REALLOC: usize = 10 * 1024; + const MAX_REALLOC: usize = solana_program::entrypoint::MAX_PERMITTED_DATA_INCREASE; let size_of_discriminant = 1; let required_realloc = size_of_discriminant * PoolRewardManager::LEN; @@ -546,7 +551,7 @@ mod tests { last_pool_reward_id: PoolRewardId(rng.gen()), } } else { - PoolRewardSlot::Occupied(PoolReward { + PoolRewardSlot::Occupied(Box::new(PoolReward { id: PoolRewardId(rng.gen()), vault: Pubkey::new_unique(), start_time_secs: rng.gen(), @@ -554,7 +559,7 @@ mod tests { total_rewards: rng.gen(), cumulative_rewards_per_share: Decimal::from_scaled_val(rng.gen()), num_user_reward_managers: rng.gen(), - }) + })) } }), } diff --git a/token-lending/sdk/src/state/reserve.rs b/token-lending/sdk/src/state/reserve.rs index 887d901ccc7..f948ad153c8 100644 --- a/token-lending/sdk/src/state/reserve.rs +++ b/token-lending/sdk/src/state/reserve.rs @@ -63,11 +63,17 @@ pub struct Reserve { /// Contains liquidity mining rewards for borrows. /// /// Added @v2.1.0 - pub borrows_pool_reward_manager: PoolRewardManager, + /// + /// TODO: measure compute units for packing/unpacking and if significant + /// then consider packing/unpacking on demand + pub borrows_pool_reward_manager: Box, /// Contains liquidity mining rewards for deposits. /// /// Added @v2.1.0 - pub deposits_pool_reward_manager: PoolRewardManager, + /// + /// TODO: measure compute units for packing/unpacking and if significant + /// then consider packing/unpacking on demand + pub deposits_pool_reward_manager: Box, } impl Reserve { @@ -1237,7 +1243,7 @@ impl IsInitialized for Reserve { } /// This is the size of the account _before_ LM feature was added. -const RESERVE_LEN_V2_0_2: 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 +pub const RESERVE_LEN_V2_0_2: 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 _after_ LM feature was added. const RESERVE_LEN_V2_1_0: usize = RESERVE_LEN_V2_0_2 + PoolRewardManager::LEN * 2; @@ -1440,12 +1446,12 @@ impl Pack for Reserve { pack_decimal(self.attributed_borrow_value, attributed_borrow_value); Pack::pack_into_slice( - &self.borrows_pool_reward_manager, + &*self.borrows_pool_reward_manager, output_for_borrows_pool_reward_manager, ); Pack::pack_into_slice( - &self.deposits_pool_reward_manager, + &*self.deposits_pool_reward_manager, output_for_deposits_pool_reward_manager, ); } @@ -1682,10 +1688,12 @@ impl Pack for Reserve { }, }; - let borrows_pool_reward_manager = - PoolRewardManager::unpack_from_slice(input_for_borrows_pool_reward_manager)?; - let deposits_pool_reward_manager = - PoolRewardManager::unpack_from_slice(input_for_deposits_pool_reward_manager)?; + let borrows_pool_reward_manager = Box::new(PoolRewardManager::unpack_from_slice( + input_for_borrows_pool_reward_manager, + )?); + let deposits_pool_reward_manager = Box::new(PoolRewardManager::unpack_from_slice( + input_for_deposits_pool_reward_manager, + )?); Ok(Self { version, @@ -1791,8 +1799,8 @@ mod test { }, rate_limiter: rand_rate_limiter(), attributed_borrow_value: rand_decimal(), - borrows_pool_reward_manager: PoolRewardManager::new_rand(&mut rng), - deposits_pool_reward_manager: PoolRewardManager::new_rand(&mut rng), + borrows_pool_reward_manager: Box::new(PoolRewardManager::new_rand(&mut rng)), + deposits_pool_reward_manager: Box::new(PoolRewardManager::new_rand(&mut rng)), }; let mut packed = [0u8; Reserve::LEN]; From 34570eddd6418382fa13a819b233d102b649a768 Mon Sep 17 00:00:00 2001 From: vanitymnm Date: Wed, 12 Mar 2025 15:23:43 +0100 Subject: [PATCH 18/30] Realloc succesful --- token-lending/cli/src/main.rs | 45 ++++++++++++++- .../program/src/processor/liquidity_mining.rs | 56 ++++++++++++------- token-lending/sdk/src/instruction.rs | 22 ++++++++ .../sdk/src/state/liquidity_mining.rs | 8 +++ token-lending/sdk/src/state/reserve.rs | 24 ++++---- ...zm44T7XG68MYKx7YisTjZu73tVovyZSjJMpmw.json | 14 +++++ .../liquidity-mining/test-reserve-realloc.sh | 20 +++++++ 7 files changed, 155 insertions(+), 34 deletions(-) create mode 100644 token-lending/tests/liquidity-mining/fixtures/BgxfHJDzm44T7XG68MYKx7YisTjZu73tVovyZSjJMpmw.json create mode 100755 token-lending/tests/liquidity-mining/test-reserve-realloc.sh diff --git a/token-lending/cli/src/main.rs b/token-lending/cli/src/main.rs index db0658a0068..63a9906a0f4 100644 --- a/token-lending/cli/src/main.rs +++ b/token-lending/cli/src/main.rs @@ -11,6 +11,7 @@ use solend_program::{ instruction::set_lending_market_owner_and_config, state::{validate_reserve_config, RateLimiterConfig}, }; +use solend_sdk::instruction::upgrade_reserve_to_v2_1_0; use solend_sdk::{ instruction::{ liquidate_obligation_and_redeem_reserve_collateral, redeem_reserve_collateral, @@ -768,6 +769,20 @@ fn main() { .help("Risk authority address"), ) ) + .subcommand( + SubCommand::with_name("upgrade-reserve") + .about("Migrate reserve to version 2.1.0") + .arg( + Arg::with_name("reserve") + .long("reserve") + .validator(is_pubkey) + .value_name("PUBKEY") + .takes_value(true) + .required(true) + .help("Reserve address"), + ) + + ) .subcommand( SubCommand::with_name("update-reserve") .about("Update a reserve config") @@ -1324,6 +1339,11 @@ fn main() { risk_authority_pubkey, ) } + ("upgrade-reserve", Some(arg_matches)) => { + let reserve_pubkey = pubkey_of(arg_matches, "reserve").unwrap(); + + command_upgrade_reserve_to_v2_1_0(&mut config, reserve_pubkey) + } ("update-reserve", Some(arg_matches)) => { let reserve_pubkey = pubkey_of(arg_matches, "reserve").unwrap(); let lending_market_owner_keypair = @@ -1973,6 +1993,29 @@ fn command_set_lending_market_owner_and_config( Ok(()) } +fn command_upgrade_reserve_to_v2_1_0(config: &mut Config, reserve_pubkey: Pubkey) -> CommandResult { + let recent_blockhash = config.rpc_client.get_latest_blockhash()?; + + let message = Message::new_with_blockhash( + &[ + ComputeBudgetInstruction::set_compute_unit_price(30101), + upgrade_reserve_to_v2_1_0( + config.lending_program_id, + reserve_pubkey, + config.fee_payer.pubkey(), + ), + ], + Some(&config.fee_payer.pubkey()), + &recent_blockhash, + ); + + let transaction = Transaction::new(&vec![config.fee_payer.as_ref()], message, recent_blockhash); + + send_transaction(config, transaction)?; + + Ok(()) +} + #[allow(clippy::too_many_arguments, clippy::unnecessary_unwrap)] fn command_update_reserve( config: &mut Config, @@ -2582,7 +2625,7 @@ fn send_transaction( CommitmentConfig::confirmed(), RpcSendTransactionConfig { preflight_commitment: Some(CommitmentLevel::Processed), - skip_preflight: false, + skip_preflight: true, // TODO encoding: None, max_retries: None, min_context_slot: None, diff --git a/token-lending/program/src/processor/liquidity_mining.rs b/token-lending/program/src/processor/liquidity_mining.rs index dcdbaf11cc6..10fd45e3fa0 100644 --- a/token-lending/program/src/processor/liquidity_mining.rs +++ b/token-lending/program/src/processor/liquidity_mining.rs @@ -26,9 +26,11 @@ use solana_program::{ clock::Clock, entrypoint::ProgramResult, msg, + program::invoke, program_error::ProgramError, pubkey::Pubkey, rent::Rent, + system_instruction, sysvar::Sysvar, }; use solend_sdk::{ @@ -221,6 +223,9 @@ pub(crate) fn process_close_pool_reward( /// Temporary ix to upgrade a reserve to LM feature added in @v2.0.2. /// Fails if reserve was not sized as @v2.0.2. /// +/// Until this ix is called for a [Reserve] account, all other ixs that try to +/// unpack the [Reserve] will fail due to size mismatch. +/// /// # Accounts /// /// See [upgrade_reserve::UpgradeReserveAccounts::from_unchecked_iter] for a list @@ -241,23 +246,21 @@ pub(crate) fn upgrade_reserve(program_id: &Pubkey, accounts: &[AccountInfo]) -> let current_rent = accounts.reserve_info.lamports(); let new_rent = Rent::get()?.minimum_balance(Reserve::LEN); - if current_rent < new_rent { + if let Some(extra_rent) = new_rent.checked_sub(current_rent) { // this will always be the case unless Solana goes UBI - let extra_rent = new_rent - current_rent; - - let mut payer_lamports = accounts.payer.try_borrow_mut_lamports()?; - if **payer_lamports < extra_rent { - msg!("Payer does not have enough lamports to cover the rent increase"); - return Err(ProgramError::InsufficientFunds); - } - - **payer_lamports -= extra_rent; - accounts - .reserve_info - .try_borrow_mut_lamports()? - .checked_add(extra_rent) - .ok_or(LendingError::MathOverflow)?; + invoke( + &system_instruction::transfer( + accounts.payer.key, + accounts.reserve_info.key, + extra_rent, + ), + &[ + accounts.payer.clone(), + accounts.reserve_info.clone(), + accounts.system_program.clone(), + ], + )?; } // @@ -278,7 +281,7 @@ pub(crate) fn upgrade_reserve(program_id: &Pubkey, accounts: &[AccountInfo]) -> // 3. // - // sanity checks that pack and unpack reserves ok + // sanity checks pack and unpack reserves is ok let reserve = Reserve::unpack(&accounts.reserve_info.data.borrow())?; Reserve::pack(reserve, &mut accounts.reserve_info.data.borrow_mut())?; @@ -719,15 +722,19 @@ mod upgrade_reserve { use super::*; pub(super) struct UpgradeReserveAccounts<'a, 'info> { - /// The pool fella who pays for this. - /// - /// ✅ is a signer - pub(super) payer: &'a AccountInfo<'info>, /// Reserve sized as v2.0.2. /// /// ✅ belongs to this program /// ✅ is sized [RESERVE_LEN_V2_0_2], ie. for sure [Reserve] account pub(super) reserve_info: &'a AccountInfo<'info>, + /// The pool fella who pays for this. + /// + /// ✅ is a signer + pub(super) payer: &'a AccountInfo<'info>, + /// The system program. + /// + /// ✅ is the system program + pub(super) system_program: &'a AccountInfo<'info>, _priv: (), } @@ -737,8 +744,9 @@ mod upgrade_reserve { program_id: &Pubkey, iter: &mut impl Iterator>, ) -> Result, ProgramError> { - let payer = next_account_info(iter)?; let reserve_info = next_account_info(iter)?; + let payer = next_account_info(iter)?; + let system_program = next_account_info(iter)?; if !payer.is_signer { msg!("Payer provided must be a signer"); @@ -755,9 +763,15 @@ mod upgrade_reserve { return Err(LendingError::InvalidAccountInput.into()); } + if system_program.key != &solana_program::system_program::id() { + msg!("System program provided must be the system program"); + return Err(LendingError::InvalidAccountInput.into()); + } + Ok(Self { payer, reserve_info, + system_program, _priv: (), }) } diff --git a/token-lending/sdk/src/instruction.rs b/token-lending/sdk/src/instruction.rs index 043b9812104..a9c19ac49f1 100644 --- a/token-lending/sdk/src/instruction.rs +++ b/token-lending/sdk/src/instruction.rs @@ -620,6 +620,10 @@ pub enum LendingInstruction { /// Temporary ix which upgrades reserves from @2.0.2 to @2.1.0 with /// liquidity mining feature. /// Once all reserves are upgraded this ix is not necessary any more. + /// + /// `[writable]` Reserve account. + /// `[writable, signer]` Fee payer. + /// `[]` System program. UpgradeReserveToV2_1_0, } @@ -2059,6 +2063,24 @@ pub fn donate_to_reserve( } } +/// Creates a `UpgradeReserveToV2_1_0` instruction. +/// Be careful, it's expensive $_$ +pub fn upgrade_reserve_to_v2_1_0( + program_id: Pubkey, + reserve_pubkey: Pubkey, + fee_payer: Pubkey, +) -> Instruction { + Instruction { + program_id, + accounts: vec![ + AccountMeta::new(reserve_pubkey, false), + AccountMeta::new(fee_payer, true), + AccountMeta::new_readonly(system_program::id(), false), + ], + data: LendingInstruction::UpgradeReserveToV2_1_0.pack(), + } +} + #[cfg(test)] mod test { use super::*; diff --git a/token-lending/sdk/src/state/liquidity_mining.rs b/token-lending/sdk/src/state/liquidity_mining.rs index 7b5d81ce4ba..5fdbfe07b74 100644 --- a/token-lending/sdk/src/state/liquidity_mining.rs +++ b/token-lending/sdk/src/state/liquidity_mining.rs @@ -323,6 +323,13 @@ impl Default for PoolRewardSlot { } } +impl PoolRewardManager { + #[inline(never)] + pub(crate) fn unpack_to_box(input: &[u8]) -> Result, ProgramError> { + Ok(Box::new(PoolRewardManager::unpack_from_slice(input)?)) + } +} + impl Sealed for PoolRewardManager {} impl Pack for PoolRewardManager { @@ -400,6 +407,7 @@ impl Pack for PoolRewardManager { } } + #[inline(never)] fn unpack_from_slice(input: &[u8]) -> Result { let mut pool_reward_manager = PoolRewardManager { total_shares: u64::from_le_bytes(*array_ref![input, 0, 8]), diff --git a/token-lending/sdk/src/state/reserve.rs b/token-lending/sdk/src/state/reserve.rs index f948ad153c8..cea55c0e21f 100644 --- a/token-lending/sdk/src/state/reserve.rs +++ b/token-lending/sdk/src/state/reserve.rs @@ -1457,9 +1457,10 @@ impl Pack for Reserve { } /// Unpacks a byte buffer into a [Reserve]. + /// // @v2.1.0 unpacks deposits_pool_reward_manager and borrows_pool_reward_manager fn unpack_from_slice(input: &[u8]) -> Result { - let input_v2_0_2 = array_ref![input, 0, RESERVE_LEN_V2_1_0]; + let input_v2_0_2 = array_ref![input, 0, RESERVE_LEN_V2_0_2]; #[allow(clippy::ptr_offset_with_cast)] let ( version, @@ -1510,8 +1511,6 @@ impl Pack for Reserve { config_attributed_borrow_limit_open, config_attributed_borrow_limit_close, _padding, - input_for_borrows_pool_reward_manager, - input_for_deposits_pool_reward_manager, ) = array_refs![ input_v2_0_2, 1, @@ -1561,9 +1560,7 @@ impl Pack for Reserve { 16, 8, 8, - 49, - PoolRewardManager::LEN, - PoolRewardManager::LEN + 49 ]; let version = u8::from_le_bytes(*version); @@ -1688,12 +1685,15 @@ impl Pack for Reserve { }, }; - let borrows_pool_reward_manager = Box::new(PoolRewardManager::unpack_from_slice( - input_for_borrows_pool_reward_manager, - )?); - let deposits_pool_reward_manager = Box::new(PoolRewardManager::unpack_from_slice( - input_for_deposits_pool_reward_manager, - )?); + let input_v2_1_0 = array_ref![input, RESERVE_LEN_V2_0_2, PoolRewardManager::LEN * 2]; + let (input_for_borrows_pool_reward_manager, input_for_deposits_pool_reward_manager) = + array_refs![input_v2_1_0, PoolRewardManager::LEN, PoolRewardManager::LEN]; + + let borrows_pool_reward_manager = + PoolRewardManager::unpack_to_box(input_for_borrows_pool_reward_manager)?; + + let deposits_pool_reward_manager = + PoolRewardManager::unpack_to_box(input_for_deposits_pool_reward_manager)?; Ok(Self { version, diff --git a/token-lending/tests/liquidity-mining/fixtures/BgxfHJDzm44T7XG68MYKx7YisTjZu73tVovyZSjJMpmw.json b/token-lending/tests/liquidity-mining/fixtures/BgxfHJDzm44T7XG68MYKx7YisTjZu73tVovyZSjJMpmw.json new file mode 100644 index 00000000000..d9a99c89850 --- /dev/null +++ b/token-lending/tests/liquidity-mining/fixtures/BgxfHJDzm44T7XG68MYKx7YisTjZu73tVovyZSjJMpmw.json @@ -0,0 +1,14 @@ +{ + "pubkey": "BgxfHJDzm44T7XG68MYKx7YisTjZu73tVovyZSjJMpmw", + "account": { + "lamports": 25199120, + "data": [ + "AUxOchMAAAAAADOzHsTv+PoomuqMlUwBYy4tdkkIzlRNaGW97xEb/2Erxvp6877brTo9ZfNqq8l0MbG75MLS9uDkfKYCA0UvXWEGbpdIlAlxcAyM9MVHGESm8j26mv3yVsBd27pvWYrqpBy+k5qDCfVkBxh//zCsVLFpSYvpn22OG/1CRGgM1PfR4p92kRK2fxPx+u4VYVsvyto2WC/rpbudgsUIcPl0HyXZm2oZhTQVAAAHa18Y7VmYq+p+dyNlAQAAsvbnrPL8IRMAAAAAAAAAAABYOESYQOANAAAAAAAAAAB47R1Wfg2+kjfDllSe3sA08hkpKYMRa2acM+p6tDx4Y/Cmw079JQAAByTVoAxu0AT/TCxk3n5czwWGco/AWYg4MQFXus/s6qBQRgNNAgcUAIDGpH6NAwAAQGNSv8YBABQAsFtMNkQAAADAr9aRNgAAP3BApl+Czb2JX5yhJpGXIgODFyy65r5NP0zSG9YtqS4TFDfMlrajzrNJL4QVAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMzPmpBj4A0AAAAAAAAAAABaXgEAAAAAAAAFTwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAO6txL1fw97n+AkAAAAAAAD/////////////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "base64" + ], + "owner": "So1endDq2YkqhipRh3WViPa8hdiSpxWy6z3Z6tMCpAo", + "executable": false, + "rentEpoch": 18446744073709551615, + "space": 619 + } +} \ No newline at end of file diff --git a/token-lending/tests/liquidity-mining/test-reserve-realloc.sh b/token-lending/tests/liquidity-mining/test-reserve-realloc.sh new file mode 100755 index 00000000000..821595125a5 --- /dev/null +++ b/token-lending/tests/liquidity-mining/test-reserve-realloc.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +# TODO: document, automate and add to CI? + +TOKEN_LENDING_PROGRAM_ID="So1endDq2YkqhipRh3WViPa8hdiSpxWy6z3Z6tMCpAo" +RESERVE_ACCOUNT_PUBKEY="BgxfHJDzm44T7XG68MYKx7YisTjZu73tVovyZSjJMpmw" # Solend Main Pool - (USDC) Reserve State + +git_root="$(git rev-parse --show-toplevel)" +test_reserve_path="${git_root}/token-lending/tests/liquidity-mining/fixtures/${RESERVE_ACCOUNT_PUBKEY}.json" +token_lending_program_path="${git_root}/target/deploy/solend_program.so" + +echo "Building the program..." +cargo-build-sbf -- -p solend-program + +echo "Starting the test validator..." +solana-test-validator --reset \ + --upgradeable-program "${TOKEN_LENDING_PROGRAM_ID}" "${token_lending_program_path}" "$(solana address)" \ + --account "${RESERVE_ACCOUNT_PUBKEY}" "${test_reserve_path}" + +# cargo run --bin solend-cli -- --url http://127.0.0.1:8899 upgrade-reserve --reserve BgxfHJDzm44T7XG68MYKx7YisTjZu73tVovyZSjJMpmw From 3faec66634b353408ae7fd50ba2065c4087f93be Mon Sep 17 00:00:00 2001 From: vanitymnm Date: Fri, 14 Mar 2025 12:56:11 +0100 Subject: [PATCH 19/30] Running test script via anchor tests --- .editorconfig | 8 + .mocharc.yml | 1 + Anchor.toml | 27 +- package.json | 20 + .../program/src/processor/liquidity_mining.rs | 2 +- token-lending/tests/liquidity-mining.ts | 66 + ...zm44T7XG68MYKx7YisTjZu73tVovyZSjJMpmw.json | 14 - .../liquidity-mining/test-reserve-realloc.sh | 20 - tsconfig.json | 10 + yarn.lock | 1181 +++++++++++++++++ 10 files changed, 1309 insertions(+), 40 deletions(-) create mode 100644 .editorconfig create mode 100644 .mocharc.yml create mode 100644 package.json create mode 100644 token-lending/tests/liquidity-mining.ts delete mode 100644 token-lending/tests/liquidity-mining/fixtures/BgxfHJDzm44T7XG68MYKx7YisTjZu73tVovyZSjJMpmw.json delete mode 100755 token-lending/tests/liquidity-mining/test-reserve-realloc.sh create mode 100644 tsconfig.json create mode 100644 yarn.lock diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000000..ccafd388660 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true + +[*.ts] +indent_size = 2 diff --git a/.mocharc.yml b/.mocharc.yml new file mode 100644 index 00000000000..5c2191843de --- /dev/null +++ b/.mocharc.yml @@ -0,0 +1 @@ +bail: true diff --git a/Anchor.toml b/Anchor.toml index c3690eaf173..bd1b269975a 100644 --- a/Anchor.toml +++ b/Anchor.toml @@ -1,14 +1,31 @@ -anchor_version = "0.13.2" +[toolchain] +package_manager = "yarn" +anchor_version = "0.28.0" + +[features] +resolution = true +skip-lint = false [workspace] -members = [ - "token-lending/program", - "token-lending/brick", -] +members = ["token-lending/program", "token-lending/brick"] [provider] cluster = "mainnet" wallet = "~/.config/solana/id.json" +[scripts] +test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 token-lending/tests/**/*.ts" + [programs.mainnet] spl_token_lending = "So1endDq2YkqhipRh3WViPa8hdiSpxWy6z3Z6tMCpAo" + +[programs.localnet] +solend_program = "So1endDq2YkqhipRh3WViPa8hdiSpxWy6z3Z6tMCpAo" + +[test.validator] +# we use some mainnet accounts for tests +url = "https://api.mainnet-beta.solana.com" + +[[test.validator.clone]] +# Solend Main Pool - (USDC) Reserve State +address = "BgxfHJDzm44T7XG68MYKx7YisTjZu73tVovyZSjJMpmw" diff --git a/package.json b/package.json new file mode 100644 index 00000000000..43611d9784c --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "license": "ISC", + "scripts": { + "lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w", + "lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check" + }, + "dependencies": { + "@coral-xyz/anchor": "^0.28.0" + }, + "devDependencies": { + "chai": "^4.3.4", + "mocha": "^9.0.3", + "ts-mocha": "^10.0.0", + "@types/bn.js": "^5.1.0", + "@types/chai": "^4.3.0", + "@types/mocha": "^9.0.0", + "typescript": "^5.7.3", + "prettier": "^2.6.2" + } +} diff --git a/token-lending/program/src/processor/liquidity_mining.rs b/token-lending/program/src/processor/liquidity_mining.rs index 10fd45e3fa0..2c462609786 100644 --- a/token-lending/program/src/processor/liquidity_mining.rs +++ b/token-lending/program/src/processor/liquidity_mining.rs @@ -247,7 +247,7 @@ pub(crate) fn upgrade_reserve(program_id: &Pubkey, accounts: &[AccountInfo]) -> let new_rent = Rent::get()?.minimum_balance(Reserve::LEN); if let Some(extra_rent) = new_rent.checked_sub(current_rent) { - // this will always be the case unless Solana goes UBI + // some reserves have more rent than necessary invoke( &system_instruction::transfer( diff --git a/token-lending/tests/liquidity-mining.ts b/token-lending/tests/liquidity-mining.ts new file mode 100644 index 00000000000..d3c988d6961 --- /dev/null +++ b/token-lending/tests/liquidity-mining.ts @@ -0,0 +1,66 @@ +/** + * $ anchor test --provider.cluster localnet --detach + */ + +import * as anchor from "@coral-xyz/anchor"; +import { PublicKey } from "@solana/web3.js"; +import { expect } from "chai"; +import { exec } from "node:child_process"; + +describe("liquidity mining", () => { + // Configure the client to use the local cluster. + anchor.setProvider(anchor.AnchorProvider.env()); + + const TEST_RESERVE_FOR_UPGRADE = + "BgxfHJDzm44T7XG68MYKx7YisTjZu73tVovyZSjJMpmw"; + + it("Upgrades reserve to 2.1.0 via CLI", async () => { + // There's an ix that upgrades a reserve to 2.1.0. + // This ix is invocable via our CLI. + // In this test case for comfort and more test coverage we invoke the CLI + // command rather than crafting the ix ourselves. + + const rpcUrl = anchor.getProvider().connection.rpcEndpoint; + + const reserveBefore = await anchor + .getProvider() + .connection.getAccountInfo(new PublicKey(TEST_RESERVE_FOR_UPGRADE)); + + expect(reserveBefore.data.length).to.eq(619); // old version data length + const expectedRentBefore = await anchor + .getProvider() + .connection.getMinimumBalanceForRentExemption(reserveBefore.data.length); + // some reserves have more rent + expect(reserveBefore.lamports).to.be.greaterThanOrEqual(expectedRentBefore); + + const command = `cargo run --quiet --bin solend-cli -- --url ${rpcUrl} upgrade-reserve --reserve ${TEST_RESERVE_FOR_UPGRADE}`; + console.log(`\$ ${command}`); + const cliProcess = exec(command); + + // let us observe progress + cliProcess.stderr.setEncoding("utf8"); + cliProcess.stderr.pipe(process.stderr); + + console.log("Waiting for command to finish..."); + const exitCode = await new Promise((resolve) => + cliProcess.on("exit", (code) => resolve(code)) + ); + + if (exitCode !== 0) { + cliProcess.stdout.setEncoding("utf8"); + console.log("CLI stdout", cliProcess.stdout.read()); + + throw new Error(`Command failed with exit code ${exitCode}`); + } + + const reserveAfter = await anchor + .getProvider() + .connection.getAccountInfo(new PublicKey(TEST_RESERVE_FOR_UPGRADE)); + + expect(reserveAfter.data.length).to.eq(8651); // new version data length + const expectedRentAfter = await anchor + .getProvider() + .connection.getMinimumBalanceForRentExemption(reserveAfter.data.length); + expect(reserveAfter.lamports).to.be.greaterThanOrEqual(expectedRentAfter); + }); +}); diff --git a/token-lending/tests/liquidity-mining/fixtures/BgxfHJDzm44T7XG68MYKx7YisTjZu73tVovyZSjJMpmw.json b/token-lending/tests/liquidity-mining/fixtures/BgxfHJDzm44T7XG68MYKx7YisTjZu73tVovyZSjJMpmw.json deleted file mode 100644 index d9a99c89850..00000000000 --- a/token-lending/tests/liquidity-mining/fixtures/BgxfHJDzm44T7XG68MYKx7YisTjZu73tVovyZSjJMpmw.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "pubkey": "BgxfHJDzm44T7XG68MYKx7YisTjZu73tVovyZSjJMpmw", - "account": { - "lamports": 25199120, - "data": [ - "AUxOchMAAAAAADOzHsTv+PoomuqMlUwBYy4tdkkIzlRNaGW97xEb/2Erxvp6877brTo9ZfNqq8l0MbG75MLS9uDkfKYCA0UvXWEGbpdIlAlxcAyM9MVHGESm8j26mv3yVsBd27pvWYrqpBy+k5qDCfVkBxh//zCsVLFpSYvpn22OG/1CRGgM1PfR4p92kRK2fxPx+u4VYVsvyto2WC/rpbudgsUIcPl0HyXZm2oZhTQVAAAHa18Y7VmYq+p+dyNlAQAAsvbnrPL8IRMAAAAAAAAAAABYOESYQOANAAAAAAAAAAB47R1Wfg2+kjfDllSe3sA08hkpKYMRa2acM+p6tDx4Y/Cmw079JQAAByTVoAxu0AT/TCxk3n5czwWGco/AWYg4MQFXus/s6qBQRgNNAgcUAIDGpH6NAwAAQGNSv8YBABQAsFtMNkQAAADAr9aRNgAAP3BApl+Czb2JX5yhJpGXIgODFyy65r5NP0zSG9YtqS4TFDfMlrajzrNJL4QVAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMzPmpBj4A0AAAAAAAAAAABaXgEAAAAAAAAFTwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAO6txL1fw97n+AkAAAAAAAD/////////////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", - "base64" - ], - "owner": "So1endDq2YkqhipRh3WViPa8hdiSpxWy6z3Z6tMCpAo", - "executable": false, - "rentEpoch": 18446744073709551615, - "space": 619 - } -} \ No newline at end of file diff --git a/token-lending/tests/liquidity-mining/test-reserve-realloc.sh b/token-lending/tests/liquidity-mining/test-reserve-realloc.sh deleted file mode 100755 index 821595125a5..00000000000 --- a/token-lending/tests/liquidity-mining/test-reserve-realloc.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/bash - -# TODO: document, automate and add to CI? - -TOKEN_LENDING_PROGRAM_ID="So1endDq2YkqhipRh3WViPa8hdiSpxWy6z3Z6tMCpAo" -RESERVE_ACCOUNT_PUBKEY="BgxfHJDzm44T7XG68MYKx7YisTjZu73tVovyZSjJMpmw" # Solend Main Pool - (USDC) Reserve State - -git_root="$(git rev-parse --show-toplevel)" -test_reserve_path="${git_root}/token-lending/tests/liquidity-mining/fixtures/${RESERVE_ACCOUNT_PUBKEY}.json" -token_lending_program_path="${git_root}/target/deploy/solend_program.so" - -echo "Building the program..." -cargo-build-sbf -- -p solend-program - -echo "Starting the test validator..." -solana-test-validator --reset \ - --upgradeable-program "${TOKEN_LENDING_PROGRAM_ID}" "${token_lending_program_path}" "$(solana address)" \ - --account "${RESERVE_ACCOUNT_PUBKEY}" "${test_reserve_path}" - -# cargo run --bin solend-cli -- --url http://127.0.0.1:8899 upgrade-reserve --reserve BgxfHJDzm44T7XG68MYKx7YisTjZu73tVovyZSjJMpmw diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000000..cd5d2e3d062 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "types": ["mocha", "chai"], + "typeRoots": ["./node_modules/@types"], + "lib": ["es2015"], + "module": "commonjs", + "target": "es6", + "esModuleInterop": true + } +} diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 00000000000..02cbfddcc0c --- /dev/null +++ b/yarn.lock @@ -0,0 +1,1181 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@babel/runtime@^7.25.0": + version "7.26.10" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.10.tgz#a07b4d8fa27af131a633d7b3524db803eb4764c2" + integrity sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw== + dependencies: + regenerator-runtime "^0.14.0" + +"@coral-xyz/anchor@^0.28.0": + version "0.28.0" + resolved "https://registry.yarnpkg.com/@coral-xyz/anchor/-/anchor-0.28.0.tgz#8345c3c9186a91f095f704d7b90cd256f7e8b2dc" + integrity sha512-kQ02Hv2ZqxtWP30WN1d4xxT4QqlOXYDxmEd3k/bbneqhV3X5QMO4LAtoUFs7otxyivOgoqam5Il5qx81FuI4vw== + dependencies: + "@coral-xyz/borsh" "^0.28.0" + "@solana/web3.js" "^1.68.0" + base64-js "^1.5.1" + bn.js "^5.1.2" + bs58 "^4.0.1" + buffer-layout "^1.2.2" + camelcase "^6.3.0" + cross-fetch "^3.1.5" + crypto-hash "^1.3.0" + eventemitter3 "^4.0.7" + js-sha256 "^0.9.0" + pako "^2.0.3" + snake-case "^3.0.4" + superstruct "^0.15.4" + toml "^3.0.0" + +"@coral-xyz/borsh@^0.28.0": + version "0.28.0" + resolved "https://registry.yarnpkg.com/@coral-xyz/borsh/-/borsh-0.28.0.tgz#fa368a2f2475bbf6f828f4657f40a52102e02b6d" + integrity sha512-/u1VTzw7XooK7rqeD7JLUSwOyRSesPUk0U37BV9zK0axJc1q0nRbKFGFLYCQ16OtdOJTTwGfGp11Lx9B45bRCQ== + dependencies: + bn.js "^5.1.2" + buffer-layout "^1.2.0" + +"@noble/curves@^1.4.2": + version "1.8.1" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.8.1.tgz#19bc3970e205c99e4bdb1c64a4785706bce497ff" + integrity sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ== + dependencies: + "@noble/hashes" "1.7.1" + +"@noble/hashes@1.7.1", "@noble/hashes@^1.4.0": + version "1.7.1" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.7.1.tgz#5738f6d765710921e7a751e00c20ae091ed8db0f" + integrity sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ== + +"@solana/buffer-layout@^4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@solana/buffer-layout/-/buffer-layout-4.0.1.tgz#b996235eaec15b1e0b5092a8ed6028df77fa6c15" + integrity sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA== + dependencies: + buffer "~6.0.3" + +"@solana/web3.js@^1.68.0": + version "1.98.0" + resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.98.0.tgz#21ecfe8198c10831df6f0cfde7f68370d0405917" + integrity sha512-nz3Q5OeyGFpFCR+erX2f6JPt3sKhzhYcSycBCSPkWjzSVDh/Rr1FqTVMRe58FKO16/ivTUcuJjeS5MyBvpkbzA== + dependencies: + "@babel/runtime" "^7.25.0" + "@noble/curves" "^1.4.2" + "@noble/hashes" "^1.4.0" + "@solana/buffer-layout" "^4.0.1" + agentkeepalive "^4.5.0" + bigint-buffer "^1.1.5" + bn.js "^5.2.1" + borsh "^0.7.0" + bs58 "^4.0.1" + buffer "6.0.3" + fast-stable-stringify "^1.0.0" + jayson "^4.1.1" + node-fetch "^2.7.0" + rpc-websockets "^9.0.2" + superstruct "^2.0.2" + +"@swc/helpers@^0.5.11": + version "0.5.15" + resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.15.tgz#79efab344c5819ecf83a43f3f9f811fc84b516d7" + integrity sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g== + dependencies: + tslib "^2.8.0" + +"@types/bn.js@^5.1.0": + version "5.1.6" + resolved "https://registry.yarnpkg.com/@types/bn.js/-/bn.js-5.1.6.tgz#9ba818eec0c85e4d3c679518428afdf611d03203" + integrity sha512-Xh8vSwUeMKeYYrj3cX4lGQgFSF/N03r+tv4AiLl1SucqV+uTQpxRcnM8AkXKHwYP9ZPXOYXRr2KPXpVlIvqh9w== + dependencies: + "@types/node" "*" + +"@types/chai@^4.3.0": + version "4.3.20" + resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.20.tgz#cb291577ed342ca92600430841a00329ba05cecc" + integrity sha512-/pC9HAB5I/xMlc5FP77qjCnI16ChlJfW0tGa0IUcFn38VJrTV6DeZ60NU5KZBtaOZqjdpwTWohz5HU1RrhiYxQ== + +"@types/connect@^3.4.33": + version "3.4.38" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858" + integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug== + dependencies: + "@types/node" "*" + +"@types/json5@^0.0.29": + version "0.0.29" + resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" + integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== + +"@types/mocha@^9.0.0": + version "9.1.1" + resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-9.1.1.tgz#e7c4f1001eefa4b8afbd1eee27a237fee3bf29c4" + integrity sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw== + +"@types/node@*": + version "22.13.10" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.13.10.tgz#df9ea358c5ed991266becc3109dc2dc9125d77e4" + integrity sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw== + dependencies: + undici-types "~6.20.0" + +"@types/node@^12.12.54": + version "12.20.55" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.55.tgz#c329cbd434c42164f846b909bd6f85b5537f6240" + integrity sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ== + +"@types/uuid@^8.3.4": + version "8.3.4" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc" + integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw== + +"@types/ws@^7.4.4": + version "7.4.7" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.4.7.tgz#f7c390a36f7a0679aa69de2d501319f4f8d9b702" + integrity sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww== + dependencies: + "@types/node" "*" + +"@types/ws@^8.2.2": + version "8.18.0" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.18.0.tgz#8a2ec491d6f0685ceaab9a9b7ff44146236993b5" + integrity sha512-8svvI3hMyvN0kKCJMvTJP/x6Y/EoQbepff882wL+Sn5QsXb3etnamgrJq4isrBxSJj5L2AuXcI0+bgkoAXGUJw== + dependencies: + "@types/node" "*" + +"@ungap/promise-all-settled@1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44" + integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q== + +JSONStream@^1.3.5: + version "1.3.5" + resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0" + integrity sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ== + dependencies: + jsonparse "^1.2.0" + through ">=2.2.7 <3" + +agentkeepalive@^4.5.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.6.0.tgz#35f73e94b3f40bf65f105219c623ad19c136ea6a" + integrity sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ== + dependencies: + humanize-ms "^1.2.1" + +ansi-colors@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" + integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +arrify@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" + integrity sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA== + +assertion-error@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" + integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw== + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +base-x@^3.0.2: + version "3.0.11" + resolved "https://registry.yarnpkg.com/base-x/-/base-x-3.0.11.tgz#40d80e2a1aeacba29792ccc6c5354806421287ff" + integrity sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA== + dependencies: + safe-buffer "^5.0.1" + +base64-js@^1.3.1, base64-js@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + +bigint-buffer@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/bigint-buffer/-/bigint-buffer-1.1.5.tgz#d038f31c8e4534c1f8d0015209bf34b4fa6dd442" + integrity sha512-trfYco6AoZ+rKhKnxA0hgX0HAbVP/s808/EuDSe2JDzUnCp/xAsli35Orvk67UrTEcwuxZqYZDmfA2RXJgxVvA== + dependencies: + bindings "^1.3.0" + +binary-extensions@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" + integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== + +bindings@^1.3.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" + integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== + dependencies: + file-uri-to-path "1.0.0" + +bn.js@^5.1.2, bn.js@^5.2.0, bn.js@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70" + integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ== + +borsh@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/borsh/-/borsh-0.7.0.tgz#6e9560d719d86d90dc589bca60ffc8a6c51fec2a" + integrity sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA== + dependencies: + bn.js "^5.2.0" + bs58 "^4.0.0" + text-encoding-utf-8 "^1.0.2" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@~3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + +browser-stdout@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" + integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== + +bs58@^4.0.0, bs58@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/bs58/-/bs58-4.0.1.tgz#be161e76c354f6f788ae4071f63f34e8c4f0a42a" + integrity sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw== + dependencies: + base-x "^3.0.2" + +buffer-from@^1.0.0, buffer-from@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +buffer-layout@^1.2.0, buffer-layout@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/buffer-layout/-/buffer-layout-1.2.2.tgz#b9814e7c7235783085f9ca4966a0cfff112259d5" + integrity sha512-kWSuLN694+KTk8SrYvCqwP2WcgQjoRCiF5b4QDvkkz8EmgD+aWAIceGFKMIAdmF/pH+vpgNV3d3kAKorcdAmWA== + +buffer@6.0.3, buffer@^6.0.3, buffer@~6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" + integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.2.1" + +bufferutil@^4.0.1: + version "4.0.9" + resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-4.0.9.tgz#6e81739ad48a95cad45a279588e13e95e24a800a" + integrity sha512-WDtdLmJvAuNNPzByAYpRo2rF1Mmradw6gvWsQKf63476DDXmomT9zUiGypLcG4ibIM67vhAj8jJRdbmEws2Aqw== + dependencies: + node-gyp-build "^4.3.0" + +camelcase@^6.0.0, camelcase@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== + +chai@^4.3.4: + version "4.5.0" + resolved "https://registry.yarnpkg.com/chai/-/chai-4.5.0.tgz#707e49923afdd9b13a8b0b47d33d732d13812fd8" + integrity sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw== + dependencies: + assertion-error "^1.1.0" + check-error "^1.0.3" + deep-eql "^4.1.3" + get-func-name "^2.0.2" + loupe "^2.3.6" + pathval "^1.1.1" + type-detect "^4.1.0" + +chalk@^4.1.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +check-error@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.3.tgz#a6502e4312a7ee969f646e83bb3ddd56281bd694" + integrity sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg== + dependencies: + get-func-name "^2.0.2" + +chokidar@3.5.3: + version "3.5.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" + integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +commander@^2.20.3: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +cross-fetch@^3.1.5: + version "3.2.0" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.2.0.tgz#34e9192f53bc757d6614304d9e5e6fb4edb782e3" + integrity sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q== + dependencies: + node-fetch "^2.7.0" + +crypto-hash@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/crypto-hash/-/crypto-hash-1.3.0.tgz#b402cb08f4529e9f4f09346c3e275942f845e247" + integrity sha512-lyAZ0EMyjDkVvz8WOeVnuCPvKVBXcMv1l5SVqO1yC7PzTwrD/pPje/BIRbWhMoPe436U+Y2nD7f5bFx0kt+Sbg== + +debug@4.3.3: + version "4.3.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" + integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== + dependencies: + ms "2.1.2" + +decamelize@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837" + integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== + +deep-eql@^4.1.3: + version "4.1.4" + resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-4.1.4.tgz#d0d3912865911bb8fac5afb4e3acfa6a28dc72b7" + integrity sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg== + dependencies: + type-detect "^4.0.0" + +delay@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/delay/-/delay-5.0.0.tgz#137045ef1b96e5071060dd5be60bf9334436bd1d" + integrity sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw== + +diff@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b" + integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== + +diff@^3.1.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" + integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== + +dot-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751" + integrity sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w== + dependencies: + no-case "^3.0.4" + tslib "^2.0.3" + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +es6-promise@^4.0.3: + version "4.2.8" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" + integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== + +es6-promisify@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203" + integrity sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ== + dependencies: + es6-promise "^4.0.3" + +escalade@^3.1.1: + version "3.2.0" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== + +escape-string-regexp@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +eventemitter3@^4.0.7: + version "4.0.7" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" + integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== + +eventemitter3@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-5.0.1.tgz#53f5ffd0a492ac800721bb42c66b841de96423c4" + integrity sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA== + +eyes@^0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/eyes/-/eyes-0.1.8.tgz#62cf120234c683785d902348a800ef3e0cc20bc0" + integrity sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ== + +fast-stable-stringify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fast-stable-stringify/-/fast-stable-stringify-1.0.0.tgz#5c5543462b22aeeefd36d05b34e51c78cb86d313" + integrity sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag== + +file-uri-to-path@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" + integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== + +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + +find-up@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +flat@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" + integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-func-name@^2.0.1, get-func-name@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.2.tgz#0d7cf20cd13fda808669ffa88f4ffc7a3943fc41" + integrity sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ== + +glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob@7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" + integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +growl@1.10.5: + version "1.10.5" + resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" + integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +he@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + +humanize-ms@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" + integrity sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ== + dependencies: + ms "^2.0.0" + +ieee754@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-glob@^4.0.1, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-plain-obj@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" + integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== + +is-unicode-supported@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" + integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +isomorphic-ws@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz#55fd4cd6c5e6491e76dc125938dd863f5cd4f2dc" + integrity sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w== + +jayson@^4.1.1: + version "4.1.3" + resolved "https://registry.yarnpkg.com/jayson/-/jayson-4.1.3.tgz#db9be2e4287d9fef4fc05b5fe367abe792c2eee8" + integrity sha512-LtXh5aYZodBZ9Fc3j6f2w+MTNcnxteMOrb+QgIouguGOulWi0lieEkOUg+HkjjFs0DGoWDds6bi4E9hpNFLulQ== + dependencies: + "@types/connect" "^3.4.33" + "@types/node" "^12.12.54" + "@types/ws" "^7.4.4" + JSONStream "^1.3.5" + commander "^2.20.3" + delay "^5.0.0" + es6-promisify "^5.0.0" + eyes "^0.1.8" + isomorphic-ws "^4.0.1" + json-stringify-safe "^5.0.1" + uuid "^8.3.2" + ws "^7.5.10" + +js-sha256@^0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/js-sha256/-/js-sha256-0.9.0.tgz#0b89ac166583e91ef9123644bd3c5334ce9d0966" + integrity sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA== + +js-yaml@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +json-stringify-safe@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== + +json5@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" + integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== + dependencies: + minimist "^1.2.0" + +jsonparse@^1.2.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" + integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg== + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +log-symbols@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== + dependencies: + chalk "^4.1.0" + is-unicode-supported "^0.1.0" + +loupe@^2.3.6: + version "2.3.7" + resolved "https://registry.yarnpkg.com/loupe/-/loupe-2.3.7.tgz#6e69b7d4db7d3ab436328013d37d1c8c3540c697" + integrity sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA== + dependencies: + get-func-name "^2.0.1" + +lower-case@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28" + integrity sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg== + dependencies: + tslib "^2.0.3" + +make-error@^1.1.1: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + +minimatch@4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-4.2.1.tgz#40d9d511a46bdc4e563c22c3080cde9c0d8299b4" + integrity sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g== + dependencies: + brace-expansion "^1.1.7" + +minimatch@^3.0.4: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimist@^1.2.0, minimist@^1.2.6: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +mkdirp@^0.5.1: + version "0.5.6" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" + integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== + dependencies: + minimist "^1.2.6" + +mocha@^9.0.3: + version "9.2.2" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-9.2.2.tgz#d70db46bdb93ca57402c809333e5a84977a88fb9" + integrity sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g== + dependencies: + "@ungap/promise-all-settled" "1.1.2" + ansi-colors "4.1.1" + browser-stdout "1.3.1" + chokidar "3.5.3" + debug "4.3.3" + diff "5.0.0" + escape-string-regexp "4.0.0" + find-up "5.0.0" + glob "7.2.0" + growl "1.10.5" + he "1.2.0" + js-yaml "4.1.0" + log-symbols "4.1.0" + minimatch "4.2.1" + ms "2.1.3" + nanoid "3.3.1" + serialize-javascript "6.0.0" + strip-json-comments "3.1.1" + supports-color "8.1.1" + which "2.0.2" + workerpool "6.2.0" + yargs "16.2.0" + yargs-parser "20.2.4" + yargs-unparser "2.0.0" + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +ms@2.1.3, ms@^2.0.0: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +nanoid@3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.1.tgz#6347a18cac88af88f58af0b3594b723d5e99bb35" + integrity sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw== + +no-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d" + integrity sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg== + dependencies: + lower-case "^2.0.2" + tslib "^2.0.3" + +node-fetch@^2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== + dependencies: + whatwg-url "^5.0.0" + +node-gyp-build@^4.3.0: + version "4.8.4" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.4.tgz#8a70ee85464ae52327772a90d66c6077a900cfc8" + integrity sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ== + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +pako@^2.0.3: + version "2.1.0" + resolved "https://registry.yarnpkg.com/pako/-/pako-2.1.0.tgz#266cc37f98c7d883545d11335c00fbd4062c9a86" + integrity sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug== + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +pathval@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.1.tgz#8534e77a77ce7ac5a2512ea21e0fdb8fcf6c3d8d" + integrity sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ== + +picomatch@^2.0.4, picomatch@^2.2.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +prettier@^2.6.2: + version "2.8.8" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" + integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== + +randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +regenerator-runtime@^0.14.0: + version "0.14.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" + integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + +rpc-websockets@^9.0.2: + version "9.1.1" + resolved "https://registry.yarnpkg.com/rpc-websockets/-/rpc-websockets-9.1.1.tgz#5764336f3623ee1c5cc8653b7335183e3c0c78bd" + integrity sha512-1IXGM/TfPT6nfYMIXkJdzn+L4JEsmb0FL1O2OBjaH03V3yuUDdKFulGLMFG6ErV+8pZ5HVC0limve01RyO+saA== + dependencies: + "@swc/helpers" "^0.5.11" + "@types/uuid" "^8.3.4" + "@types/ws" "^8.2.2" + buffer "^6.0.3" + eventemitter3 "^5.0.1" + uuid "^8.3.2" + ws "^8.5.0" + optionalDependencies: + bufferutil "^4.0.1" + utf-8-validate "^5.0.2" + +safe-buffer@^5.0.1, safe-buffer@^5.1.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +serialize-javascript@6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8" + integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag== + dependencies: + randombytes "^2.1.0" + +snake-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-3.0.4.tgz#4f2bbd568e9935abdfd593f34c691dadb49c452c" + integrity sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg== + dependencies: + dot-case "^3.0.4" + tslib "^2.0.3" + +source-map-support@^0.5.6: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.6.0: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +string-width@^4.1.0, string-width@^4.2.0: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== + +strip-json-comments@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +superstruct@^0.15.4: + version "0.15.5" + resolved "https://registry.yarnpkg.com/superstruct/-/superstruct-0.15.5.tgz#0f0a8d3ce31313f0d84c6096cd4fa1bfdedc9dab" + integrity sha512-4AOeU+P5UuE/4nOUkmcQdW5y7i9ndt1cQd/3iUe+LTz3RxESf/W/5lg4B74HbDMMv8PHnPnGCQFH45kBcrQYoQ== + +superstruct@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/superstruct/-/superstruct-2.0.2.tgz#3f6d32fbdc11c357deff127d591a39b996300c54" + integrity sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A== + +supports-color@8.1.1: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +text-encoding-utf-8@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz#585b62197b0ae437e3c7b5d0af27ac1021e10d13" + integrity sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg== + +"through@>=2.2.7 <3": + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +toml@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/toml/-/toml-3.0.0.tgz#342160f1af1904ec9d204d03a5d61222d762c5ee" + integrity sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w== + +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== + +ts-mocha@^10.0.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/ts-mocha/-/ts-mocha-10.1.0.tgz#17a1c055f5f7733fd82447c4420740db87221bc8" + integrity sha512-T0C0Xm3/WqCuF2tpa0GNGESTBoKZaiqdUP8guNv4ZY316AFXlyidnrzQ1LUrCT0Wb1i3J0zFTgOh/55Un44WdA== + dependencies: + ts-node "7.0.1" + optionalDependencies: + tsconfig-paths "^3.5.0" + +ts-node@7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-7.0.1.tgz#9562dc2d1e6d248d24bc55f773e3f614337d9baf" + integrity sha512-BVwVbPJRspzNh2yfslyT1PSbl5uIk03EZlb493RKHN4qej/D06n1cEhjlOJG69oFsE7OT8XjpTUcYf6pKTLMhw== + dependencies: + arrify "^1.0.0" + buffer-from "^1.1.0" + diff "^3.1.0" + make-error "^1.1.1" + minimist "^1.2.0" + mkdirp "^0.5.1" + source-map-support "^0.5.6" + yn "^2.0.0" + +tsconfig-paths@^3.5.0: + version "3.15.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz#5299ec605e55b1abb23ec939ef15edaf483070d4" + integrity sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg== + dependencies: + "@types/json5" "^0.0.29" + json5 "^1.0.2" + minimist "^1.2.6" + strip-bom "^3.0.0" + +tslib@^2.0.3, tslib@^2.8.0: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + +type-detect@^4.0.0, type-detect@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.1.0.tgz#deb2453e8f08dcae7ae98c626b13dddb0155906c" + integrity sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw== + +typescript@^5.7.3: + version "5.8.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.2.tgz#8170b3702f74b79db2e5a96207c15e65807999e4" + integrity sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ== + +undici-types@~6.20.0: + version "6.20.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.20.0.tgz#8171bf22c1f588d1554d55bf204bc624af388433" + integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg== + +utf-8-validate@^5.0.2: + version "5.0.10" + resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-5.0.10.tgz#d7d10ea39318171ca982718b6b96a8d2442571a2" + integrity sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ== + dependencies: + node-gyp-build "^4.3.0" + +uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== + +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + +which@2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +workerpool@6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.0.tgz#827d93c9ba23ee2019c3ffaff5c27fccea289e8b" + integrity sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A== + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +ws@^7.5.10: + version "7.5.10" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9" + integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ== + +ws@^8.5.0: + version "8.18.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.1.tgz#ea131d3784e1dfdff91adb0a4a116b127515e3cb" + integrity sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w== + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yargs-parser@20.2.4: + version "20.2.4" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54" + integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== + +yargs-parser@^20.2.2: + version "20.2.9" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" + integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== + +yargs-unparser@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb" + integrity sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA== + dependencies: + camelcase "^6.0.0" + decamelize "^4.0.0" + flat "^5.0.2" + is-plain-obj "^2.1.0" + +yargs@16.2.0: + version "16.2.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" + integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + +yn@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/yn/-/yn-2.0.0.tgz#e5adabc8acf408f6385fc76495684c88e6af689a" + integrity sha512-uTv8J/wiWTgUTg+9vLTi//leUl5vDQS6uii/emeTb2ssY7vl6QWf2fFbIIGjnhjvbdKlU0ed7QPgY1htTC86jQ== + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== From a1bf81195919a90f37062ee35d7e6d72d492f8ec Mon Sep 17 00:00:00 2001 From: vanitymnm Date: Fri, 14 Mar 2025 13:01:34 +0100 Subject: [PATCH 20/30] Version bump --- Cargo.lock | 6 +++--- Cargo.toml | 3 +++ token-lending/cli/Cargo.toml | 10 +++++----- token-lending/cli/src/main.rs | 2 +- token-lending/program/Cargo.toml | 4 ++-- .../program/src/processor/liquidity_mining.rs | 3 ++- token-lending/sdk/Cargo.toml | 2 +- token-lending/sdk/src/instruction.rs | 1 - 8 files changed, 17 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 88adf94e7f6..37fe074561d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5412,7 +5412,7 @@ dependencies = [ [[package]] name = "solend-program" -version = "2.0.2" +version = "2.1.0" dependencies = [ "anchor-lang 0.28.0", "assert_matches", @@ -5441,7 +5441,7 @@ dependencies = [ [[package]] name = "solend-program-cli" -version = "2.0.2" +version = "2.1.0" dependencies = [ "bincode", "clap 2.34.0", @@ -5462,7 +5462,7 @@ dependencies = [ [[package]] name = "solend-sdk" -version = "2.0.2" +version = "2.1.0" dependencies = [ "arrayref", "assert_matches", diff --git a/Cargo.toml b/Cargo.toml index a28824d2685..829068fcb4e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,9 @@ members = [ "token-lending/oracles", ] +[workspace.package] +version = "2.1.0" + [profile.dev] split-debuginfo = "unpacked" diff --git a/token-lending/cli/Cargo.toml b/token-lending/cli/Cargo.toml index 8888352835b..a59aa534750 100644 --- a/token-lending/cli/Cargo.toml +++ b/token-lending/cli/Cargo.toml @@ -1,12 +1,12 @@ [package] +name = "solend-program-cli" +version.workspace = true authors = ["Solend Maintainers "] description = "Solend Program CLI" edition = "2018" homepage = "https://solend.fi" license = "Apache-2.0" -name = "solend-program-cli" repository = "https://github.com/solendprotocol/solana-program-library" -version = "2.0.2" [dependencies] clap = "=2.34.0" @@ -16,9 +16,9 @@ solana-client = "1.14.10" solana-logger = "1.14.10" solana-sdk = "1.14.10" solana-program = "1.14.10" -solend-sdk = { path="../sdk" } -solend-program = { path="../program", features = [ "no-entrypoint" ] } -spl-token = { version = "3.3.0", features=["no-entrypoint"] } +solend-sdk = { path = "../sdk" } +solend-program = { path = "../program", features = ["no-entrypoint"] } +spl-token = { version = "3.3.0", features = ["no-entrypoint"] } spl-associated-token-account = "1.0" solana-account-decoder = "1.14.10" reqwest = { version = "0.12.2", features = ["blocking", "json"] } diff --git a/token-lending/cli/src/main.rs b/token-lending/cli/src/main.rs index 63a9906a0f4..8c30b314b91 100644 --- a/token-lending/cli/src/main.rs +++ b/token-lending/cli/src/main.rs @@ -2625,7 +2625,7 @@ fn send_transaction( CommitmentConfig::confirmed(), RpcSendTransactionConfig { preflight_commitment: Some(CommitmentLevel::Processed), - skip_preflight: true, // TODO + skip_preflight: false, encoding: None, max_retries: None, min_context_slot: None, diff --git a/token-lending/program/Cargo.toml b/token-lending/program/Cargo.toml index a58c4d08c06..44661e3a6ac 100644 --- a/token-lending/program/Cargo.toml +++ b/token-lending/program/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "solend-program" -version = "2.0.2" +version.workspace = true description = "Solend Program" authors = ["Solend Maintainers "] repository = "https://github.com/solendprotocol/solana-program-library" @@ -18,7 +18,7 @@ bytemuck = "1.5.1" solana-program = "=1.16.20" solend-sdk = { path = "../sdk" } oracles = { path = "../oracles" } -spl-token = { version = "3.3.0", features=["no-entrypoint"] } +spl-token = { version = "3.3.0", features = ["no-entrypoint"] } static_assertions = "1.1.0" [dev-dependencies] diff --git a/token-lending/program/src/processor/liquidity_mining.rs b/token-lending/program/src/processor/liquidity_mining.rs index 2c462609786..1c802e5bdfd 100644 --- a/token-lending/program/src/processor/liquidity_mining.rs +++ b/token-lending/program/src/processor/liquidity_mining.rs @@ -247,7 +247,8 @@ pub(crate) fn upgrade_reserve(program_id: &Pubkey, accounts: &[AccountInfo]) -> let new_rent = Rent::get()?.minimum_balance(Reserve::LEN); if let Some(extra_rent) = new_rent.checked_sub(current_rent) { - // some reserves have more rent than necessary + // some reserves have more rent than necessary, let's not assume that + // the payer always needs to add more rent invoke( &system_instruction::transfer( diff --git a/token-lending/sdk/Cargo.toml b/token-lending/sdk/Cargo.toml index 267ed2e09b9..7ebce7c8d64 100644 --- a/token-lending/sdk/Cargo.toml +++ b/token-lending/sdk/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "solend-sdk" -version = "2.0.2" +version.workspace = true description = "Solend Sdk" authors = ["Solend Maintainers "] repository = "https://github.com/solendprotocol/solana-program-library" diff --git a/token-lending/sdk/src/instruction.rs b/token-lending/sdk/src/instruction.rs index a9c19ac49f1..5302817f46b 100644 --- a/token-lending/sdk/src/instruction.rs +++ b/token-lending/sdk/src/instruction.rs @@ -2064,7 +2064,6 @@ pub fn donate_to_reserve( } /// Creates a `UpgradeReserveToV2_1_0` instruction. -/// Be careful, it's expensive $_$ pub fn upgrade_reserve_to_v2_1_0( program_id: Pubkey, reserve_pubkey: Pubkey, From 0bf99130a797870469aaafbdac04d4c67f79c562 Mon Sep 17 00:00:00 2001 From: vanitymnm Date: Fri, 14 Mar 2025 13:08:59 +0100 Subject: [PATCH 21/30] Using latest cache action --- .github/workflows/pull-request-token-lending.yml | 14 +++++++------- .github/workflows/pull-request.yml | 14 +++++++------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/pull-request-token-lending.yml b/.github/workflows/pull-request-token-lending.yml index f1c1c1ff844..5e88e428059 100644 --- a/.github/workflows/pull-request-token-lending.yml +++ b/.github/workflows/pull-request-token-lending.yml @@ -3,13 +3,13 @@ name: Token Lending Pull Request on: pull_request: paths: - - 'token-lending/**' - - 'token/**' + - "token-lending/**" + - "token/**" push: branches: [master] paths: - - 'token-lending/**' - - 'token/**' + - "token-lending/**" + - "token/**" jobs: cargo-test-bpf: @@ -30,20 +30,20 @@ jobs: override: true profile: minimal - - uses: actions/cache@v2 + - uses: actions/cache@v4 with: path: | ~/.cargo/registry ~/.cargo/git key: cargo-build-${{ hashFiles('**/Cargo.lock') }}-${{ env.RUST_STABLE}} - - uses: actions/cache@v2 + - uses: actions/cache@v4 with: path: | ~/.cargo/bin/rustfilt key: cargo-bpf-bins-${{ runner.os }} - - uses: actions/cache@v2 + - uses: actions/cache@v4 with: path: | ~/.cache diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 2221f07590c..badf172312b 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -3,11 +3,11 @@ name: Pull Request on: pull_request: paths-ignore: - - 'docs/**' + - "docs/**" push: branches: [master, upcoming] paths-ignore: - - 'docs/**' + - "docs/**" jobs: all_github_action_checks: @@ -59,7 +59,7 @@ jobs: profile: minimal components: clippy - - uses: actions/cache@v2 + - uses: actions/cache@v4 with: path: | ~/.cargo/registry @@ -96,7 +96,7 @@ jobs: override: true profile: minimal - - uses: actions/cache@v2 + - uses: actions/cache@v4 with: path: | ~/.cargo/registry @@ -104,13 +104,13 @@ jobs: # target # Removed due to build dependency caching conflicts key: cargo-build-${{ hashFiles('**/Cargo.lock') }}-${{ env.RUST_STABLE}} - - uses: actions/cache@v2 + - uses: actions/cache@v4 with: path: | ~/.cargo/bin/rustfilt key: cargo-bpf-bins-${{ runner.os }} - - uses: actions/cache@v2 + - uses: actions/cache@v4 with: path: | ~/.cache @@ -143,7 +143,7 @@ jobs: override: true profile: minimal - - uses: actions/cache@v2 + - uses: actions/cache@v4 with: path: | ~/.cargo/registry From 7e19f5cb35be6245e0ef3122ba5f5945a80f8432 Mon Sep 17 00:00:00 2001 From: vanitymnm Date: Sun, 16 Mar 2025 20:16:58 +0100 Subject: [PATCH 22/30] Packs and unpacks user managers --- .gitignore | 1 + token-lending/program/tests/init_reserve.rs | 2 + .../sdk/src/state/liquidity_mining.rs | 238 +++++++++++++++--- 3 files changed, 209 insertions(+), 32 deletions(-) diff --git a/.gitignore b/.gitignore index 7a244d9bf0d..b7e8b8e693f 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ hfuzz_workspace **/*.so **/.DS_Store test-ledger +cargo-test-*.profraw diff --git a/token-lending/program/tests/init_reserve.rs b/token-lending/program/tests/init_reserve.rs index a66f8d59209..6e199e86a66 100644 --- a/token-lending/program/tests/init_reserve.rs +++ b/token-lending/program/tests/init_reserve.rs @@ -182,6 +182,8 @@ async fn test_success() { config: reserve_config, rate_limiter: RateLimiter::new(RateLimiterConfig::default(), 1001), attributed_borrow_value: Decimal::zero(), + borrows_pool_reward_manager: Default::default(), + deposits_pool_reward_manager: Default::default(), } ); } diff --git a/token-lending/sdk/src/state/liquidity_mining.rs b/token-lending/sdk/src/state/liquidity_mining.rs index 5fdbfe07b74..099f5652671 100644 --- a/token-lending/sdk/src/state/liquidity_mining.rs +++ b/token-lending/sdk/src/state/liquidity_mining.rs @@ -110,9 +110,10 @@ pub struct PoolReward { } /// Tracks user's LM rewards for a specific pool (reserve.) +#[derive(Debug, PartialEq, Eq, Default)] pub struct UserRewardManager { /// User cannot both borrow and deposit in the same reserve. - /// This manager is unique for this reserve within the [Obligation]. + /// This manager is unique for this reserve within an obligation. /// /// We know whether to use [crate::state::Reserve]'s /// `deposits_pool_reward_manager` or `borrows_pool_reward_manager` based on @@ -130,13 +131,25 @@ pub struct UserRewardManager { 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>, + /// The indices on [Self::rewards] are _not_ correlated with + /// [PoolRewardManager::pool_rewards]. + /// Instead, this vector only tracks meaningful rewards for the user. + /// See [UserReward::pool_reward_index]. + /// + /// This is a diversion from the Suilend implementation. + pub rewards: Vec, } /// Track user rewards for a specific [PoolReward]. +#[derive(Debug, PartialEq, Eq, Default)] pub struct UserReward { + /// Which [PoolReward] within the reserve's index does this [UserReward] + /// correspond to. + /// + /// # (Un)packing + /// There are ever only going to be at most [MAX_REWARDS]. + /// We therefore pack this value into a byte. + pub pool_reward_index: usize, /// Each pool reward gets an ID which is monotonically increasing with each /// new reward added to the pool. pub pool_reward_id: PoolRewardId, @@ -233,26 +246,56 @@ impl UserRewardManager { return Ok(()); } - self.rewards - .resize_with(pool_reward_manager.pool_rewards.len(), || None); - - for (reward_index, pool_reward) in pool_reward_manager.pool_rewards.iter_mut().enumerate() { + for (pool_reward_index, pool_reward) in + pool_reward_manager.pool_rewards.iter_mut().enumerate() + { let PoolRewardSlot::Occupied(pool_reward) = pool_reward else { // no reward to track continue; }; let end_time_secs = pool_reward.start_time_secs + pool_reward.duration_secs as u64; + let has_ended = self.last_update_time_secs > end_time_secs; + + let maybe_user_reward = self + .rewards + .iter_mut() + .enumerate() + .find(|(_, r)| r.pool_reward_index == pool_reward_index); + + match maybe_user_reward { + Some((user_reward_index, user_reward)) + if has_ended && user_reward.earned_rewards == Decimal::zero() => + { + // Reward period ended and there's nothing to crank. + // We can clean up this user reward. + // We're fine with swap remove bcs `user_reward_index` is meaningless. + // SAFETY: We got the index from enumeration, so must exist/ + self.rewards.swap_remove(user_reward_index); + pool_reward.num_user_reward_managers -= 1; + } + _ if has_ended => { + // reward period over & there are rewards yet to be cracked + } + Some((_, user_reward)) => { + // user is already accruing rewards, add the difference - match self.rewards.get_mut(reward_index) { - None => unreachable!("We've just resized the rewards."), - Some(None) if self.last_update_time_secs > end_time_secs => { - // reward period ended, skip + let new_reward_amount = pool_reward + .cumulative_rewards_per_share + .try_sub(user_reward.cumulative_rewards_per_share)? + .try_mul(Decimal::from(self.share))?; + + user_reward.earned_rewards = + user_reward.earned_rewards.try_add(new_reward_amount)?; + + user_reward.cumulative_rewards_per_share = + pool_reward.cumulative_rewards_per_share; } - Some(None) => { + None => { // user did not yet start accruing rewards let new_user_reward = UserReward { + pool_reward_index, pool_reward_id: pool_reward.id, cumulative_rewards_per_share: pool_reward.cumulative_rewards_per_share, earned_rewards: if self.last_update_time_secs <= pool_reward.start_time_secs @@ -269,25 +312,9 @@ impl UserRewardManager { }, }; - // we resized this vector to match the pool rewards - self.rewards[reward_index] = Some(new_user_reward); - + self.rewards.push(new_user_reward); pool_reward.num_user_reward_managers += 1; } - Some(Some(user_reward)) => { - // user is already accruing rewards, add the difference - - let new_reward_amount = pool_reward - .cumulative_rewards_per_share - .try_sub(user_reward.cumulative_rewards_per_share)? - .try_mul(Decimal::from(self.share))?; - - user_reward.earned_rewards = - user_reward.earned_rewards.try_add(new_reward_amount)?; - - user_reward.cumulative_rewards_per_share = - pool_reward.cumulative_rewards_per_share; - } } } @@ -400,6 +427,7 @@ impl Pack for PoolRewardManager { *dst_duration_secs = duration_secs.to_le_bytes(); *dst_total_rewards = total_rewards.to_le_bytes(); *dst_num_user_reward_managers = num_user_reward_managers.to_le_bytes(); + // TBD: do we want to ceil? pack_decimal( cumulative_rewards_per_share, dst_cumulative_rewards_per_share_wads, @@ -465,6 +493,119 @@ impl Pack for PoolRewardManager { } } +impl UserReward { + /// - [UserReward::pool_reward_index] truncated to a byte + /// - [PoolRewardId] + /// - packed [Decimal] + /// - packed [Decimal] + pub const LEN: usize = 1 + PoolRewardId::LEN + 16 + 16; +} + +impl UserRewardManager { + /// [Self] is dynamically sized based on how many [PoolReward]s are there + /// for the given [Self::reserve]. + /// + /// This is the maximum length a manager can have. + const MAX_LEN: usize = Self::HEAD_LEN + MAX_REWARDS * UserReward::LEN; + + /// Length of data before [Self::rewards] tail. + /// + /// - [Self::reserve] + /// - [Self::share] + /// - [Self::last_update_time_secs] + /// - [Self::rewards] vector length + const HEAD_LEN: usize = PUBKEY_BYTES + 8 + 8 + 8; + + /// Because [Self] is dynamically sized we don't implement [Pack] that + /// contains a misleading const `LEN`. + pub(crate) fn pack_into_slice(&self, output: &mut [u8]) { + let raw_user_reward_manager = array_mut_ref![output, 0, UserRewardManager::HEAD_LEN]; + + let (dst_reserve, dst_share, dst_last_update_time_secs, dst_user_rewards_len) = mut_array_refs![ + raw_user_reward_manager, + PUBKEY_BYTES, + 8, // share + 8, // last_update_time_secs + 8 // length of rewards array that's next to come + ]; + + dst_share.copy_from_slice(&self.share.to_le_bytes()); + dst_last_update_time_secs.copy_from_slice(&self.last_update_time_secs.to_le_bytes()); + dst_reserve.copy_from_slice(self.reserve.as_ref()); + dst_user_rewards_len.copy_from_slice(&self.rewards.len().to_le_bytes()); + + for (index, user_reward) in self.rewards.iter().enumerate() { + let offset = Self::HEAD_LEN + index * UserReward::LEN; + let raw_user_reward = array_mut_ref![output, offset, UserReward::LEN]; + + let ( + dst_pool_reward_index, + dst_pool_reward_id, + dst_earned_rewards, + dst_cumulative_rewards_per_share, + ) = mut_array_refs![raw_user_reward, 1, PoolRewardId::LEN, 16, 16]; + + dst_pool_reward_id.copy_from_slice(&user_reward.pool_reward_id.0.to_le_bytes()); + pack_decimal(user_reward.earned_rewards, dst_earned_rewards); + pack_decimal( + user_reward.cumulative_rewards_per_share, + dst_cumulative_rewards_per_share, + ); + let pool_reward_index = { + assert!(user_reward.pool_reward_index < MAX_REWARDS); + assert!(MAX_REWARDS < u8::MAX as _); + // will always fit + user_reward.pool_reward_index as u8 + }; + dst_pool_reward_index.copy_from_slice(&pool_reward_index.to_le_bytes()); + } + } + + pub(crate) fn unpack_from_slice(input: &[u8]) -> Result { + let raw_user_reward_manager_head = array_ref![input, 0, UserRewardManager::HEAD_LEN]; + + let (src_reserve, src_share, src_last_update_time_secs, src_user_rewards_len) = array_refs![ + raw_user_reward_manager_head, + PUBKEY_BYTES, + 8, // share + 8, // last_update_time_secs + 8 // length of rewards array that's next to come + ]; + + let reserve = Pubkey::new_from_array(*src_reserve); + let user_rewards_len = usize::from_le_bytes(*src_user_rewards_len); + let share = u64::from_le_bytes(*src_share); + let last_update_time_secs = u64::from_le_bytes(*src_last_update_time_secs); + + let mut rewards = Vec::with_capacity(user_rewards_len); + for index in 0..user_rewards_len { + let offset = Self::HEAD_LEN + index * UserReward::LEN; + let raw_user_reward = array_ref![input, offset, UserReward::LEN]; + + let ( + src_pool_reward_index, + src_pool_reward_id, + src_earned_rewards, + src_cumulative_rewards_per_share, + ) = array_refs![raw_user_reward, 1, PoolRewardId::LEN, 16, 16]; + + rewards.push(UserReward { + pool_reward_index: u8::from_le_bytes(*src_pool_reward_index) as _, + pool_reward_id: PoolRewardId(u32::from_le_bytes(*src_pool_reward_id)), + earned_rewards: unpack_decimal(src_earned_rewards), + cumulative_rewards_per_share: unpack_decimal(src_cumulative_rewards_per_share), + }); + } + + Ok(Self { + reserve, + share, + last_update_time_secs, + rewards, + }) + } +} + #[cfg(test)] mod tests { //! TODO: Rewrite these tests from their Suilend counterparts. @@ -478,18 +619,30 @@ mod tests { (0..100u32).prop_perturb(|_, mut rng| PoolRewardManager::new_rand(&mut rng)) } + fn user_reward_manager_strategy() -> impl Strategy { + (0..100u32).prop_perturb(|_, mut rng| UserRewardManager::new_rand(&mut rng)) + } + proptest! { #[test] - fn it_packs_and_unpacks(pool_reward_manager in pool_reward_manager_strategy()) { + fn it_packs_and_unpacks_pool_reward_manager(pool_reward_manager in pool_reward_manager_strategy()) { let mut packed = vec![0u8; PoolRewardManager::LEN]; Pack::pack_into_slice(&pool_reward_manager, &mut packed); let unpacked = PoolRewardManager::unpack_from_slice(&packed).unwrap(); prop_assert_eq!(pool_reward_manager, unpacked); } + + #[test] + fn it_packs_and_unpacks_user_reward_manager(user_reward_manager in user_reward_manager_strategy()) { + let mut packed = vec![0u8; UserRewardManager::MAX_LEN]; + user_reward_manager.pack_into_slice(&mut packed); + let unpacked = UserRewardManager::unpack_from_slice(&packed).unwrap(); + prop_assert_eq!(user_reward_manager, unpacked); + } } #[test] - fn it_unpacks_empty_bytes_as_default() { + fn it_unpacks_empty_pool_reward_manager_bytes_as_default() { let packed = vec![0u8; PoolRewardManager::LEN]; let unpacked = PoolRewardManager::unpack_from_slice(&packed).unwrap(); assert_eq!(unpacked, PoolRewardManager::default()); @@ -573,4 +726,25 @@ mod tests { } } } + + impl UserRewardManager { + pub(crate) fn new_rand(rng: &mut impl Rng) -> Self { + let rewards_len = rng.gen_range(0..MAX_REWARDS); + Self { + reserve: Pubkey::new_unique(), + share: rng.gen(), + last_update_time_secs: rng.gen(), + rewards: std::iter::from_fn(|| { + Some(UserReward { + pool_reward_index: rng.gen_range(0..MAX_REWARDS), + pool_reward_id: PoolRewardId(rng.gen()), + earned_rewards: Decimal::from_scaled_val(rng.gen()), + cumulative_rewards_per_share: Decimal::from_scaled_val(rng.gen()), + }) + }) + .take(rewards_len) + .collect(), + } + } + } } From 1486fd94f81d4756285eb84ef8606b6fd07533ad Mon Sep 17 00:00:00 2001 From: vanitymnm Date: Mon, 17 Mar 2025 09:24:10 +0100 Subject: [PATCH 23/30] Removing Pack trait from Obligation --- token-lending/program/src/processor.rs | 2 +- .../sdk/src/state/liquidity_mining.rs | 27 +++--- token-lending/sdk/src/state/obligation.rs | 82 ++++++++++++++++++- 3 files changed, 96 insertions(+), 15 deletions(-) diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs index 4006289dc76..b458291b1b6 100644 --- a/token-lending/program/src/processor.rs +++ b/token-lending/program/src/processor.rs @@ -983,7 +983,7 @@ fn process_init_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) -> Pro let token_program_id = next_account_info(account_info_iter)?; assert_rent_exempt(rent, obligation_info)?; - let mut obligation = assert_uninitialized::(obligation_info)?; + let mut obligation = Obligation::unpack_uninitialized(&obligation_info.data.borrow())?; if obligation_info.owner != program_id { msg!("Obligation provided is not owned by the lending program"); return Err(LendingError::InvalidAccountOwner.into()); diff --git a/token-lending/sdk/src/state/liquidity_mining.rs b/token-lending/sdk/src/state/liquidity_mining.rs index 099f5652671..e4f98a5a29b 100644 --- a/token-lending/sdk/src/state/liquidity_mining.rs +++ b/token-lending/sdk/src/state/liquidity_mining.rs @@ -14,7 +14,7 @@ use solana_program::{ use super::pack_decimal; /// Determines the size of [PoolRewardManager] -const MAX_REWARDS: usize = 50; +pub const MAX_REWARDS: usize = 50; /// Cannot create a reward shorter than this. pub const MIN_REWARD_PERIOD_SECS: u64 = 3_600; @@ -110,7 +110,7 @@ pub struct PoolReward { } /// Tracks user's LM rewards for a specific pool (reserve.) -#[derive(Debug, PartialEq, Eq, Default)] +#[derive(Debug, PartialEq, Eq, Default, Clone)] pub struct UserRewardManager { /// User cannot both borrow and deposit in the same reserve. /// This manager is unique for this reserve within an obligation. @@ -141,7 +141,7 @@ pub struct UserRewardManager { } /// Track user rewards for a specific [PoolReward]. -#[derive(Debug, PartialEq, Eq, Default)] +#[derive(Debug, PartialEq, Eq, Default, Clone)] pub struct UserReward { /// Which [PoolReward] within the reserve's index does this [UserReward] /// correspond to. @@ -506,15 +506,15 @@ impl UserRewardManager { /// for the given [Self::reserve]. /// /// This is the maximum length a manager can have. - const MAX_LEN: usize = Self::HEAD_LEN + MAX_REWARDS * UserReward::LEN; + pub const MAX_LEN: usize = Self::HEAD_LEN + MAX_REWARDS * UserReward::LEN; /// Length of data before [Self::rewards] tail. /// /// - [Self::reserve] /// - [Self::share] /// - [Self::last_update_time_secs] - /// - [Self::rewards] vector length - const HEAD_LEN: usize = PUBKEY_BYTES + 8 + 8 + 8; + /// - [Self::rewards] vector length as u8 + const HEAD_LEN: usize = PUBKEY_BYTES + 8 + 8 + 1; /// Because [Self] is dynamically sized we don't implement [Pack] that /// contains a misleading const `LEN`. @@ -526,13 +526,20 @@ impl UserRewardManager { PUBKEY_BYTES, 8, // share 8, // last_update_time_secs - 8 // length of rewards array that's next to come + 1 // length of rewards array that's next to come ]; dst_share.copy_from_slice(&self.share.to_le_bytes()); dst_last_update_time_secs.copy_from_slice(&self.last_update_time_secs.to_le_bytes()); dst_reserve.copy_from_slice(self.reserve.as_ref()); - dst_user_rewards_len.copy_from_slice(&self.rewards.len().to_le_bytes()); + dst_user_rewards_len.copy_from_slice( + &({ + assert!(MAX_REWARDS >= self.rewards.len()); + assert!(u8::MAX >= MAX_REWARDS as _); + self.rewards.len() as u8 + }) + .to_le_bytes(), + ); for (index, user_reward) in self.rewards.iter().enumerate() { let offset = Self::HEAD_LEN + index * UserReward::LEN; @@ -569,11 +576,11 @@ impl UserRewardManager { PUBKEY_BYTES, 8, // share 8, // last_update_time_secs - 8 // length of rewards array that's next to come + 1 // length of rewards array that's next to come ]; let reserve = Pubkey::new_from_array(*src_reserve); - let user_rewards_len = usize::from_le_bytes(*src_user_rewards_len); + let user_rewards_len = u8::from_le_bytes(*src_user_rewards_len) as _; let share = u64::from_le_bytes(*src_share); let last_update_time_secs = u64::from_le_bytes(*src_last_update_time_secs); diff --git a/token-lending/sdk/src/state/obligation.rs b/token-lending/sdk/src/state/obligation.rs index d4bbe50e5e8..aeb2f187e03 100644 --- a/token-lending/sdk/src/state/obligation.rs +++ b/token-lending/sdk/src/state/obligation.rs @@ -9,7 +9,7 @@ use solana_program::{ entrypoint::ProgramResult, msg, program_error::ProgramError, - program_pack::{IsInitialized, Pack, Sealed}, + program_pack::{IsInitialized, Sealed}, pubkey::{Pubkey, PUBKEY_BYTES}, }; use std::{ @@ -21,6 +21,13 @@ use std::{ pub const MAX_OBLIGATION_RESERVES: usize = 10; /// Lending market obligation state +/// +/// # (Un)packing +/// [Obligation] used to implement `Pack` in versions prior to 2.1.0. +/// Now [Obligation] is dynamically sized based on the reserves in +/// [Obligation::user_reward_managers]. +/// We manually implement packing and unpacking functions the the `Pack` trait +/// instead. #[derive(Clone, Debug, Default, PartialEq)] pub struct Obligation { /// Version of the struct @@ -63,6 +70,12 @@ pub struct Obligation { pub borrowing_isolated_asset: bool, /// Obligation can be marked as closeable pub closeable: bool, + /// Collects liquidity mining rewards for positions (collateral/borrows). + /// + /// # (Un)packing + /// If there are no rewards to be collected then the obligation is packed + /// as if there was no liquidity mining feature involved. + pub user_reward_managers: Vec, } /// These are the two foundational user interactions in a borrow-lending protocol. @@ -426,10 +439,61 @@ 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 { - const LEN: usize = OBLIGATION_LEN_V1; +impl Obligation { + /// Obligation with no Liquidity Mining Rewards + const MIN_LEN: usize = OBLIGATION_LEN_V1; + + /// Maximum account size for obligation. + /// Scenario in which all reserves have all associated rewards filled. + /// + /// - [Self::user_reward_managers] vec length in u8 + /// - [Self::user_reward_managers] vector + const MAX_LEN: usize = Self::MIN_LEN + 1 + MAX_OBLIGATION_RESERVES * UserRewardManager::MAX_LEN; + + /// Unpacks from slice but returns an error if the account is already + /// initialized. + pub fn unpack_uninitialized(input: &[u8]) -> Result { + let account = Self::unpack_unchecked(&input)?; + if account.is_initialized() { + Err(LendingError::AlreadyInitialized.into()) + } else { + Ok(account) + } + } + + /// Unpack from slice and check if initialized + pub fn unpack(input: &[u8]) -> Result + where + Self: IsInitialized, + { + let value = Self::unpack_unchecked(input)?; + if value.is_initialized() { + Ok(value) + } else { + Err(ProgramError::UninitializedAccount) + } + } + + /// Unpack from slice without checking if initialized + pub fn unpack_unchecked(input: &[u8]) -> Result { + // TODO: add discriminant + if !(Self::MIN_LEN..=Self::MAX_LEN).contains(&input.len()) { + return Err(ProgramError::InvalidAccountData); + } + Self::unpack_from_slice(input) + } + + /// Pack into slice + pub fn pack(src: Self, dst: &mut [u8]) -> Result<(), ProgramError> { + // TODO: add discriminant + if !(Self::MIN_LEN..=Self::MAX_LEN).contains(&dst.len()) { + return Err(ProgramError::InvalidAccountData); + } + src.pack_into_slice(dst); + Ok(()) + } - // @v2.1.0 TODO: pack vec of user reward managers + /// @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_V1]; #[allow(clippy::ptr_offset_with_cast)] @@ -633,6 +697,8 @@ impl Pack for Obligation { offset += OBLIGATION_LIQUIDITY_LEN; } + let user_reward_managers = Vec::new(); // TODO + Ok(Self { version, last_update: LastUpdate { @@ -652,6 +718,7 @@ impl Pack for Obligation { super_unhealthy_borrow_value: unpack_decimal(super_unhealthy_borrow_value), borrowing_isolated_asset: unpack_bool(borrowing_isolated_asset)?, closeable: unpack_bool(closeable)?, + user_reward_managers, }) } } @@ -715,6 +782,13 @@ mod test { super_unhealthy_borrow_value: rand_decimal(), borrowing_isolated_asset: rng.gen(), closeable: rng.gen(), + user_reward_managers: { + let user_reward_managers_len = rng.gen_range(0..=MAX_OBLIGATION_RESERVES); + + std::iter::repeat_with(|| UserRewardManager::new_rand(&mut rng)) + .take(user_reward_managers_len) + .collect() + }, }; let mut packed = [0u8; OBLIGATION_LEN_V1]; From 8163fc874cc4bbd3d5db94947f19e43f01318ef0 Mon Sep 17 00:00:00 2001 From: vanitymnm Date: Mon, 17 Mar 2025 13:39:12 +0100 Subject: [PATCH 24/30] Adding account discriminator --- .../program/src/processor/liquidity_mining.rs | 2 +- .../program/tests/init_lending_market.rs | 2 +- .../program/tests/init_obligation.rs | 2 +- token-lending/program/tests/init_reserve.rs | 2 +- .../program/tests/refresh_obligation.rs | 2 +- token-lending/sdk/src/state/lending_market.rs | 121 ++++++++++---- .../sdk/src/state/liquidity_mining.rs | 11 +- token-lending/sdk/src/state/mod.rs | 106 +++++++++++- token-lending/sdk/src/state/obligation.rs | 153 ++++++++++++++---- token-lending/sdk/src/state/reserve.rs | 107 +++++++++--- 10 files changed, 413 insertions(+), 95 deletions(-) diff --git a/token-lending/program/src/processor/liquidity_mining.rs b/token-lending/program/src/processor/liquidity_mining.rs index 1c802e5bdfd..b16ed16e424 100644 --- a/token-lending/program/src/processor/liquidity_mining.rs +++ b/token-lending/program/src/processor/liquidity_mining.rs @@ -282,7 +282,7 @@ pub(crate) fn upgrade_reserve(program_id: &Pubkey, accounts: &[AccountInfo]) -> // 3. // - // sanity checks pack and unpack reserves is ok + // updates version and discriminator let reserve = Reserve::unpack(&accounts.reserve_info.data.borrow())?; Reserve::pack(reserve, &mut accounts.reserve_info.data.borrow_mut())?; diff --git a/token-lending/program/tests/init_lending_market.rs b/token-lending/program/tests/init_lending_market.rs index 7549463dc9b..de9b88d206e 100644 --- a/token-lending/program/tests/init_lending_market.rs +++ b/token-lending/program/tests/init_lending_market.rs @@ -12,7 +12,7 @@ use solana_sdk::signer::Signer; use solana_sdk::transaction::TransactionError; use solend_program::error::LendingError; use solend_program::instruction::init_lending_market; -use solend_program::state::{LendingMarket, RateLimiter, PROGRAM_VERSION}; +use solend_program::state::{LendingMarket, RateLimiter, PROGRAM_VERSION_2_0_2}; #[tokio::test] async fn test_success() { diff --git a/token-lending/program/tests/init_obligation.rs b/token-lending/program/tests/init_obligation.rs index 943f5768d6a..2b720c69f8d 100644 --- a/token-lending/program/tests/init_obligation.rs +++ b/token-lending/program/tests/init_obligation.rs @@ -13,7 +13,7 @@ use solana_sdk::transaction::TransactionError; use solend_program::error::LendingError; use solend_program::instruction::init_obligation; use solend_program::math::Decimal; -use solend_program::state::{LastUpdate, LendingMarket, Obligation, PROGRAM_VERSION}; +use solend_program::state::{LastUpdate, LendingMarket, Obligation, PROGRAM_VERSION_2_0_2}; async fn setup() -> (SolendProgramTest, Info, User) { let (test, lending_market, _, _, _, user) = diff --git a/token-lending/program/tests/init_reserve.rs b/token-lending/program/tests/init_reserve.rs index 6e199e86a66..6ad45ad2d56 100644 --- a/token-lending/program/tests/init_reserve.rs +++ b/token-lending/program/tests/init_reserve.rs @@ -31,7 +31,7 @@ use solend_program::state::RateLimiter; use solend_program::state::Reserve; use solend_program::state::ReserveCollateral; use solend_program::state::ReserveLiquidity; -use solend_program::state::PROGRAM_VERSION; +use solend_program::state::PROGRAM_VERSION_2_0_2; use solend_program::NULL_PUBKEY; use solend_program::{ diff --git a/token-lending/program/tests/refresh_obligation.rs b/token-lending/program/tests/refresh_obligation.rs index b16a0f3a519..94891359095 100644 --- a/token-lending/program/tests/refresh_obligation.rs +++ b/token-lending/program/tests/refresh_obligation.rs @@ -12,7 +12,7 @@ use solend_program::instruction::refresh_obligation; use solend_program::processor::process_instruction; use solend_program::state::ObligationCollateral; -use solend_sdk::state::PROGRAM_VERSION; +use solend_sdk::state::PROGRAM_VERSION_2_0_2; use std::collections::HashSet; use helpers::solend_program_test::{setup_world, BalanceChecker, Info, SolendProgramTest, User}; diff --git a/token-lending/sdk/src/state/lending_market.rs b/token-lending/sdk/src/state/lending_market.rs index 1836dc76e2d..492e307ccfe 100644 --- a/token-lending/sdk/src/state/lending_market.rs +++ b/token-lending/sdk/src/state/lending_market.rs @@ -10,8 +10,17 @@ use solana_program::{ /// Lending market state #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct LendingMarket { - /// Version of lending market - pub version: u8, + /// For historical reasons we mask both program version and account + /// discriminator onto 1 byte. + /// + /// For accounts last used with version prior to @v2.1.0 this will be equal + /// to [ProgramVersion::V2_0_2]. + /// + /// For uninitialized accounts, this will be equal to [ProgramVersion::Uninitialized]. + /// + /// Accounts after including @v2.1.0 use first 4 bits for discriminator and + /// last 4 bits for program version. + pub discriminator_and_version: u8, /// Bump seed for derived authority address pub bump_seed: u8, /// Owner authority which can add new reserves @@ -43,7 +52,10 @@ impl LendingMarket { /// Initialize a lending market pub fn init(&mut self, params: InitLendingMarketParams) { - self.version = PROGRAM_VERSION; + self.discriminator_and_version = set_discriminator_and_version( + AccountDiscriminator::LendingMarket, + ProgramVersion::V2_1_0, + ); self.bump_seed = params.bump_seed; self.owner = params.owner; self.quote_currency = params.quote_currency; @@ -76,7 +88,10 @@ pub struct InitLendingMarketParams { impl Sealed for LendingMarket {} impl IsInitialized for LendingMarket { fn is_initialized(&self) -> bool { - self.version != UNINITIALIZED_VERSION + match extract_discriminator_and_version(self.discriminator_and_version) { + Ok((_, version)) => !matches!(version, ProgramVersion::Uninitialized), + Err(_) => unreachable!("There is no path to invalid discriminator/version"), + } } } @@ -88,7 +103,7 @@ impl Pack for LendingMarket { let output = array_mut_ref![output, 0, LENDING_MARKET_LEN]; #[allow(clippy::ptr_offset_with_cast)] let ( - version, + discriminator_and_version, bump_seed, owner, quote_currency, @@ -114,7 +129,7 @@ impl Pack for LendingMarket { 8 ]; - *version = self.version.to_le_bytes(); + *discriminator_and_version = self.discriminator_and_version.to_le_bytes(); *bump_seed = self.bump_seed.to_le_bytes(); owner.copy_from_slice(self.owner.as_ref()); quote_currency.copy_from_slice(self.quote_currency.as_ref()); @@ -138,7 +153,7 @@ impl Pack for LendingMarket { let input = array_ref![input, 0, LENDING_MARKET_LEN]; #[allow(clippy::ptr_offset_with_cast)] let ( - version, + discriminator_and_version, bump_seed, owner, quote_currency, @@ -164,15 +179,34 @@ impl Pack for LendingMarket { 8 ]; - let version = u8::from_le_bytes(*version); - if version > PROGRAM_VERSION { - msg!("Lending market version does not match lending program version"); - return Err(ProgramError::InvalidAccountData); - } + match extract_discriminator_and_version(u8::from_le_bytes(*discriminator_and_version)) { + Ok((AccountDiscriminator::LendingMarket, ProgramVersion::V2_1_0)) => { + // migrated and all ok + } + Ok((AccountDiscriminator::LendingMarket, ProgramVersion::V2_0_2)) => { + // Not migrated yet, will do during unpacking. + // There's no other change than discriminator & version. + } + Ok((AccountDiscriminator::LendingMarket, _)) => { + msg!("Lending market version does not match lending program version"); + return Err(ProgramError::InvalidAccountData); + } + Ok((_, _)) => { + msg!("Lending market discriminator does not match"); + return Err(ProgramError::InvalidAccountData); + } + Err(e) => { + msg!("Lending market has an unexpected first byte value"); + return Err(e); + } + }; let owner_pubkey = Pubkey::new_from_array(*owner); Ok(Self { - version, + discriminator_and_version: set_discriminator_and_version( + AccountDiscriminator::LendingMarket, + ProgramVersion::V2_1_0, + ), bump_seed: u8::from_le_bytes(*bump_seed), owner: owner_pubkey, quote_currency: *quote_currency, @@ -202,29 +236,56 @@ mod test { use super::*; use rand::Rng; + impl LendingMarket { + fn new_rand(rng: &mut impl Rng) -> Self { + Self { + discriminator_and_version: set_discriminator_and_version( + AccountDiscriminator::LendingMarket, + ProgramVersion::V2_1_0, + ), + bump_seed: rng.gen(), + owner: Pubkey::new_unique(), + quote_currency: [rng.gen(); 32], + token_program_id: Pubkey::new_unique(), + oracle_program_id: Pubkey::new_unique(), + switchboard_oracle_program_id: Pubkey::new_unique(), + rate_limiter: rand_rate_limiter(), + whitelisted_liquidator: if rng.gen_bool(0.5) { + None + } else { + Some(Pubkey::new_unique()) + }, + risk_authority: Pubkey::new_unique(), + } + } + } + #[test] - fn pack_and_unpack_lending_market() { + fn pack_and_unpack_lending_market_v2_1_0() { let mut rng = rand::thread_rng(); - let lending_market = LendingMarket { - version: PROGRAM_VERSION, - bump_seed: rng.gen(), - owner: Pubkey::new_unique(), - quote_currency: [rng.gen(); 32], - token_program_id: Pubkey::new_unique(), - oracle_program_id: Pubkey::new_unique(), - switchboard_oracle_program_id: Pubkey::new_unique(), - rate_limiter: rand_rate_limiter(), - whitelisted_liquidator: if rng.gen_bool(0.5) { - None - } else { - Some(Pubkey::new_unique()) - }, - risk_authority: Pubkey::new_unique(), - }; + let lending_market = LendingMarket::new_rand(&mut rng); + + let mut packed = vec![0u8; LendingMarket::LEN]; + LendingMarket::pack(lending_market.clone(), &mut packed).unwrap(); + let unpacked = LendingMarket::unpack_from_slice(&packed).unwrap(); + assert_eq!(unpacked, lending_market); + } + + #[test] + fn pack_and_unpack_lending_market_v2_0_2() { + let mut rng = rand::thread_rng(); + let mut lending_market = LendingMarket::new_rand(&mut rng); + // this is what version looked like before the upgrade to v2.1.0 + lending_market.discriminator_and_version = ProgramVersion::V2_0_2 as _; let mut packed = vec![0u8; LendingMarket::LEN]; LendingMarket::pack(lending_market.clone(), &mut packed).unwrap(); let unpacked = LendingMarket::unpack_from_slice(&packed).unwrap(); + // upgraded + lending_market.discriminator_and_version = set_discriminator_and_version( + AccountDiscriminator::LendingMarket, + ProgramVersion::V2_1_0, + ); assert_eq!(unpacked, lending_market); } } diff --git a/token-lending/sdk/src/state/liquidity_mining.rs b/token-lending/sdk/src/state/liquidity_mining.rs index e4f98a5a29b..6ac52717720 100644 --- a/token-lending/sdk/src/state/liquidity_mining.rs +++ b/token-lending/sdk/src/state/liquidity_mining.rs @@ -516,8 +516,15 @@ impl UserRewardManager { /// - [Self::rewards] vector length as u8 const HEAD_LEN: usize = PUBKEY_BYTES + 8 + 8 + 1; + /// How many bytes are needed to pack this [UserRewardManager]. + pub(crate) fn size_in_bytes_when_packed(&self) -> usize { + Self::HEAD_LEN + self.rewards.len() * UserReward::LEN + } + /// Because [Self] is dynamically sized we don't implement [Pack] that /// contains a misleading const `LEN`. + /// + /// We return how many bytes were written. pub(crate) fn pack_into_slice(&self, output: &mut [u8]) { let raw_user_reward_manager = array_mut_ref![output, 0, UserRewardManager::HEAD_LEN]; @@ -534,8 +541,8 @@ impl UserRewardManager { dst_reserve.copy_from_slice(self.reserve.as_ref()); dst_user_rewards_len.copy_from_slice( &({ - assert!(MAX_REWARDS >= self.rewards.len()); - assert!(u8::MAX >= MAX_REWARDS as _); + debug_assert!(MAX_REWARDS >= self.rewards.len()); + debug_assert!(u8::MAX >= MAX_REWARDS as _); self.rewards.len() as u8 }) .to_le_bytes(), diff --git a/token-lending/sdk/src/state/mod.rs b/token-lending/sdk/src/state/mod.rs index c18c9793dfa..e5d0a9deb37 100644 --- a/token-lending/sdk/src/state/mod.rs +++ b/token-lending/sdk/src/state/mod.rs @@ -13,10 +13,12 @@ pub use lending_market::*; pub use lending_market_metadata::*; pub use liquidity_mining::*; pub use obligation::*; +use program_version::ProgramVersion; pub use rate_limiter::*; pub use reserve::*; use crate::math::{Decimal, WAD}; +use discriminator::AccountDiscriminator; use solana_program::{msg, program_error::ProgramError}; /// Collateral tokens are initially valued at a ratio of 5:1 (collateral:liquidity) @@ -24,17 +26,107 @@ use solana_program::{msg, program_error::ProgramError}; pub const INITIAL_COLLATERAL_RATIO: u64 = 1; const INITIAL_COLLATERAL_RATE: u64 = INITIAL_COLLATERAL_RATIO * WAD; -/// Current version of the program and all new accounts created -pub const PROGRAM_VERSION: u8 = 1; - -/// Accounts are created with data zeroed out, so uninitialized state instances -/// will have the version set to 0. -pub const UNINITIALIZED_VERSION: u8 = 0; - /// Number of slots per year // 2 (slots per second) * 60 * 60 * 24 * 365 = 63072000 pub const SLOTS_PER_YEAR: u64 = 63072000; +pub mod program_version { + //! There can be at the moment at most 16 different program versions. + //! Extrapolating from the current program history this should be good enough. + //! The program versions can also wrap if sufficient precautions are taken. + + /// Match the second 4 bits of an account data against this enum to determine + /// the program version. + pub enum ProgramVersion { + /// Account is not initialized yet. + Uninitialized = 0, + /// Version of the program and all new accounts created until inclusive version + /// @v2.0.2 + /// + /// These versions will have no account discriminator. + V2_0_2 = 1, + /// Version of the program and all new accounts created from inclusive version + /// @v2.1.0 (liquidity mining) + /// + /// Will have an associated account discriminator. + V2_1_0 = 2, + } + + /// Version of the program and all new accounts created until inclusive version + /// @v2.0.2 + pub const V2_0_2: u8 = 1; + + /// Version of the program and all new accounts created from inclusive version + /// @v2.1.0 (liquidity mining) + pub const V2_1_0: u8 = 2; + + /// Accounts are created with data zeroed out, so uninitialized state instances + /// will have the version set to 0. + pub const UNINITIALIZED: u8 = 0; +} + +pub mod discriminator { + //! First 4 bits determine the account kind. + //! + //! There can be at the moment at most 15 different discriminators. + //! Extrapolating from the current program history this should be good enough. + + /// Match the first 4 bits of an account data against this enum to determine + /// the account type. + pub enum AccountDiscriminator { + /// Account is not initialized yet. + Uninitialized = 0, + /// [crate::state::LendingMarket] + LendingMarket = 1, + /// [crate::state::Reserve] + Reserve = 2, + /// [crate::state::Obligation] + Obligation = 3, + } +} + +/// There can be at the moment at most 16 different program versions. +/// Extrapolating from the current program history this should be good enough. +/// The program versions can also wrap if sufficient precautions are taken. +fn set_discriminator_and_version(discriminator: AccountDiscriminator, version: ProgramVersion) -> u8 { + let discriminator = discriminator as u8; + debug_assert!(discriminator <= 0x0F); + let version = version as u8; + debug_assert!(version <= 0x0F); + + (discriminator << 4) | (version & 0x0F) +} + +/// First 4 bytes are the discriminator, next 4 bytes are the version. +fn extract_discriminator_and_version( + byte: u8, +) -> Result<(AccountDiscriminator, ProgramVersion), ProgramError> { + let version = match byte & 0x0F { + 0 => ProgramVersion::Uninitialized, + 1 => ProgramVersion::V2_0_2, + 2 => ProgramVersion::V2_1_0, + 3..=16 => { + // unused + return Err(ProgramError::InvalidAccountData); + } + _ => unreachable!("Version is out of bounds"), + }; + + let discriminator = match (byte >> 4) & 0x0F { + 0 => AccountDiscriminator::Uninitialized, + 1 => AccountDiscriminator::LendingMarket, + 2 => AccountDiscriminator::Reserve, + 3 => AccountDiscriminator::Obligation, + 4..=16 => { + // unused + return Err(ProgramError::InvalidAccountData); + } + 17.. => unreachable!("Discriminator is out of bounds"), + }; + + Ok((discriminator, version)) +} + // Helpers fn pack_decimal(decimal: Decimal, dst: &mut [u8; 16]) { *dst = decimal diff --git a/token-lending/sdk/src/state/obligation.rs b/token-lending/sdk/src/state/obligation.rs index aeb2f187e03..4225fc7f642 100644 --- a/token-lending/sdk/src/state/obligation.rs +++ b/token-lending/sdk/src/state/obligation.rs @@ -30,8 +30,17 @@ pub const MAX_OBLIGATION_RESERVES: usize = 10; /// instead. #[derive(Clone, Debug, Default, PartialEq)] pub struct Obligation { - /// Version of the struct - pub version: u8, + /// For historical reasons we mask both program version and account + /// discriminator onto 1 byte. + /// + /// For accounts last used with version prior to @v2.1.0 this will be equal + /// to [ProgramVersion::V2_0_2]. + /// + /// For uninitialized accounts, this will be equal to [ProgramVersion::Uninitialized]. + /// + /// Accounts after including @v2.1.0 use first 4 bits for discriminator and + /// last 4 bits for program version. + pub discriminator_and_version: u8, /// Last update to collateral, liquidity, or their market values pub last_update: LastUpdate, /// Lending market address @@ -97,7 +106,8 @@ impl Obligation { /// Initialize an obligation pub fn init(&mut self, params: InitObligationParams) { - self.version = PROGRAM_VERSION; + self.discriminator_and_version = + set_discriminator_and_version(AccountDiscriminator::Obligation, ProgramVersion::V2_1_0); self.last_update = LastUpdate::new(params.current_slot); self.lending_market = params.lending_market; self.owner = params.owner; @@ -327,7 +337,10 @@ pub struct InitObligationParams { impl Sealed for Obligation {} impl IsInitialized for Obligation { fn is_initialized(&self) -> bool { - self.version != UNINITIALIZED_VERSION + match extract_discriminator_and_version(self.discriminator_and_version) { + Ok((_, version)) => !matches!(version, ProgramVersion::Uninitialized), + Err(_) => unreachable!("There is no path to invalid discriminator/version"), + } } } @@ -462,10 +475,7 @@ impl Obligation { } /// Unpack from slice and check if initialized - pub fn unpack(input: &[u8]) -> Result - where - Self: IsInitialized, - { + pub fn unpack(input: &[u8]) -> Result { let value = Self::unpack_unchecked(input)?; if value.is_initialized() { Ok(value) @@ -493,12 +503,12 @@ impl Obligation { Ok(()) } - /// @v2.1.0 TODO: pack vec of user reward managers + /// Since @v2.1.0 we pack vec of user reward managers fn pack_into_slice(&self, dst: &mut [u8]) { let output = array_mut_ref![dst, 0, OBLIGATION_LEN_V1]; #[allow(clippy::ptr_offset_with_cast)] let ( - version, + discriminator_and_version, last_update_slot, last_update_stale, lending_market, @@ -539,7 +549,7 @@ impl Obligation { ]; // obligation - *version = self.version.to_le_bytes(); + *discriminator_and_version = self.discriminator_and_version.to_le_bytes(); *last_update_slot = self.last_update.slot.to_le_bytes(); pack_bool(self.last_update.stale, last_update_stale); lending_market.copy_from_slice(self.lending_market.as_ref()); @@ -600,15 +610,37 @@ impl Obligation { pack_decimal(liquidity.market_value, market_value); offset += OBLIGATION_LIQUIDITY_LEN; } + + if !self.user_reward_managers.is_empty() { + // if the underlying buffer doesn't have enough space then we panic + + debug_assert!(MAX_OBLIGATION_RESERVES >= self.user_reward_managers.len()); + debug_assert!(u8::MAX > MAX_OBLIGATION_RESERVES as _); + let user_reward_managers_len = self.user_reward_managers.len() as u8; + dst[OBLIGATION_LEN_V1] = user_reward_managers_len; + + let mut offset = OBLIGATION_LEN_V1 + 1; + for user_reward_manager in self.user_reward_managers.iter() { + user_reward_manager.pack_into_slice(&mut dst[offset..]); + offset += user_reward_manager.size_in_bytes_when_packed(); + } + } else if dst.len() > OBLIGATION_LEN_V1 { + // set the length to 0 if obligation was resized before + + dst[OBLIGATION_LEN_V1] = 0; + }; + + // Any data after offset is garbage, but we don't zero it out bcs + // it costs CU and we'd have to do it bit by bit to avoid stack overflows. } /// Unpacks a byte buffer into an [Obligation]. - // @v2.1.0 TODO: unpack vector of optional user reward managers + /// Since @v2.1.0 we unpack vector of user reward managers fn unpack_from_slice(src: &[u8]) -> Result { let input = array_ref![src, 0, OBLIGATION_LEN_V1]; #[allow(clippy::ptr_offset_with_cast)] let ( - version, + discriminator_and_version, last_update_slot, last_update_stale, lending_market, @@ -648,11 +680,28 @@ impl Obligation { OBLIGATION_COLLATERAL_LEN + (OBLIGATION_LIQUIDITY_LEN * (MAX_OBLIGATION_RESERVES - 1)) ]; - let version = u8::from_le_bytes(*version); - if version > PROGRAM_VERSION { - msg!("Obligation version does not match lending program version"); - return Err(ProgramError::InvalidAccountData); - } + match extract_discriminator_and_version(u8::from_le_bytes(*discriminator_and_version)) { + Ok((AccountDiscriminator::Obligation, ProgramVersion::V2_1_0)) => { + // migrated and all ok + } + Ok((AccountDiscriminator::Uninitialized, ProgramVersion::V2_0_2)) + if input.len() == OBLIGATION_LEN_V1 => + { + // not migrated yet, so must have the old size + } + Ok((AccountDiscriminator::Obligation, _)) => { + msg!("Obligation version does not match lending program version"); + return Err(ProgramError::InvalidAccountData); + } + Ok((_, _)) => { + msg!("Obligation discriminator does not match"); + return Err(ProgramError::InvalidAccountData); + } + Err(e) => { + msg!("Obligation has an unexpected first byte value"); + return Err(e); + } + }; let deposits_len = u8::from_le_bytes(*deposits_len); let borrows_len = u8::from_le_bytes(*borrows_len); @@ -697,10 +746,27 @@ impl Obligation { offset += OBLIGATION_LIQUIDITY_LEN; } - let user_reward_managers = Vec::new(); // TODO + let user_reward_managers = match src.get(OBLIGATION_LEN_V1) { + Some(len @ 1..) => { + let mut user_reward_managers = Vec::with_capacity(*len as _); + + let mut offset = OBLIGATION_LEN_V1 + 1; + for _ in 0..*len { + let user_reward_manager = UserRewardManager::unpack_from_slice(&src[offset..])?; + offset += user_reward_manager.size_in_bytes_when_packed(); + user_reward_managers.push(user_reward_manager); + } + + user_reward_managers + } + _ => Vec::new(), + }; Ok(Self { - version, + discriminator_and_version: set_discriminator_and_version( + AccountDiscriminator::Obligation, + ProgramVersion::V2_1_0, + ), last_update: LastUpdate { slot: u64::from_le_bytes(*last_update_slot), stale: unpack_bool(last_update_stale)?, @@ -749,12 +815,13 @@ mod test { Decimal::from_scaled_val(rand::thread_rng().gen()) } - #[test] - fn pack_and_unpack_obligation() { - let mut rng = rand::thread_rng(); - for _ in 0..100 { - let obligation = Obligation { - version: PROGRAM_VERSION, + impl Obligation { + fn new_rand(rng: &mut impl Rng) -> Self { + Self { + discriminator_and_version: set_discriminator_and_version( + AccountDiscriminator::Obligation, + ProgramVersion::V2_1_0, + ), last_update: LastUpdate { slot: rng.gen(), stale: rng.gen(), @@ -785,19 +852,47 @@ mod test { user_reward_managers: { let user_reward_managers_len = rng.gen_range(0..=MAX_OBLIGATION_RESERVES); - std::iter::repeat_with(|| UserRewardManager::new_rand(&mut rng)) + std::iter::repeat_with(|| UserRewardManager::new_rand(rng)) .take(user_reward_managers_len) .collect() }, - }; + } + } + } + + #[test] + fn pack_and_unpack_obligation_v2_1_0() { + let mut rng = rand::thread_rng(); + for _ in 0..100 { + let obligation = Obligation::new_rand(&mut rng); - let mut packed = [0u8; OBLIGATION_LEN_V1]; + let mut packed = [0u8; Obligation::MAX_LEN]; Obligation::pack(obligation.clone(), &mut packed).unwrap(); let unpacked = Obligation::unpack(&packed).unwrap(); assert_eq!(obligation, unpacked); } } + #[test] + fn pack_and_unpack_obligation_v2_0_2() { + let mut rng = rand::thread_rng(); + for _ in 0..100 { + let mut obligation = Obligation::new_rand(&mut rng); + // this is what version looked like before the upgrade to v2.1.0 + obligation.discriminator_and_version = ProgramVersion::V2_0_2 as _; + + let mut packed = [0u8; Obligation::MAX_LEN]; + Obligation::pack(obligation.clone(), &mut packed).unwrap(); + let unpacked = Obligation::unpack(&packed).unwrap(); + // upgraded + obligation.discriminator_and_version = set_discriminator_and_version( + AccountDiscriminator::Obligation, + ProgramVersion::V2_1_0, + ); + assert_eq!(obligation, unpacked); + } + } + #[test] fn obligation_accrue_interest_failure() { assert_eq!( diff --git a/token-lending/sdk/src/state/reserve.rs b/token-lending/sdk/src/state/reserve.rs index cea55c0e21f..c83efaab640 100644 --- a/token-lending/sdk/src/state/reserve.rs +++ b/token-lending/sdk/src/state/reserve.rs @@ -44,8 +44,18 @@ pub const MIN_SCALED_PRICE_OFFSET_BPS: i64 = -2000; /// Lending market reserve state #[derive(Clone, Debug, Default, PartialEq)] pub struct Reserve { - /// Version of the struct - pub version: u8, + /// For historical reasons we mask both program version and account + /// discriminator onto 1 byte. + /// + /// For accounts last used with version prior to @v2.1.0 this will be equal + /// to [ProgramVersion::V2_0_2]. + /// + /// For uninitialized accounts, this will be equal to + /// [Discriminator::Uninitialized] and [ProgramVersion::Uninitialized]. + /// + /// Accounts after including @v2.1.0 use first 4 bits for discriminator and + /// last 4 bits for program version. + pub discriminator_and_version: u8, /// Last slot when supply and rates updated pub last_update: LastUpdate, /// Lending market address @@ -86,7 +96,8 @@ impl Reserve { /// Initialize a reserve pub fn init(&mut self, params: InitReserveParams) { - self.version = PROGRAM_VERSION; + self.discriminator_and_version = + set_discriminator_and_version(AccountDiscriminator::Reserve, ProgramVersion::V2_1_0); self.last_update = LastUpdate::new(params.current_slot); self.lending_market = params.lending_market; self.liquidity = params.liquidity; @@ -1238,7 +1249,10 @@ pub enum FeeCalculation { impl Sealed for Reserve {} impl IsInitialized for Reserve { fn is_initialized(&self) -> bool { - self.version != UNINITIALIZED_VERSION + match extract_discriminator_and_version(self.discriminator_and_version) { + Ok((_, version)) => !matches!(version, ProgramVersion::Uninitialized), + Err(_) => unreachable!("There is no path to invalid discriminator/version"), + } } } @@ -1257,7 +1271,7 @@ impl Pack for Reserve { let output = array_mut_ref![output, 0, Reserve::LEN]; #[allow(clippy::ptr_offset_with_cast)] let ( - version, + discriminator_and_version, last_update_slot, last_update_stale, lending_market, @@ -1362,7 +1376,7 @@ impl Pack for Reserve { ]; // reserve - *version = self.version.to_le_bytes(); + *discriminator_and_version = self.discriminator_and_version.to_le_bytes(); *last_update_slot = self.last_update.slot.to_le_bytes(); pack_bool(self.last_update.stale, last_update_stale); lending_market.copy_from_slice(self.lending_market.as_ref()); @@ -1463,7 +1477,7 @@ impl Pack for Reserve { let input_v2_0_2 = array_ref![input, 0, RESERVE_LEN_V2_0_2]; #[allow(clippy::ptr_offset_with_cast)] let ( - version, + discriminator_and_version, last_update_slot, last_update_stale, lending_market, @@ -1563,11 +1577,28 @@ impl Pack for Reserve { 49 ]; - let version = u8::from_le_bytes(*version); - if version > PROGRAM_VERSION { - msg!("Reserve version does not match lending program version"); - return Err(ProgramError::InvalidAccountData); - } + match extract_discriminator_and_version(u8::from_le_bytes(*discriminator_and_version)) { + Ok((AccountDiscriminator::Reserve, ProgramVersion::V2_1_0)) => { + // migrated and all ok + } + Ok((AccountDiscriminator::Uninitialized, ProgramVersion::V2_0_2)) => { + // Not migrated yet, will do during unpacking. + // We checked earlier that the input buffer was already resized. + debug_assert_eq!(input.len(), RESERVE_LEN_V2_1_0); + } + Ok((AccountDiscriminator::Reserve, _)) => { + msg!("Reserve version does not match lending program version"); + return Err(ProgramError::InvalidAccountData); + } + Ok((_, _)) => { + msg!("Reserve discriminator does not match"); + return Err(ProgramError::InvalidAccountData); + } + Err(e) => { + msg!("Reserve has an unexpected first byte value"); + return Err(e); + } + }; let optimal_utilization_rate = u8::from_le_bytes(*config_optimal_utilization_rate); let max_borrow_rate = u8::from_le_bytes(*config_max_borrow_rate); @@ -1696,7 +1727,10 @@ impl Pack for Reserve { PoolRewardManager::unpack_to_box(input_for_deposits_pool_reward_manager)?; Ok(Self { - version, + discriminator_and_version: set_discriminator_and_version( + AccountDiscriminator::Reserve, + ProgramVersion::V2_1_0, + ), last_update, lending_market: Pubkey::new_from_array(*lending_market), liquidity, @@ -1724,10 +1758,8 @@ mod test { Decimal::from_scaled_val(rand::thread_rng().gen()) } - #[test] - fn pack_and_unpack_reserve() { - let mut rng = rand::thread_rng(); - for _ in 0..100 { + impl Reserve { + fn new_rand(rng: &mut impl Rng) -> Self { let optimal_utilization_rate = rng.gen(); let liquidation_bonus: u8 = rng.gen(); let liquidation_threshold: u8 = rng.gen(); @@ -1742,8 +1774,11 @@ mod test { None }; - let reserve = Reserve { - version: PROGRAM_VERSION, + Self { + discriminator_and_version: set_discriminator_and_version( + AccountDiscriminator::Reserve, + ProgramVersion::V2_1_0, + ), last_update: LastUpdate { slot: rng.gen(), stale: rng.gen(), @@ -1799,13 +1834,41 @@ mod test { }, rate_limiter: rand_rate_limiter(), attributed_borrow_value: rand_decimal(), - borrows_pool_reward_manager: Box::new(PoolRewardManager::new_rand(&mut rng)), - deposits_pool_reward_manager: Box::new(PoolRewardManager::new_rand(&mut rng)), - }; + borrows_pool_reward_manager: Box::new(PoolRewardManager::new_rand(rng)), + deposits_pool_reward_manager: Box::new(PoolRewardManager::new_rand(rng)), + } + } + } + + #[test] + fn pack_and_unpack_reserve_v2_1_0() { + let mut rng = rand::thread_rng(); + for _ in 0..100 { + let reserve = Reserve::new_rand(&mut rng); + + let mut packed = [0u8; Reserve::LEN]; + Reserve::pack(reserve.clone(), &mut packed).unwrap(); + let unpacked = Reserve::unpack(&packed).unwrap(); + assert_eq!(reserve, unpacked); + } + } + + #[test] + fn pack_and_unpack_reserve_v2_0_2() { + let mut rng = rand::thread_rng(); + for _ in 0..100 { + let mut reserve = Reserve::new_rand(&mut rng); + // this is what version looked like before the upgrade to v2.1.0 + reserve.discriminator_and_version = ProgramVersion::V2_0_2 as _; let mut packed = [0u8; Reserve::LEN]; Reserve::pack(reserve.clone(), &mut packed).unwrap(); let unpacked = Reserve::unpack(&packed).unwrap(); + // upgraded + reserve.discriminator_and_version = set_discriminator_and_version( + AccountDiscriminator::Reserve, + ProgramVersion::V2_1_0, + ); assert_eq!(reserve, unpacked); } } From 67e0908fdd2eb79bec44e336d52f7496bc590d95 Mon Sep 17 00:00:00 2001 From: vanitymnm Date: Mon, 17 Mar 2025 14:01:17 +0100 Subject: [PATCH 25/30] Fix compile errors in tests --- .../program/tests/attributed_borrows.rs | 8 ++-- .../tests/borrow_obligation_liquidity.rs | 6 +-- token-lending/program/tests/borrow_weight.rs | 8 ++-- .../tests/deposit_obligation_collateral.rs | 2 +- ...rve_liquidity_and_obligation_collateral.rs | 2 +- token-lending/program/tests/forgive_debt.rs | 3 +- token-lending/program/tests/helpers/mod.rs | 22 +++++----- .../tests/helpers/solend_program_test.rs | 39 +++++++++++------ .../program/tests/init_lending_market.rs | 10 ++++- .../program/tests/init_obligation.rs | 11 ++++- token-lending/program/tests/init_reserve.rs | 17 ++++---- .../program/tests/isolated_tier_assets.rs | 4 +- ...uidate_obligation_and_redeem_collateral.rs | 4 +- .../tests/mark_obligation_as_closeable.rs | 2 +- .../program/tests/obligation_end_to_end.rs | 4 +- .../program/tests/outflow_rate_limits.rs | 4 +- .../program/tests/refresh_obligation.rs | 43 ++++++++++++------- .../program/tests/refresh_reserve.rs | 6 +-- .../tests/repay_obligation_liquidity.rs | 2 +- .../tests/withdraw_obligation_collateral.rs | 4 +- ...ollateral_and_redeem_reserve_collateral.rs | 2 +- token-lending/sdk/src/state/mod.rs | 7 ++- token-lending/sdk/src/state/obligation.rs | 17 ++++++-- 23 files changed, 141 insertions(+), 86 deletions(-) diff --git a/token-lending/program/tests/attributed_borrows.rs b/token-lending/program/tests/attributed_borrows.rs index 6d62ccfb657..39a7bafd28d 100644 --- a/token-lending/program/tests/attributed_borrows.rs +++ b/token-lending/program/tests/attributed_borrows.rs @@ -22,7 +22,7 @@ use solana_program::native_token::LAMPORTS_PER_SOL; use solend_sdk::math::Decimal; -use solend_program::state::{Obligation, ReserveConfig}; +use solend_program::state::ReserveConfig; use solend_sdk::state::ReserveFees; mod helpers; @@ -398,7 +398,7 @@ async fn test_calculations() { .await .unwrap(); - let obligation_post = test.load_account::(obligations[0].pubkey).await; + let obligation_post = test.load_obligation(obligations[0].pubkey).await; // obligation 0 after borrowing 10 usd // usdc.borrow_attribution = 80 / 100 * 30 = 24 @@ -688,7 +688,7 @@ async fn test_withdraw() { Decimal::from_percent(250) ); - let obligation_post = test.load_account::(obligations[0].pubkey).await; + let obligation_post = test.load_obligation(obligations[0].pubkey).await; assert_eq!( obligation_post.account.deposits[0].attributed_borrow_value, Decimal::from(7500u64) @@ -733,7 +733,7 @@ async fn test_withdraw() { Decimal::zero() ); - let obligation_post = test.load_account::(obligations[0].pubkey).await; + let obligation_post = test.load_obligation(obligations[0].pubkey).await; assert_eq!( obligation_post.account.deposits[0].attributed_borrow_value, Decimal::from(10u64) diff --git a/token-lending/program/tests/borrow_obligation_liquidity.rs b/token-lending/program/tests/borrow_obligation_liquidity.rs index 9f23189461b..881a7bbb90d 100644 --- a/token-lending/program/tests/borrow_obligation_liquidity.rs +++ b/token-lending/program/tests/borrow_obligation_liquidity.rs @@ -77,7 +77,7 @@ async fn setup( .unwrap(); // populate deposit value correctly. - let obligation = test.load_account::(obligation.pubkey).await; + let obligation = test.load_obligation(obligation.pubkey).await; lending_market .refresh_obligation(&mut test, &obligation) .await @@ -86,7 +86,7 @@ async fn setup( let lending_market = test.load_account(lending_market.pubkey).await; let usdc_reserve = test.load_account(usdc_reserve.pubkey).await; let wsol_reserve = test.load_account(wsol_reserve.pubkey).await; - let obligation = test.load_account::(obligation.pubkey).await; + let obligation = test.load_obligation(obligation.pubkey).await; let host_fee_receiver = User::new_with_balances(&mut test, &[(&wsol_mint::id(), 0)]).await; ( @@ -244,7 +244,7 @@ async fn test_success() { wsol_reserve_post, expected_wsol_reserve_post ); - let obligation_post = test.load_account::(obligation.pubkey).await; + let obligation_post = test.load_obligation(obligation.pubkey).await; assert_eq!( obligation_post.account, Obligation { diff --git a/token-lending/program/tests/borrow_weight.rs b/token-lending/program/tests/borrow_weight.rs index 1f8c306a052..9cbeadaedff 100644 --- a/token-lending/program/tests/borrow_weight.rs +++ b/token-lending/program/tests/borrow_weight.rs @@ -37,7 +37,7 @@ async fn test_refresh_obligation() { .await .unwrap(); - let obligation_post = test.load_account::(obligation.pubkey).await; + let obligation_post = test.load_obligation(obligation.pubkey).await; // obligation has borrowed 10 sol and sol = $10 but since borrow weight == 2, the // borrowed_value is 200 instead of 100. @@ -134,7 +134,7 @@ async fn test_borrow() { .await .unwrap(); - let obligation_post = test.load_account::(obligation.pubkey).await; + let obligation_post = test.load_obligation(obligation.pubkey).await; // - usdc ltv is 0.5, // - sol borrow weight is 2 // max you can borrow is 100 * 0.5 / 2 = 2.5 SOL @@ -176,7 +176,7 @@ async fn test_borrow() { test.advance_clock_by_slots(1).await; - let obligation = test.load_account::(obligation.pubkey).await; + let obligation = test.load_obligation(obligation.pubkey).await; // max withdraw { @@ -302,7 +302,7 @@ async fn test_liquidation() { .await .unwrap(); - let obligation_post = test.load_account::(obligation.pubkey).await; + let obligation_post = test.load_obligation(obligation.pubkey).await; // - usdc ltv is 0.5, // - sol borrow weight is 1 // max you can borrow is 100 * 0.5 = 5 SOL diff --git a/token-lending/program/tests/deposit_obligation_collateral.rs b/token-lending/program/tests/deposit_obligation_collateral.rs index c0879d9abe2..8992464dcbb 100644 --- a/token-lending/program/tests/deposit_obligation_collateral.rs +++ b/token-lending/program/tests/deposit_obligation_collateral.rs @@ -80,7 +80,7 @@ async fn test_success() { let usdc_reserve_post = test.load_account(usdc_reserve.pubkey).await; assert_eq!(usdc_reserve, usdc_reserve_post); - let obligation_post = test.load_account::(obligation.pubkey).await; + let obligation_post = test.load_obligation(obligation.pubkey).await; assert_eq!( obligation_post.account, Obligation { diff --git a/token-lending/program/tests/deposit_reserve_liquidity_and_obligation_collateral.rs b/token-lending/program/tests/deposit_reserve_liquidity_and_obligation_collateral.rs index 579d80b3d56..6b6779fc9b9 100644 --- a/token-lending/program/tests/deposit_reserve_liquidity_and_obligation_collateral.rs +++ b/token-lending/program/tests/deposit_reserve_liquidity_and_obligation_collateral.rs @@ -119,7 +119,7 @@ async fn test_success() { } ); - let obligation_post = test.load_account::(obligation.pubkey).await; + let obligation_post = test.load_obligation(obligation.pubkey).await; assert_eq!( obligation_post.account, Obligation { diff --git a/token-lending/program/tests/forgive_debt.rs b/token-lending/program/tests/forgive_debt.rs index 63c5c0fb702..1d6360d301f 100644 --- a/token-lending/program/tests/forgive_debt.rs +++ b/token-lending/program/tests/forgive_debt.rs @@ -165,7 +165,7 @@ async fn test_forgive_debt_success_easy() { .await .unwrap(); - let obligation_post = test.load_account::(obligations[0].pubkey).await; + let obligation_post = test.load_obligation(obligations[0].pubkey).await; assert_eq!( obligation_post.account, Obligation { @@ -182,6 +182,7 @@ async fn test_forgive_debt_success_easy() { allowed_borrow_value: Decimal::zero(), unhealthy_borrow_value: Decimal::zero(), super_unhealthy_borrow_value: Decimal::zero(), + user_reward_managers: obligation_post.account.user_reward_managers.clone(), ..obligations[0].account } ); diff --git a/token-lending/program/tests/helpers/mod.rs b/token-lending/program/tests/helpers/mod.rs index 0562b38d34f..9683c86ab74 100644 --- a/token-lending/program/tests/helpers/mod.rs +++ b/token-lending/program/tests/helpers/mod.rs @@ -117,25 +117,25 @@ pub mod bonk_mint { } pub trait AddPacked { + fn add_packed(&mut self, pubkey: Pubkey, amount: u64, data: &[u8], owner: &Pubkey); + fn add_packable_account( &mut self, pubkey: Pubkey, amount: u64, - data: &T, + unpacked: &T, owner: &Pubkey, - ); + ) { + let mut data = vec![0; T::get_packed_len()]; + unpacked.pack_into_slice(&mut data); + self.add_packed(pubkey, amount, &data, owner); + } } impl AddPacked for ProgramTest { - fn add_packable_account( - &mut self, - pubkey: Pubkey, - amount: u64, - data: &T, - owner: &Pubkey, - ) { - let mut account = Account::new(amount, T::get_packed_len(), owner); - data.pack_into_slice(&mut account.data); + fn add_packed(&mut self, pubkey: Pubkey, amount: u64, data: &[u8], owner: &Pubkey) { + let mut account = Account::new(amount, data.len(), owner); + account.data.copy_from_slice(data); self.add_account(pubkey, account); } } diff --git a/token-lending/program/tests/helpers/solend_program_test.rs b/token-lending/program/tests/helpers/solend_program_test.rs index 05e1705d350..e082a77f872 100644 --- a/token-lending/program/tests/helpers/solend_program_test.rs +++ b/token-lending/program/tests/helpers/solend_program_test.rs @@ -254,6 +254,21 @@ impl SolendProgramTest { } } + pub async fn load_obligation(&mut self, acc_pk: Pubkey) -> Info { + let acc = self + .context + .banks_client + .get_account(acc_pk) + .await + .unwrap() + .unwrap(); + + Info { + pubkey: acc_pk, + account: Obligation::unpack(&acc.data).unwrap(), + } + } + pub async fn load_zeroable_account(&mut self, acc_pk: Pubkey) -> Info { let acc = self .context @@ -1002,8 +1017,8 @@ impl Info { system_instruction::create_account( &test.context.payer.pubkey(), &obligation_keypair.pubkey(), - Rent::minimum_balance(&Rent::default(), Obligation::LEN), - Obligation::LEN as u64, + Rent::minimum_balance(&Rent::default(), Obligation::MIN_LEN), + Obligation::MIN_LEN as u64, &solend_program::id(), ), init_obligation( @@ -1018,9 +1033,7 @@ impl Info { .process_transaction(&instructions, Some(&[&obligation_keypair, &user.keypair])) .await { - Ok(()) => Ok(test - .load_account::(obligation_keypair.pubkey()) - .await), + Ok(()) => Ok(test.load_obligation(obligation_keypair.pubkey()).await), Err(e) => Err(e), } } @@ -1080,7 +1093,7 @@ impl Info { obligation: &Info, extra_reserve: Option<&Info>, ) -> Vec { - let obligation = test.load_account::(obligation.pubkey).await; + let obligation = test.load_obligation(obligation.pubkey).await; let reserve_pubkeys: Vec = { let mut r = HashSet::new(); r.extend( @@ -1174,7 +1187,7 @@ impl Info { host_fee_receiver_pubkey: Option, liquidity_amount: u64, ) -> Result<(), BanksClientError> { - let obligation = test.load_account::(obligation.pubkey).await; + let obligation = test.load_obligation(obligation.pubkey).await; let refresh_ixs = self .build_refresh_instructions(test, &obligation, Some(borrow_reserve)) @@ -1343,7 +1356,7 @@ impl Info { user: &User, collateral_amount: u64, ) -> Result<(), BanksClientError> { - let obligation = test.load_account::(obligation.pubkey).await; + let obligation = test.load_obligation(obligation.pubkey).await; let refresh_ixs = self .build_refresh_instructions(test, &obligation, None) @@ -1451,7 +1464,7 @@ impl Info { reserve: &Info, liquidity_amount: u64, ) -> Result<(), BanksClientError> { - let obligation = test.load_account::(obligation.pubkey).await; + let obligation = test.load_obligation(obligation.pubkey).await; let mut instructions = self .build_refresh_instructions(test, &obligation, None) @@ -1815,7 +1828,7 @@ pub async fn scenario_1( .unwrap(); // borrow 10 SOL against 100k cUSDC. - let obligation = test.load_account::(obligation.pubkey).await; + let obligation = test.load_obligation(obligation.pubkey).await; lending_market .borrow_obligation_liquidity( &mut test, @@ -1840,7 +1853,7 @@ pub async fn scenario_1( .unwrap(); // populate deposit value correctly. - let obligation = test.load_account::(obligation.pubkey).await; + let obligation = test.load_obligation(obligation.pubkey).await; lending_market .refresh_obligation(&mut test, &obligation) .await @@ -1849,7 +1862,7 @@ pub async fn scenario_1( let lending_market = test.load_account(lending_market.pubkey).await; let usdc_reserve = test.load_account(usdc_reserve.pubkey).await; let wsol_reserve = test.load_account(wsol_reserve.pubkey).await; - let obligation = test.load_account::(obligation.pubkey).await; + let obligation = test.load_obligation(obligation.pubkey).await; ( test, @@ -2021,7 +2034,7 @@ pub async fn custom_scenario( .await .unwrap(); - *obligation = test.load_account::(obligation.pubkey).await; + *obligation = test.load_obligation(obligation.pubkey).await; } // load accounts into reserve diff --git a/token-lending/program/tests/init_lending_market.rs b/token-lending/program/tests/init_lending_market.rs index de9b88d206e..f3c2bb4830e 100644 --- a/token-lending/program/tests/init_lending_market.rs +++ b/token-lending/program/tests/init_lending_market.rs @@ -12,7 +12,10 @@ use solana_sdk::signer::Signer; use solana_sdk::transaction::TransactionError; use solend_program::error::LendingError; use solend_program::instruction::init_lending_market; -use solend_program::state::{LendingMarket, RateLimiter, PROGRAM_VERSION_2_0_2}; +use solend_program::state::{ + discriminator::AccountDiscriminator, program_version::ProgramVersion, + set_discriminator_and_version, LendingMarket, RateLimiter, +}; #[tokio::test] async fn test_success() { @@ -28,7 +31,10 @@ async fn test_success() { assert_eq!( lending_market.account, LendingMarket { - version: PROGRAM_VERSION, + discriminator_and_version: set_discriminator_and_version( + AccountDiscriminator::LendingMarket, + ProgramVersion::V2_1_0 + ), bump_seed: lending_market.account.bump_seed, // TODO test this field owner: lending_market_owner.keypair.pubkey(), quote_currency: QUOTE_CURRENCY, diff --git a/token-lending/program/tests/init_obligation.rs b/token-lending/program/tests/init_obligation.rs index 2b720c69f8d..4de7a52e8f7 100644 --- a/token-lending/program/tests/init_obligation.rs +++ b/token-lending/program/tests/init_obligation.rs @@ -13,7 +13,10 @@ use solana_sdk::transaction::TransactionError; use solend_program::error::LendingError; use solend_program::instruction::init_obligation; use solend_program::math::Decimal; -use solend_program::state::{LastUpdate, LendingMarket, Obligation, PROGRAM_VERSION_2_0_2}; +use solend_program::state::{ + discriminator::AccountDiscriminator, program_version::ProgramVersion, + set_discriminator_and_version, LastUpdate, LendingMarket, Obligation, +}; async fn setup() -> (SolendProgramTest, Info, User) { let (test, lending_market, _, _, _, user) = @@ -34,7 +37,10 @@ async fn test_success() { assert_eq!( obligation.account, Obligation { - version: PROGRAM_VERSION, + discriminator_and_version: set_discriminator_and_version( + AccountDiscriminator::Obligation, + ProgramVersion::V2_1_0 + ), last_update: LastUpdate { slot: 1000, stale: true @@ -52,6 +58,7 @@ async fn test_success() { super_unhealthy_borrow_value: Decimal::zero(), borrowing_isolated_asset: false, closeable: false, + user_reward_managers: Vec::new(), } ); } diff --git a/token-lending/program/tests/init_reserve.rs b/token-lending/program/tests/init_reserve.rs index 6ad45ad2d56..699d1e23434 100644 --- a/token-lending/program/tests/init_reserve.rs +++ b/token-lending/program/tests/init_reserve.rs @@ -25,22 +25,20 @@ use solana_sdk::{ signature::{Keypair, Signer}, transaction::TransactionError, }; -use solend_program::state::LastUpdate; -use solend_program::state::RateLimiter; -use solend_program::state::Reserve; -use solend_program::state::ReserveCollateral; -use solend_program::state::ReserveLiquidity; -use solend_program::state::PROGRAM_VERSION_2_0_2; use solend_program::NULL_PUBKEY; +use solend_program::state::{ + discriminator::AccountDiscriminator, program_version::ProgramVersion, + set_discriminator_and_version, LastUpdate, LendingMarket, RateLimiter, Reserve, + ReserveCollateral, ReserveLiquidity, +}; use solend_program::{ error::LendingError, instruction::init_reserve, math::Decimal, state::{RateLimiterConfig, ReserveConfig, ReserveFees}, }; -use solend_sdk::state::LendingMarket; use spl_token::state::{Account as Token, Mint}; async fn setup() -> (SolendProgramTest, Info, User) { @@ -154,7 +152,10 @@ async fn test_success() { assert_eq!( wsol_reserve.account, Reserve { - version: PROGRAM_VERSION, + discriminator_and_version: set_discriminator_and_version( + AccountDiscriminator::Reserve, + ProgramVersion::V2_1_0 + ), last_update: LastUpdate { slot: 1001, stale: true diff --git a/token-lending/program/tests/isolated_tier_assets.rs b/token-lending/program/tests/isolated_tier_assets.rs index e6615ce95cc..714e257fa6b 100644 --- a/token-lending/program/tests/isolated_tier_assets.rs +++ b/token-lending/program/tests/isolated_tier_assets.rs @@ -76,7 +76,7 @@ async fn test_refresh_obligation() { .await .unwrap(); - let obligation = test.load_account::(obligations[0].pubkey).await; + let obligation = test.load_obligation(obligations[0].pubkey).await; assert!(!obligation.account.borrowing_isolated_asset); test.advance_clock_by_slots(1).await; @@ -104,7 +104,7 @@ async fn test_refresh_obligation() { .await .unwrap(); - let obligation_post = test.load_account::(obligations[0].pubkey).await; + let obligation_post = test.load_obligation(obligations[0].pubkey).await; assert_eq!( obligation_post.account, diff --git a/token-lending/program/tests/liquidate_obligation_and_redeem_collateral.rs b/token-lending/program/tests/liquidate_obligation_and_redeem_collateral.rs index 009fe817e8c..75eedf36ffa 100644 --- a/token-lending/program/tests/liquidate_obligation_and_redeem_collateral.rs +++ b/token-lending/program/tests/liquidate_obligation_and_redeem_collateral.rs @@ -205,7 +205,7 @@ async fn test_success_new() { } ); - let obligation_post = test.load_account::(obligation.pubkey).await; + let obligation_post = test.load_obligation(obligation.pubkey).await; assert_eq!( obligation_post.account, Obligation { @@ -382,7 +382,7 @@ async fn test_success_insufficient_liquidity() { .await .unwrap(); - let obligation = test.load_account::(obligation.pubkey).await; + let obligation = test.load_obligation(obligation.pubkey).await; lending_market .borrow_obligation_liquidity( &mut test, diff --git a/token-lending/program/tests/mark_obligation_as_closeable.rs b/token-lending/program/tests/mark_obligation_as_closeable.rs index 16b81c45ff5..c2c698f6750 100644 --- a/token-lending/program/tests/mark_obligation_as_closeable.rs +++ b/token-lending/program/tests/mark_obligation_as_closeable.rs @@ -129,7 +129,7 @@ async fn test_mark_obligation_as_closeable_success() { .await .unwrap(); - let obligation_post = test.load_account::(obligations[0].pubkey).await; + let obligation_post = test.load_obligation(obligations[0].pubkey).await; assert_eq!( obligation_post.account, Obligation { diff --git a/token-lending/program/tests/obligation_end_to_end.rs b/token-lending/program/tests/obligation_end_to_end.rs index d29bbe15b9e..575824a8e3d 100644 --- a/token-lending/program/tests/obligation_end_to_end.rs +++ b/token-lending/program/tests/obligation_end_to_end.rs @@ -71,7 +71,7 @@ async fn test_success() { .await .unwrap(); - let obligation = test.load_account(obligation.pubkey).await; + let obligation = test.load_obligation(obligation.pubkey).await; lending_market .borrow_obligation_liquidity( &mut test, @@ -89,7 +89,7 @@ async fn test_success() { .await .unwrap(); - let obligation = test.load_account(obligation.pubkey).await; + let obligation = test.load_obligation(obligation.pubkey).await; lending_market .withdraw_obligation_collateral_and_redeem_reserve_collateral( &mut test, diff --git a/token-lending/program/tests/outflow_rate_limits.rs b/token-lending/program/tests/outflow_rate_limits.rs index 35c953bf1de..f42dba03f28 100644 --- a/token-lending/program/tests/outflow_rate_limits.rs +++ b/token-lending/program/tests/outflow_rate_limits.rs @@ -78,7 +78,7 @@ async fn setup( .unwrap(); // populate deposit value correctly. - let obligation = test.load_account::(obligation.pubkey).await; + let obligation = test.load_obligation(obligation.pubkey).await; lending_market .refresh_obligation(&mut test, &obligation) .await @@ -87,7 +87,7 @@ async fn setup( let lending_market = test.load_account(lending_market.pubkey).await; let usdc_reserve = test.load_account(usdc_reserve.pubkey).await; let wsol_reserve = test.load_account(wsol_reserve.pubkey).await; - let obligation = test.load_account::(obligation.pubkey).await; + let obligation = test.load_obligation(obligation.pubkey).await; let host_fee_receiver = User::new_with_balances(&mut test, &[(&wsol_mint::id(), 0)]).await; ( diff --git a/token-lending/program/tests/refresh_obligation.rs b/token-lending/program/tests/refresh_obligation.rs index 94891359095..752e9703a05 100644 --- a/token-lending/program/tests/refresh_obligation.rs +++ b/token-lending/program/tests/refresh_obligation.rs @@ -12,7 +12,6 @@ use solend_program::instruction::refresh_obligation; use solend_program::processor::process_instruction; use solend_program::state::ObligationCollateral; -use solend_sdk::state::PROGRAM_VERSION_2_0_2; use std::collections::HashSet; use helpers::solend_program_test::{setup_world, BalanceChecker, Info, SolendProgramTest, User}; @@ -21,7 +20,10 @@ use solana_program::native_token::LAMPORTS_PER_SOL; use solana_program_test::*; use solana_sdk::signature::Keypair; use solend_program::state::SLOTS_PER_YEAR; -use solend_program::state::{LastUpdate, ObligationLiquidity, ReserveFees, ReserveLiquidity}; +use solend_program::state::{ + discriminator::AccountDiscriminator, program_version::ProgramVersion, + set_discriminator_and_version, LastUpdate, ObligationLiquidity, ReserveFees, ReserveLiquidity, +}; use solend_program::{ math::{Decimal, TryAdd, TryDiv, TryMul}, @@ -101,7 +103,7 @@ async fn setup() -> ( .unwrap(); // borrow 6 SOL against 100k cUSDC. - let obligation = test.load_account::(obligation.pubkey).await; + let obligation = test.load_obligation(obligation.pubkey).await; lending_market .borrow_obligation_liquidity( &mut test, @@ -121,7 +123,7 @@ async fn setup() -> ( .unwrap(); // populate deposit value correctly. - let obligation = test.load_account::(obligation.pubkey).await; + let obligation = test.load_obligation(obligation.pubkey).await; lending_market .refresh_obligation(&mut test, &obligation) .await @@ -130,7 +132,7 @@ async fn setup() -> ( let lending_market = test.load_account(lending_market.pubkey).await; let usdc_reserve = test.load_account(usdc_reserve.pubkey).await; let wsol_reserve = test.load_account(wsol_reserve.pubkey).await; - let obligation = test.load_account::(obligation.pubkey).await; + let obligation = test.load_obligation(obligation.pubkey).await; ( test, @@ -246,7 +248,7 @@ async fn test_success() { } ); - let obligation_post = test.load_account::(obligation.pubkey).await; + let obligation_post = test.load_obligation(obligation.pubkey).await; assert_eq!( obligation_post.account, @@ -415,7 +417,7 @@ async fn test_obligation_liquidity_ordering() { .await .unwrap(); - let obligation = test.load_account::(obligations[0].pubkey).await; + let obligation = test.load_obligation(obligations[0].pubkey).await; let max_reserve = reserves.iter().max_by_key(|r| r.pubkey).unwrap(); assert!(obligation.account.borrows[0].borrow_reserve == max_reserve.pubkey); @@ -441,7 +443,7 @@ async fn test_obligation_liquidity_ordering() { .await .unwrap(); - let obligation = test.load_account::(obligations[0].pubkey).await; + let obligation = test.load_obligation(obligations[0].pubkey).await; assert!(obligation.account.borrows[0].borrow_reserve == wsol_reserve.pubkey); lending_market @@ -466,7 +468,7 @@ async fn test_obligation_liquidity_ordering() { .await .unwrap(); - let obligation = test.load_account::(obligations[0].pubkey).await; + let obligation = test.load_obligation(obligations[0].pubkey).await; assert!(obligation.account.borrows[0].borrow_reserve == usdc_reserve.pubkey); } @@ -479,7 +481,10 @@ async fn test_normalize_obligation() { ); let reserve_1 = Reserve { - version: PROGRAM_VERSION, + discriminator_and_version: set_discriminator_and_version( + AccountDiscriminator::Reserve, + ProgramVersion::V2_1_0, + ), last_update: LastUpdate { slot: 1, stale: false, @@ -496,7 +501,10 @@ async fn test_normalize_obligation() { ); let reserve_2 = Reserve { - version: PROGRAM_VERSION, + discriminator_and_version: set_discriminator_and_version( + AccountDiscriminator::Reserve, + ProgramVersion::V2_1_0, + ), last_update: LastUpdate { slot: 1, stale: false, @@ -514,7 +522,10 @@ async fn test_normalize_obligation() { let obligation_pubkey = Pubkey::new_unique(); let obligation = Obligation { - version: PROGRAM_VERSION, + discriminator_and_version: set_discriminator_and_version( + AccountDiscriminator::Obligation, + ProgramVersion::V2_1_0, + ), deposits: vec![ ObligationCollateral { deposit_reserve: reserve_1_pubkey, @@ -542,10 +553,12 @@ async fn test_normalize_obligation() { ..Obligation::default() }; - test.add_packable_account( + let mut packed_obligation = vec![0; obligation.size_in_bytes_when_packed()]; + obligation.pack_into_slice(&mut packed_obligation); + test.add_packed( obligation_pubkey, u32::MAX as u64, - &obligation, + &packed_obligation, &solend_program::id(), ); @@ -563,7 +576,7 @@ async fn test_normalize_obligation() { )]; test.process_transaction(&ix, None).await.unwrap(); - let o = test.load_account::(obligation_pubkey).await; + let o = test.load_obligation(obligation_pubkey).await; assert_eq!( o.account.deposits, vec![ObligationCollateral { diff --git a/token-lending/program/tests/refresh_reserve.rs b/token-lending/program/tests/refresh_reserve.rs index 7e84a2bf70b..a979fb7fe91 100644 --- a/token-lending/program/tests/refresh_reserve.rs +++ b/token-lending/program/tests/refresh_reserve.rs @@ -105,7 +105,7 @@ async fn setup() -> ( .unwrap(); // borrow 6 SOL against 100k cUSDC. All sol is borrowed, so the borrow rate should be at max. - let obligation = test.load_account::(obligation.pubkey).await; + let obligation = test.load_obligation(obligation.pubkey).await; lending_market .borrow_obligation_liquidity( &mut test, @@ -125,7 +125,7 @@ async fn setup() -> ( .unwrap(); // populate deposit value correctly. - let obligation = test.load_account::(obligation.pubkey).await; + let obligation = test.load_obligation(obligation.pubkey).await; lending_market .refresh_obligation(&mut test, &obligation) .await @@ -134,7 +134,7 @@ async fn setup() -> ( let lending_market = test.load_account(lending_market.pubkey).await; let usdc_reserve = test.load_account(usdc_reserve.pubkey).await; let wsol_reserve = test.load_account(wsol_reserve.pubkey).await; - let obligation = test.load_account::(obligation.pubkey).await; + let obligation = test.load_obligation(obligation.pubkey).await; ( test, diff --git a/token-lending/program/tests/repay_obligation_liquidity.rs b/token-lending/program/tests/repay_obligation_liquidity.rs index c3e9cd7a958..989da4fab21 100644 --- a/token-lending/program/tests/repay_obligation_liquidity.rs +++ b/token-lending/program/tests/repay_obligation_liquidity.rs @@ -90,7 +90,7 @@ async fn test_success() { } ); - let obligation_post = test.load_account::(obligation.pubkey).await; + let obligation_post = test.load_obligation(obligation.pubkey).await; assert_eq!( obligation_post.account, Obligation { diff --git a/token-lending/program/tests/withdraw_obligation_collateral.rs b/token-lending/program/tests/withdraw_obligation_collateral.rs index 6cd535dec27..faf8073df32 100644 --- a/token-lending/program/tests/withdraw_obligation_collateral.rs +++ b/token-lending/program/tests/withdraw_obligation_collateral.rs @@ -48,7 +48,7 @@ async fn test_success_withdraw_fixed_amount() { let usdc_reserve_post = test.load_account::(usdc_reserve.pubkey).await; assert_eq!(usdc_reserve_post.account, usdc_reserve.account); - let obligation_post = test.load_account::(obligation.pubkey).await; + let obligation_post = test.load_obligation(obligation.pubkey).await; assert_eq!( obligation_post.account, Obligation { @@ -113,7 +113,7 @@ async fn test_success_withdraw_max() { let usdc_reserve_post = test.load_account::(usdc_reserve.pubkey).await; assert_eq!(usdc_reserve_post.account, usdc_reserve.account); - let obligation_post = test.load_account::(obligation.pubkey).await; + let obligation_post = test.load_obligation(obligation.pubkey).await; assert_eq!( obligation_post.account, Obligation { diff --git a/token-lending/program/tests/withdraw_obligation_collateral_and_redeem_reserve_collateral.rs b/token-lending/program/tests/withdraw_obligation_collateral_and_redeem_reserve_collateral.rs index 8ce586d8b14..3cfc66f072f 100644 --- a/token-lending/program/tests/withdraw_obligation_collateral_and_redeem_reserve_collateral.rs +++ b/token-lending/program/tests/withdraw_obligation_collateral_and_redeem_reserve_collateral.rs @@ -130,7 +130,7 @@ async fn test_success() { } ); - let obligation_post = test.load_account::(obligation.pubkey).await; + let obligation_post = test.load_obligation(obligation.pubkey).await; assert_eq!( obligation_post.account, Obligation { diff --git a/token-lending/sdk/src/state/mod.rs b/token-lending/sdk/src/state/mod.rs index e5d0a9deb37..9cd9de40389 100644 --- a/token-lending/sdk/src/state/mod.rs +++ b/token-lending/sdk/src/state/mod.rs @@ -88,7 +88,10 @@ pub mod discriminator { /// There can be at the moment at most 16 different program versions. /// Extrapolating from the current program history this should be good enough. /// The program versions can also wrap if sufficient precautions are taken. -fn set_discriminator_and_version(discriminator: AccountDiscriminator, version: ProgramVersion) -> u8 { +pub fn set_discriminator_and_version( + discriminator: AccountDiscriminator, + version: ProgramVersion, +) -> u8 { let discriminator = discriminator as u8; debug_assert!(discriminator <= 0x0F); let version = version as u8; @@ -98,7 +101,7 @@ fn set_discriminator_and_version(discriminator: AccountDiscriminator, version: P } /// First 4 bytes are the discriminator, next 4 bytes are the version. -fn extract_discriminator_and_version( +pub fn extract_discriminator_and_version( byte: u8, ) -> Result<(AccountDiscriminator, ProgramVersion), ProgramError> { let version = match byte & 0x0F { diff --git a/token-lending/sdk/src/state/obligation.rs b/token-lending/sdk/src/state/obligation.rs index 4225fc7f642..d58596b724c 100644 --- a/token-lending/sdk/src/state/obligation.rs +++ b/token-lending/sdk/src/state/obligation.rs @@ -454,7 +454,7 @@ const OBLIGATION_LEN_V1: usize = 1300; // 1 + 8 + 1 + 32 + 32 + 16 + 16 + 16 + 1 // @TODO: break this up by obligation / collateral / liquidity https://git.io/JOCca impl Obligation { /// Obligation with no Liquidity Mining Rewards - const MIN_LEN: usize = OBLIGATION_LEN_V1; + pub const MIN_LEN: usize = OBLIGATION_LEN_V1; /// Maximum account size for obligation. /// Scenario in which all reserves have all associated rewards filled. @@ -463,6 +463,17 @@ impl Obligation { /// - [Self::user_reward_managers] vector const MAX_LEN: usize = Self::MIN_LEN + 1 + MAX_OBLIGATION_RESERVES * UserRewardManager::MAX_LEN; + /// How many bytes are needed to pack this [UserRewardManager]. + pub fn size_in_bytes_when_packed(&self) -> usize { + let mut size = OBLIGATION_LEN_V1 + 1; + + for reward_manager in &self.user_reward_managers { + size += reward_manager.size_in_bytes_when_packed(); + } + + size + } + /// Unpacks from slice but returns an error if the account is already /// initialized. pub fn unpack_uninitialized(input: &[u8]) -> Result { @@ -504,7 +515,7 @@ impl Obligation { } /// Since @v2.1.0 we pack vec of user reward managers - fn pack_into_slice(&self, dst: &mut [u8]) { + pub fn pack_into_slice(&self, dst: &mut [u8]) { let output = array_mut_ref![dst, 0, OBLIGATION_LEN_V1]; #[allow(clippy::ptr_offset_with_cast)] let ( @@ -636,7 +647,7 @@ impl Obligation { /// Unpacks a byte buffer into an [Obligation]. /// Since @v2.1.0 we unpack vector of user reward managers - fn unpack_from_slice(src: &[u8]) -> Result { + pub fn unpack_from_slice(src: &[u8]) -> Result { let input = array_ref![src, 0, OBLIGATION_LEN_V1]; #[allow(clippy::ptr_offset_with_cast)] let ( From d9968a66cf6dbcac629ddafecbff2507f0ea0f1c Mon Sep 17 00:00:00 2001 From: vanitymnm Date: Mon, 17 Mar 2025 15:24:41 +0100 Subject: [PATCH 26/30] Making the BPF tests pass --- token-lending/cli/src/main.rs | 1 - .../program/src/processor/liquidity_mining.rs | 6 ++-- .../tests/helpers/solend_program_test.rs | 34 ++++++++++--------- token-lending/sdk/src/state/lending_market.rs | 18 ++++++---- .../sdk/src/state/liquidity_mining.rs | 3 ++ token-lending/sdk/src/state/mod.rs | 12 ------- token-lending/sdk/src/state/obligation.rs | 16 ++++++--- token-lending/sdk/src/state/reserve.rs | 18 ++++++---- 8 files changed, 59 insertions(+), 49 deletions(-) diff --git a/token-lending/cli/src/main.rs b/token-lending/cli/src/main.rs index 8c30b314b91..afa365e7325 100644 --- a/token-lending/cli/src/main.rs +++ b/token-lending/cli/src/main.rs @@ -781,7 +781,6 @@ fn main() { .required(true) .help("Reserve address"), ) - ) .subcommand( SubCommand::with_name("update-reserve") diff --git a/token-lending/program/src/processor/liquidity_mining.rs b/token-lending/program/src/processor/liquidity_mining.rs index b16ed16e424..a14bf1f4b9b 100644 --- a/token-lending/program/src/processor/liquidity_mining.rs +++ b/token-lending/program/src/processor/liquidity_mining.rs @@ -7,9 +7,9 @@ //! implementation of the same feature. //! //! There are three admin-only ixs: -//! - [add_pool_reward] -//! - [cancel_pool_reward] -//! - [close_pool_reward] +//! - [add_pool_reward] (TODO: add bpf tests) +//! - [cancel_pool_reward] (TODO: add bpf tests) +//! - [close_pool_reward] (TODO: add bpf tests) //! //! [suilend-lm]: https://github.com/solendprotocol/suilend/blob/dc53150416f352053ac3acbb320ee143409c4a5d/contracts/suilend/sources/liquidity_mining.move#L2 diff --git a/token-lending/program/tests/helpers/solend_program_test.rs b/token-lending/program/tests/helpers/solend_program_test.rs index e082a77f872..48d0bbedd79 100644 --- a/token-lending/program/tests/helpers/solend_program_test.rs +++ b/token-lending/program/tests/helpers/solend_program_test.rs @@ -1,3 +1,5 @@ +// TODO: code budgets were increased. export them to consts and optimize code to again lower them + use bytemuck::checked::from_bytes; use oracles::switchboard_on_demand_mainnet; @@ -671,7 +673,7 @@ impl SolendProgramTest { let res = self .process_transaction( &[ - ComputeBudgetInstruction::set_compute_unit_limit(80_000), + ComputeBudgetInstruction::set_compute_unit_limit(100_011), init_reserve( solend_program::id(), liquidity_amount, @@ -857,7 +859,7 @@ impl Info { liquidity_amount: u64, ) -> Result<(), BanksClientError> { let instructions = [ - ComputeBudgetInstruction::set_compute_unit_limit(50_000), + ComputeBudgetInstruction::set_compute_unit_limit(100_012), deposit_reserve_liquidity( solend_program::id(), liquidity_amount, @@ -885,7 +887,7 @@ impl Info { liquidity_amount: u64, ) -> Result<(), BanksClientError> { let instructions = [ - ComputeBudgetInstruction::set_compute_unit_limit(50_000), + ComputeBudgetInstruction::set_compute_unit_limit(100_013), donate_to_reserve( solend_program::id(), liquidity_amount, @@ -919,7 +921,7 @@ impl Info { let oracle = oracle.unwrap_or(&default_oracle); let instructions = [ - ComputeBudgetInstruction::set_compute_unit_limit(30_000), + ComputeBudgetInstruction::set_compute_unit_limit(30_014), update_reserve_config( solend_program::id(), config, @@ -946,7 +948,7 @@ impl Info { liquidity_amount: u64, ) -> Result<(), BanksClientError> { let instructions = [ - ComputeBudgetInstruction::set_compute_unit_limit(70_000), + ComputeBudgetInstruction::set_compute_unit_limit(150_015), deposit_reserve_liquidity_and_obligation_collateral( solend_program::id(), liquidity_amount, @@ -979,7 +981,7 @@ impl Info { collateral_amount: u64, ) -> Result<(), BanksClientError> { let instructions = [ - ComputeBudgetInstruction::set_compute_unit_limit(58_000), + ComputeBudgetInstruction::set_compute_unit_limit(150_016), refresh_reserve( solend_program::id(), reserve.pubkey, @@ -1013,7 +1015,7 @@ impl Info { user: &User, ) -> Result, BanksClientError> { let instructions = [ - ComputeBudgetInstruction::set_compute_unit_limit(10_000), + ComputeBudgetInstruction::set_compute_unit_limit(10_001), system_instruction::create_account( &test.context.payer.pubkey(), &obligation_keypair.pubkey(), @@ -1047,7 +1049,7 @@ impl Info { collateral_amount: u64, ) -> Result<(), BanksClientError> { let instructions = [ - ComputeBudgetInstruction::set_compute_unit_limit(38_000), + ComputeBudgetInstruction::set_compute_unit_limit(150_002), deposit_obligation_collateral( solend_program::id(), collateral_amount, @@ -1073,7 +1075,7 @@ impl Info { ) -> Result<(), BanksClientError> { test.process_transaction( &[ - ComputeBudgetInstruction::set_compute_unit_limit(2_000_000), + ComputeBudgetInstruction::set_compute_unit_limit(2_000_003), refresh_reserve( solend_program::id(), reserve.pubkey, @@ -1172,7 +1174,7 @@ impl Info { Err(e) => return Err(e), }; - let mut instructions = vec![ComputeBudgetInstruction::set_compute_unit_limit(1_000_000)]; + let mut instructions = vec![ComputeBudgetInstruction::set_compute_unit_limit(1_000_004)]; instructions.push(refresh_reserve_instructions.last().unwrap().clone()); test.process_transaction(&instructions, None).await @@ -1194,7 +1196,7 @@ impl Info { .await; test.process_transaction(&refresh_ixs, None).await.unwrap(); - let mut instructions = vec![ComputeBudgetInstruction::set_compute_unit_limit(100_000)]; + let mut instructions = vec![ComputeBudgetInstruction::set_compute_unit_limit(140_005)]; instructions.push(borrow_obligation_liquidity( solend_program::id(), liquidity_amount, @@ -1228,7 +1230,7 @@ impl Info { liquidity_amount: u64, ) -> Result<(), BanksClientError> { let instructions = [ - ComputeBudgetInstruction::set_compute_unit_limit(35_000), + ComputeBudgetInstruction::set_compute_unit_limit(80_006), repay_obligation_liquidity( solend_program::id(), liquidity_amount, @@ -1252,7 +1254,7 @@ impl Info { reserve: &Info, ) -> Result<(), BanksClientError> { let instructions = [ - ComputeBudgetInstruction::set_compute_unit_limit(50_000), + ComputeBudgetInstruction::set_compute_unit_limit(100_007), refresh_reserve( solend_program::id(), reserve.pubkey, @@ -1288,7 +1290,7 @@ impl Info { test.process_transaction( &[ - ComputeBudgetInstruction::set_compute_unit_limit(110_000), + ComputeBudgetInstruction::set_compute_unit_limit(280_008), liquidate_obligation_and_redeem_reserve_collateral( solend_program::id(), liquidity_amount, @@ -1365,7 +1367,7 @@ impl Info { test.process_transaction( &[ - ComputeBudgetInstruction::set_compute_unit_limit(110_000), + ComputeBudgetInstruction::set_compute_unit_limit(250_009), withdraw_obligation_collateral_and_redeem_reserve_collateral( solend_program::id(), collateral_amount, @@ -1409,7 +1411,7 @@ impl Info { test.process_transaction( &[ - ComputeBudgetInstruction::set_compute_unit_limit(100_000), + ComputeBudgetInstruction::set_compute_unit_limit(120_010), withdraw_obligation_collateral( solend_program::id(), collateral_amount, diff --git a/token-lending/sdk/src/state/lending_market.rs b/token-lending/sdk/src/state/lending_market.rs index 492e307ccfe..60a86b200ab 100644 --- a/token-lending/sdk/src/state/lending_market.rs +++ b/token-lending/sdk/src/state/lending_market.rs @@ -179,13 +179,22 @@ impl Pack for LendingMarket { 8 ]; - match extract_discriminator_and_version(u8::from_le_bytes(*discriminator_and_version)) { + let mut discriminator_and_version = u8::from_le_bytes(*discriminator_and_version); + match extract_discriminator_and_version(discriminator_and_version) { Ok((AccountDiscriminator::LendingMarket, ProgramVersion::V2_1_0)) => { // migrated and all ok } Ok((AccountDiscriminator::LendingMarket, ProgramVersion::V2_0_2)) => { - // Not migrated yet, will do during unpacking. + // Not migrated yet. // There's no other change than discriminator & version. + + discriminator_and_version = set_discriminator_and_version( + AccountDiscriminator::LendingMarket, + ProgramVersion::V2_1_0, + ); + } + Ok((AccountDiscriminator::Uninitialized, ProgramVersion::Uninitialized)) => { + // uninitialized account } Ok((AccountDiscriminator::LendingMarket, _)) => { msg!("Lending market version does not match lending program version"); @@ -203,10 +212,7 @@ impl Pack for LendingMarket { let owner_pubkey = Pubkey::new_from_array(*owner); Ok(Self { - discriminator_and_version: set_discriminator_and_version( - AccountDiscriminator::LendingMarket, - ProgramVersion::V2_1_0, - ), + discriminator_and_version, bump_seed: u8::from_le_bytes(*bump_seed), owner: owner_pubkey, quote_currency: *quote_currency, diff --git a/token-lending/sdk/src/state/liquidity_mining.rs b/token-lending/sdk/src/state/liquidity_mining.rs index 6ac52717720..56e76f00fac 100644 --- a/token-lending/sdk/src/state/liquidity_mining.rs +++ b/token-lending/sdk/src/state/liquidity_mining.rs @@ -423,6 +423,8 @@ impl Pack for PoolRewardManager { dst_id.copy_from_slice(&id.0.to_le_bytes()); dst_vault.copy_from_slice(vault.as_ref()); + + // TBD: these values don't have to be written if the slot is vacant *dst_start_time_secs = start_time_secs.to_le_bytes(); *dst_duration_secs = duration_secs.to_le_bytes(); *dst_total_rewards = total_rewards.to_le_bytes(); @@ -459,6 +461,7 @@ impl Pack for PoolRewardManager { raw_pool_reward, PoolRewardId::LEN, PUBKEY_BYTES, + // TBD: these values don't have to be referenced if the slot is vacant 8, // start_time_secs 4, // duration_secs 8, // total_rewards diff --git a/token-lending/sdk/src/state/mod.rs b/token-lending/sdk/src/state/mod.rs index 9cd9de40389..b255c0c4f18 100644 --- a/token-lending/sdk/src/state/mod.rs +++ b/token-lending/sdk/src/state/mod.rs @@ -51,18 +51,6 @@ pub mod program_version { /// Will have an associated account discriminator. V2_1_0 = 2, } - - /// Version of the program and all new accounts created until inclusive version - /// @v2.0.2 - pub const V2_0_2: u8 = 1; - - /// Version of the program and all new accounts created from inclusive version - /// @v2.1.0 (liquidity mining) - pub const V2_1_0: u8 = 2; - - /// Accounts are created with data zeroed out, so uninitialized state instances - /// will have the version set to 0. - pub const UNINITIALIZED: u8 = 0; } pub mod discriminator { diff --git a/token-lending/sdk/src/state/obligation.rs b/token-lending/sdk/src/state/obligation.rs index d58596b724c..f28da35fcba 100644 --- a/token-lending/sdk/src/state/obligation.rs +++ b/token-lending/sdk/src/state/obligation.rs @@ -691,7 +691,8 @@ impl Obligation { OBLIGATION_COLLATERAL_LEN + (OBLIGATION_LIQUIDITY_LEN * (MAX_OBLIGATION_RESERVES - 1)) ]; - match extract_discriminator_and_version(u8::from_le_bytes(*discriminator_and_version)) { + let mut discriminator_and_version = u8::from_le_bytes(*discriminator_and_version); + match extract_discriminator_and_version(discriminator_and_version) { Ok((AccountDiscriminator::Obligation, ProgramVersion::V2_1_0)) => { // migrated and all ok } @@ -699,6 +700,14 @@ impl Obligation { if input.len() == OBLIGATION_LEN_V1 => { // not migrated yet, so must have the old size + + discriminator_and_version = set_discriminator_and_version( + AccountDiscriminator::Obligation, + ProgramVersion::V2_1_0, + ); + } + Ok((AccountDiscriminator::Uninitialized, ProgramVersion::Uninitialized)) => { + // uninitialized account } Ok((AccountDiscriminator::Obligation, _)) => { msg!("Obligation version does not match lending program version"); @@ -774,10 +783,7 @@ impl Obligation { }; Ok(Self { - discriminator_and_version: set_discriminator_and_version( - AccountDiscriminator::Obligation, - ProgramVersion::V2_1_0, - ), + discriminator_and_version, last_update: LastUpdate { slot: u64::from_le_bytes(*last_update_slot), stale: unpack_bool(last_update_stale)?, diff --git a/token-lending/sdk/src/state/reserve.rs b/token-lending/sdk/src/state/reserve.rs index c83efaab640..7d30f130d57 100644 --- a/token-lending/sdk/src/state/reserve.rs +++ b/token-lending/sdk/src/state/reserve.rs @@ -1577,14 +1577,23 @@ impl Pack for Reserve { 49 ]; - match extract_discriminator_and_version(u8::from_le_bytes(*discriminator_and_version)) { + let mut discriminator_and_version = u8::from_le_bytes(*discriminator_and_version); + match extract_discriminator_and_version(discriminator_and_version) { Ok((AccountDiscriminator::Reserve, ProgramVersion::V2_1_0)) => { // migrated and all ok } Ok((AccountDiscriminator::Uninitialized, ProgramVersion::V2_0_2)) => { - // Not migrated yet, will do during unpacking. + // Not migrated yet. // We checked earlier that the input buffer was already resized. debug_assert_eq!(input.len(), RESERVE_LEN_V2_1_0); + + discriminator_and_version = set_discriminator_and_version( + AccountDiscriminator::Reserve, + ProgramVersion::V2_1_0, + ); + } + Ok((AccountDiscriminator::Uninitialized, ProgramVersion::Uninitialized)) => { + // uninitialized account } Ok((AccountDiscriminator::Reserve, _)) => { msg!("Reserve version does not match lending program version"); @@ -1727,10 +1736,7 @@ impl Pack for Reserve { PoolRewardManager::unpack_to_box(input_for_deposits_pool_reward_manager)?; Ok(Self { - discriminator_and_version: set_discriminator_and_version( - AccountDiscriminator::Reserve, - ProgramVersion::V2_1_0, - ), + discriminator_and_version, last_update, lending_market: Pubkey::new_from_array(*lending_market), liquidity, From 54d0e1d28a12b4368c2d3b9c053ff6e1229b30f8 Mon Sep 17 00:00:00 2001 From: vanitymnm Date: Fri, 21 Mar 2025 10:45:40 +0100 Subject: [PATCH 27/30] Removing version --- .../program/src/processor/liquidity_mining.rs | 14 ++- .../program/tests/init_lending_market.rs | 10 +- .../program/tests/init_obligation.rs | 8 +- token-lending/program/tests/init_reserve.rs | 8 +- .../program/tests/refresh_obligation.rs | 19 +-- token-lending/sdk/src/error.rs | 6 + token-lending/sdk/src/state/lending_market.rs | 94 ++++++-------- token-lending/sdk/src/state/mod.rs | 115 +++++++----------- token-lending/sdk/src/state/obligation.rs | 84 ++++--------- token-lending/sdk/src/state/reserve.rs | 102 ++++++---------- 10 files changed, 168 insertions(+), 292 deletions(-) diff --git a/token-lending/program/src/processor/liquidity_mining.rs b/token-lending/program/src/processor/liquidity_mining.rs index a14bf1f4b9b..96d1b7a0849 100644 --- a/token-lending/program/src/processor/liquidity_mining.rs +++ b/token-lending/program/src/processor/liquidity_mining.rs @@ -33,6 +33,7 @@ use solana_program::{ system_instruction, sysvar::Sysvar, }; +use solend_sdk::state::discriminator::AccountDiscriminator; use solend_sdk::{ error::LendingError, state::{LendingMarket, PositionKind, Reserve}, @@ -282,9 +283,16 @@ pub(crate) fn upgrade_reserve(program_id: &Pubkey, accounts: &[AccountInfo]) -> // 3. // - // updates version and discriminator - let reserve = Reserve::unpack(&accounts.reserve_info.data.borrow())?; - Reserve::pack(reserve, &mut accounts.reserve_info.data.borrow_mut())?; + // we upgrade discriminator as we've checked that the account is indeed + // a reserve account in [UpgradeReserveAccounts::from_unchecked_iter] + let mut data = accounts.reserve_info.data.borrow_mut(); + data[0] = AccountDiscriminator::Reserve as u8; + // Now the reserve can unpack fine and doesn't have to worry about + // migrations. + // Instead it returns an error on an invalid discriminator. + // This way a reserve cannot be mistaken for an obligation. + let reserve = Reserve::unpack(&data)?; + Reserve::pack(reserve, &mut data)?; Ok(()) } diff --git a/token-lending/program/tests/init_lending_market.rs b/token-lending/program/tests/init_lending_market.rs index f3c2bb4830e..1443ccc01b3 100644 --- a/token-lending/program/tests/init_lending_market.rs +++ b/token-lending/program/tests/init_lending_market.rs @@ -12,10 +12,7 @@ use solana_sdk::signer::Signer; use solana_sdk::transaction::TransactionError; use solend_program::error::LendingError; use solend_program::instruction::init_lending_market; -use solend_program::state::{ - discriminator::AccountDiscriminator, program_version::ProgramVersion, - set_discriminator_and_version, LendingMarket, RateLimiter, -}; +use solend_program::state::{discriminator::AccountDiscriminator, LendingMarket, RateLimiter}; #[tokio::test] async fn test_success() { @@ -31,10 +28,7 @@ async fn test_success() { assert_eq!( lending_market.account, LendingMarket { - discriminator_and_version: set_discriminator_and_version( - AccountDiscriminator::LendingMarket, - ProgramVersion::V2_1_0 - ), + discriminator: AccountDiscriminator::LendingMarket, bump_seed: lending_market.account.bump_seed, // TODO test this field owner: lending_market_owner.keypair.pubkey(), quote_currency: QUOTE_CURRENCY, diff --git a/token-lending/program/tests/init_obligation.rs b/token-lending/program/tests/init_obligation.rs index 4de7a52e8f7..1747113fa15 100644 --- a/token-lending/program/tests/init_obligation.rs +++ b/token-lending/program/tests/init_obligation.rs @@ -14,8 +14,7 @@ use solend_program::error::LendingError; use solend_program::instruction::init_obligation; use solend_program::math::Decimal; use solend_program::state::{ - discriminator::AccountDiscriminator, program_version::ProgramVersion, - set_discriminator_and_version, LastUpdate, LendingMarket, Obligation, + discriminator::AccountDiscriminator, LastUpdate, LendingMarket, Obligation, }; async fn setup() -> (SolendProgramTest, Info, User) { @@ -37,10 +36,7 @@ async fn test_success() { assert_eq!( obligation.account, Obligation { - discriminator_and_version: set_discriminator_and_version( - AccountDiscriminator::Obligation, - ProgramVersion::V2_1_0 - ), + discriminator: AccountDiscriminator::Obligation, last_update: LastUpdate { slot: 1000, stale: true diff --git a/token-lending/program/tests/init_reserve.rs b/token-lending/program/tests/init_reserve.rs index 699d1e23434..62489668912 100644 --- a/token-lending/program/tests/init_reserve.rs +++ b/token-lending/program/tests/init_reserve.rs @@ -29,8 +29,7 @@ use solana_sdk::{ use solend_program::NULL_PUBKEY; use solend_program::state::{ - discriminator::AccountDiscriminator, program_version::ProgramVersion, - set_discriminator_and_version, LastUpdate, LendingMarket, RateLimiter, Reserve, + discriminator::AccountDiscriminator, LastUpdate, LendingMarket, RateLimiter, Reserve, ReserveCollateral, ReserveLiquidity, }; use solend_program::{ @@ -152,10 +151,7 @@ async fn test_success() { assert_eq!( wsol_reserve.account, Reserve { - discriminator_and_version: set_discriminator_and_version( - AccountDiscriminator::Reserve, - ProgramVersion::V2_1_0 - ), + discriminator: AccountDiscriminator::Reserve, last_update: LastUpdate { slot: 1001, stale: true diff --git a/token-lending/program/tests/refresh_obligation.rs b/token-lending/program/tests/refresh_obligation.rs index 752e9703a05..6c3397a897f 100644 --- a/token-lending/program/tests/refresh_obligation.rs +++ b/token-lending/program/tests/refresh_obligation.rs @@ -21,8 +21,8 @@ use solana_program_test::*; use solana_sdk::signature::Keypair; use solend_program::state::SLOTS_PER_YEAR; use solend_program::state::{ - discriminator::AccountDiscriminator, program_version::ProgramVersion, - set_discriminator_and_version, LastUpdate, ObligationLiquidity, ReserveFees, ReserveLiquidity, + discriminator::AccountDiscriminator, LastUpdate, ObligationLiquidity, ReserveFees, + ReserveLiquidity, }; use solend_program::{ @@ -481,10 +481,7 @@ async fn test_normalize_obligation() { ); let reserve_1 = Reserve { - discriminator_and_version: set_discriminator_and_version( - AccountDiscriminator::Reserve, - ProgramVersion::V2_1_0, - ), + discriminator: AccountDiscriminator::Reserve, last_update: LastUpdate { slot: 1, stale: false, @@ -501,10 +498,7 @@ async fn test_normalize_obligation() { ); let reserve_2 = Reserve { - discriminator_and_version: set_discriminator_and_version( - AccountDiscriminator::Reserve, - ProgramVersion::V2_1_0, - ), + discriminator: AccountDiscriminator::Reserve, last_update: LastUpdate { slot: 1, stale: false, @@ -522,10 +516,7 @@ async fn test_normalize_obligation() { let obligation_pubkey = Pubkey::new_unique(); let obligation = Obligation { - discriminator_and_version: set_discriminator_and_version( - AccountDiscriminator::Obligation, - ProgramVersion::V2_1_0, - ), + discriminator: AccountDiscriminator::Obligation, deposits: vec![ ObligationCollateral { deposit_reserve: reserve_1_pubkey, diff --git a/token-lending/sdk/src/error.rs b/token-lending/sdk/src/error.rs index e5f6302199c..f3b050d7687 100644 --- a/token-lending/sdk/src/error.rs +++ b/token-lending/sdk/src/error.rs @@ -217,6 +217,12 @@ pub enum LendingError { /// Cannot close token account #[error("Cannot close token account")] CloseTokenAccountFailed, + /// Not an account discriminator + #[error("Given leading byte does not match any account discriminator")] + InvalidAccountDiscriminator, + /// Trying to use an account that hasn't been migrated + #[error("Trying to use an account that hasn't been migrated")] + AccountNotMigrated, } impl From for ProgramError { diff --git a/token-lending/sdk/src/state/lending_market.rs b/token-lending/sdk/src/state/lending_market.rs index 60a86b200ab..ada34c4d3ca 100644 --- a/token-lending/sdk/src/state/lending_market.rs +++ b/token-lending/sdk/src/state/lending_market.rs @@ -1,3 +1,7 @@ +use std::convert::TryFrom; + +use crate::error::LendingError; + use super::*; use arrayref::{array_mut_ref, array_ref, array_refs, mut_array_refs}; use solana_program::{ @@ -10,17 +14,13 @@ use solana_program::{ /// Lending market state #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct LendingMarket { - /// For historical reasons we mask both program version and account - /// discriminator onto 1 byte. + /// For uninitialized accounts, this will be equal to [AccountDiscriminator::Uninitialized]. + /// Otherwise this is [AccountDiscriminator::LendingMarket]. /// + /// # Note /// For accounts last used with version prior to @v2.1.0 this will be equal - /// to [ProgramVersion::V2_0_2]. - /// - /// For uninitialized accounts, this will be equal to [ProgramVersion::Uninitialized]. - /// - /// Accounts after including @v2.1.0 use first 4 bits for discriminator and - /// last 4 bits for program version. - pub discriminator_and_version: u8, + /// to [PROGRAM_VERSION_2_0_2]. + pub discriminator: AccountDiscriminator, /// Bump seed for derived authority address pub bump_seed: u8, /// Owner authority which can add new reserves @@ -52,10 +52,7 @@ impl LendingMarket { /// Initialize a lending market pub fn init(&mut self, params: InitLendingMarketParams) { - self.discriminator_and_version = set_discriminator_and_version( - AccountDiscriminator::LendingMarket, - ProgramVersion::V2_1_0, - ); + self.discriminator = AccountDiscriminator::LendingMarket; self.bump_seed = params.bump_seed; self.owner = params.owner; self.quote_currency = params.quote_currency; @@ -88,10 +85,7 @@ pub struct InitLendingMarketParams { impl Sealed for LendingMarket {} impl IsInitialized for LendingMarket { fn is_initialized(&self) -> bool { - match extract_discriminator_and_version(self.discriminator_and_version) { - Ok((_, version)) => !matches!(version, ProgramVersion::Uninitialized), - Err(_) => unreachable!("There is no path to invalid discriminator/version"), - } + !matches!(self.discriminator, AccountDiscriminator::Uninitialized) } } @@ -103,7 +97,7 @@ impl Pack for LendingMarket { let output = array_mut_ref![output, 0, LENDING_MARKET_LEN]; #[allow(clippy::ptr_offset_with_cast)] let ( - discriminator_and_version, + discriminator, bump_seed, owner, quote_currency, @@ -129,7 +123,7 @@ impl Pack for LendingMarket { 8 ]; - *discriminator_and_version = self.discriminator_and_version.to_le_bytes(); + discriminator[0] = self.discriminator as _; *bump_seed = self.bump_seed.to_le_bytes(); owner.copy_from_slice(self.owner.as_ref()); quote_currency.copy_from_slice(self.quote_currency.as_ref()); @@ -153,7 +147,7 @@ impl Pack for LendingMarket { let input = array_ref![input, 0, LENDING_MARKET_LEN]; #[allow(clippy::ptr_offset_with_cast)] let ( - discriminator_and_version, + discriminator, bump_seed, owner, quote_currency, @@ -179,40 +173,30 @@ impl Pack for LendingMarket { 8 ]; - let mut discriminator_and_version = u8::from_le_bytes(*discriminator_and_version); - match extract_discriminator_and_version(discriminator_and_version) { - Ok((AccountDiscriminator::LendingMarket, ProgramVersion::V2_1_0)) => { - // migrated and all ok - } - Ok((AccountDiscriminator::LendingMarket, ProgramVersion::V2_0_2)) => { - // Not migrated yet. - // There's no other change than discriminator & version. - - discriminator_and_version = set_discriminator_and_version( - AccountDiscriminator::LendingMarket, - ProgramVersion::V2_1_0, - ); - } - Ok((AccountDiscriminator::Uninitialized, ProgramVersion::Uninitialized)) => { - // uninitialized account - } - Ok((AccountDiscriminator::LendingMarket, _)) => { - msg!("Lending market version does not match lending program version"); - return Err(ProgramError::InvalidAccountData); - } - Ok((_, _)) => { + let discriminator = match AccountDiscriminator::try_from(discriminator) { + Ok(d @ AccountDiscriminator::Uninitialized) => d, // yet to be set + Ok(d @ AccountDiscriminator::LendingMarket) => d, // migrated to v2.1.0 + Ok(_) => { msg!("Lending market discriminator does not match"); - return Err(ProgramError::InvalidAccountData); + return Err(LendingError::InvalidAccountDiscriminator.into()); } - Err(e) => { - msg!("Lending market has an unexpected first byte value"); - return Err(e); + Err(LendingError::AccountNotMigrated) => { + // We're migrating the account from v2.0.2 to v2.1.0. + // The reason this is safe to do is conveyed in these asserts: + debug_assert_eq!(Self::LEN, input.len()); + debug_assert!(Self::LEN < Reserve::LEN); + debug_assert!(Self::LEN < RESERVE_LEN_V2_0_2); + debug_assert!(Self::LEN < Obligation::MIN_LEN); + // Ie. there's no confusion with other account types. + + AccountDiscriminator::LendingMarket } + Err(e) => return Err(e.into()), }; let owner_pubkey = Pubkey::new_from_array(*owner); Ok(Self { - discriminator_and_version, + discriminator, bump_seed: u8::from_le_bytes(*bump_seed), owner: owner_pubkey, quote_currency: *quote_currency, @@ -245,10 +229,7 @@ mod test { impl LendingMarket { fn new_rand(rng: &mut impl Rng) -> Self { Self { - discriminator_and_version: set_discriminator_and_version( - AccountDiscriminator::LendingMarket, - ProgramVersion::V2_1_0, - ), + discriminator: AccountDiscriminator::LendingMarket, bump_seed: rng.gen(), owner: Pubkey::new_unique(), quote_currency: [rng.gen(); 32], @@ -280,18 +261,15 @@ mod test { #[test] fn pack_and_unpack_lending_market_v2_0_2() { let mut rng = rand::thread_rng(); - let mut lending_market = LendingMarket::new_rand(&mut rng); - // this is what version looked like before the upgrade to v2.1.0 - lending_market.discriminator_and_version = ProgramVersion::V2_0_2 as _; + let lending_market = LendingMarket::new_rand(&mut rng); let mut packed = vec![0u8; LendingMarket::LEN]; LendingMarket::pack(lending_market.clone(), &mut packed).unwrap(); + // this is what version looked like before the upgrade to v2.1.0 + packed[0] = PROGRAM_VERSION_2_0_2; + let unpacked = LendingMarket::unpack_from_slice(&packed).unwrap(); // upgraded - lending_market.discriminator_and_version = set_discriminator_and_version( - AccountDiscriminator::LendingMarket, - ProgramVersion::V2_1_0, - ); assert_eq!(unpacked, lending_market); } } diff --git a/token-lending/sdk/src/state/mod.rs b/token-lending/sdk/src/state/mod.rs index b255c0c4f18..2e3dda4b3fc 100644 --- a/token-lending/sdk/src/state/mod.rs +++ b/token-lending/sdk/src/state/mod.rs @@ -13,7 +13,6 @@ pub use lending_market::*; pub use lending_market_metadata::*; pub use liquidity_mining::*; pub use obligation::*; -use program_version::ProgramVersion; pub use rate_limiter::*; pub use reserve::*; @@ -30,92 +29,64 @@ const INITIAL_COLLATERAL_RATE: u64 = INITIAL_COLLATERAL_RATIO * WAD; // 2 (slots per second) * 60 * 60 * 24 * 365 = 63072000 pub const SLOTS_PER_YEAR: u64 = 63072000; -pub mod program_version { - //! There can be at the moment at most 16 different program versions. - //! Extrapolating from the current program history this should be good enough. - //! The program versions can also wrap if sufficient precautions are taken. - - /// Match the second 4 bits of an account data against this enum to determine - /// the program version. - pub enum ProgramVersion { - /// Account is not initialized yet. - Uninitialized = 0, - /// Version of the program and all new accounts created until inclusive version - /// @v2.0.2 - /// - /// These versions will have no account discriminator. - V2_0_2 = 1, - /// Version of the program and all new accounts created from inclusive version - /// @v2.1.0 (liquidity mining) - /// - /// Will have an associated account discriminator. - V2_1_0 = 2, - } -} +/// Unmigrated accounts have this as their leading byte. +pub const PROGRAM_VERSION_2_0_2: u8 = 1; pub mod discriminator { - //! First 4 bits determine the account kind. - //! - //! There can be at the moment at most 15 different discriminators. - //! Extrapolating from the current program history this should be good enough. + //! First 1 byte determines the account kind. - /// Match the first 4 bits of an account data against this enum to determine + use std::convert::TryFrom; + + use crate::error::LendingError; + + /// Match the first byte of an account data against this enum to determine /// the account type. + /// + /// # Note + /// + /// In versions before @v2.1.0 this byte represented program version. + /// That's why we skip value `1u8`. + #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub enum AccountDiscriminator { /// Account is not initialized yet. + #[default] Uninitialized = 0, /// [crate::state::LendingMarket] - LendingMarket = 1, + LendingMarket = 2, /// [crate::state::Reserve] - Reserve = 2, + Reserve = 3, /// [crate::state::Obligation] - Obligation = 3, + Obligation = 4, } -} -/// There can be at the moment at most 16 different program versions. -/// Extrapolating from the current program history this should be good enough. -/// The program versions can also wrap if sufficient precautions are taken. -pub fn set_discriminator_and_version( - discriminator: AccountDiscriminator, - version: ProgramVersion, -) -> u8 { - let discriminator = discriminator as u8; - debug_assert!(discriminator <= 0x0F); - let version = version as u8; - debug_assert!(version <= 0x0F); - - (discriminator << 4) | (version & 0x0F) -} + impl TryFrom for AccountDiscriminator { + type Error = LendingError; -/// First 4 bytes are the discriminator, next 4 bytes are the version. -pub fn extract_discriminator_and_version( - byte: u8, -) -> Result<(AccountDiscriminator, ProgramVersion), ProgramError> { - let version = match byte & 0x0F { - 0 => ProgramVersion::Uninitialized, - 1 => ProgramVersion::V2_0_2, - 2 => ProgramVersion::V2_1_0, - 3..=16 => { - // unused - return Err(ProgramError::InvalidAccountData); - } - _ => unreachable!("Version is out of bounds"), - }; - - let discriminator = match (byte >> 4) & 0x0F { - 0 => AccountDiscriminator::Uninitialized, - 1 => AccountDiscriminator::LendingMarket, - 2 => AccountDiscriminator::Reserve, - 3 => AccountDiscriminator::Obligation, - 4..=16 => { - // unused - return Err(ProgramError::InvalidAccountData); + fn try_from(value: u8) -> Result { + match value { + // the account data were just created and are filled with 0s + 0 => Ok(Self::Uninitialized), + + // we skip 1 because it was used for program version + 1 => Err(Self::Error::AccountNotMigrated), + + // valid accounts + 2 => Ok(Self::LendingMarket), + 3 => Ok(Self::Reserve), + 4 => Ok(Self::Obligation), + + _ => Err(Self::Error::InvalidAccountDiscriminator), + } } - 17.. => unreachable!("Discriminator is out of bounds"), - }; + } + + impl TryFrom<&[u8; 1]> for AccountDiscriminator { + type Error = LendingError; - Ok((discriminator, version)) + fn try_from(value: &[u8; 1]) -> Result { + Self::try_from(value[0]) + } + } } // Helpers diff --git a/token-lending/sdk/src/state/obligation.rs b/token-lending/sdk/src/state/obligation.rs index f28da35fcba..1d0c88558e6 100644 --- a/token-lending/sdk/src/state/obligation.rs +++ b/token-lending/sdk/src/state/obligation.rs @@ -30,17 +30,13 @@ pub const MAX_OBLIGATION_RESERVES: usize = 10; /// instead. #[derive(Clone, Debug, Default, PartialEq)] pub struct Obligation { - /// For historical reasons we mask both program version and account - /// discriminator onto 1 byte. + /// For uninitialized accounts, this will be equal to [AccountDiscriminator::Uninitialized]. + /// Otherwise this is [AccountDiscriminator::Obligation]. /// + /// # Note /// For accounts last used with version prior to @v2.1.0 this will be equal - /// to [ProgramVersion::V2_0_2]. - /// - /// For uninitialized accounts, this will be equal to [ProgramVersion::Uninitialized]. - /// - /// Accounts after including @v2.1.0 use first 4 bits for discriminator and - /// last 4 bits for program version. - pub discriminator_and_version: u8, + /// to [PROGRAM_VERSION_2_0_2]. + pub discriminator: AccountDiscriminator, /// Last update to collateral, liquidity, or their market values pub last_update: LastUpdate, /// Lending market address @@ -106,8 +102,7 @@ impl Obligation { /// Initialize an obligation pub fn init(&mut self, params: InitObligationParams) { - self.discriminator_and_version = - set_discriminator_and_version(AccountDiscriminator::Obligation, ProgramVersion::V2_1_0); + self.discriminator = AccountDiscriminator::Obligation; self.last_update = LastUpdate::new(params.current_slot); self.lending_market = params.lending_market; self.owner = params.owner; @@ -337,10 +332,7 @@ pub struct InitObligationParams { impl Sealed for Obligation {} impl IsInitialized for Obligation { fn is_initialized(&self) -> bool { - match extract_discriminator_and_version(self.discriminator_and_version) { - Ok((_, version)) => !matches!(version, ProgramVersion::Uninitialized), - Err(_) => unreachable!("There is no path to invalid discriminator/version"), - } + !matches!(self.discriminator, AccountDiscriminator::Uninitialized) } } @@ -519,7 +511,7 @@ impl Obligation { let output = array_mut_ref![dst, 0, OBLIGATION_LEN_V1]; #[allow(clippy::ptr_offset_with_cast)] let ( - discriminator_and_version, + discriminator, last_update_slot, last_update_stale, lending_market, @@ -560,7 +552,7 @@ impl Obligation { ]; // obligation - *discriminator_and_version = self.discriminator_and_version.to_le_bytes(); + discriminator[0] = self.discriminator as _; *last_update_slot = self.last_update.slot.to_le_bytes(); pack_bool(self.last_update.stale, last_update_stale); lending_market.copy_from_slice(self.lending_market.as_ref()); @@ -651,7 +643,7 @@ impl Obligation { let input = array_ref![src, 0, OBLIGATION_LEN_V1]; #[allow(clippy::ptr_offset_with_cast)] let ( - discriminator_and_version, + discriminator, last_update_slot, last_update_stale, lending_market, @@ -691,36 +683,20 @@ impl Obligation { OBLIGATION_COLLATERAL_LEN + (OBLIGATION_LIQUIDITY_LEN * (MAX_OBLIGATION_RESERVES - 1)) ]; - let mut discriminator_and_version = u8::from_le_bytes(*discriminator_and_version); - match extract_discriminator_and_version(discriminator_and_version) { - Ok((AccountDiscriminator::Obligation, ProgramVersion::V2_1_0)) => { - // migrated and all ok - } - Ok((AccountDiscriminator::Uninitialized, ProgramVersion::V2_0_2)) - if input.len() == OBLIGATION_LEN_V1 => - { - // not migrated yet, so must have the old size - - discriminator_and_version = set_discriminator_and_version( - AccountDiscriminator::Obligation, - ProgramVersion::V2_1_0, - ); - } - Ok((AccountDiscriminator::Uninitialized, ProgramVersion::Uninitialized)) => { - // uninitialized account - } - Ok((AccountDiscriminator::Obligation, _)) => { - msg!("Obligation version does not match lending program version"); - return Err(ProgramError::InvalidAccountData); - } - Ok((_, _)) => { + let discriminator = match AccountDiscriminator::try_from(discriminator) { + Ok(d @ AccountDiscriminator::Uninitialized) => d, // yet to be set + Ok(d @ AccountDiscriminator::Obligation) => d, // migrated to v2.1.0 + Ok(_) => { msg!("Obligation discriminator does not match"); - return Err(ProgramError::InvalidAccountData); + return Err(LendingError::InvalidAccountDiscriminator.into()); } - Err(e) => { - msg!("Obligation has an unexpected first byte value"); - return Err(e); + Err(LendingError::AccountNotMigrated) => { + // We're migrating the account from v2.0.2 to v2.1.0. + debug_assert_eq!(OBLIGATION_LEN_V1, input.len()); + + AccountDiscriminator::Obligation } + Err(e) => return Err(e.into()), }; let deposits_len = u8::from_le_bytes(*deposits_len); @@ -783,7 +759,7 @@ impl Obligation { }; Ok(Self { - discriminator_and_version, + discriminator, last_update: LastUpdate { slot: u64::from_le_bytes(*last_update_slot), stale: unpack_bool(last_update_stale)?, @@ -835,10 +811,7 @@ mod test { impl Obligation { fn new_rand(rng: &mut impl Rng) -> Self { Self { - discriminator_and_version: set_discriminator_and_version( - AccountDiscriminator::Obligation, - ProgramVersion::V2_1_0, - ), + discriminator: AccountDiscriminator::Obligation, last_update: LastUpdate { slot: rng.gen(), stale: rng.gen(), @@ -894,18 +867,15 @@ mod test { fn pack_and_unpack_obligation_v2_0_2() { let mut rng = rand::thread_rng(); for _ in 0..100 { - let mut obligation = Obligation::new_rand(&mut rng); - // this is what version looked like before the upgrade to v2.1.0 - obligation.discriminator_and_version = ProgramVersion::V2_0_2 as _; + let obligation = Obligation::new_rand(&mut rng); let mut packed = [0u8; Obligation::MAX_LEN]; Obligation::pack(obligation.clone(), &mut packed).unwrap(); + // this is what version looked like before the upgrade to v2.1.0 + packed[0] = PROGRAM_VERSION_2_0_2; + let unpacked = Obligation::unpack(&packed).unwrap(); // upgraded - obligation.discriminator_and_version = set_discriminator_and_version( - AccountDiscriminator::Obligation, - ProgramVersion::V2_1_0, - ); assert_eq!(obligation, unpacked); } } diff --git a/token-lending/sdk/src/state/reserve.rs b/token-lending/sdk/src/state/reserve.rs index 7d30f130d57..c447e9b03a2 100644 --- a/token-lending/sdk/src/state/reserve.rs +++ b/token-lending/sdk/src/state/reserve.rs @@ -44,18 +44,13 @@ pub const MIN_SCALED_PRICE_OFFSET_BPS: i64 = -2000; /// Lending market reserve state #[derive(Clone, Debug, Default, PartialEq)] pub struct Reserve { - /// For historical reasons we mask both program version and account - /// discriminator onto 1 byte. + /// For uninitialized accounts, this will be equal to [AccountDiscriminator::Uninitialized]. + /// Otherwise this is [AccountDiscriminator::Reserve]. /// + /// # Note /// For accounts last used with version prior to @v2.1.0 this will be equal - /// to [ProgramVersion::V2_0_2]. - /// - /// For uninitialized accounts, this will be equal to - /// [Discriminator::Uninitialized] and [ProgramVersion::Uninitialized]. - /// - /// Accounts after including @v2.1.0 use first 4 bits for discriminator and - /// last 4 bits for program version. - pub discriminator_and_version: u8, + /// to [PROGRAM_VERSION_2_0_2]. + pub discriminator: AccountDiscriminator, /// Last slot when supply and rates updated pub last_update: LastUpdate, /// Lending market address @@ -96,8 +91,7 @@ impl Reserve { /// Initialize a reserve pub fn init(&mut self, params: InitReserveParams) { - self.discriminator_and_version = - set_discriminator_and_version(AccountDiscriminator::Reserve, ProgramVersion::V2_1_0); + self.discriminator = AccountDiscriminator::Reserve; self.last_update = LastUpdate::new(params.current_slot); self.lending_market = params.lending_market; self.liquidity = params.liquidity; @@ -1249,10 +1243,7 @@ pub enum FeeCalculation { impl Sealed for Reserve {} impl IsInitialized for Reserve { fn is_initialized(&self) -> bool { - match extract_discriminator_and_version(self.discriminator_and_version) { - Ok((_, version)) => !matches!(version, ProgramVersion::Uninitialized), - Err(_) => unreachable!("There is no path to invalid discriminator/version"), - } + !matches!(self.discriminator, AccountDiscriminator::Uninitialized) } } @@ -1266,12 +1257,11 @@ impl Pack for Reserve { // @TODO: break this up by reserve / liquidity / collateral / config https://git.io/JOCca // @v2.1.0: packs deposits_pool_reward_manager and borrows_pool_reward_manager - // @v2.1.0 TODO: add discriminator fn pack_into_slice(&self, output: &mut [u8]) { let output = array_mut_ref![output, 0, Reserve::LEN]; #[allow(clippy::ptr_offset_with_cast)] let ( - discriminator_and_version, + discriminator, last_update_slot, last_update_stale, lending_market, @@ -1318,7 +1308,7 @@ impl Pack for Reserve { attributed_borrow_value, config_attributed_borrow_limit_open, config_attributed_borrow_limit_close, - _padding, // TODO: use some of this for discriminator + _padding, output_for_borrows_pool_reward_manager, output_for_deposits_pool_reward_manager, ) = mut_array_refs![ @@ -1376,7 +1366,7 @@ impl Pack for Reserve { ]; // reserve - *discriminator_and_version = self.discriminator_and_version.to_le_bytes(); + discriminator[0] = self.discriminator as _; *last_update_slot = self.last_update.slot.to_le_bytes(); pack_bool(self.last_update.stale, last_update_stale); lending_market.copy_from_slice(self.lending_market.as_ref()); @@ -1477,7 +1467,7 @@ impl Pack for Reserve { let input_v2_0_2 = array_ref![input, 0, RESERVE_LEN_V2_0_2]; #[allow(clippy::ptr_offset_with_cast)] let ( - discriminator_and_version, + discriminator, last_update_slot, last_update_stale, lending_market, @@ -1577,36 +1567,18 @@ impl Pack for Reserve { 49 ]; - let mut discriminator_and_version = u8::from_le_bytes(*discriminator_and_version); - match extract_discriminator_and_version(discriminator_and_version) { - Ok((AccountDiscriminator::Reserve, ProgramVersion::V2_1_0)) => { - // migrated and all ok - } - Ok((AccountDiscriminator::Uninitialized, ProgramVersion::V2_0_2)) => { - // Not migrated yet. - // We checked earlier that the input buffer was already resized. - debug_assert_eq!(input.len(), RESERVE_LEN_V2_1_0); - - discriminator_and_version = set_discriminator_and_version( - AccountDiscriminator::Reserve, - ProgramVersion::V2_1_0, - ); - } - Ok((AccountDiscriminator::Uninitialized, ProgramVersion::Uninitialized)) => { - // uninitialized account - } - Ok((AccountDiscriminator::Reserve, _)) => { - msg!("Reserve version does not match lending program version"); - return Err(ProgramError::InvalidAccountData); - } - Ok((_, _)) => { + // Reserve migration v2.0.2 to v2.1.0 happens outside of the + // unpack method because there's no reliable way to ensure that we're + // migrating a reserve and not an obligation that's dynamically resized + // to the same length as a reserve. + let discriminator = match AccountDiscriminator::try_from(discriminator) { + Ok(d @ AccountDiscriminator::Uninitialized) => d, // yet to be set + Ok(d @ AccountDiscriminator::Reserve) => d, // migrated to v2.1.0 + Ok(_) => { msg!("Reserve discriminator does not match"); - return Err(ProgramError::InvalidAccountData); - } - Err(e) => { - msg!("Reserve has an unexpected first byte value"); - return Err(e); + return Err(LendingError::InvalidAccountDiscriminator.into()); } + Err(e) => return Err(e.into()), }; let optimal_utilization_rate = u8::from_le_bytes(*config_optimal_utilization_rate); @@ -1736,7 +1708,7 @@ impl Pack for Reserve { PoolRewardManager::unpack_to_box(input_for_deposits_pool_reward_manager)?; Ok(Self { - discriminator_and_version, + discriminator, last_update, lending_market: Pubkey::new_from_array(*lending_market), liquidity, @@ -1781,10 +1753,7 @@ mod test { }; Self { - discriminator_and_version: set_discriminator_and_version( - AccountDiscriminator::Reserve, - ProgramVersion::V2_1_0, - ), + discriminator: AccountDiscriminator::Reserve, last_update: LastUpdate { slot: rng.gen(), stale: rng.gen(), @@ -1862,21 +1831,18 @@ mod test { #[test] fn pack_and_unpack_reserve_v2_0_2() { let mut rng = rand::thread_rng(); - for _ in 0..100 { - let mut reserve = Reserve::new_rand(&mut rng); - // this is what version looked like before the upgrade to v2.1.0 - reserve.discriminator_and_version = ProgramVersion::V2_0_2 as _; + let reserve = Reserve::new_rand(&mut rng); - let mut packed = [0u8; Reserve::LEN]; - Reserve::pack(reserve.clone(), &mut packed).unwrap(); - let unpacked = Reserve::unpack(&packed).unwrap(); - // upgraded - reserve.discriminator_and_version = set_discriminator_and_version( - AccountDiscriminator::Reserve, - ProgramVersion::V2_1_0, - ); - assert_eq!(reserve, unpacked); - } + let mut packed = [0u8; Reserve::LEN]; + Reserve::pack(reserve.clone(), &mut packed).unwrap(); + // this is what version looked like before the upgrade to v2.1.0 + packed[0] = PROGRAM_VERSION_2_0_2; + + // reserve must be upgraded with a special ix + assert_eq!( + Reserve::unpack(&packed).unwrap_err(), + LendingError::AccountNotMigrated.into() + ); } const MAX_LIQUIDITY: u64 = u64::MAX / 5; From 268a0c1474224f807667f498a0b891c5498796d9 Mon Sep 17 00:00:00 2001 From: vanitymnm Date: Fri, 21 Mar 2025 11:50:16 +0100 Subject: [PATCH 28/30] Skipping packing/unpacking for vacant slots --- .../sdk/src/state/liquidity_mining.rs | 226 +++++++++++------- 1 file changed, 138 insertions(+), 88 deletions(-) diff --git a/token-lending/sdk/src/state/liquidity_mining.rs b/token-lending/sdk/src/state/liquidity_mining.rs index 56e76f00fac..fc59369ccb5 100644 --- a/token-lending/sdk/src/state/liquidity_mining.rs +++ b/token-lending/sdk/src/state/liquidity_mining.rs @@ -61,6 +61,9 @@ pub enum PoolRewardSlot { Vacant { /// Increment this ID when adding new [PoolReward]. last_pool_reward_id: PoolRewardId, + /// An optimization to avoid writing data that has not changed. + /// When vacating a slot we set this to true. + has_been_vacated_in_this_tx: bool, }, /// Reward has not been closed yet. /// @@ -325,7 +328,16 @@ impl UserRewardManager { } impl PoolReward { - const LEN: usize = PoolRewardId::LEN + PUBKEY_BYTES + 8 + 4 + 8 + 8 + 16; + const LEN: usize = Self::HEAD_LEN + Self::TAIL_LEN; + + const HEAD_LEN: usize = PoolRewardId::LEN + PUBKEY_BYTES; + + /// - `start_time_secs`` + /// - `duration_secs`` + /// - `total_rewards`` + /// - `num_user_reward_managers`` + /// - `cumulative_rewards_per_share`` + const TAIL_LEN: usize = 8 + 4 + 8 + 8 + 16; } impl PoolRewardId { @@ -346,6 +358,9 @@ impl Default for PoolRewardSlot { fn default() -> Self { Self::Vacant { last_pool_reward_id: PoolRewardId(0), + // this is used for initialization of the pool reward manager so + // it makes sense as there are 0s in the account data already + has_been_vacated_in_this_tx: false, } } } @@ -367,73 +382,61 @@ impl Pack for PoolRewardManager { output[0..8].copy_from_slice(&self.total_shares.to_le_bytes()); output[8..16].copy_from_slice(&self.last_update_time_secs.to_le_bytes()); - for (index, pool_reward_slot) in self.pool_rewards.iter().enumerate() { + let rewards_to_pack = self + .pool_rewards + .iter() + .enumerate() + .filter(|(_, s)| s.should_be_packed()); + + for (index, pool_reward_slot) in rewards_to_pack { let offset = 16 + index * PoolReward::LEN; - let raw_pool_reward = array_mut_ref![output, offset, PoolReward::LEN]; - let ( - dst_id, - dst_vault, - dst_start_time_secs, - dst_duration_secs, - dst_total_rewards, - dst_num_user_reward_managers, - dst_cumulative_rewards_per_share_wads, - ) = mut_array_refs![ - raw_pool_reward, - PoolRewardId::LEN, - PUBKEY_BYTES, - 8, // start_time_secs - 4, // duration_secs - 8, // total_rewards - 8, // num_user_reward_managers - 16 // cumulative_rewards_per_share - ]; + let raw_pool_reward_head = array_mut_ref![output, offset, PoolReward::HEAD_LEN]; + let (dst_id, dst_vault) = + mut_array_refs![raw_pool_reward_head, PoolRewardId::LEN, PUBKEY_BYTES]; - let ( - id, - vault, - start_time_secs, - duration_secs, - total_rewards, - num_user_reward_managers, - cumulative_rewards_per_share, - ) = match pool_reward_slot { + match pool_reward_slot { PoolRewardSlot::Vacant { - last_pool_reward_id, - } => ( - *last_pool_reward_id, - Pubkey::default(), - 0u64, - 0u32, - 0u64, - 0u64, - Decimal::zero(), - ), - PoolRewardSlot::Occupied(pool_reward) => ( - pool_reward.id, - pool_reward.vault, - pool_reward.start_time_secs, - pool_reward.duration_secs, - pool_reward.total_rewards, - pool_reward.num_user_reward_managers, - pool_reward.cumulative_rewards_per_share, - ), + last_pool_reward_id: PoolRewardId(id), + .. + } => { + dst_id.copy_from_slice(&id.to_le_bytes()); + dst_vault.copy_from_slice(Pubkey::default().as_ref()); + } + PoolRewardSlot::Occupied(pool_reward) => { + dst_id.copy_from_slice(&pool_reward.id.0.to_le_bytes()); + dst_vault.copy_from_slice(pool_reward.vault.as_ref()); + + let raw_pool_reward_tail = + array_mut_ref![output, offset + PoolReward::HEAD_LEN, PoolReward::TAIL_LEN]; + + let ( + dst_start_time_secs, + dst_duration_secs, + dst_total_rewards, + dst_num_user_reward_managers, + dst_cumulative_rewards_per_share_wads, + ) = mut_array_refs![ + raw_pool_reward_tail, + 8, // start_time_secs + 4, // duration_secs + 8, // total_rewards + 8, // num_user_reward_managers + 16 // cumulative_rewards_per_share + ]; + + *dst_start_time_secs = pool_reward.start_time_secs.to_le_bytes(); + *dst_duration_secs = pool_reward.duration_secs.to_le_bytes(); + *dst_total_rewards = pool_reward.total_rewards.to_le_bytes(); + *dst_num_user_reward_managers = + pool_reward.num_user_reward_managers.to_le_bytes(); + // TBD: do we want to ceil? + pack_decimal( + pool_reward.cumulative_rewards_per_share, + dst_cumulative_rewards_per_share_wads, + ); + } }; - - dst_id.copy_from_slice(&id.0.to_le_bytes()); - dst_vault.copy_from_slice(vault.as_ref()); - - // TBD: these values don't have to be written if the slot is vacant - *dst_start_time_secs = start_time_secs.to_le_bytes(); - *dst_duration_secs = duration_secs.to_le_bytes(); - *dst_total_rewards = total_rewards.to_le_bytes(); - *dst_num_user_reward_managers = num_user_reward_managers.to_le_bytes(); - // TBD: do we want to ceil? - pack_decimal( - cumulative_rewards_per_share, - dst_cumulative_rewards_per_share_wads, - ); } } @@ -447,37 +450,40 @@ impl Pack for PoolRewardManager { for index in 0..MAX_REWARDS { let offset = 8 + 8 + index * PoolReward::LEN; - let raw_pool_reward = array_ref![input, offset, PoolReward::LEN]; + let raw_pool_reward_head = array_ref![input, offset, PoolReward::HEAD_LEN]; - let ( - src_id, - src_vault, - src_start_time_secs, - src_duration_secs, - src_total_rewards, - src_num_user_reward_managers, - src_cumulative_rewards_per_share_wads, - ) = array_refs![ - raw_pool_reward, - PoolRewardId::LEN, - PUBKEY_BYTES, - // TBD: these values don't have to be referenced if the slot is vacant - 8, // start_time_secs - 4, // duration_secs - 8, // total_rewards - 8, // num_user_reward_managers - 16 // cumulative_rewards_per_share - ]; + let (src_id, src_vault) = + array_refs![raw_pool_reward_head, PoolRewardId::LEN, PUBKEY_BYTES]; - let vault = Pubkey::new_from_array(*src_vault); let pool_reward_id = PoolRewardId(u32::from_le_bytes(*src_id)); + let vault = Pubkey::new_from_array(*src_vault); // SAFETY: ok to assign because we know the index is less than length pool_reward_manager.pool_rewards[index] = if vault == Pubkey::default() { PoolRewardSlot::Vacant { last_pool_reward_id: pool_reward_id, + // nope, has been vacant since unpack + has_been_vacated_in_this_tx: false, } } else { + let raw_pool_reward_tail = + array_ref![input, offset + PoolReward::HEAD_LEN, PoolReward::TAIL_LEN]; + + let ( + src_start_time_secs, + src_duration_secs, + src_total_rewards, + src_num_user_reward_managers, + src_cumulative_rewards_per_share_wads, + ) = array_refs![ + raw_pool_reward_tail, + 8, // start_time_secs + 4, // duration_secs + 8, // total_rewards + 8, // num_user_reward_managers + 16 // cumulative_rewards_per_share + ]; + PoolRewardSlot::Occupied(Box::new(PoolReward { id: pool_reward_id, vault, @@ -496,6 +502,21 @@ impl Pack for PoolRewardManager { } } +impl PoolRewardSlot { + /// If we know for sure that data hasn't changed then we can just skip packing. + fn should_be_packed(&self) -> bool { + let for_sure_has_not_changed = matches!( + self, + Self::Vacant { + has_been_vacated_in_this_tx: false, + .. + } + ); + + !for_sure_has_not_changed + } +} + impl UserReward { /// - [UserReward::pool_reward_index] truncated to a byte /// - [PoolRewardId] @@ -633,7 +654,7 @@ mod tests { use rand::Rng; fn pool_reward_manager_strategy() -> impl Strategy { - (0..100u32).prop_perturb(|_, mut rng| PoolRewardManager::new_rand(&mut rng)) + (0..1u32).prop_perturb(|_, mut rng| PoolRewardManager::new_rand(&mut rng)) } fn user_reward_manager_strategy() -> impl Strategy { @@ -646,7 +667,13 @@ mod tests { let mut packed = vec![0u8; PoolRewardManager::LEN]; Pack::pack_into_slice(&pool_reward_manager, &mut packed); let unpacked = PoolRewardManager::unpack_from_slice(&packed).unwrap(); - prop_assert_eq!(pool_reward_manager, unpacked); + + prop_assert_eq!(pool_reward_manager.last_update_time_secs, unpacked.last_update_time_secs); + prop_assert_eq!(pool_reward_manager.total_shares, unpacked.total_shares); + + for (og, unpacked) in pool_reward_manager.pool_rewards.iter().zip(unpacked.pool_rewards.iter()) { + prop_assert_eq!(og, unpacked); + } } #[test] @@ -658,6 +685,27 @@ mod tests { } } + #[test] + fn it_packs_id_if_vacated_in_this_tx() { + let mut m = PoolRewardManager::default(); + m.pool_rewards[0] = PoolRewardSlot::Vacant { + last_pool_reward_id: PoolRewardId(69), + has_been_vacated_in_this_tx: true, + }; + + let mut packed = vec![0u8; PoolRewardManager::LEN]; + m.pack_into_slice(&mut packed); + let unpacked = PoolRewardManager::unpack_from_slice(&packed).unwrap(); + + assert_eq!( + unpacked.pool_rewards[0], + PoolRewardSlot::Vacant { + last_pool_reward_id: PoolRewardId(69), + has_been_vacated_in_this_tx: false, + } + ); + } + #[test] fn it_unpacks_empty_pool_reward_manager_bytes_as_default() { let packed = vec![0u8; PoolRewardManager::LEN]; @@ -669,7 +717,8 @@ mod tests { matches!( pool_reward, PoolRewardSlot::Vacant { - last_pool_reward_id: PoolRewardId(0) + last_pool_reward_id: PoolRewardId(0), + has_been_vacated_in_this_tx: false, } ) }); @@ -726,7 +775,8 @@ mod tests { if is_vacant { PoolRewardSlot::Vacant { - last_pool_reward_id: PoolRewardId(rng.gen()), + last_pool_reward_id: Default::default(), + has_been_vacated_in_this_tx: false, } } else { PoolRewardSlot::Occupied(Box::new(PoolReward { From d7d072bc36dd38c65bdccee8e2f90b1998b51ca6 Mon Sep 17 00:00:00 2001 From: vanitymnm Date: Fri, 21 Mar 2025 13:25:58 +0100 Subject: [PATCH 29/30] Extracting CU budgets to consts --- .../tests/helpers/solend_program_test.rs | 67 ++++++++++++++----- 1 file changed, 51 insertions(+), 16 deletions(-) diff --git a/token-lending/program/tests/helpers/solend_program_test.rs b/token-lending/program/tests/helpers/solend_program_test.rs index 48d0bbedd79..20755025461 100644 --- a/token-lending/program/tests/helpers/solend_program_test.rs +++ b/token-lending/program/tests/helpers/solend_program_test.rs @@ -59,6 +59,25 @@ use std::{ use super::mock_pyth::{init, set_price}; use super::mock_pyth_pull::{init as init_pull, set_price as set_price_pull}; +mod cu_budgets { + pub(super) const INIT_OBLIGATION: u32 = 5_001; + pub(super) const DEPOSIT_OBLIGATION_COLLATERAL: u32 = 70_002; + pub(super) const REFRESH_RESERVE: u32 = 2_000_003; + pub(super) const REFRESH_OBLIGATION: u32 = 1_000_004; + pub(super) const BORROW_OBLIGATION_LIQUIDITY: u32 = 140_005; + pub(super) const REPAY_OBLIGATION_LIQUIDITY: u32 = 70_006; + pub(super) const REDEEM_FEES: u32 = 80_007; + pub(super) const LIQUIDATE_OBLIGATION_AND_REDEEM_RESERVE_COLLATERAL: u32 = 200_008; + pub(super) const WITHDRAW_OBLIGATION_COLLATERAL_AND_REDEEM_RESERVE_COLLATERAL: u32 = 200_009; + pub(super) const WITHDRAW_OBLIGATION_COLLATERAL: u32 = 100_010; + pub(super) const INIT_RESERVE: u32 = 90_011; + pub(super) const DEPOSIT: u32 = 70_012; + pub(super) const DONATE_TO_RESERVE: u32 = 50_013; + pub(super) const UPDATE_RESERVE_CONFIG: u32 = 25_014; + pub(super) const DEPOSIT_RESERVE_LIQUIDITY_AND_OBLIGATION_COLLATERAL: u32 = 130_015; + pub(super) const REDEEM: u32 = 130_016; +} + pub struct SolendProgramTest { pub context: ProgramTestContext, rent: Rent, @@ -673,7 +692,7 @@ impl SolendProgramTest { let res = self .process_transaction( &[ - ComputeBudgetInstruction::set_compute_unit_limit(100_011), + ComputeBudgetInstruction::set_compute_unit_limit(cu_budgets::INIT_RESERVE), init_reserve( solend_program::id(), liquidity_amount, @@ -859,7 +878,7 @@ impl Info { liquidity_amount: u64, ) -> Result<(), BanksClientError> { let instructions = [ - ComputeBudgetInstruction::set_compute_unit_limit(100_012), + ComputeBudgetInstruction::set_compute_unit_limit(cu_budgets::DEPOSIT), deposit_reserve_liquidity( solend_program::id(), liquidity_amount, @@ -887,7 +906,7 @@ impl Info { liquidity_amount: u64, ) -> Result<(), BanksClientError> { let instructions = [ - ComputeBudgetInstruction::set_compute_unit_limit(100_013), + ComputeBudgetInstruction::set_compute_unit_limit(cu_budgets::DONATE_TO_RESERVE), donate_to_reserve( solend_program::id(), liquidity_amount, @@ -921,7 +940,7 @@ impl Info { let oracle = oracle.unwrap_or(&default_oracle); let instructions = [ - ComputeBudgetInstruction::set_compute_unit_limit(30_014), + ComputeBudgetInstruction::set_compute_unit_limit(cu_budgets::UPDATE_RESERVE_CONFIG), update_reserve_config( solend_program::id(), config, @@ -948,7 +967,9 @@ impl Info { liquidity_amount: u64, ) -> Result<(), BanksClientError> { let instructions = [ - ComputeBudgetInstruction::set_compute_unit_limit(150_015), + ComputeBudgetInstruction::set_compute_unit_limit( + cu_budgets::DEPOSIT_RESERVE_LIQUIDITY_AND_OBLIGATION_COLLATERAL, + ), deposit_reserve_liquidity_and_obligation_collateral( solend_program::id(), liquidity_amount, @@ -981,7 +1002,7 @@ impl Info { collateral_amount: u64, ) -> Result<(), BanksClientError> { let instructions = [ - ComputeBudgetInstruction::set_compute_unit_limit(150_016), + ComputeBudgetInstruction::set_compute_unit_limit(cu_budgets::REDEEM), refresh_reserve( solend_program::id(), reserve.pubkey, @@ -1015,7 +1036,7 @@ impl Info { user: &User, ) -> Result, BanksClientError> { let instructions = [ - ComputeBudgetInstruction::set_compute_unit_limit(10_001), + ComputeBudgetInstruction::set_compute_unit_limit(cu_budgets::INIT_OBLIGATION), system_instruction::create_account( &test.context.payer.pubkey(), &obligation_keypair.pubkey(), @@ -1049,7 +1070,9 @@ impl Info { collateral_amount: u64, ) -> Result<(), BanksClientError> { let instructions = [ - ComputeBudgetInstruction::set_compute_unit_limit(150_002), + ComputeBudgetInstruction::set_compute_unit_limit( + cu_budgets::DEPOSIT_OBLIGATION_COLLATERAL, + ), deposit_obligation_collateral( solend_program::id(), collateral_amount, @@ -1075,7 +1098,7 @@ impl Info { ) -> Result<(), BanksClientError> { test.process_transaction( &[ - ComputeBudgetInstruction::set_compute_unit_limit(2_000_003), + ComputeBudgetInstruction::set_compute_unit_limit(cu_budgets::REFRESH_RESERVE), refresh_reserve( solend_program::id(), reserve.pubkey, @@ -1174,7 +1197,9 @@ impl Info { Err(e) => return Err(e), }; - let mut instructions = vec![ComputeBudgetInstruction::set_compute_unit_limit(1_000_004)]; + let mut instructions = vec![ComputeBudgetInstruction::set_compute_unit_limit( + cu_budgets::REFRESH_OBLIGATION, + )]; instructions.push(refresh_reserve_instructions.last().unwrap().clone()); test.process_transaction(&instructions, None).await @@ -1196,7 +1221,9 @@ impl Info { .await; test.process_transaction(&refresh_ixs, None).await.unwrap(); - let mut instructions = vec![ComputeBudgetInstruction::set_compute_unit_limit(140_005)]; + let mut instructions = vec![ComputeBudgetInstruction::set_compute_unit_limit( + cu_budgets::BORROW_OBLIGATION_LIQUIDITY, + )]; instructions.push(borrow_obligation_liquidity( solend_program::id(), liquidity_amount, @@ -1230,7 +1257,9 @@ impl Info { liquidity_amount: u64, ) -> Result<(), BanksClientError> { let instructions = [ - ComputeBudgetInstruction::set_compute_unit_limit(80_006), + ComputeBudgetInstruction::set_compute_unit_limit( + cu_budgets::REPAY_OBLIGATION_LIQUIDITY, + ), repay_obligation_liquidity( solend_program::id(), liquidity_amount, @@ -1254,7 +1283,7 @@ impl Info { reserve: &Info, ) -> Result<(), BanksClientError> { let instructions = [ - ComputeBudgetInstruction::set_compute_unit_limit(100_007), + ComputeBudgetInstruction::set_compute_unit_limit(cu_budgets::REDEEM_FEES), refresh_reserve( solend_program::id(), reserve.pubkey, @@ -1290,7 +1319,9 @@ impl Info { test.process_transaction( &[ - ComputeBudgetInstruction::set_compute_unit_limit(280_008), + ComputeBudgetInstruction::set_compute_unit_limit( + cu_budgets::LIQUIDATE_OBLIGATION_AND_REDEEM_RESERVE_COLLATERAL, + ), liquidate_obligation_and_redeem_reserve_collateral( solend_program::id(), liquidity_amount, @@ -1367,7 +1398,9 @@ impl Info { test.process_transaction( &[ - ComputeBudgetInstruction::set_compute_unit_limit(250_009), + ComputeBudgetInstruction::set_compute_unit_limit( + cu_budgets::WITHDRAW_OBLIGATION_COLLATERAL_AND_REDEEM_RESERVE_COLLATERAL, + ), withdraw_obligation_collateral_and_redeem_reserve_collateral( solend_program::id(), collateral_amount, @@ -1411,7 +1444,9 @@ impl Info { test.process_transaction( &[ - ComputeBudgetInstruction::set_compute_unit_limit(120_010), + ComputeBudgetInstruction::set_compute_unit_limit( + cu_budgets::WITHDRAW_OBLIGATION_COLLATERAL, + ), withdraw_obligation_collateral( solend_program::id(), collateral_amount, From 8671efef994397dbf44c5610463ec98340780812 Mon Sep 17 00:00:00 2001 From: vanitymnm Date: Fri, 21 Mar 2025 13:31:20 +0100 Subject: [PATCH 30/30] Removing some stale TODOs --- token-lending/program/tests/helpers/solend_program_test.rs | 2 -- token-lending/sdk/src/state/obligation.rs | 2 -- 2 files changed, 4 deletions(-) diff --git a/token-lending/program/tests/helpers/solend_program_test.rs b/token-lending/program/tests/helpers/solend_program_test.rs index 20755025461..b7979450c98 100644 --- a/token-lending/program/tests/helpers/solend_program_test.rs +++ b/token-lending/program/tests/helpers/solend_program_test.rs @@ -1,5 +1,3 @@ -// TODO: code budgets were increased. export them to consts and optimize code to again lower them - use bytemuck::checked::from_bytes; use oracles::switchboard_on_demand_mainnet; diff --git a/token-lending/sdk/src/state/obligation.rs b/token-lending/sdk/src/state/obligation.rs index 1d0c88558e6..8696853932b 100644 --- a/token-lending/sdk/src/state/obligation.rs +++ b/token-lending/sdk/src/state/obligation.rs @@ -489,7 +489,6 @@ impl Obligation { /// Unpack from slice without checking if initialized pub fn unpack_unchecked(input: &[u8]) -> Result { - // TODO: add discriminant if !(Self::MIN_LEN..=Self::MAX_LEN).contains(&input.len()) { return Err(ProgramError::InvalidAccountData); } @@ -498,7 +497,6 @@ impl Obligation { /// Pack into slice pub fn pack(src: Self, dst: &mut [u8]) -> Result<(), ProgramError> { - // TODO: add discriminant if !(Self::MIN_LEN..=Self::MAX_LEN).contains(&dst.len()) { return Err(ProgramError::InvalidAccountData); }