From 9e76b2630ae3b491c01999d782f87cf0ee21b9be Mon Sep 17 00:00:00 2001
From: vanity mnm <vanity@solend.fi>
Date: Fri, 7 Mar 2025 10:54:45 +0100
Subject: [PATCH 01/14] Removing libssl1.1 dependency as it can no longer be
 installed on CI (#199)

---
 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 5fc80c2ae7204c197778143dce8c8d427e1c70e4 Mon Sep 17 00:00:00 2001
From: vanity mnm <vanity@solend.fi>
Date: Fri, 14 Mar 2025 13:02:31 +0100
Subject: [PATCH 02/14] [Liquidity Mining] Creating data structures (1) (#197)

---
 .../sdk/src/state/liquidity_mining.rs         | 147 ++++++++++++++++++
 token-lending/sdk/src/state/obligation.rs     |  26 +++-
 token-lending/sdk/src/state/reserve.rs        |  15 +-
 3 files changed, 176 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..c4a73849ebc
--- /dev/null
+++ b/token-lending/sdk/src/state/liquidity_mining.rs
@@ -0,0 +1,147 @@
+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:
+/// - 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.
+///
+/// # 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 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);
+
+/// # (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<Option<UserReward>>,
+}
+
+/// 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::<PoolReward>();
+
+        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..e17bcfa2f21 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,15 @@ 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
+/// 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;
+    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 +538,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<Self, ProgramError> {
-        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 +705,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<Self, ProgramError> {
-        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 beb136f89aab6be725c6b85dd8af980ab6a8836d Mon Sep 17 00:00:00 2001
From: vanity mnm <vanity@solend.fi>
Date: Fri, 14 Mar 2025 13:04:44 +0100
Subject: [PATCH 03/14] [Liquidity Mining] Adding admin ixs for reward
 management (2) (#198)

---
 token-lending/program/src/processor.rs        |  75 ++
 .../program/src/processor/liquidity_mining.rs | 714 ++++++++++++++++++
 token-lending/sdk/src/error.rs                |   8 +
 token-lending/sdk/src/instruction.rs          | 152 +++-
 token-lending/sdk/src/state/mod.rs            |   2 +
 token-lending/sdk/src/state/obligation.rs     |  12 +
 6 files changed, 962 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..470558d4f38 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::CloseTokenAccountFailed.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..76defa6c5e6
--- /dev/null
+++ b/token-lending/program/src/processor/liquidity_mining.rs
@@ -0,0 +1,714 @@
+//! 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,
+};
+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, &params, &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, &params, &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, &params, &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, LendingError> {
+    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<'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 {
+        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,
+
+        _priv: (),
+    }
+
+    /// Use [Self::from_unchecked_iter] to validate the accounts except for
+    /// * `reward_token_vault_info`
+    /// * `rent_info`
+    pub(super) struct AddPoolRewardAccounts<'a, 'info> {
+        /// ✅ 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>,
+        /// ✅ 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
+        /// ✅ 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<Reserve>,
+
+        _priv: (),
+    }
+
+    impl AddPoolRewardParams {
+        pub(super) fn new(
+            position_kind: PositionKind,
+            start_time_secs: u64,
+            end_time_secs: u64,
+            reward_token_amount: u64,
+        ) -> Result<Self, ProgramError> {
+            let clock = &Clock::get()?;
+
+            let start_time_secs = start_time_secs.max(clock.unix_timestamp as u64);
+
+            if start_time_secs <= end_time_secs {
+                msg!("Pool reward must end after it starts");
+                return Err(LendingError::MathOverflow.into());
+            }
+
+            let duration_secs: u32 = {
+                // SAFETY: just checked that start time is strictly smaller
+                let d = end_time_secs - start_time_secs;
+                d.try_into().map_err(|_| {
+                    msg!("Pool reward duration is too long");
+                    LendingError::MathOverflow
+                })?
+            };
+            if MIN_REWARD_PERIOD_SECS > duration_secs as u64 {
+                msg!("Pool reward duration must be at least {MIN_REWARD_PERIOD_SECS} secs");
+                return Err(LendingError::PoolRewardPeriodTooShort.into());
+            }
+
+            if reward_token_amount == 0 {
+                msg!("Pool reward amount must be greater than zero");
+                return Err(LendingError::InvalidAmount.into());
+            }
+
+            Ok(Self {
+                position_kind,
+                start_time_secs,
+                duration_secs,
+                reward_token_amount,
+
+                _priv: (),
+            })
+        }
+    }
+
+    impl<'a, 'info> AddPoolRewardAccounts<'a, 'info> {
+        pub(super) fn from_unchecked_iter(
+            program_id: &Pubkey,
+            params: &AddPoolRewardParams,
+            iter: &mut impl Iterator<Item = &'a AccountInfo<'info>>,
+        ) -> Result<AddPoolRewardAccounts<'a, 'info>, ProgramError> {
+            let reserve_info = next_account_info(iter)?;
+            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());
+            }
+
+            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,
+                reward_authority_info,
+                reward_token_vault_info,
+                lending_market_info,
+                lending_market_owner_info,
+                rent_info,
+                token_program_info,
+
+                reserve,
+
+                _priv: (),
+            })
+        }
+    }
+}
+
+mod cancel_pool_reward {
+    use super::*;
+
+    pub(super) struct CancelPoolRewardParams {
+        position_kind: PositionKind,
+        pool_reward_index: u64,
+
+        _priv: (),
+    }
+
+    /// Use [Self::from_unchecked_iter] to validate the accounts.
+    pub(super) struct CancelPoolRewardAccounts<'a, 'info> {
+        /// ✅ 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<Reserve>,
+
+        _priv: (),
+    }
+
+    impl<'a, 'info> CancelPoolRewardAccounts<'a, 'info> {
+        pub(super) fn from_unchecked_iter(
+            program_id: &Pubkey,
+            params: &CancelPoolRewardParams,
+            iter: &mut impl Iterator<Item = &'a AccountInfo<'info>>,
+        ) -> Result<CancelPoolRewardAccounts<'a, 'info>, 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 {
+                position_kind,
+                pool_reward_index,
+
+                _priv: (),
+            }
+        }
+    }
+}
+
+mod close_pool_reward {
+    use super::*;
+
+    pub(super) struct ClosePoolRewardParams {
+        position_kind: PositionKind,
+        pool_reward_index: u64,
+
+        _priv: (),
+    }
+
+    /// 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<Reserve>,
+    }
+
+    impl<'a, 'info> ClosePoolRewardAccounts<'a, 'info> {
+        pub(super) fn from_unchecked_iter(
+            program_id: &Pubkey,
+            params: &ClosePoolRewardParams,
+            iter: &mut impl Iterator<Item = &'a AccountInfo<'info>>,
+        ) -> Result<ClosePoolRewardAccounts<'a, 'info>, 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 {
+                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,
+
+                _priv: (),
+            })
+        }
+    }
+
+    impl ClosePoolRewardParams {
+        pub(super) fn new(position_kind: PositionKind, pool_reward_index: u64) -> Self {
+            Self {
+                position_kind,
+                pool_reward_index,
+
+                _priv: (),
+            }
+        }
+    }
+}
+
+/// 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<Box<Reserve>, 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..e5f6302199c 100644
--- a/token-lending/sdk/src/error.rs
+++ b/token-lending/sdk/src/error.rs
@@ -209,6 +209,14 @@ 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")]
+    PoolRewardPeriodTooShort,
+
+    // 60
+    /// Cannot close token account
+    #[error("Cannot close token account")]
+    CloseTokenAccountFailed,
 }
 
 impl From<LendingError> for ProgramError {
diff --git a/token-lending/sdk/src/instruction.rs b/token-lending/sdk/src/instruction.rs
index 4340458ad0f..de75c22a66e 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,91 @@ 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
+    ///         * Reward mint pubkey
+    ///    `[writable]` 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.
+    ///
+    ///    `[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,
+        /// 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.
+    ///
+    ///    `[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,
+        /// Identifies a reward within a reserve's deposits/borrows rewards.
+        pool_reward_index: u64,
+    },
 }
 
 impl LendingInstruction {
@@ -786,6 +871,34 @@ impl LendingInstruction {
                 let (liquidity_amount, _rest) = Self::unpack_u64(rest)?;
                 Self::DonateToReserve { liquidity_amount }
             }
+            25 => {
+                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)?;
+                Self::AddPoolReward {
+                    position_kind,
+                    start_time_secs,
+                    end_time_secs,
+                    token_amount,
+                }
+            }
+            26 => {
+                let (position_kind, rest) = Self::unpack_try_from_u8(rest)?;
+                let (pool_reward_index, _rest) = Self::unpack_u64(rest)?;
+                Self::ClosePoolReward {
+                    position_kind,
+                    pool_reward_index,
+                }
+            }
+            27 => {
+                let (position_kind, rest) = Self::unpack_try_from_u8(rest)?;
+                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());
@@ -835,6 +948,15 @@ impl LendingInstruction {
         Ok((value, rest))
     }
 
+    fn unpack_try_from_u8<T>(input: &[u8]) -> Result<(T, &[u8]), ProgramError>
+    where
+        T: TryFrom<u8>,
+        ProgramError: From<<T as TryFrom<u8>>::Error>,
+    {
+        let (byte, rest) = Self::unpack_u8(input)?;
+        Ok((T::try_from(byte)?, rest))
+    }
+
     fn unpack_bytes32(input: &[u8]) -> Result<(&[u8; 32], &[u8]), ProgramError> {
         if input.len() < 32 {
             msg!("32 bytes cannot be unpacked");
@@ -1085,6 +1207,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::*;
diff --git a/token-lending/sdk/src/state/obligation.rs b/token-lending/sdk/src/state/obligation.rs
index e17bcfa2f21..d4bbe50e5e8 100644
--- a/token-lending/sdk/src/state/obligation.rs
+++ b/token-lending/sdk/src/state/obligation.rs
@@ -656,6 +656,18 @@ impl Pack for Obligation {
     }
 }
 
+impl TryFrom<u8> for PositionKind {
+    type Error = ProgramError;
+
+    fn try_from(value: u8) -> Result<Self, Self::Error> {
+        match value {
+            0 => Ok(PositionKind::Deposit),
+            1 => Ok(PositionKind::Borrow),
+            _ => Err(LendingError::InstructionUnpackError.into()),
+        }
+    }
+}
+
 #[cfg(test)]
 mod test {
     use super::*;

From 2587270183b355f876c47067ef72edb4c5fb5388 Mon Sep 17 00:00:00 2001
From: vanity mnm <vanity@solend.fi>
Date: Fri, 14 Mar 2025 13:07:18 +0100
Subject: [PATCH 04/14] [Liquidity Mining] Math for reward accrual (3)  (#200)

---
 .../program/src/processor/liquidity_mining.rs |   5 +-
 .../sdk/src/state/liquidity_mining.rs         | 194 +++++++++++++++++-
 2 files changed, 192 insertions(+), 7 deletions(-)

diff --git a/token-lending/program/src/processor/liquidity_mining.rs b/token-lending/program/src/processor/liquidity_mining.rs
index 76defa6c5e6..ed8b274ed06 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..860c0b24e4f 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,158 @@ 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)?;
+
+            // 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))?;
+
+            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 CreatingNewUserRewardManager {
+    /// If we are creating a [UserRewardManager] then we want to populate it.
+    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: CreatingNewUserRewardManager,
+    ) -> Result<(), ProgramError> {
+        pool_reward_manager.update(clock)?;
+
+        let curr_unix_timestamp_secs = clock.unix_timestamp as u64;
+
+        if matches!(
+            creating_new_reward_manager,
+            CreatingNewUserRewardManager::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,
+                                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;
+                }
+                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 +300,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 389a7d835abd93ce37c7138c7489de689c845012 Mon Sep 17 00:00:00 2001
From: vanity mnm <vanity@solend.fi>
Date: Fri, 14 Mar 2025 13:10:09 +0100
Subject: [PATCH 05/14] [Liquidity Mining] Reserve packing & unpacking (4)
 (#202)

---
 .editorconfig                                 |    8 +
 .../workflows/pull-request-token-lending.yml  |   14 +-
 .github/workflows/pull-request.yml            |   14 +-
 .mocharc.yml                                  |    1 +
 Anchor.toml                                   |   27 +-
 Cargo.lock                                    |   25 +-
 Cargo.toml                                    |    8 +-
 package.json                                  |   20 +
 token-lending/cli/Cargo.toml                  |   10 +-
 token-lending/cli/src/main.rs                 |   43 +
 token-lending/program/Cargo.toml              |    4 +-
 token-lending/program/src/processor.rs        |    6 +
 .../program/src/processor/liquidity_mining.rs |  134 ++
 token-lending/sdk/Cargo.toml                  |   10 +-
 token-lending/sdk/src/instruction.rs          |   33 +
 .../sdk/src/state/liquidity_mining.rs         |  285 +++-
 token-lending/sdk/src/state/reserve.rs        |  267 ++--
 token-lending/tests/liquidity-mining.ts       |   66 +
 tsconfig.json                                 |   10 +
 yarn.lock                                     | 1181 +++++++++++++++++
 20 files changed, 1988 insertions(+), 178 deletions(-)
 create mode 100644 .editorconfig
 create mode 100644 .mocharc.yml
 create mode 100644 package.json
 create mode 100644 token-lending/tests/liquidity-mining.ts
 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/.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
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/Cargo.lock b/Cargo.lock
index 63624df360b..37fe074561d 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",
@@ -5419,7 +5412,7 @@ dependencies = [
 
 [[package]]
 name = "solend-program"
-version = "2.0.2"
+version = "2.1.0"
 dependencies = [
  "anchor-lang 0.28.0",
  "assert_matches",
@@ -5448,7 +5441,7 @@ dependencies = [
 
 [[package]]
 name = "solend-program-cli"
-version = "2.0.2"
+version = "2.1.0"
 dependencies = [
  "bincode",
  "clap 2.34.0",
@@ -5469,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 7daa3e7968f..829068fcb4e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -3,8 +3,12 @@ members = [
   "token-lending/cli",
   "token-lending/program",
   "token-lending/sdk",
-  "token-lending/brick"
-, "token-lending/oracles"]
+  "token-lending/brick",
+  "token-lending/oracles",
+]
+
+[workspace.package]
+version = "2.1.0"
 
 [profile.dev]
 split-debuginfo = "unpacked"
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/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 <maintainers@solend.fi>"]
 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 db0658a0068..8c30b314b91 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,
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 <maintainers@solend.fi>"]
 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.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..1c802e5bdfd 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::{
@@ -37,6 +39,7 @@ use solend_sdk::{
 };
 use spl_token::state::Account as TokenAccount;
 use std::convert::TryInto;
+use upgrade_reserve::UpgradeReserveAccounts;
 
 /// # Accounts
 ///
@@ -217,6 +220,75 @@ 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.
+///
+/// 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
+/// 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 let Some(extra_rent) = new_rent.checked_sub(current_rent) {
+        // some reserves have more rent than necessary, let's not assume that
+        // the payer always needs to add more rent
+
+        invoke(
+            &system_instruction::transfer(
+                accounts.payer.key,
+                accounts.reserve_info.key,
+                extra_rent,
+            ),
+            &[
+                accounts.payer.clone(),
+                accounts.reserve_info.clone(),
+                accounts.system_program.clone(),
+            ],
+        )?;
+    }
+
+    //
+    // 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 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())?;
+
+    Ok(())
+}
+
 /// Unpacks a spl_token [TokenAccount].
 fn unpack_token_account(data: &[u8]) -> Result<TokenAccount, LendingError> {
     TokenAccount::unpack(data).map_err(|_| LendingError::InvalidTokenAccount)
@@ -645,6 +717,68 @@ mod close_pool_reward {
     }
 }
 
+mod upgrade_reserve {
+    use solend_sdk::state::RESERVE_LEN_V2_0_2;
+
+    use super::*;
+
+    pub(super) struct UpgradeReserveAccounts<'a, '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: (),
+    }
+
+    impl<'a, 'info> UpgradeReserveAccounts<'a, 'info> {
+        pub(super) fn from_unchecked_iter(
+            program_id: &Pubkey,
+            iter: &mut impl Iterator<Item = &'a AccountInfo<'info>>,
+        ) -> Result<UpgradeReserveAccounts<'a, 'info>, ProgramError> {
+            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");
+                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());
+            }
+
+            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: (),
+            })
+        }
+    }
+}
+
 /// Common checks within the admin ixs are:
 ///
 /// * ✅ `reserve_info` belongs to this program
diff --git a/token-lending/sdk/Cargo.toml b/token-lending/sdk/Cargo.toml
index f969a318766..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 <maintainers@solend.fi>"]
 repository = "https://github.com/solendprotocol/solana-program-library"
@@ -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/instruction.rs b/token-lending/sdk/src/instruction.rs
index de75c22a66e..5302817f46b 100644
--- a/token-lending/sdk/src/instruction.rs
+++ b/token-lending/sdk/src/instruction.rs
@@ -613,6 +613,18 @@ 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.
+    ///
+    ///    `[writable]` Reserve account.
+    ///    `[writable, signer]` Fee payer.
+    ///    `[]` System program.
+    UpgradeReserveToV2_1_0,
 }
 
 impl LendingInstruction {
@@ -899,6 +911,7 @@ impl LendingInstruction {
                     pool_reward_index,
                 }
             }
+            255 => Self::UpgradeReserveToV2_1_0,
             _ => {
                 msg!("Instruction cannot be unpacked");
                 return Err(LendingError::InstructionUnpackError.into());
@@ -1235,6 +1248,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
     }
@@ -2047,6 +2063,23 @@ pub fn donate_to_reserve(
     }
 }
 
+/// Creates a `UpgradeReserveToV2_1_0` instruction.
+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 860c0b24e4f..5fdbfe07b74 100644
--- a/token-lending/sdk/src/state/liquidity_mining.rs
+++ b/token-lending/sdk/src/state/liquidity_mining.rs
@@ -1,19 +1,28 @@
 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, PUBKEY_BYTES},
 };
-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;
+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 = 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 +55,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 {
@@ -53,10 +63,22 @@ 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<PoolReward>),
 }
 
 /// 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,16 +99,13 @@ 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
     /// `unlocked_rewards = (total_rewards * time_passed) / (total_time)`
+    ///
+    /// # (Un)Packing
+    /// We only store 16 most significant digits.
     pub cumulative_rewards_per_share: Decimal,
 }
 
@@ -173,8 +192,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,24 +297,222 @@ 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::<Self>();
+}
+
+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 PoolRewardManager {
+    #[inline(never)]
+    pub(crate) fn unpack_to_box(input: &[u8]) -> Result<Box<Self>, ProgramError> {
+        Ok(Box::new(PoolRewardManager::unpack_from_slice(input)?))
+    }
+}
+
+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(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,
+                ),
+            };
+
+            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,
+            );
+        }
+    }
+
+    #[inline(never)]
+    fn unpack_from_slice(input: &[u8]) -> Result<Self, ProgramError> {
+        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(Box::new(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)]
 mod tests {
     //! TODO: Rewrite these tests from their Suilend counterparts.
     //! TODO: Calculate test coverage and add tests for missing branches.
 
     use super::*;
+    use proptest::prelude::*;
+    use rand::Rng;
+
+    fn pool_reward_manager_strategy() -> impl Strategy<Value = PoolRewardManager> {
+        (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());
+
+        // 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 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::<PoolReward>();
-
-        println!("assert {required_realloc} <= {MAX_REALLOC}");
+        let required_realloc = size_of_discriminant * PoolRewardManager::LEN;
         assert!(required_realloc <= MAX_REALLOC);
     }
 
@@ -330,4 +545,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(Box::new(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 2e53ba3e19d..cea55c0e21f 100644
--- a/token-lending/sdk/src/state/reserve.rs
+++ b/token-lending/sdk/src/state/reserve.rs
@@ -60,6 +60,20 @@ pub struct Reserve {
     pub rate_limiter: RateLimiter,
     /// Attributed borrows in USD
     pub attributed_borrow_value: Decimal,
+    /// Contains liquidity mining rewards for borrows.
+    ///
+    /// Added @v2.1.0
+    ///
+    /// TODO: measure compute units for packing/unpacking and if significant
+    ///       then consider packing/unpacking on demand
+    pub borrows_pool_reward_manager: Box<PoolRewardManager>,
+    /// Contains liquidity mining rewards for deposits.
+    ///
+    /// Added @v2.1.0
+    ///
+    /// TODO: measure compute units for packing/unpacking and if significant
+    ///       then consider packing/unpacking on demand
+    pub deposits_pool_reward_manager: Box<PoolRewardManager>,
 }
 
 impl Reserve {
@@ -1229,14 +1243,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
+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;
+
 impl Pack for Reserve {
-    const LEN: usize = RESERVE_LEN_V1;
+    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_V1];
+        let output = array_mut_ref![output, 0, Reserve::LEN];
         #[allow(clippy::ptr_offset_with_cast)]
         let (
             version,
@@ -1286,7 +1304,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,
@@ -1336,7 +1356,9 @@ impl Pack for Reserve {
             16,
             8,
             8,
-            49
+            49,
+            PoolRewardManager::LEN,
+            PoolRewardManager::LEN
         ];
 
         // reserve
@@ -1422,14 +1444,23 @@ 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].
-    // @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<Self, ProgramError> {
-        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 +1512,7 @@ impl Pack for Reserve {
             config_attributed_borrow_limit_close,
             _padding,
         ) = array_refs![
-            input,
+            input_v2_0_2,
             1,
             8,
             1,
@@ -1553,110 +1584,128 @@ 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),
             },
-            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),
+            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))
             },
-            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
+            // 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 {
-                    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
+                }
             },
+            attributed_borrow_limit_close: {
+                let value = u64::from_le_bytes(*config_attributed_borrow_limit_close);
+                if value == 0 {
+                    u64::MAX
+                } else {
+                    value
+                }
+            },
+        };
+
+        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,
+            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,
+            deposits_pool_reward_manager,
         })
     }
 }
@@ -1750,6 +1799,8 @@ 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)),
             };
 
             let mut packed = [0u8; Reserve::LEN];
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<number>((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/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 ea05dd3490ff992350c5782b74f2acc49fff5433 Mon Sep 17 00:00:00 2001
From: vanity mnm <vanity@solend.fi>
Date: Fri, 21 Mar 2025 13:34:37 +0100
Subject: [PATCH 06/14] [Liquidity Mining] Obligation packing & unpacking (5)
 (#204)

* optimized reserve (un)packing

* obligation dynamically (un)packs

* fixed BPF tests

*  using account discriminator
---
 .gitignore                                    |   1 +
 token-lending/cli/src/main.rs                 |   1 -
 token-lending/program/src/processor.rs        |   2 +-
 .../program/src/processor/liquidity_mining.rs |  20 +-
 .../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      | 106 ++--
 .../program/tests/init_lending_market.rs      |   4 +-
 .../program/tests/init_obligation.rs          |   7 +-
 token-lending/program/tests/init_reserve.rs   |  15 +-
 .../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       |  34 +-
 .../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/error.rs                |   6 +
 token-lending/sdk/src/state/lending_market.rs | 105 ++--
 .../sdk/src/state/liquidity_mining.rs         | 475 +++++++++++++-----
 token-lending/sdk/src/state/mod.rs            |  68 ++-
 token-lending/sdk/src/state/obligation.rs     | 210 ++++++--
 token-lending/sdk/src/state/reserve.rs        |  83 ++-
 31 files changed, 909 insertions(+), 311 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/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.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>(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/program/src/processor/liquidity_mining.rs b/token-lending/program/src/processor/liquidity_mining.rs
index 1c802e5bdfd..96d1b7a0849 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
 
@@ -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.
     //
 
-    // 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())?;
+    // 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/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::<Obligation>(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::<Obligation>(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::<Obligation>(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>(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>(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>(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>(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>(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>(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>(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>(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>(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::<Obligation>(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<T: Pack>(
         &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<T: Pack>(
-        &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..b7979450c98 100644
--- a/token-lending/program/tests/helpers/solend_program_test.rs
+++ b/token-lending/program/tests/helpers/solend_program_test.rs
@@ -57,6 +57,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,
@@ -254,6 +273,21 @@ impl SolendProgramTest {
         }
     }
 
+    pub async fn load_obligation(&mut self, acc_pk: Pubkey) -> Info<Obligation> {
+        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<T: Pod + Copy>(&mut self, acc_pk: Pubkey) -> Info<T> {
         let acc = self
             .context
@@ -656,7 +690,7 @@ impl SolendProgramTest {
         let res = self
             .process_transaction(
                 &[
-                    ComputeBudgetInstruction::set_compute_unit_limit(80_000),
+                    ComputeBudgetInstruction::set_compute_unit_limit(cu_budgets::INIT_RESERVE),
                     init_reserve(
                         solend_program::id(),
                         liquidity_amount,
@@ -842,7 +876,7 @@ impl Info<LendingMarket> {
         liquidity_amount: u64,
     ) -> Result<(), BanksClientError> {
         let instructions = [
-            ComputeBudgetInstruction::set_compute_unit_limit(50_000),
+            ComputeBudgetInstruction::set_compute_unit_limit(cu_budgets::DEPOSIT),
             deposit_reserve_liquidity(
                 solend_program::id(),
                 liquidity_amount,
@@ -870,7 +904,7 @@ impl Info<LendingMarket> {
         liquidity_amount: u64,
     ) -> Result<(), BanksClientError> {
         let instructions = [
-            ComputeBudgetInstruction::set_compute_unit_limit(50_000),
+            ComputeBudgetInstruction::set_compute_unit_limit(cu_budgets::DONATE_TO_RESERVE),
             donate_to_reserve(
                 solend_program::id(),
                 liquidity_amount,
@@ -904,7 +938,7 @@ impl Info<LendingMarket> {
         let oracle = oracle.unwrap_or(&default_oracle);
 
         let instructions = [
-            ComputeBudgetInstruction::set_compute_unit_limit(30_000),
+            ComputeBudgetInstruction::set_compute_unit_limit(cu_budgets::UPDATE_RESERVE_CONFIG),
             update_reserve_config(
                 solend_program::id(),
                 config,
@@ -931,7 +965,9 @@ impl Info<LendingMarket> {
         liquidity_amount: u64,
     ) -> Result<(), BanksClientError> {
         let instructions = [
-            ComputeBudgetInstruction::set_compute_unit_limit(70_000),
+            ComputeBudgetInstruction::set_compute_unit_limit(
+                cu_budgets::DEPOSIT_RESERVE_LIQUIDITY_AND_OBLIGATION_COLLATERAL,
+            ),
             deposit_reserve_liquidity_and_obligation_collateral(
                 solend_program::id(),
                 liquidity_amount,
@@ -964,7 +1000,7 @@ impl Info<LendingMarket> {
         collateral_amount: u64,
     ) -> Result<(), BanksClientError> {
         let instructions = [
-            ComputeBudgetInstruction::set_compute_unit_limit(58_000),
+            ComputeBudgetInstruction::set_compute_unit_limit(cu_budgets::REDEEM),
             refresh_reserve(
                 solend_program::id(),
                 reserve.pubkey,
@@ -998,12 +1034,12 @@ impl Info<LendingMarket> {
         user: &User,
     ) -> Result<Info<Obligation>, BanksClientError> {
         let instructions = [
-            ComputeBudgetInstruction::set_compute_unit_limit(10_000),
+            ComputeBudgetInstruction::set_compute_unit_limit(cu_budgets::INIT_OBLIGATION),
             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 +1054,7 @@ impl Info<LendingMarket> {
             .process_transaction(&instructions, Some(&[&obligation_keypair, &user.keypair]))
             .await
         {
-            Ok(()) => Ok(test
-                .load_account::<Obligation>(obligation_keypair.pubkey())
-                .await),
+            Ok(()) => Ok(test.load_obligation(obligation_keypair.pubkey()).await),
             Err(e) => Err(e),
         }
     }
@@ -1034,7 +1068,9 @@ impl Info<LendingMarket> {
         collateral_amount: u64,
     ) -> Result<(), BanksClientError> {
         let instructions = [
-            ComputeBudgetInstruction::set_compute_unit_limit(38_000),
+            ComputeBudgetInstruction::set_compute_unit_limit(
+                cu_budgets::DEPOSIT_OBLIGATION_COLLATERAL,
+            ),
             deposit_obligation_collateral(
                 solend_program::id(),
                 collateral_amount,
@@ -1060,7 +1096,7 @@ impl Info<LendingMarket> {
     ) -> Result<(), BanksClientError> {
         test.process_transaction(
             &[
-                ComputeBudgetInstruction::set_compute_unit_limit(2_000_000),
+                ComputeBudgetInstruction::set_compute_unit_limit(cu_budgets::REFRESH_RESERVE),
                 refresh_reserve(
                     solend_program::id(),
                     reserve.pubkey,
@@ -1080,7 +1116,7 @@ impl Info<LendingMarket> {
         obligation: &Info<Obligation>,
         extra_reserve: Option<&Info<Reserve>>,
     ) -> Vec<Instruction> {
-        let obligation = test.load_account::<Obligation>(obligation.pubkey).await;
+        let obligation = test.load_obligation(obligation.pubkey).await;
         let reserve_pubkeys: Vec<Pubkey> = {
             let mut r = HashSet::new();
             r.extend(
@@ -1159,7 +1195,9 @@ impl Info<LendingMarket> {
             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(
+            cu_budgets::REFRESH_OBLIGATION,
+        )];
         instructions.push(refresh_reserve_instructions.last().unwrap().clone());
 
         test.process_transaction(&instructions, None).await
@@ -1174,14 +1212,16 @@ impl Info<LendingMarket> {
         host_fee_receiver_pubkey: Option<Pubkey>,
         liquidity_amount: u64,
     ) -> Result<(), BanksClientError> {
-        let obligation = test.load_account::<Obligation>(obligation.pubkey).await;
+        let obligation = test.load_obligation(obligation.pubkey).await;
 
         let refresh_ixs = self
             .build_refresh_instructions(test, &obligation, Some(borrow_reserve))
             .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(
+            cu_budgets::BORROW_OBLIGATION_LIQUIDITY,
+        )];
         instructions.push(borrow_obligation_liquidity(
             solend_program::id(),
             liquidity_amount,
@@ -1215,7 +1255,9 @@ impl Info<LendingMarket> {
         liquidity_amount: u64,
     ) -> Result<(), BanksClientError> {
         let instructions = [
-            ComputeBudgetInstruction::set_compute_unit_limit(35_000),
+            ComputeBudgetInstruction::set_compute_unit_limit(
+                cu_budgets::REPAY_OBLIGATION_LIQUIDITY,
+            ),
             repay_obligation_liquidity(
                 solend_program::id(),
                 liquidity_amount,
@@ -1239,7 +1281,7 @@ impl Info<LendingMarket> {
         reserve: &Info<Reserve>,
     ) -> Result<(), BanksClientError> {
         let instructions = [
-            ComputeBudgetInstruction::set_compute_unit_limit(50_000),
+            ComputeBudgetInstruction::set_compute_unit_limit(cu_budgets::REDEEM_FEES),
             refresh_reserve(
                 solend_program::id(),
                 reserve.pubkey,
@@ -1275,7 +1317,9 @@ impl Info<LendingMarket> {
 
         test.process_transaction(
             &[
-                ComputeBudgetInstruction::set_compute_unit_limit(110_000),
+                ComputeBudgetInstruction::set_compute_unit_limit(
+                    cu_budgets::LIQUIDATE_OBLIGATION_AND_REDEEM_RESERVE_COLLATERAL,
+                ),
                 liquidate_obligation_and_redeem_reserve_collateral(
                     solend_program::id(),
                     liquidity_amount,
@@ -1343,7 +1387,7 @@ impl Info<LendingMarket> {
         user: &User,
         collateral_amount: u64,
     ) -> Result<(), BanksClientError> {
-        let obligation = test.load_account::<Obligation>(obligation.pubkey).await;
+        let obligation = test.load_obligation(obligation.pubkey).await;
 
         let refresh_ixs = self
             .build_refresh_instructions(test, &obligation, None)
@@ -1352,7 +1396,9 @@ impl Info<LendingMarket> {
 
         test.process_transaction(
             &[
-                ComputeBudgetInstruction::set_compute_unit_limit(110_000),
+                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,
@@ -1396,7 +1442,9 @@ impl Info<LendingMarket> {
 
         test.process_transaction(
             &[
-                ComputeBudgetInstruction::set_compute_unit_limit(100_000),
+                ComputeBudgetInstruction::set_compute_unit_limit(
+                    cu_budgets::WITHDRAW_OBLIGATION_COLLATERAL,
+                ),
                 withdraw_obligation_collateral(
                     solend_program::id(),
                     collateral_amount,
@@ -1451,7 +1499,7 @@ impl Info<LendingMarket> {
         reserve: &Info<Reserve>,
         liquidity_amount: u64,
     ) -> Result<(), BanksClientError> {
-        let obligation = test.load_account::<Obligation>(obligation.pubkey).await;
+        let obligation = test.load_obligation(obligation.pubkey).await;
 
         let mut instructions = self
             .build_refresh_instructions(test, &obligation, None)
@@ -1815,7 +1863,7 @@ pub async fn scenario_1(
         .unwrap();
 
     // borrow 10 SOL against 100k cUSDC.
-    let obligation = test.load_account::<Obligation>(obligation.pubkey).await;
+    let obligation = test.load_obligation(obligation.pubkey).await;
     lending_market
         .borrow_obligation_liquidity(
             &mut test,
@@ -1840,7 +1888,7 @@ pub async fn scenario_1(
         .unwrap();
 
     // populate deposit value correctly.
-    let obligation = test.load_account::<Obligation>(obligation.pubkey).await;
+    let obligation = test.load_obligation(obligation.pubkey).await;
     lending_market
         .refresh_obligation(&mut test, &obligation)
         .await
@@ -1849,7 +1897,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>(obligation.pubkey).await;
+    let obligation = test.load_obligation(obligation.pubkey).await;
 
     (
         test,
@@ -2021,7 +2069,7 @@ pub async fn custom_scenario(
             .await
             .unwrap();
 
-        *obligation = test.load_account::<Obligation>(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 7549463dc9b..1443ccc01b3 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::{discriminator::AccountDiscriminator, LendingMarket, RateLimiter};
 
 #[tokio::test]
 async fn test_success() {
@@ -28,7 +28,7 @@ async fn test_success() {
     assert_eq!(
         lending_market.account,
         LendingMarket {
-            version: PROGRAM_VERSION,
+            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 943f5768d6a..1747113fa15 100644
--- a/token-lending/program/tests/init_obligation.rs
+++ b/token-lending/program/tests/init_obligation.rs
@@ -13,7 +13,9 @@ 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::{
+    discriminator::AccountDiscriminator, LastUpdate, LendingMarket, Obligation,
+};
 
 async fn setup() -> (SolendProgramTest, Info<LendingMarket>, User) {
     let (test, lending_market, _, _, _, user) =
@@ -34,7 +36,7 @@ async fn test_success() {
     assert_eq!(
         obligation.account,
         Obligation {
-            version: PROGRAM_VERSION,
+            discriminator: AccountDiscriminator::Obligation,
             last_update: LastUpdate {
                 slot: 1000,
                 stale: true
@@ -52,6 +54,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 a66f8d59209..62489668912 100644
--- a/token-lending/program/tests/init_reserve.rs
+++ b/token-lending/program/tests/init_reserve.rs
@@ -25,22 +25,19 @@ 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;
 use solend_program::NULL_PUBKEY;
 
+use solend_program::state::{
+    discriminator::AccountDiscriminator, 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<LendingMarket>, User) {
@@ -154,7 +151,7 @@ async fn test_success() {
     assert_eq!(
         wsol_reserve.account,
         Reserve {
-            version: PROGRAM_VERSION,
+            discriminator: AccountDiscriminator::Reserve,
             last_update: LastUpdate {
                 slot: 1001,
                 stale: true
@@ -182,6 +179,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/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::<Obligation>(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::<Obligation>(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>(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>(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::<Obligation>(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>(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>(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 b16a0f3a519..6c3397a897f 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;
 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, 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>(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>(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>(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>(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::<Obligation>(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::<Obligation>(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::<Obligation>(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,7 @@ async fn test_normalize_obligation() {
     );
 
     let reserve_1 = Reserve {
-        version: PROGRAM_VERSION,
+        discriminator: AccountDiscriminator::Reserve,
         last_update: LastUpdate {
             slot: 1,
             stale: false,
@@ -496,7 +498,7 @@ async fn test_normalize_obligation() {
     );
 
     let reserve_2 = Reserve {
-        version: PROGRAM_VERSION,
+        discriminator: AccountDiscriminator::Reserve,
         last_update: LastUpdate {
             slot: 1,
             stale: false,
@@ -514,7 +516,7 @@ async fn test_normalize_obligation() {
 
     let obligation_pubkey = Pubkey::new_unique();
     let obligation = Obligation {
-        version: PROGRAM_VERSION,
+        discriminator: AccountDiscriminator::Obligation,
         deposits: vec![
             ObligationCollateral {
                 deposit_reserve: reserve_1_pubkey,
@@ -542,10 +544,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 +567,7 @@ async fn test_normalize_obligation() {
     )];
     test.process_transaction(&ix, None).await.unwrap();
 
-    let o = test.load_account::<Obligation>(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>(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>(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>(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>(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::<Reserve>(usdc_reserve.pubkey).await;
     assert_eq!(usdc_reserve_post.account, usdc_reserve.account);
 
-    let obligation_post = test.load_account::<Obligation>(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::<Reserve>(usdc_reserve.pubkey).await;
     assert_eq!(usdc_reserve_post.account, usdc_reserve.account);
 
-    let obligation_post = test.load_account::<Obligation>(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>(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/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<LendingError> for ProgramError {
diff --git a/token-lending/sdk/src/state/lending_market.rs b/token-lending/sdk/src/state/lending_market.rs
index 1836dc76e2d..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,8 +14,13 @@ use solana_program::{
 /// Lending market state
 #[derive(Clone, Debug, Default, PartialEq, Eq)]
 pub struct LendingMarket {
-    /// Version of lending market
-    pub version: u8,
+    /// 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 [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
@@ -43,7 +52,7 @@ impl LendingMarket {
 
     /// Initialize a lending market
     pub fn init(&mut self, params: InitLendingMarketParams) {
-        self.version = PROGRAM_VERSION;
+        self.discriminator = AccountDiscriminator::LendingMarket;
         self.bump_seed = params.bump_seed;
         self.owner = params.owner;
         self.quote_currency = params.quote_currency;
@@ -76,7 +85,7 @@ pub struct InitLendingMarketParams {
 impl Sealed for LendingMarket {}
 impl IsInitialized for LendingMarket {
     fn is_initialized(&self) -> bool {
-        self.version != UNINITIALIZED_VERSION
+        !matches!(self.discriminator, AccountDiscriminator::Uninitialized)
     }
 }
 
@@ -88,7 +97,7 @@ impl Pack for LendingMarket {
         let output = array_mut_ref![output, 0, LENDING_MARKET_LEN];
         #[allow(clippy::ptr_offset_with_cast)]
         let (
-            version,
+            discriminator,
             bump_seed,
             owner,
             quote_currency,
@@ -114,7 +123,7 @@ impl Pack for LendingMarket {
             8
         ];
 
-        *version = self.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());
@@ -138,7 +147,7 @@ impl Pack for LendingMarket {
         let input = array_ref![input, 0, LENDING_MARKET_LEN];
         #[allow(clippy::ptr_offset_with_cast)]
         let (
-            version,
+            discriminator,
             bump_seed,
             owner,
             quote_currency,
@@ -164,15 +173,30 @@ 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);
-        }
+        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(LendingError::InvalidAccountDiscriminator.into());
+            }
+            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 {
-            version,
+            discriminator,
             bump_seed: u8::from_le_bytes(*bump_seed),
             owner: owner_pubkey,
             quote_currency: *quote_currency,
@@ -202,29 +226,50 @@ mod test {
     use super::*;
     use rand::Rng;
 
+    impl LendingMarket {
+        fn new_rand(rng: &mut impl Rng) -> Self {
+            Self {
+                discriminator: AccountDiscriminator::LendingMarket,
+                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 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
         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 5fdbfe07b74..fc59369ccb5 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;
@@ -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.
     ///
@@ -110,9 +113,10 @@ pub struct PoolReward {
 }
 
 /// Tracks user's LM rewards for a specific pool (reserve.)
+#[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 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 +134,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<Option<UserReward>>,
+    /// 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<UserReward>,
 }
 
 /// Track user rewards for a specific [PoolReward].
+#[derive(Debug, PartialEq, Eq, Default, Clone)]
 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 +249,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
+
+                    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)?;
 
-            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
+                    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 +315,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;
-                }
             }
         }
 
@@ -298,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 {
@@ -319,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,
         }
     }
 }
@@ -340,70 +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());
-            *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,
-            );
         }
     }
 
@@ -417,36 +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,
-                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,
@@ -465,6 +502,148 @@ 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]
+    /// - 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.
+    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 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];
+
+        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
+            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(
+            &({
+                debug_assert!(MAX_REWARDS >= self.rewards.len());
+                debug_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;
+            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<Self, ProgramError> {
+        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
+            1  // length of rewards array that's next to come
+        ];
+
+        let reserve = Pubkey::new_from_array(*src_reserve);
+        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);
+
+        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.
@@ -475,21 +654,60 @@ mod tests {
     use rand::Rng;
 
     fn pool_reward_manager_strategy() -> impl Strategy<Value = PoolRewardManager> {
-        (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<Value = UserRewardManager> {
+        (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);
+
+            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]
+        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_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];
         let unpacked = PoolRewardManager::unpack_from_slice(&packed).unwrap();
         assert_eq!(unpacked, PoolRewardManager::default());
@@ -499,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,
                 }
             )
         });
@@ -556,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 {
@@ -573,4 +793,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(),
+            }
+        }
+    }
 }
diff --git a/token-lending/sdk/src/state/mod.rs b/token-lending/sdk/src/state/mod.rs
index c18c9793dfa..2e3dda4b3fc 100644
--- a/token-lending/sdk/src/state/mod.rs
+++ b/token-lending/sdk/src/state/mod.rs
@@ -17,6 +17,7 @@ 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 +25,70 @@ 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;
 
+/// Unmigrated accounts have this as their leading byte.
+pub const PROGRAM_VERSION_2_0_2: u8 = 1;
+
+pub mod discriminator {
+    //! First 1 byte determines the account kind.
+
+    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 = 2,
+        /// [crate::state::Reserve]
+        Reserve = 3,
+        /// [crate::state::Obligation]
+        Obligation = 4,
+    }
+
+    impl TryFrom<u8> for AccountDiscriminator {
+        type Error = LendingError;
+
+        fn try_from(value: u8) -> Result<Self, Self::Error> {
+            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),
+            }
+        }
+    }
+
+    impl TryFrom<&[u8; 1]> for AccountDiscriminator {
+        type Error = LendingError;
+
+        fn try_from(value: &[u8; 1]) -> Result<Self, Self::Error> {
+            Self::try_from(value[0])
+        }
+    }
+}
+
 // 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 d4bbe50e5e8..8696853932b 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,10 +21,22 @@ 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
-    pub version: u8,
+    /// 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 [PROGRAM_VERSION_2_0_2].
+    pub discriminator: AccountDiscriminator,
     /// Last update to collateral, liquidity, or their market values
     pub last_update: LastUpdate,
     /// Lending market address
@@ -63,6 +75,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<UserRewardManager>,
 }
 
 /// These are the two foundational user interactions in a borrow-lending protocol.
@@ -84,7 +102,7 @@ impl Obligation {
 
     /// Initialize an obligation
     pub fn init(&mut self, params: InitObligationParams) {
-        self.version = PROGRAM_VERSION;
+        self.discriminator = AccountDiscriminator::Obligation;
         self.last_update = LastUpdate::new(params.current_slot);
         self.lending_market = params.lending_market;
         self.owner = params.owner;
@@ -314,7 +332,7 @@ pub struct InitObligationParams {
 impl Sealed for Obligation {}
 impl IsInitialized for Obligation {
     fn is_initialized(&self) -> bool {
-        self.version != UNINITIALIZED_VERSION
+        !matches!(self.discriminator, AccountDiscriminator::Uninitialized)
     }
 }
 
@@ -426,15 +444,72 @@ 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
+    pub 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;
+
+    /// 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<Self, ProgramError> {
+        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<Self, ProgramError> {
+        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<Self, ProgramError> {
+        if !(Self::MIN_LEN..=Self::MAX_LEN).contains(&input.len()) {
+            return Err(ProgramError::InvalidAccountData);
+        }
+        Self::unpack_from_slice(input)
+    }
 
-    // @v2.1.0 TODO: pack vec of user reward managers
-    fn pack_into_slice(&self, dst: &mut [u8]) {
+    /// Pack into slice
+    pub fn pack(src: Self, dst: &mut [u8]) -> Result<(), ProgramError> {
+        if !(Self::MIN_LEN..=Self::MAX_LEN).contains(&dst.len()) {
+            return Err(ProgramError::InvalidAccountData);
+        }
+        src.pack_into_slice(dst);
+        Ok(())
+    }
+
+    /// Since @v2.1.0 we pack vec of user reward managers
+    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 (
-            version,
+            discriminator,
             last_update_slot,
             last_update_stale,
             lending_market,
@@ -475,7 +550,7 @@ impl Pack for Obligation {
         ];
 
         // obligation
-        *version = self.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());
@@ -536,15 +611,37 @@ impl Pack for 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
-    fn unpack_from_slice(src: &[u8]) -> Result<Self, ProgramError> {
+    /// Since @v2.1.0 we unpack vector of user reward managers
+    pub fn unpack_from_slice(src: &[u8]) -> Result<Self, ProgramError> {
         let input = array_ref![src, 0, OBLIGATION_LEN_V1];
         #[allow(clippy::ptr_offset_with_cast)]
         let (
-            version,
+            discriminator,
             last_update_slot,
             last_update_stale,
             lending_market,
@@ -584,11 +681,21 @@ impl Pack for 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);
-        }
+        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(LendingError::InvalidAccountDiscriminator.into());
+            }
+            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);
         let borrows_len = u8::from_le_bytes(*borrows_len);
@@ -633,8 +740,24 @@ impl Pack for Obligation {
             offset += OBLIGATION_LIQUIDITY_LEN;
         }
 
+        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,
             last_update: LastUpdate {
                 slot: u64::from_le_bytes(*last_update_slot),
                 stale: unpack_bool(last_update_stale)?,
@@ -652,6 +775,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,
         })
     }
 }
@@ -682,12 +806,10 @@ 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: AccountDiscriminator::Obligation,
                 last_update: LastUpdate {
                     slot: rng.gen(),
                     stale: rng.gen(),
@@ -715,15 +837,47 @@ 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(rng))
+                        .take(user_reward_managers_len)
+                        .collect()
+                },
+            }
+        }
+    }
 
-            let mut packed = [0u8; OBLIGATION_LEN_V1];
+    #[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::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 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
+            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..c447e9b03a2 100644
--- a/token-lending/sdk/src/state/reserve.rs
+++ b/token-lending/sdk/src/state/reserve.rs
@@ -44,8 +44,13 @@ 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 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 [PROGRAM_VERSION_2_0_2].
+    pub discriminator: AccountDiscriminator,
     /// Last slot when supply and rates updated
     pub last_update: LastUpdate,
     /// Lending market address
@@ -86,7 +91,7 @@ impl Reserve {
 
     /// Initialize a reserve
     pub fn init(&mut self, params: InitReserveParams) {
-        self.version = PROGRAM_VERSION;
+        self.discriminator = AccountDiscriminator::Reserve;
         self.last_update = LastUpdate::new(params.current_slot);
         self.lending_market = params.lending_market;
         self.liquidity = params.liquidity;
@@ -1238,7 +1243,7 @@ pub enum FeeCalculation {
 impl Sealed for Reserve {}
 impl IsInitialized for Reserve {
     fn is_initialized(&self) -> bool {
-        self.version != UNINITIALIZED_VERSION
+        !matches!(self.discriminator, AccountDiscriminator::Uninitialized)
     }
 }
 
@@ -1252,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 (
-            version,
+            discriminator,
             last_update_slot,
             last_update_stale,
             lending_market,
@@ -1304,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![
@@ -1362,7 +1366,7 @@ impl Pack for Reserve {
         ];
 
         // reserve
-        *version = self.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());
@@ -1463,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 (
-            version,
+            discriminator,
             last_update_slot,
             last_update_stale,
             lending_market,
@@ -1563,11 +1567,19 @@ 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);
-        }
+        // 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(LendingError::InvalidAccountDiscriminator.into());
+            }
+            Err(e) => return Err(e.into()),
+        };
 
         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 +1708,7 @@ impl Pack for Reserve {
             PoolRewardManager::unpack_to_box(input_for_deposits_pool_reward_manager)?;
 
         Ok(Self {
-            version,
+            discriminator,
             last_update,
             lending_market: Pubkey::new_from_array(*lending_market),
             liquidity,
@@ -1724,10 +1736,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 +1752,8 @@ mod test {
                 None
             };
 
-            let reserve = Reserve {
-                version: PROGRAM_VERSION,
+            Self {
+                discriminator: AccountDiscriminator::Reserve,
                 last_update: LastUpdate {
                     slot: rng.gen(),
                     stale: rng.gen(),
@@ -1799,9 +1809,17 @@ 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();
@@ -1810,6 +1828,23 @@ mod test {
         }
     }
 
+    #[test]
+    fn pack_and_unpack_reserve_v2_0_2() {
+        let mut rng = rand::thread_rng();
+        let reserve = Reserve::new_rand(&mut rng);
+
+        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;
 
     fn utilizations() -> impl Strategy<Value = (u8, u8)> {

From 17ac78400e0e48c7dc0e2ee90797468fa4a87bdc Mon Sep 17 00:00:00 2001
From: vanity mnm <vanity@solend.fi>
Date: Thu, 27 Mar 2025 10:35:11 +0100
Subject: [PATCH 07/14] [Liquidity Mining] Adding claim ix & connecting all ixs
 to account logic (6) (#205)

---
 token-lending/program/src/processor.rs        |  12 +-
 .../program/src/processor/liquidity_mining.rs | 799 ++----------------
 .../liquidity_mining/add_pool_reward.rs       | 266 ++++++
 .../liquidity_mining/cancel_pool_reward.rs    | 168 ++++
 .../liquidity_mining/claim_user_reward.rs     | 235 ++++++
 .../liquidity_mining/close_pool_reward.rs     | 200 +++++
 .../liquidity_mining/upgrade_reserve.rs       | 146 ++++
 token-lending/sdk/src/error.rs                |   3 +
 token-lending/sdk/src/instruction.rs          |  33 +-
 token-lending/sdk/src/state/lending_market.rs |   1 +
 .../sdk/src/state/liquidity_mining.rs         | 158 +++-
 token-lending/sdk/src/state/obligation.rs     |  34 +-
 token-lending/sdk/src/state/reserve.rs        |   1 +
 13 files changed, 1289 insertions(+), 767 deletions(-)
 create mode 100644 token-lending/program/src/processor/liquidity_mining/add_pool_reward.rs
 create mode 100644 token-lending/program/src/processor/liquidity_mining/cancel_pool_reward.rs
 create mode 100644 token-lending/program/src/processor/liquidity_mining/claim_user_reward.rs
 create mode 100644 token-lending/program/src/processor/liquidity_mining/close_pool_reward.rs
 create mode 100644 token-lending/program/src/processor/liquidity_mining/upgrade_reserve.rs

diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs
index b458291b1b6..ac16c902381 100644
--- a/token-lending/program/src/processor.rs
+++ b/token-lending/program/src/processor.rs
@@ -211,7 +211,7 @@ pub fn process_instruction(
             token_amount,
         } => {
             msg!("Instruction: Add Pool Reward");
-            liquidity_mining::process_add_pool_reward(
+            liquidity_mining::add_pool_reward::process(
                 program_id,
                 position_kind,
                 start_time_secs,
@@ -225,7 +225,7 @@ pub fn process_instruction(
             pool_reward_index,
         } => {
             msg!("Instruction: Cancel Pool Reward");
-            liquidity_mining::process_cancel_pool_reward(
+            liquidity_mining::cancel_pool_reward::process(
                 program_id,
                 position_kind,
                 pool_reward_index,
@@ -237,18 +237,22 @@ pub fn process_instruction(
             pool_reward_index,
         } => {
             msg!("Instruction: Close Pool Reward");
-            liquidity_mining::process_close_pool_reward(
+            liquidity_mining::close_pool_reward::process(
                 program_id,
                 position_kind,
                 pool_reward_index,
                 accounts,
             )
         }
+        LendingInstruction::ClaimReward => {
+            msg!("Instruction: Claim Reward");
+            liquidity_mining::claim_user_reward::process(program_id, accounts)
+        }
 
         // temporary ix for upgrade
         LendingInstruction::UpgradeReserveToV2_1_0 => {
             msg!("Instruction: Upgrade Reserve to v2.1.0");
-            liquidity_mining::upgrade_reserve(program_id, accounts)
+            liquidity_mining::upgrade_reserve::process(program_id, accounts)
         }
     }
 }
diff --git a/token-lending/program/src/processor/liquidity_mining.rs b/token-lending/program/src/processor/liquidity_mining.rs
index 96d1b7a0849..8a2ebf3f9b2 100644
--- a/token-lending/program/src/processor/liquidity_mining.rs
+++ b/token-lending/program/src/processor/liquidity_mining.rs
@@ -11,291 +11,27 @@
 //! - [cancel_pool_reward] (TODO: add bpf tests)
 //! - [close_pool_reward] (TODO: add bpf tests)
 //!
+//! There is an ix related to migration:
+//! - [upgrade_reserve] (TODO: add bpf tests)
+//!
+//! There is one user ix:
+//! - [claim_user_reward] (TODO: add bpf tests)
+//!
 //! [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,
-};
-use add_pool_reward::{AddPoolRewardAccounts, AddPoolRewardParams};
-use cancel_pool_reward::{CancelPoolRewardAccounts, CancelPoolRewardParams};
-use close_pool_reward::{ClosePoolRewardAccounts, ClosePoolRewardParams};
+pub(crate) mod add_pool_reward;
+pub(crate) mod cancel_pool_reward;
+pub(crate) mod claim_user_reward;
+pub(crate) mod close_pool_reward;
+pub(crate) mod upgrade_reserve;
+
 use solana_program::program_pack::Pack;
-use solana_program::{
-    account_info::{next_account_info, AccountInfo},
-    clock::Clock,
-    entrypoint::ProgramResult,
-    msg,
-    program::invoke,
-    program_error::ProgramError,
-    pubkey::Pubkey,
-    rent::Rent,
-    system_instruction,
-    sysvar::Sysvar,
-};
-use solend_sdk::state::discriminator::AccountDiscriminator;
+use solana_program::{account_info::AccountInfo, msg, program_error::ProgramError, pubkey::Pubkey};
 use solend_sdk::{
     error::LendingError,
-    state::{LendingMarket, PositionKind, Reserve},
+    state::{LendingMarket, Reserve},
 };
 use spl_token::state::Account as TokenAccount;
-use std::convert::TryInto;
-use upgrade_reserve::UpgradeReserveAccounts;
-
-/// # 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, &params, &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, &params, &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, &params, &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(())
-}
-
-/// 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
-/// 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 let Some(extra_rent) = new_rent.checked_sub(current_rent) {
-        // some reserves have more rent than necessary, let's not assume that
-        // the payer always needs to add more rent
-
-        invoke(
-            &system_instruction::transfer(
-                accounts.payer.key,
-                accounts.reserve_info.key,
-                extra_rent,
-            ),
-            &[
-                accounts.payer.clone(),
-                accounts.reserve_info.clone(),
-                accounts.system_program.clone(),
-            ],
-        )?;
-    }
-
-    //
-    // 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.
-    //
-
-    // 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(())
-}
 
 /// Unpacks a spl_token [TokenAccount].
 fn unpack_token_account(data: &[u8]) -> Result<TokenAccount, LendingError> {
@@ -303,6 +39,8 @@ fn unpack_token_account(data: &[u8]) -> Result<TokenAccount, LendingError> {
 }
 
 /// Derives the reward vault authority PDA address.
+///
+/// TODO: Accept a bump seed to avoid recalculating it.
 fn reward_vault_authority(
     program_id: &Pubkey,
     lending_market_key: &Pubkey,
@@ -328,466 +66,41 @@ 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.
-    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
-    pub(super) struct AddPoolRewardParams {
-        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,
-
-        _priv: (),
-    }
-
-    /// Use [Self::from_unchecked_iter] to validate the accounts except for
-    /// * `reward_token_vault_info`
-    /// * `rent_info`
-    pub(super) struct AddPoolRewardAccounts<'a, 'info> {
-        /// ✅ 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>,
-        /// ✅ 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
-        /// ✅ 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<Reserve>,
-
-        _priv: (),
-    }
-
-    impl AddPoolRewardParams {
-        pub(super) fn new(
-            position_kind: PositionKind,
-            start_time_secs: u64,
-            end_time_secs: u64,
-            reward_token_amount: u64,
-        ) -> Result<Self, ProgramError> {
-            let clock = &Clock::get()?;
-
-            let start_time_secs = start_time_secs.max(clock.unix_timestamp as u64);
-
-            if start_time_secs <= end_time_secs {
-                msg!("Pool reward must end after it starts");
-                return Err(LendingError::MathOverflow.into());
-            }
-
-            let duration_secs: u32 = {
-                // SAFETY: just checked that start time is strictly smaller
-                let d = end_time_secs - start_time_secs;
-                d.try_into().map_err(|_| {
-                    msg!("Pool reward duration is too long");
-                    LendingError::MathOverflow
-                })?
-            };
-            if MIN_REWARD_PERIOD_SECS > duration_secs as u64 {
-                msg!("Pool reward duration must be at least {MIN_REWARD_PERIOD_SECS} secs");
-                return Err(LendingError::PoolRewardPeriodTooShort.into());
-            }
-
-            if reward_token_amount == 0 {
-                msg!("Pool reward amount must be greater than zero");
-                return Err(LendingError::InvalidAmount.into());
-            }
-
-            Ok(Self {
-                position_kind,
-                start_time_secs,
-                duration_secs,
-                reward_token_amount,
-
-                _priv: (),
-            })
-        }
-    }
-
-    impl<'a, 'info> AddPoolRewardAccounts<'a, 'info> {
-        pub(super) fn from_unchecked_iter(
-            program_id: &Pubkey,
-            params: &AddPoolRewardParams,
-            iter: &mut impl Iterator<Item = &'a AccountInfo<'info>>,
-        ) -> Result<AddPoolRewardAccounts<'a, 'info>, ProgramError> {
-            let reserve_info = next_account_info(iter)?;
-            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());
-            }
-
-            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,
-                reward_authority_info,
-                reward_token_vault_info,
-                lending_market_info,
-                lending_market_owner_info,
-                rent_info,
-                token_program_info,
-
-                reserve,
-
-                _priv: (),
-            })
-        }
-    }
-}
-
-mod cancel_pool_reward {
-    use super::*;
-
-    pub(super) struct CancelPoolRewardParams {
-        position_kind: PositionKind,
-        pool_reward_index: u64,
-
-        _priv: (),
-    }
-
-    /// Use [Self::from_unchecked_iter] to validate the accounts.
-    pub(super) struct CancelPoolRewardAccounts<'a, 'info> {
-        /// ✅ 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<Reserve>,
-
-        _priv: (),
-    }
-
-    impl<'a, 'info> CancelPoolRewardAccounts<'a, 'info> {
-        pub(super) fn from_unchecked_iter(
-            program_id: &Pubkey,
-            params: &CancelPoolRewardParams,
-            iter: &mut impl Iterator<Item = &'a AccountInfo<'info>>,
-        ) -> Result<CancelPoolRewardAccounts<'a, 'info>, 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 {
-                position_kind,
-                pool_reward_index,
-
-                _priv: (),
-            }
-        }
-    }
-}
-
-mod close_pool_reward {
-    use super::*;
-
-    pub(super) struct ClosePoolRewardParams {
-        position_kind: PositionKind,
-        pool_reward_index: u64,
-
-        _priv: (),
-    }
-
-    /// 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<Reserve>,
-    }
-
-    impl<'a, 'info> ClosePoolRewardAccounts<'a, 'info> {
-        pub(super) fn from_unchecked_iter(
-            program_id: &Pubkey,
-            params: &ClosePoolRewardParams,
-            iter: &mut impl Iterator<Item = &'a AccountInfo<'info>>,
-        ) -> Result<ClosePoolRewardAccounts<'a, 'info>, 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 {
-                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,
-
-                _priv: (),
-            })
-        }
-    }
-
-    impl ClosePoolRewardParams {
-        pub(super) fn new(position_kind: PositionKind, pool_reward_index: u64) -> Self {
-            Self {
-                position_kind,
-                pool_reward_index,
+/// Does all the checks of [check_and_unpack_pool_reward_accounts] and additionally:
+///
+/// * ✅ `lending_market_owner_info` is a signer
+/// * ✅ `lending_market_owner_info` matches `lending_market_info`
+fn check_and_unpack_pool_reward_accounts_for_admin_ixs<'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<(LendingMarket, Box<Reserve>), ProgramError> {
+    let (lending_market, reserve) = check_and_unpack_pool_reward_accounts(
+        program_id,
+        reserve_info,
+        reward_mint_info,
+        reward_authority_info,
+        lending_market_info,
+        token_program_info,
+    )?;
 
-                _priv: (),
-            }
-        }
+    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());
     }
-}
-
-mod upgrade_reserve {
-    use solend_sdk::state::RESERVE_LEN_V2_0_2;
-
-    use super::*;
-
-    pub(super) struct UpgradeReserveAccounts<'a, '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: (),
+    if !lending_market_owner_info.is_signer {
+        msg!("Lending market owner provided must be a signer");
+        return Err(LendingError::InvalidSigner.into());
     }
 
-    impl<'a, 'info> UpgradeReserveAccounts<'a, 'info> {
-        pub(super) fn from_unchecked_iter(
-            program_id: &Pubkey,
-            iter: &mut impl Iterator<Item = &'a AccountInfo<'info>>,
-        ) -> Result<UpgradeReserveAccounts<'a, 'info>, ProgramError> {
-            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");
-                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());
-            }
-
-            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: (),
-            })
-        }
-    }
+    Ok((lending_market, reserve))
 }
 
-/// Common checks within the admin ixs are:
+/// Checks that:
 ///
 /// * ✅ `reserve_info` belongs to this program
 /// * ✅ `reserve_info` unpacks
@@ -795,20 +108,16 @@ mod upgrade_reserve {
 /// * ✅ `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>(
+/// * ✅ `reward_mint_info` belongs to the token program
+fn check_and_unpack_pool_reward_accounts<'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<Box<Reserve>, ProgramError> {
+) -> Result<(LendingMarket, Box<Reserve>), ProgramError> {
     if reserve_info.owner != program_id {
         msg!("Reserve provided is not owned by the lending program");
         return Err(LendingError::InvalidAccountOwner.into());
@@ -831,13 +140,9 @@ fn check_pool_reward_accounts_for_admin_ixs_and_unpack_reserve<'info>(
         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());
+    if reward_mint_info.owner != token_program_info.key {
+        msg!("Reward mint provided must be owned by the token program");
+        return Err(LendingError::InvalidTokenOwner.into());
     }
 
     let (expected_reward_vault_authority, _bump_seed) = reward_vault_authority(
@@ -851,5 +156,5 @@ fn check_pool_reward_accounts_for_admin_ixs_and_unpack_reserve<'info>(
         return Err(LendingError::InvalidAccountInput.into());
     }
 
-    Ok(reserve)
+    Ok((lending_market, reserve))
 }
diff --git a/token-lending/program/src/processor/liquidity_mining/add_pool_reward.rs b/token-lending/program/src/processor/liquidity_mining/add_pool_reward.rs
new file mode 100644
index 00000000000..540815a3b51
--- /dev/null
+++ b/token-lending/program/src/processor/liquidity_mining/add_pool_reward.rs
@@ -0,0 +1,266 @@
+//! Adds a new pool reward to a reserve.
+//!
+//! Each pool reward has a unique vault that holds the reward tokens.
+
+use crate::processor::{
+    assert_rent_exempt, spl_token_init_account, spl_token_transfer, TokenInitializeAccountParams,
+    TokenTransferParams,
+};
+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::state::MIN_REWARD_PERIOD_SECS;
+use solend_sdk::{
+    error::LendingError,
+    state::{PositionKind, Reserve},
+};
+use std::convert::TryInto;
+
+use super::{check_and_unpack_pool_reward_accounts_for_admin_ixs, unpack_token_account};
+
+/// Use [Self::new] to validate the parameters.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+struct AddPoolRewardParams {
+    position_kind: PositionKind,
+    /// At least the current timestamp.
+    start_time_secs: u64,
+    /// Larger than [MIN_REWARD_PERIOD_SECS].
+    duration_secs: u32,
+    /// Larger than zero.
+    reward_token_amount: u64,
+}
+
+/// Use [Self::from_unchecked_iter] to validate the accounts except for
+/// * `reward_token_vault_info`
+/// * `rent_info`
+struct AddPoolRewardAccounts<'a, 'info> {
+    /// ✅ belongs to this program
+    /// ✅ unpacks
+    /// ✅ belongs to `lending_market_info`
+    /// ✅ is writable
+    reserve_info: &'a AccountInfo<'info>,
+    /// ✅ belongs to the token program
+    reward_mint_info: &'a AccountInfo<'info>,
+    /// ✅ belongs to the token program
+    /// ✅ owned by `lending_market_owner_info`
+    /// ✅ has enough tokens
+    /// ✅ matches `reward_mint_info`
+    /// ✅ is writable
+    reward_token_source_info: &'a AccountInfo<'info>,
+    /// ✅ seed of `lending_market_info`, `reserve_info`, `reward_mint_info`
+    reward_authority_info: &'a AccountInfo<'info>,
+    /// ✅ belongs to the token program
+    /// ✅ has no data
+    /// ✅ is writable
+    /// ❓ we don't yet know whether it's rent exempt
+    reward_token_vault_info: &'a AccountInfo<'info>,
+    /// ✅ belongs to this program
+    /// ✅ unpacks
+    _lending_market_info: &'a AccountInfo<'info>,
+    /// ✅ is a signer
+    /// ✅ matches `lending_market_info`
+    /// TBD: do we want to create another signer authority to be able to
+    /// delegate reward management to a softer multisig?
+    lending_market_owner_info: &'a AccountInfo<'info>,
+    /// ❓ we don't yet whether this is rent info
+    rent_info: &'a AccountInfo<'info>,
+    /// ✅ matches `lending_market_info`
+    token_program_info: &'a AccountInfo<'info>,
+
+    reserve: Box<Reserve>,
+}
+
+/// # 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(
+    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, &params, &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(())
+}
+
+impl AddPoolRewardParams {
+    fn new(
+        position_kind: PositionKind,
+        start_time_secs: u64,
+        end_time_secs: u64,
+        reward_token_amount: u64,
+    ) -> Result<Self, ProgramError> {
+        let clock = &Clock::get()?;
+
+        let start_time_secs = start_time_secs.max(clock.unix_timestamp as u64);
+
+        if start_time_secs <= end_time_secs {
+            msg!("Pool reward must end after it starts");
+            return Err(LendingError::MathOverflow.into());
+        }
+
+        let duration_secs: u32 = {
+            // SAFETY: just checked that start time is strictly smaller
+            let d = end_time_secs - start_time_secs;
+            d.try_into().map_err(|_| {
+                msg!("Pool reward duration is too long");
+                LendingError::MathOverflow
+            })?
+        };
+        if MIN_REWARD_PERIOD_SECS > duration_secs as u64 {
+            msg!("Pool reward duration must be at least {MIN_REWARD_PERIOD_SECS} secs");
+            return Err(LendingError::PoolRewardPeriodTooShort.into());
+        }
+
+        if reward_token_amount == 0 {
+            msg!("Pool reward amount must be greater than zero");
+            return Err(LendingError::InvalidAmount.into());
+        }
+
+        Ok(Self {
+            position_kind,
+            start_time_secs,
+            duration_secs,
+            reward_token_amount,
+        })
+    }
+}
+
+impl<'a, 'info> AddPoolRewardAccounts<'a, 'info> {
+    fn from_unchecked_iter(
+        program_id: &Pubkey,
+        params: &AddPoolRewardParams,
+        iter: &mut impl Iterator<Item = &'a AccountInfo<'info>>,
+    ) -> Result<AddPoolRewardAccounts<'a, 'info>, ProgramError> {
+        let reserve_info = next_account_info(iter)?;
+        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_and_unpack_pool_reward_accounts_for_admin_ixs(
+            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());
+        }
+
+        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());
+        }
+
+        // check that accounts that should be writable are writable
+
+        if !reward_token_vault_info.is_writable {
+            msg!("Reward token vault provided must be writable");
+            return Err(ProgramError::InvalidAccountData);
+        }
+        if !reserve_info.is_writable {
+            msg!("Reserve provided must be writable");
+            return Err(ProgramError::InvalidAccountData);
+        }
+        if !reward_token_source_info.is_writable {
+            msg!("Reward token source provided must be writable");
+            return Err(ProgramError::InvalidAccountData);
+        }
+
+        Ok(Self {
+            reserve_info,
+            reward_mint_info,
+            reward_token_source_info,
+            reward_authority_info,
+            reward_token_vault_info,
+            _lending_market_info: lending_market_info,
+            lending_market_owner_info,
+            rent_info,
+            token_program_info,
+
+            reserve,
+        })
+    }
+}
diff --git a/token-lending/program/src/processor/liquidity_mining/cancel_pool_reward.rs b/token-lending/program/src/processor/liquidity_mining/cancel_pool_reward.rs
new file mode 100644
index 00000000000..b1962fced57
--- /dev/null
+++ b/token-lending/program/src/processor/liquidity_mining/cancel_pool_reward.rs
@@ -0,0 +1,168 @@
+use crate::processor::liquidity_mining::{
+    check_and_unpack_pool_reward_accounts_for_admin_ixs, unpack_token_account,
+};
+use crate::processor::{spl_token_transfer, TokenTransferParams};
+use solana_program::program_pack::Pack;
+use solana_program::sysvar::Sysvar;
+use solana_program::{
+    account_info::{next_account_info, AccountInfo},
+    clock::Clock,
+    entrypoint::ProgramResult,
+    msg,
+    program_error::ProgramError,
+    pubkey::Pubkey,
+};
+use solend_sdk::{
+    error::LendingError,
+    state::{PositionKind, Reserve},
+};
+
+use super::reward_vault_authority_seeds;
+
+/// Use [Self::from_unchecked_iter] to validate the accounts.
+struct CancelPoolRewardAccounts<'a, 'info> {
+    /// ✅ belongs to this program
+    /// ✅ unpacks
+    /// ✅ belongs to `lending_market_info`
+    /// ✅ is writable
+    reserve_info: &'a AccountInfo<'info>,
+    /// ✅ belongs to the token program
+    reward_mint_info: &'a AccountInfo<'info>,
+    /// ✅ belongs to the token program
+    /// ✅ matches `reward_mint_info`
+    /// ✅ is writable
+    reward_token_destination_info: &'a AccountInfo<'info>,
+    /// ✅ seed of `lending_market_info`, `reserve_info`, `reward_mint_info`
+    reward_authority_info: &'a AccountInfo<'info>,
+    /// ❓ we don't know whether it matches the reward vault pubkey stored in [Reserve]
+    /// ✅ is writable
+    reward_token_vault_info: &'a AccountInfo<'info>,
+    /// ✅ belongs to this program
+    /// ✅ unpacks
+    lending_market_info: &'a AccountInfo<'info>,
+    /// ✅ is a signer
+    /// ✅ matches `lending_market_info`
+    _lending_market_owner_info: &'a AccountInfo<'info>,
+    /// ✅ matches `lending_market_info`
+    token_program_info: &'a AccountInfo<'info>,
+
+    reserve: Box<Reserve>,
+}
+
+/// # 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(
+    program_id: &Pubkey,
+    position_kind: PositionKind,
+    pool_reward_index: usize,
+    accounts: &[AccountInfo],
+) -> ProgramResult {
+    let mut accounts =
+        CancelPoolRewardAccounts::from_unchecked_iter(program_id, &mut accounts.iter())?;
+
+    // 1.
+
+    let pool_reward_manager = match position_kind {
+        PositionKind::Borrow => &mut accounts.reserve.borrows_pool_reward_manager,
+        PositionKind::Deposit => &mut accounts.reserve.deposits_pool_reward_manager,
+    };
+    let (expected_vault, unallocated_rewards) =
+        pool_reward_manager.cancel_pool_reward(pool_reward_index, &Clock::get()?)?;
+
+    if expected_vault != *accounts.reward_token_vault_info.key {
+        msg!("Reward vault provided does not match the reward vault pubkey stored in [Reserve]");
+        return Err(LendingError::InvalidAccountInput.into());
+    }
+
+    // 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(())
+}
+
+impl<'a, 'info> CancelPoolRewardAccounts<'a, 'info> {
+    fn from_unchecked_iter(
+        program_id: &Pubkey,
+        iter: &mut impl Iterator<Item = &'a AccountInfo<'info>>,
+    ) -> Result<CancelPoolRewardAccounts<'a, 'info>, 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_and_unpack_pool_reward_accounts_for_admin_ixs(
+            program_id,
+            reserve_info,
+            reward_mint_info,
+            reward_authority_info,
+            lending_market_info,
+            lending_market_owner_info,
+            token_program_info,
+        )?;
+
+        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.mint != *reward_mint_info.key {
+            msg!("Reward token destination mint does not match the reward mint provided");
+            return Err(LendingError::InvalidAccountInput.into());
+        }
+
+        // check that accounts that should be writable are writable
+
+        if !reward_token_vault_info.is_writable {
+            msg!("Reward token vault provided must be writable");
+            return Err(ProgramError::InvalidAccountData);
+        }
+        if !reserve_info.is_writable {
+            msg!("Reserve provided must be writable");
+            return Err(ProgramError::InvalidAccountData);
+        }
+        if !reward_token_destination_info.is_writable {
+            msg!("Reward token destination provided must be writable");
+            return Err(ProgramError::InvalidAccountData);
+        }
+
+        Ok(Self {
+            reserve_info,
+            reward_mint_info,
+            reward_token_destination_info,
+            reward_authority_info,
+            reward_token_vault_info,
+            lending_market_info,
+            _lending_market_owner_info: lending_market_owner_info,
+            token_program_info,
+
+            reserve,
+        })
+    }
+}
diff --git a/token-lending/program/src/processor/liquidity_mining/claim_user_reward.rs b/token-lending/program/src/processor/liquidity_mining/claim_user_reward.rs
new file mode 100644
index 00000000000..5737aba352d
--- /dev/null
+++ b/token-lending/program/src/processor/liquidity_mining/claim_user_reward.rs
@@ -0,0 +1,235 @@
+use crate::processor::{spl_token_transfer, TokenTransferParams};
+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,
+    sysvar::Sysvar,
+};
+use solend_sdk::state::{CreatingNewUserRewardManager, Obligation};
+use solend_sdk::{
+    error::LendingError,
+    state::{PositionKind, Reserve},
+};
+
+use super::{
+    check_and_unpack_pool_reward_accounts, reward_vault_authority_seeds, unpack_token_account,
+};
+
+/// Use [Self::from_unchecked_iter] to validate the accounts.
+struct ClaimUserReward<'a, 'info> {
+    /// ✅ belongs to this program
+    /// ✅ unpacks
+    /// ✅ matches `lending_market_info`
+    /// ✅ is writable
+    obligation_info: &'a AccountInfo<'info>,
+    /// ✅ belongs to the token program
+    /// ✅ is writable
+    /// ✅ matches `reward_mint_info`
+    /// ✅ owned by the obligation owner
+    obligation_owner_token_account_info: &'a AccountInfo<'info>,
+    /// ✅ belongs to this program
+    /// ✅ unpacks
+    /// ✅ belongs to `lending_market_info`
+    /// ✅ is writable
+    reserve_info: &'a AccountInfo<'info>,
+    /// ✅ belongs to the token program
+    reward_mint_info: &'a AccountInfo<'info>,
+    /// ✅ seed of `lending_market_info`, `reserve_info`, `reward_mint_info`
+    reward_authority_info: &'a AccountInfo<'info>,
+    /// ✅ belongs to the token program
+    /// ✅ unpacks to a [TokenAccount]
+    /// ✅ owned by `reward_authority_info`
+    /// ✅ matches `reward_mint_info`
+    /// ✅ is writable
+    reward_token_vault_info: &'a AccountInfo<'info>,
+    /// ✅ belongs to this program
+    /// ✅ unpacks
+    lending_market_info: &'a AccountInfo<'info>,
+    /// ✅ matches `lending_market_info`
+    token_program_info: &'a AccountInfo<'info>,
+
+    obligation: Box<Obligation>,
+    reserve: Box<Reserve>,
+}
+
+/// # Effects
+///
+/// 1. Updates the user reward manager with the pool reward manager and accrues rewards
+/// 2. Withdraws all eligible rewards from [UserRewardManager].
+///    Eligible rewards are those that match the vault and user has earned any.
+/// 3. Transfers the withdrawn rewards to the user's token account.
+/// 4. Packs all changes into account buffers for [Obligation] and [Reserve].
+pub(crate) fn process(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
+    let mut accounts = ClaimUserReward::from_unchecked_iter(program_id, &mut accounts.iter())?;
+
+    // 1.
+
+    let position_kind = accounts
+        .obligation
+        .find_position_kind(*accounts.reserve_info.key)?;
+
+    let Some(user_reward_manager) = accounts
+        .obligation
+        .find_user_reward_manager_mut(*accounts.reserve_info.key)
+    else {
+        // Let's not error if a user has no rewards to claim for this reserve.
+        // Having this ix idempotent makes cranking easier.
+        return Ok(());
+    };
+
+    let pool_reward_manager = match position_kind {
+        PositionKind::Borrow => &mut accounts.reserve.borrows_pool_reward_manager,
+        PositionKind::Deposit => &mut accounts.reserve.deposits_pool_reward_manager,
+    };
+
+    let clock = &Clock::get()?;
+
+    // Syncs the pool reward manager with the user manager and accrues rewards.
+    // If we wanted to optimize CU usage then we could make a dedicated update
+    // function only for claiming rewards to avoid iterating twice over the rewards.
+    user_reward_manager.update(pool_reward_manager, clock, CreatingNewUserRewardManager::No)?;
+
+    // 2.
+
+    let total_reward_amount = user_reward_manager.claim_rewards(
+        pool_reward_manager,
+        *accounts.reward_token_vault_info.key,
+        clock,
+    )?;
+
+    // 3.
+
+    spl_token_transfer(TokenTransferParams {
+        source: accounts.reward_token_vault_info.clone(),
+        destination: accounts.obligation_owner_token_account_info.clone(),
+        amount: total_reward_amount,
+        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.
+
+    Obligation::pack(
+        *accounts.obligation,
+        &mut accounts.obligation_info.data.borrow_mut(),
+    )?;
+
+    Reserve::pack(
+        *accounts.reserve,
+        &mut accounts.reserve_info.data.borrow_mut(),
+    )?;
+
+    Ok(())
+}
+
+impl<'a, 'info> ClaimUserReward<'a, 'info> {
+    fn from_unchecked_iter(
+        program_id: &Pubkey,
+        iter: &mut impl Iterator<Item = &'a AccountInfo<'info>>,
+    ) -> Result<ClaimUserReward<'a, 'info>, ProgramError> {
+        let obligation_info = next_account_info(iter)?;
+        let obligation_owner_token_account_info = next_account_info(iter)?;
+        let reserve_info = next_account_info(iter)?;
+        let reward_mint_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 token_program_info = next_account_info(iter)?;
+
+        let (_, reserve) = check_and_unpack_pool_reward_accounts(
+            program_id,
+            reserve_info,
+            reward_mint_info,
+            reward_authority_info,
+            lending_market_info,
+            token_program_info,
+        )?;
+
+        if obligation_info.owner != program_id {
+            msg!("Obligation provided is not owned by the lending program");
+            return Err(LendingError::InvalidAccountOwner.into());
+        }
+
+        let obligation = Box::new(Obligation::unpack(&obligation_info.data.borrow())?);
+
+        if obligation.lending_market != *lending_market_info.key {
+            msg!("Obligation lending market does not match the lending market provided");
+            return Err(LendingError::InvalidAccountInput.into());
+        }
+
+        if obligation_owner_token_account_info.owner != token_program_info.key {
+            msg!("Obligation owner token account provided must be owned by the token program");
+            return Err(LendingError::InvalidTokenOwner.into());
+        }
+        let obligation_owner_token_account =
+            unpack_token_account(&obligation_owner_token_account_info.data.borrow())?;
+
+        if obligation_owner_token_account.owner != obligation.owner {
+            msg!(
+                "Obligation owner token account owner does not match the obligation owner provided"
+            );
+            return Err(LendingError::InvalidAccountInput.into());
+        }
+        if obligation_owner_token_account.mint != *reward_mint_info.key {
+            msg!("Obligation owner token account mint does not match the reward mint provided");
+            return Err(LendingError::InvalidAccountInput.into());
+        }
+
+        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());
+        }
+        let reward_token_vault = unpack_token_account(&reward_token_vault_info.data.borrow())?;
+
+        if reward_token_vault.owner != *reward_authority_info.key {
+            msg!("Reward token vault owner does not match the reward authority provided");
+            return Err(LendingError::InvalidAccountInput.into());
+        }
+        if reward_token_vault.mint != *reward_mint_info.key {
+            msg!("Reward token vault mint does not match the reward mint provided");
+            return Err(LendingError::InvalidAccountInput.into());
+        }
+
+        // check that accounts that should be writable are writable
+
+        if !obligation_info.is_writable {
+            msg!("Obligation provided must be writable");
+            return Err(ProgramError::InvalidAccountData);
+        }
+        if !obligation_owner_token_account_info.is_writable {
+            msg!("Obligation owner token account provided must be writable");
+            return Err(ProgramError::InvalidAccountData);
+        }
+        if !reward_token_vault_info.is_writable {
+            msg!("Reward token vault provided must be writable");
+            return Err(ProgramError::InvalidAccountData);
+        }
+        if !reserve_info.is_writable {
+            msg!("Reserve provided must be writable");
+            return Err(ProgramError::InvalidAccountData);
+        }
+
+        Ok(Self {
+            obligation_info,
+            obligation_owner_token_account_info,
+            reserve_info,
+            reward_mint_info,
+            reward_authority_info,
+            reward_token_vault_info,
+            lending_market_info,
+            token_program_info,
+
+            reserve,
+            obligation,
+        })
+    }
+}
diff --git a/token-lending/program/src/processor/liquidity_mining/close_pool_reward.rs b/token-lending/program/src/processor/liquidity_mining/close_pool_reward.rs
new file mode 100644
index 00000000000..dae1990534f
--- /dev/null
+++ b/token-lending/program/src/processor/liquidity_mining/close_pool_reward.rs
@@ -0,0 +1,200 @@
+//! Closes a pool reward, making its slot vacant and ready for a new reward.
+//!
+//! Before closing a pool reward that pool reward must first be cancelled
+//! and all rewards must be claimed by the users.
+//!
+//! The claim ix is permission-less and therefore it can be cranked.
+
+use solana_program::program_pack::Pack;
+use solana_program::{
+    account_info::{next_account_info, AccountInfo},
+    entrypoint::ProgramResult,
+    msg,
+    program_error::ProgramError,
+    pubkey::Pubkey,
+};
+use solend_sdk::{
+    error::LendingError,
+    state::{PositionKind, Reserve},
+};
+use spl_token::state::Account as TokenAccount;
+
+use crate::processor::{
+    spl_token_close_account, spl_token_transfer, TokenCloseAccountParams, TokenTransferParams,
+};
+
+use super::{
+    check_and_unpack_pool_reward_accounts_for_admin_ixs, reward_vault_authority_seeds,
+    unpack_token_account,
+};
+
+/// Use [Self::from_unchecked_iter] to validate the accounts.
+struct ClosePoolRewardAccounts<'a, 'info> {
+    /// ✅ belongs to this program
+    /// ✅ unpacks
+    /// ✅ belongs to `lending_market_info`
+    /// ✅ is writable
+    reserve_info: &'a AccountInfo<'info>,
+    /// ✅ belongs to the token program
+    reward_mint_info: &'a AccountInfo<'info>,
+    /// ✅ belongs to the token program
+    /// ✅ owned by `lending_market_owner_info`
+    /// ✅ matches `reward_mint_info`
+    /// ✅ is writable
+    reward_token_destination_info: &'a AccountInfo<'info>,
+    /// ✅ seed of `lending_market_info`, `reserve_info`, `reward_mint_info`
+    reward_authority_info: &'a AccountInfo<'info>,
+    /// ❓ we don't know whether it matches vault in the [Reserve]
+    /// ✅ is writable
+    /// ✅ unpacks
+    reward_token_vault_info: &'a AccountInfo<'info>,
+    /// ✅ belongs to this program
+    /// ✅ unpacks
+    lending_market_info: &'a AccountInfo<'info>,
+    /// ✅ is a signer
+    /// ✅ matches `lending_market_info`
+    lending_market_owner_info: &'a AccountInfo<'info>,
+    /// ✅ matches `lending_market_info`
+    token_program_info: &'a AccountInfo<'info>,
+
+    reserve: Box<Reserve>,
+    reward_token_vault: TokenAccount,
+}
+
+/// # Effects
+///
+/// 1. Closes reward in the [Reserve] account if all users have claimed.
+/// 2. Transfers dust to the `reward_token_destination` account.
+/// 3. Closes reward vault token account.
+/// 3. Packs all changes into account buffers.
+pub(crate) fn process(
+    program_id: &Pubkey,
+    position_kind: PositionKind,
+    pool_reward_index: usize,
+    accounts: &[AccountInfo],
+) -> ProgramResult {
+    let mut accounts =
+        ClosePoolRewardAccounts::from_unchecked_iter(program_id, &mut accounts.iter())?;
+
+    // 1.
+
+    let pool_reward_manager = match position_kind {
+        PositionKind::Borrow => &mut accounts.reserve.borrows_pool_reward_manager,
+        PositionKind::Deposit => &mut accounts.reserve.deposits_pool_reward_manager,
+    };
+    let expected_vault = pool_reward_manager.close_pool_reward(pool_reward_index)?;
+    if expected_vault != *accounts.reward_token_vault_info.key {
+        msg!("Reward token vault provided does not match the expected vault");
+        return Err(LendingError::InvalidAccountInput.into());
+    }
+
+    // 2.
+
+    spl_token_transfer(TokenTransferParams {
+        source: accounts.reward_token_vault_info.clone(),
+        destination: accounts.reward_token_destination_info.clone(),
+        amount: accounts.reward_token_vault.amount,
+        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(())
+}
+
+impl<'a, 'info> ClosePoolRewardAccounts<'a, 'info> {
+    fn from_unchecked_iter(
+        program_id: &Pubkey,
+        iter: &mut impl Iterator<Item = &'a AccountInfo<'info>>,
+    ) -> Result<ClosePoolRewardAccounts<'a, 'info>, 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_and_unpack_pool_reward_accounts_for_admin_ixs(
+            program_id,
+            reserve_info,
+            reward_mint_info,
+            reward_authority_info,
+            lending_market_info,
+            lending_market_owner_info,
+            token_program_info,
+        )?;
+
+        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.mint != *reward_mint_info.key {
+            msg!("Reward token destination mint does not match the reward mint provided");
+            return Err(LendingError::InvalidAccountInput.into());
+        }
+
+        let reward_token_vault = unpack_token_account(&reward_token_vault_info.data.borrow())?;
+        if reward_token_vault.mint != *reward_mint_info.key {
+            msg!("Reward token vault mint does not match the reward mint provided");
+            return Err(LendingError::InvalidAccountInput.into());
+        }
+
+        // check that accounts that should be writable are writable
+
+        if !reserve_info.is_writable {
+            msg!("Reserve provided must be writable");
+            return Err(ProgramError::InvalidAccountData);
+        }
+        if !reward_token_destination_info.is_writable {
+            msg!("Reward token destination provided must be writable");
+            return Err(ProgramError::InvalidAccountData);
+        }
+        if !reward_token_vault_info.is_writable {
+            msg!("Reward token vault provided must be writable");
+            return Err(ProgramError::InvalidAccountData);
+        }
+
+        Ok(Self {
+            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,
+            reward_token_vault,
+        })
+    }
+}
diff --git a/token-lending/program/src/processor/liquidity_mining/upgrade_reserve.rs b/token-lending/program/src/processor/liquidity_mining/upgrade_reserve.rs
new file mode 100644
index 00000000000..35e97e5e773
--- /dev/null
+++ b/token-lending/program/src/processor/liquidity_mining/upgrade_reserve.rs
@@ -0,0 +1,146 @@
+use solana_program::program_pack::Pack;
+use solana_program::{
+    account_info::{next_account_info, AccountInfo},
+    entrypoint::ProgramResult,
+    msg,
+    program::invoke,
+    program_error::ProgramError,
+    pubkey::Pubkey,
+    rent::Rent,
+    system_instruction,
+    sysvar::Sysvar,
+};
+use solend_sdk::state::discriminator::AccountDiscriminator;
+use solend_sdk::state::RESERVE_LEN_V2_0_2;
+use solend_sdk::{error::LendingError, state::Reserve};
+
+struct UpgradeReserveAccounts<'a, 'info> {
+    /// Reserve sized as v2.0.2.
+    ///
+    /// ✅ belongs to this program
+    /// ✅ is sized [RESERVE_LEN_V2_0_2], ie. for sure [Reserve] account
+    /// ✅ is writable
+    reserve_info: &'a AccountInfo<'info>,
+    /// The pool fella who pays for this.
+    ///
+    /// ✅ is a signer
+    /// ✅ is writable
+    payer: &'a AccountInfo<'info>,
+    /// The system program.
+    ///
+    /// ✅ is the system program
+    system_program: &'a AccountInfo<'info>,
+}
+
+/// 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.
+///
+/// # 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 process(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 let Some(extra_rent) = new_rent.checked_sub(current_rent) {
+        // some reserves have more rent than necessary, let's not assume that
+        // the payer always needs to add more rent
+
+        invoke(
+            &system_instruction::transfer(
+                accounts.payer.key,
+                accounts.reserve_info.key,
+                extra_rent,
+            ),
+            &[
+                accounts.payer.clone(),
+                accounts.reserve_info.clone(),
+                accounts.system_program.clone(),
+            ],
+        )?;
+    }
+
+    // 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.
+
+    // 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(())
+}
+
+impl<'a, 'info> UpgradeReserveAccounts<'a, 'info> {
+    fn from_unchecked_iter(
+        program_id: &Pubkey,
+        iter: &mut impl Iterator<Item = &'a AccountInfo<'info>>,
+    ) -> Result<UpgradeReserveAccounts<'a, 'info>, ProgramError> {
+        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");
+            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());
+        }
+
+        if system_program.key != &solana_program::system_program::id() {
+            msg!("System program provided must be the system program");
+            return Err(LendingError::InvalidAccountInput.into());
+        }
+
+        // check that accounts that should be writable are writable
+
+        if !payer.is_writable {
+            msg!("Payer provided must be writable");
+            return Err(ProgramError::InvalidAccountData);
+        }
+        if !reserve_info.is_writable {
+            msg!("Reserve provided must be writable");
+            return Err(ProgramError::InvalidAccountData);
+        }
+
+        Ok(Self {
+            payer,
+            reserve_info,
+            system_program,
+        })
+    }
+}
diff --git a/token-lending/sdk/src/error.rs b/token-lending/sdk/src/error.rs
index f3b050d7687..a48d29270a1 100644
--- a/token-lending/sdk/src/error.rs
+++ b/token-lending/sdk/src/error.rs
@@ -223,6 +223,9 @@ pub enum LendingError {
     /// Trying to use an account that hasn't been migrated
     #[error("Trying to use an account that hasn't been migrated")]
     AccountNotMigrated,
+    /// There's no pool reward that matches the given parameters
+    #[error("There's no pool reward that matches the given parameters")]
+    NoPoolRewardMatches,
 }
 
 impl From<LendingError> for ProgramError {
diff --git a/token-lending/sdk/src/instruction.rs b/token-lending/sdk/src/instruction.rs
index 5302817f46b..68d95c87468 100644
--- a/token-lending/sdk/src/instruction.rs
+++ b/token-lending/sdk/src/instruction.rs
@@ -578,13 +578,12 @@ pub enum LendingInstruction {
     ///    `[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,
         /// Identifies a reward within a reserve's deposits/borrows rewards.
-        pool_reward_index: u64,
+        pool_reward_index: usize,
     },
 
     // 27
@@ -605,15 +604,33 @@ pub enum LendingInstruction {
     ///    `[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,
         /// Identifies a reward within a reserve's deposits/borrows rewards.
-        pool_reward_index: u64,
+        pool_reward_index: usize,
     },
 
+    /// 28
+    /// ClaimReward
+    ///
+    /// * User can claim rewards from their obligation.
+    ///
+    ///   `[writable]` Obligation account.
+    ///   `[writable]` Obligation owner reward receiving token account.
+    ///   `[writable]` Reserve account.
+    ///   `[]` Reward mint.
+    ///   `[]` 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.
+    ///   `[]` Token program.
+    ClaimReward,
+
     // 255
     /// UpgradeReserveToV2_1_0
     ///
@@ -900,7 +917,7 @@ impl LendingInstruction {
                 let (pool_reward_index, _rest) = Self::unpack_u64(rest)?;
                 Self::ClosePoolReward {
                     position_kind,
-                    pool_reward_index,
+                    pool_reward_index: pool_reward_index as _,
                 }
             }
             27 => {
@@ -908,9 +925,10 @@ impl LendingInstruction {
                 let (pool_reward_index, _rest) = Self::unpack_u64(rest)?;
                 Self::CancelPoolReward {
                     position_kind,
-                    pool_reward_index,
+                    pool_reward_index: pool_reward_index as _,
                 }
             }
+            28 => Self::ClaimReward,
             255 => Self::UpgradeReserveToV2_1_0,
             _ => {
                 msg!("Instruction cannot be unpacked");
@@ -1248,6 +1266,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::ClaimReward => {
+                buf.push(28);
+            }
             Self::UpgradeReserveToV2_1_0 => {
                 buf.push(255);
             }
diff --git a/token-lending/sdk/src/state/lending_market.rs b/token-lending/sdk/src/state/lending_market.rs
index ada34c4d3ca..3185a69caa7 100644
--- a/token-lending/sdk/src/state/lending_market.rs
+++ b/token-lending/sdk/src/state/lending_market.rs
@@ -180,6 +180,7 @@ impl Pack for LendingMarket {
                 msg!("Lending market discriminator does not match");
                 return Err(LendingError::InvalidAccountDiscriminator.into());
             }
+            #[allow(clippy::assertions_on_constants)]
             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:
diff --git a/token-lending/sdk/src/state/liquidity_mining.rs b/token-lending/sdk/src/state/liquidity_mining.rs
index fc59369ccb5..0c39cadc4b7 100644
--- a/token-lending/sdk/src/state/liquidity_mining.rs
+++ b/token-lending/sdk/src/state/liquidity_mining.rs
@@ -1,17 +1,18 @@
+use super::pack_decimal;
 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::msg;
 use solana_program::program_pack::{Pack, Sealed};
 use solana_program::{
     clock::Clock,
     program_error::ProgramError,
     pubkey::{Pubkey, PUBKEY_BYTES},
 };
-
-use super::pack_decimal;
+use std::convert::TryFrom;
 
 /// Determines the size of [PoolRewardManager]
 pub const MAX_REWARDS: usize = 50;
@@ -74,6 +75,7 @@ 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
@@ -92,6 +94,10 @@ pub struct PoolReward {
     /// Monotonically increasing time taken from clock sysvar.
     pub start_time_secs: u64,
     /// For how long (since start time) will this reward be releasing tokens.
+    ///
+    /// # Reward cancellation
+    ///
+    /// Is cut short if the reward is cancelled.
     pub duration_secs: u32,
     /// Total token amount to distribute.
     /// The token account that holds the rewards holds at least this much in
@@ -169,6 +175,65 @@ pub struct UserReward {
 }
 
 impl PoolRewardManager {
+    /// Sets the duration of the pool reward to now.
+    /// Returns the amount of unallocated rewards and the vault they are in.
+    pub fn cancel_pool_reward(
+        &mut self,
+        pool_reward_index: usize,
+        clock: &Clock,
+    ) -> Result<(Pubkey, u64), ProgramError> {
+        self.update(clock)?;
+
+        let Some(PoolRewardSlot::Occupied(pool_reward)) =
+            self.pool_rewards.get_mut(pool_reward_index)
+        else {
+            msg!("Cannot cancel a non-existent pool reward");
+            return Err(ProgramError::InvalidArgument);
+        };
+
+        if pool_reward.has_ended(clock) {
+            msg!("Cannot cancel a pool reward that has already ended");
+            return Err(LendingError::InvalidAccountInput.into());
+        }
+
+        let since_start_secs = clock.unix_timestamp as u64 - pool_reward.start_time_secs;
+        let unlocked_rewards = Decimal::from(pool_reward.total_rewards)
+            .try_mul(Decimal::from(since_start_secs))?
+            .try_div(Decimal::from(pool_reward.duration_secs as u64))?
+            .try_floor_u64()?;
+        let remaining_rewards = pool_reward.total_rewards - unlocked_rewards;
+
+        pool_reward.duration_secs =
+            u32::try_from(since_start_secs).expect("New duration to be strictly shorter");
+
+        Ok((pool_reward.vault, remaining_rewards))
+    }
+
+    /// Closes a pool reward if it has been cancelled before.
+    /// Returns the vault the rewards are in.
+    pub fn close_pool_reward(&mut self, pool_reward_index: usize) -> Result<Pubkey, ProgramError> {
+        let Some(PoolRewardSlot::Occupied(pool_reward)) =
+            self.pool_rewards.get_mut(pool_reward_index)
+        else {
+            msg!("Cannot close a non-existent pool reward");
+            return Err(ProgramError::InvalidArgument);
+        };
+
+        if pool_reward.num_user_reward_managers > 0 {
+            msg!("Cannot close a pool reward with active user reward managers");
+            return Err(LendingError::InvalidAccountInput.into());
+        }
+
+        let vault = pool_reward.vault;
+
+        self.pool_rewards[pool_reward_index] = PoolRewardSlot::Vacant {
+            last_pool_reward_id: pool_reward.id,
+            has_been_vacated_in_this_tx: true,
+        };
+
+        Ok(vault)
+    }
+
     /// 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;
@@ -219,19 +284,70 @@ impl PoolRewardManager {
     }
 }
 
-enum CreatingNewUserRewardManager {
+/// When creating a new [UserRewardManager] we need to know whether we should
+/// populate it with rewards or not.
+pub enum CreatingNewUserRewardManager {
     /// If we are creating a [UserRewardManager] then we want to populate it.
     Yes,
+    /// If we are updating an existing [UserRewardManager] then we don't want
+    /// to populate it.
     No,
 }
 
 impl UserRewardManager {
+    /// Claims all rewards that the user has earned.
+    /// Returns how many tokens should be transferred to the user.
+    ///
+    /// # Note
+    /// Errors if there is no pool reward with this vault.
+    pub fn claim_rewards(
+        &mut self,
+        pool_reward_manager: &mut PoolRewardManager,
+        vault: Pubkey,
+        clock: &Clock,
+    ) -> Result<u64, ProgramError> {
+        let (pool_reward_index, pool_reward) = pool_reward_manager
+            .pool_rewards
+            .iter()
+            .enumerate()
+            .find_map(move |(index, slot)| match slot {
+                PoolRewardSlot::Occupied(pool_reward) if pool_reward.vault == vault => {
+                    Some((index, pool_reward))
+                }
+                _ => None,
+            })
+            .ok_or(LendingError::NoPoolRewardMatches)?;
+
+        let Some(user_reward) = self.rewards.iter_mut().find(|user_reward| {
+            user_reward.pool_reward_index == pool_reward_index
+                && user_reward.pool_reward_id == pool_reward.id
+        }) else {
+            // User is not tracking this reward, nothing to claim.
+            // Let's be graceful and make this a no-op.
+            // Prevents failures when multiple parties crank rewards.
+            return Ok(0);
+        };
+
+        let to_claim = user_reward.withdraw_earned_rewards()?;
+
+        if pool_reward.has_ended(clock) {
+            // If pool reward has ended then it will be removed from the user
+            // reward manager in the next update call.
+            //
+            // We could also complicate matters by doing updates in place when
+            // needed to save on CU if necessary.
+            self.update(pool_reward_manager, clock, CreatingNewUserRewardManager::No)?;
+        }
+
+        Ok(to_claim)
+    }
+
     /// Should be updated before any interaction with rewards.
     ///
     /// # Assumption
     /// Invoker has checked that this [PoolRewardManager] matches the
     /// [UserRewardManager].
-    fn update(
+    pub fn update(
         &mut self,
         pool_reward_manager: &mut PoolRewardManager,
         clock: &Clock,
@@ -257,23 +373,23 @@ impl UserRewardManager {
                 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);
 
+            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;
+
             match maybe_user_reward {
                 Some((user_reward_index, user_reward))
-                    if has_ended && user_reward.earned_rewards == Decimal::zero() =>
+                    if has_ended && user_reward.earned_rewards.try_floor_u64()? == 0 =>
                 {
                     // 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/
+                    // SAFETY: We got the index from enumeration, so must exist.
                     self.rewards.swap_remove(user_reward_index);
                     pool_reward.num_user_reward_managers -= 1;
                 }
@@ -338,6 +454,12 @@ impl PoolReward {
     /// - `num_user_reward_managers``
     /// - `cumulative_rewards_per_share``
     const TAIL_LEN: usize = 8 + 4 + 8 + 8 + 16;
+
+    /// Returns whether the reward has ended.
+    pub fn has_ended(&self, clock: &Clock) -> bool {
+        let end_time_secs = self.start_time_secs + self.duration_secs as u64;
+        clock.unix_timestamp as u64 > end_time_secs
+    }
 }
 
 impl PoolRewardId {
@@ -452,6 +574,7 @@ impl Pack for PoolRewardManager {
             let offset = 8 + 8 + index * PoolReward::LEN;
             let raw_pool_reward_head = array_ref![input, offset, PoolReward::HEAD_LEN];
 
+            #[allow(clippy::ptr_offset_with_cast)]
             let (src_id, src_vault) =
                 array_refs![raw_pool_reward_head, PoolRewardId::LEN, PUBKEY_BYTES];
 
@@ -523,6 +646,20 @@ impl UserReward {
     /// - packed [Decimal]
     /// - packed [Decimal]
     pub const LEN: usize = 1 + PoolRewardId::LEN + 16 + 16;
+
+    /// Removes all earned rewards from [Self] and returns them.
+    ///
+    /// # Note
+    /// Decimals are truncated to u64, dust is kept.
+    fn withdraw_earned_rewards(&mut self) -> Result<u64, ProgramError> {
+        let reward_amount = self.earned_rewards.try_floor_u64()?;
+
+        if reward_amount > 0 {
+            self.earned_rewards = self.earned_rewards.try_sub(reward_amount.into())?;
+        }
+
+        Ok(reward_amount)
+    }
 }
 
 impl UserRewardManager {
@@ -600,8 +737,10 @@ impl UserRewardManager {
     }
 
     pub(crate) fn unpack_from_slice(input: &[u8]) -> Result<Self, ProgramError> {
+        #[allow(clippy::ptr_offset_with_cast)]
         let raw_user_reward_manager_head = array_ref![input, 0, UserRewardManager::HEAD_LEN];
 
+        #[allow(clippy::ptr_offset_with_cast)]
         let (src_reserve, src_share, src_last_update_time_secs, src_user_rewards_len) = array_refs![
             raw_user_reward_manager_head,
             PUBKEY_BYTES,
@@ -620,6 +759,7 @@ impl UserRewardManager {
             let offset = Self::HEAD_LEN + index * UserReward::LEN;
             let raw_user_reward = array_ref![input, offset, UserReward::LEN];
 
+            #[allow(clippy::ptr_offset_with_cast)]
             let (
                 src_pool_reward_index,
                 src_pool_reward_id,
diff --git a/token-lending/sdk/src/state/obligation.rs b/token-lending/sdk/src/state/obligation.rs
index 8696853932b..94fcd8eccac 100644
--- a/token-lending/sdk/src/state/obligation.rs
+++ b/token-lending/sdk/src/state/obligation.rs
@@ -313,6 +313,38 @@ impl Obligation {
             .iter()
             .position(|liquidity| liquidity.borrow_reserve == borrow_reserve)
     }
+
+    /// Find whether the reserve is a deposit or borrow
+    pub fn find_position_kind(&self, reserve: Pubkey) -> Result<PositionKind, ProgramError> {
+        if self
+            .deposits
+            .iter()
+            .any(|collateral| collateral.deposit_reserve == reserve)
+        {
+            return Ok(PositionKind::Deposit);
+        }
+
+        if self
+            .borrows
+            .iter()
+            .any(|liquidity| liquidity.borrow_reserve == reserve)
+        {
+            return Ok(PositionKind::Borrow);
+        }
+
+        msg!("Reserve not found in obligation");
+        Err(LendingError::InvalidAccountInput.into())
+    }
+
+    /// Returns [UserRewardManager] for the given reserve
+    pub fn find_user_reward_manager_mut(
+        &mut self,
+        reserve: Pubkey,
+    ) -> Option<&mut UserRewardManager> {
+        self.user_reward_managers
+            .iter_mut()
+            .find(|user_reward_manager| user_reward_manager.reserve == reserve)
+    }
 }
 
 /// Initialize an obligation
@@ -469,7 +501,7 @@ impl Obligation {
     /// Unpacks from slice but returns an error if the account is already
     /// initialized.
     pub fn unpack_uninitialized(input: &[u8]) -> Result<Self, ProgramError> {
-        let account = Self::unpack_unchecked(&input)?;
+        let account = Self::unpack_unchecked(input)?;
         if account.is_initialized() {
             Err(LendingError::AlreadyInitialized.into())
         } else {
diff --git a/token-lending/sdk/src/state/reserve.rs b/token-lending/sdk/src/state/reserve.rs
index c447e9b03a2..17d85438f23 100644
--- a/token-lending/sdk/src/state/reserve.rs
+++ b/token-lending/sdk/src/state/reserve.rs
@@ -1698,6 +1698,7 @@ impl Pack for Reserve {
         };
 
         let input_v2_1_0 = array_ref![input, RESERVE_LEN_V2_0_2, PoolRewardManager::LEN * 2];
+        #[allow(clippy::ptr_offset_with_cast)]
         let (input_for_borrows_pool_reward_manager, input_for_deposits_pool_reward_manager) =
             array_refs![input_v2_1_0, PoolRewardManager::LEN, PoolRewardManager::LEN];
 

From 590c58c95442f056457b3188568c4bb13c7c3d07 Mon Sep 17 00:00:00 2001
From: vanity mnm <vanity@solend.fi>
Date: Fri, 28 Mar 2025 20:55:35 +0100
Subject: [PATCH 08/14] Adding logic for adding pool reward ix (#206)

---
 .../liquidity_mining/add_pool_reward.rs       | 89 ++++---------------
 .../liquidity_mining/cancel_pool_reward.rs    | 10 +--
 .../liquidity_mining/claim_user_reward.rs     | 14 +--
 .../liquidity_mining/close_pool_reward.rs     |  9 +-
 token-lending/sdk/src/error.rs                |  3 +
 .../sdk/src/state/liquidity_mining.rs         | 76 +++++++++++++++-
 token-lending/sdk/src/state/reserve.rs        | 11 +++
 7 files changed, 117 insertions(+), 95 deletions(-)

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

From 4e58ca78256da6aed7e35c5f6ba2727a6f5a4780 Mon Sep 17 00:00:00 2001
From: vanity mnm <vanity@solend.fi>
Date: Sun, 6 Apr 2025 16:53:34 +0200
Subject: [PATCH 09/14] [Liquidity Mining] Obligation realloc (8) (#207)

* Improving Reserve pack, unpack in ixs

* Progress with realloc tests

* Adding tests for shares tracking

* Removing unused comment

* More tests

* Addressing clippy review

* Downgrading package lock

* All BPF tests pass

* Fixing repay tests in obligation

* CU units are reverted back to their original values

* Emit program logs on CI

* Adding Suilend test 'test_pool_reward_manager_basic'

* Adding Solend test 'test_pool_reward_manager_multiple_rewards'

* Adding more Suilend tests

* Copying remaining liq. mining tests from Suilend

* Increasing CU budget allowances

* Fixing ix mutable borrows
---
 Cargo.lock                                    |  26 +-
 Cargo.toml                                    |   1 +
 ci/cargo-test-bpf.sh                          |   6 +-
 coverage.sh                                   |   5 +-
 token-lending/program/Cargo.toml              |  19 +-
 token-lending/program/src/processor.rs        | 506 +++++++------
 .../program/src/processor/account_borrow.rs   | 216 ++++++
 .../program/src/processor/liquidity_mining.rs |  25 +-
 .../liquidity_mining/add_pool_reward.rs       |  24 +-
 .../liquidity_mining/cancel_pool_reward.rs    |  24 +-
 .../liquidity_mining/claim_user_reward.rs     |  93 ++-
 .../liquidity_mining/close_pool_reward.rs     |  18 +-
 .../program/tests/attributed_borrows.rs       |  19 +-
 .../tests/borrow_obligation_liquidity.rs      |  52 +-
 token-lending/program/tests/borrow_weight.rs  |   2 +-
 .../tests/deposit_obligation_collateral.rs    |  49 +-
 ...rve_liquidity_and_obligation_collateral.rs |  46 +-
 token-lending/program/tests/forgive_debt.rs   |   9 +-
 .../tests/helpers/solend_program_test.rs      |  58 +-
 .../program/tests/init_obligation.rs          |   2 +-
 .../program/tests/isolated_tier_assets.rs     |  37 +-
 .../program/tests/liquidate_obligation.rs     |   2 +-
 ...uidate_obligation_and_redeem_collateral.rs |  59 +-
 .../program/tests/outflow_rate_limits.rs      |   2 +-
 .../tests/repay_obligation_liquidity.rs       |  31 +-
 token-lending/program/tests/two_prices.rs     |   6 +-
 .../tests/withdraw_obligation_collateral.rs   |  74 +-
 ...ollateral_and_redeem_reserve_collateral.rs |  22 +-
 token-lending/sdk/Cargo.toml                  |   1 +
 token-lending/sdk/src/instruction.rs          |   6 +-
 .../sdk/src/state/liquidity_mining.rs         | 710 ++++++++++++++++--
 token-lending/sdk/src/state/obligation.rs     |  81 +-
 token-lending/sdk/src/state/rate_limiter.rs   |   2 +-
 33 files changed, 1757 insertions(+), 476 deletions(-)
 create mode 100644 token-lending/program/src/processor/account_borrow.rs

diff --git a/Cargo.lock b/Cargo.lock
index 37fe074561d..75fb11b8532 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1469,6 +1469,12 @@ dependencies = [
  "zeroize",
 ]
 
+[[package]]
+name = "diff"
+version = "0.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
+
 [[package]]
 name = "digest"
 version = "0.9.0"
@@ -3276,6 +3282,16 @@ version = "0.2.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
 
+[[package]]
+name = "pretty_assertions"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d"
+dependencies = [
+ "diff",
+ "yansi 1.0.1",
+]
+
 [[package]]
 name = "proc-macro-crate"
 version = "0.1.5"
@@ -3356,7 +3372,7 @@ dependencies = [
  "quote 1.0.36",
  "syn 1.0.109",
  "version_check",
- "yansi",
+ "yansi 0.5.1",
 ]
 
 [[package]]
@@ -5422,6 +5438,7 @@ dependencies = [
  "bytemuck",
  "log",
  "oracles",
+ "pretty_assertions",
  "proptest",
  "pyth-sdk-solana",
  "pyth-solana-receiver-sdk",
@@ -5472,6 +5489,7 @@ dependencies = [
  "log",
  "num-derive 0.3.3",
  "num-traits",
+ "pretty_assertions",
  "proptest",
  "rand 0.8.5",
  "serde",
@@ -7186,6 +7204,12 @@ version = "0.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
 
+[[package]]
+name = "yansi"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
+
 [[package]]
 name = "yasna"
 version = "0.5.2"
diff --git a/Cargo.toml b/Cargo.toml
index 829068fcb4e..40d3135d61f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,4 +1,5 @@
 [workspace]
+resolver = "2"
 members = [
   "token-lending/cli",
   "token-lending/program",
diff --git a/ci/cargo-test-bpf.sh b/ci/cargo-test-bpf.sh
index ea30a02f18a..b40acbeca45 100755
--- a/ci/cargo-test-bpf.sh
+++ b/ci/cargo-test-bpf.sh
@@ -8,7 +8,7 @@ source ./ci/solana-version.sh
 
 export RUSTFLAGS="-D warnings"
 export RUSTBACKTRACE=1
-
+export RUST_LOG="warn,tarpc=error,solana_runtime::message_processor=debug"
 
 usage() {
   exitcode=0
@@ -33,11 +33,11 @@ run_dir=$(pwd)
 if [[ -d $run_dir/program ]]; then
   # Build/test just one BPF program
   cd $run_dir/program
-  RUST_LOG="error" cargo +"$rust_stable" test-bpf --features test-bpf -j 1 -- --nocapture
+  cargo +"$rust_stable" test-bpf --features test-bpf -j 1 -- --nocapture
 else
   # Build/test all BPF programs
   for directory in $(ls -d $run_dir/*/); do
     cd $directory
-    RUST_LOG="error" cargo +"$rust_stable" test-bpf --features test-bpf -j 1 -- --nocapture
+    cargo +"$rust_stable" test-bpf --features test-bpf -j 1 -- --nocapture
   done
 fi
diff --git a/coverage.sh b/coverage.sh
index 3bef941b59a..30e23283fce 100755
--- a/coverage.sh
+++ b/coverage.sh
@@ -21,10 +21,11 @@ RUST_LOG="error" CARGO_INCREMENTAL=0 RUSTFLAGS='-Cinstrument-coverage' LLVM_PROF
 # generate report
 mkdir -p target/coverage/html
 
-grcov . --binary-path ./target/debug/deps/ -s . -t html --branch --ignore-not-existing --ignore '../*' --ignore "/*" -o target/coverage/html
+grcov . --branch --binary-path ./target/debug/deps/ -s . -t html --ignore-not-existing --ignore '../*' --ignore "/*" -o target/coverage/html
 
-grcov . --binary-path ./target/debug/deps/ -s . -t lcov --branch --ignore-not-existing --ignore '../*' --ignore "/*" -o target/coverage/tests.lcov
+grcov . --branch --binary-path ./target/debug/deps/ -s . -t lcov --ignore-not-existing --ignore '../*' --ignore "/*" -o target/coverage/tests.lcov
 
 # cleanup
 rm *.profraw || true
 rm **/**/*.profraw || true
+
diff --git a/token-lending/program/Cargo.toml b/token-lending/program/Cargo.toml
index 44661e3a6ac..ae8a379dd44 100644
--- a/token-lending/program/Cargo.toml
+++ b/token-lending/program/Cargo.toml
@@ -8,6 +8,8 @@ license = "Apache-2.0"
 edition = "2018"
 
 [features]
+custom-heap = []
+custom-panic = []
 no-entrypoint = []
 test-bpf = []
 
@@ -24,22 +26,23 @@ static_assertions = "1.1.0"
 [dev-dependencies]
 anchor-lang = "0.28.0"
 assert_matches = "1.5.0"
-bytemuck = "1.5.1"
 base64 = "0.13"
-log = "0.4.14"
-proptest = "1.0"
-solana-program-test = "=1.16.20"
-solana-sdk = "=1.16.20"
-serde = ">=1.0.140"
-serde_yaml = "0.8"
-thiserror = "1.0"
 bincode = "1.3.3"
 borsh = "0.10.3"
+bytemuck = "1.5.1"
+log = "0.4.14"
+pretty_assertions = "1.4.1"
+proptest = "1.0"
 pyth-sdk-solana = "0.8.0"
 pyth-solana-receiver-sdk = "0.3.0"
+serde = ">=1.0.140"
+serde_yaml = "0.8"
+solana-program-test = "=1.16.20"
+solana-sdk = "=1.16.20"
 switchboard-on-demand = "0.1.12"
 switchboard-program = "0.2.0"
 switchboard-v2 = "0.1.3"
+thiserror = "1.0"
 
 [lib]
 crate-type = ["cdylib", "lib"]
diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs
index ac16c902381..55e58f04587 100644
--- a/token-lending/program/src/processor.rs
+++ b/token-lending/program/src/processor.rs
@@ -1,5 +1,6 @@
 //! Program state processor
 
+mod account_borrow;
 mod liquidity_mining;
 
 use crate::state::Bonus;
@@ -15,6 +16,7 @@ use crate::{
         ReserveCollateral, ReserveConfig, ReserveLiquidity,
     },
 };
+use account_borrow::ReserveBorrow;
 use bytemuck::bytes_of;
 use oracles::get_single_price;
 use oracles::get_single_price_unchecked;
@@ -37,6 +39,7 @@ use solana_program::{
     sysvar::instructions::{load_current_index_checked, load_instruction_at_checked},
     sysvar::{clock::Clock, rent::Rent, Sysvar},
 };
+use solend_sdk::state::PositionKind;
 use solend_sdk::{
     math::SaturatingSub,
     state::{LendingMarketMetadata, RateLimiter, RateLimiterConfig, ReserveType},
@@ -470,7 +473,6 @@ fn process_init_reserve(
     });
 
     let collateral_amount = reserve.deposit_liquidity(liquidity_amount)?;
-    Reserve::pack(reserve, &mut reserve_info.data.borrow_mut())?;
 
     spl_token_init_account(TokenInitializeAccountParams {
         account: reserve_liquidity_supply_info.clone(),
@@ -530,7 +532,7 @@ fn process_init_reserve(
         token_program: token_program_id.clone(),
     })?;
 
-    Ok(())
+    Reserve::pack(reserve, &mut reserve_info.data.borrow_mut())
 }
 
 fn validate_extra_oracle(
@@ -573,29 +575,27 @@ fn process_refresh_reserve(program_id: &Pubkey, accounts: &[AccountInfo]) -> Pro
     let clock = &Clock::get()?;
 
     let extra_oracle_account_info = next_account_info(account_info_iter).ok();
+
+    let mut reserve = ReserveBorrow::new_mut(program_id, reserve_info)?;
+
     _refresh_reserve(
-        program_id,
-        reserve_info,
+        &mut reserve,
         pyth_price_info,
         Some(switchboard_feed_info),
         clock,
         extra_oracle_account_info,
-    )
+    )?;
+
+    Ok(())
 }
 
 fn _refresh_reserve<'a>(
-    program_id: &Pubkey,
-    reserve_info: &AccountInfo<'a>,
+    reserve: &mut ReserveBorrow,
     pyth_price_info: &AccountInfo<'a>,
     switchboard_feed_info: Option<&AccountInfo<'a>>,
     clock: &Clock,
     extra_oracle_account_info: Option<&AccountInfo<'a>>,
 ) -> ProgramResult {
-    let mut reserve = Box::new(Reserve::unpack(&reserve_info.data.borrow())?);
-    if reserve_info.owner != program_id {
-        msg!("Reserve provided is not owned by the lending program");
-        return Err(LendingError::InvalidAccountOwner.into());
-    }
     if &reserve.liquidity.pyth_oracle_pubkey != pyth_price_info.key {
         msg!("Reserve liquidity pyth oracle does not match the reserve liquidity pyth oracle provided");
         return Err(LendingError::InvalidAccountInput.into());
@@ -647,27 +647,16 @@ fn _refresh_reserve<'a>(
         reserve.liquidity.smoothed_market_price = reserve.liquidity.market_price;
     }
 
-    Reserve::pack(*reserve, &mut reserve_info.data.borrow_mut())?;
+    _refresh_reserve_interest(reserve, clock)?;
 
-    _refresh_reserve_interest(program_id, reserve_info, clock)
+    Ok(())
 }
 
 /// Lite version of refresh_reserve that should be used when the oracle price doesn't need to be updated
 /// BE CAREFUL WHEN USING THIS
-fn _refresh_reserve_interest(
-    program_id: &Pubkey,
-    reserve_info: &AccountInfo<'_>,
-    clock: &Clock,
-) -> ProgramResult {
-    let mut reserve = Box::new(Reserve::unpack(&reserve_info.data.borrow())?);
-    if reserve_info.owner != program_id {
-        msg!("Reserve provided is not owned by the lending program");
-        return Err(LendingError::InvalidAccountOwner.into());
-    }
-
+fn _refresh_reserve_interest(reserve: &mut ReserveBorrow, clock: &Clock) -> ProgramResult {
     reserve.accrue_interest(clock.slot)?;
     reserve.last_update.update_slot(clock.slot);
-    Reserve::pack(*reserve, &mut reserve_info.data.borrow_mut())?;
 
     Ok(())
 }
@@ -694,13 +683,15 @@ fn process_deposit_reserve_liquidity(
     let clock = &Clock::get()?;
     let token_program_id = next_account_info(account_info_iter)?;
 
-    _refresh_reserve_interest(program_id, reserve_info, clock)?;
+    let mut reserve = ReserveBorrow::new_mut(program_id, reserve_info)?;
+
+    _refresh_reserve_interest(&mut reserve, clock)?;
     _deposit_reserve_liquidity(
         program_id,
         liquidity_amount,
         source_liquidity_info,
         destination_collateral_info,
-        reserve_info,
+        &mut reserve,
         reserve_liquidity_supply_info,
         reserve_collateral_mint_info,
         lending_market_info,
@@ -719,7 +710,7 @@ fn _deposit_reserve_liquidity<'a>(
     liquidity_amount: u64,
     source_liquidity_info: &AccountInfo<'a>,
     destination_collateral_info: &AccountInfo<'a>,
-    reserve_info: &AccountInfo<'a>,
+    reserve: &mut ReserveBorrow,
     reserve_liquidity_supply_info: &AccountInfo<'a>,
     reserve_collateral_mint_info: &AccountInfo<'a>,
     lending_market_info: &AccountInfo<'a>,
@@ -737,11 +728,6 @@ fn _deposit_reserve_liquidity<'a>(
         msg!("Lending market token program does not match the token program provided");
         return Err(LendingError::InvalidTokenProgram.into());
     }
-    let mut reserve = Box::new(Reserve::unpack(&reserve_info.data.borrow())?);
-    if reserve_info.owner != program_id {
-        msg!("Reserve provided is not owned by the lending program");
-        return Err(LendingError::InvalidAccountOwner.into());
-    }
     if &reserve.lending_market != lending_market_info.key {
         msg!("Reserve lending market does not match the lending market provided");
         return Err(LendingError::InvalidAccountInput.into());
@@ -792,7 +778,6 @@ fn _deposit_reserve_liquidity<'a>(
 
     let collateral_amount = reserve.deposit_liquidity(liquidity_amount)?;
     reserve.last_update.mark_stale();
-    Reserve::pack(*reserve, &mut reserve_info.data.borrow_mut())?;
 
     spl_token_transfer(TokenTransferParams {
         source: source_liquidity_info.clone(),
@@ -837,12 +822,14 @@ fn process_redeem_reserve_collateral(
     let clock = &Clock::get()?;
     let token_program_id = next_account_info(account_info_iter)?;
 
+    let mut reserve = ReserveBorrow::new_mut(program_id, reserve_info)?;
+
     _redeem_reserve_collateral(
         program_id,
         collateral_amount,
         source_collateral_info,
         destination_liquidity_info,
-        reserve_info,
+        &mut reserve,
         reserve_collateral_mint_info,
         reserve_liquidity_supply_info,
         lending_market_info,
@@ -852,10 +839,8 @@ fn process_redeem_reserve_collateral(
         token_program_id,
         true,
     )?;
-    let mut reserve = Box::new(Reserve::unpack(&reserve_info.data.borrow())?);
-    reserve.last_update.mark_stale();
-    Reserve::pack(*reserve, &mut reserve_info.data.borrow_mut())?;
 
+    reserve.last_update.mark_stale();
     Ok(())
 }
 
@@ -865,7 +850,7 @@ fn _redeem_reserve_collateral<'a>(
     collateral_amount: u64,
     source_collateral_info: &AccountInfo<'a>,
     destination_liquidity_info: &AccountInfo<'a>,
-    reserve_info: &AccountInfo<'a>,
+    reserve: &mut ReserveBorrow,
     reserve_collateral_mint_info: &AccountInfo<'a>,
     reserve_liquidity_supply_info: &AccountInfo<'a>,
     lending_market_info: &AccountInfo<'a>,
@@ -885,11 +870,6 @@ fn _redeem_reserve_collateral<'a>(
         return Err(LendingError::InvalidTokenProgram.into());
     }
 
-    let mut reserve = Box::new(Reserve::unpack(&reserve_info.data.borrow())?);
-    if reserve_info.owner != program_id {
-        msg!("Reserve provided is not owned by the lending program");
-        return Err(LendingError::InvalidAccountOwner.into());
-    }
     if &reserve.lending_market != lending_market_info.key {
         msg!("Reserve lending market does not match the lending market provided");
         return Err(LendingError::InvalidAccountInput.into());
@@ -952,7 +932,6 @@ fn _redeem_reserve_collateral<'a>(
     }
 
     reserve.last_update.mark_stale();
-    Reserve::pack(*reserve, &mut reserve_info.data.borrow_mut())?;
     LendingMarket::pack(lending_market, &mut lending_market_info.data.borrow_mut())?;
 
     spl_token_burn(TokenBurnParams {
@@ -1042,14 +1021,9 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) ->
 
     for (index, collateral) in obligation.deposits.iter_mut().enumerate() {
         let deposit_reserve_info = next_account_info(account_info_iter)?;
-        if deposit_reserve_info.owner != program_id {
-            msg!(
-                "Deposit reserve provided for collateral {} is not owned by the lending program",
-                index
-            );
-            return Err(LendingError::InvalidAccountOwner.into());
-        }
-        if collateral.deposit_reserve != *deposit_reserve_info.key {
+        let deposit_reserve = ReserveBorrow::new(program_id, deposit_reserve_info)?;
+
+        if collateral.deposit_reserve != deposit_reserve.key() {
             msg!(
                 "Deposit reserve of collateral {} does not match the deposit reserve provided",
                 index
@@ -1057,7 +1031,6 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) ->
             return Err(LendingError::InvalidAccountInput.into());
         }
 
-        let deposit_reserve = Box::new(Reserve::unpack(&deposit_reserve_info.data.borrow())?);
         if deposit_reserve.last_update.is_stale(clock.slot)? {
             msg!(
                 "Deposit reserve provided for collateral {} is stale and must be refreshed in the current slot",
@@ -1094,14 +1067,9 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) ->
     let mut max_borrow_weight = None;
     for (index, liquidity) in obligation.borrows.iter_mut().enumerate() {
         let borrow_reserve_info = next_account_info(account_info_iter)?;
-        if borrow_reserve_info.owner != program_id {
-            msg!(
-                "Borrow reserve provided for liquidity {} is not owned by the lending program",
-                index
-            );
-            return Err(LendingError::InvalidAccountOwner.into());
-        }
-        if liquidity.borrow_reserve != *borrow_reserve_info.key {
+        let borrow_reserve = ReserveBorrow::new(program_id, borrow_reserve_info)?;
+
+        if liquidity.borrow_reserve != borrow_reserve.key() {
             msg!(
                 "Borrow reserve of liquidity {} does not match the borrow reserve provided",
                 index
@@ -1109,7 +1077,6 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) ->
             return Err(LendingError::InvalidAccountInput.into());
         }
 
-        let borrow_reserve = Box::new(Reserve::unpack(&borrow_reserve_info.data.borrow())?);
         if borrow_reserve.last_update.is_stale(clock.slot)? {
             msg!(
                 "Borrow reserve provided for liquidity {} is stale and must be refreshed in the current slot",
@@ -1180,7 +1147,8 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) ->
 
     obligation.last_update.update_slot(clock.slot);
 
-    let (_, close_exceeded) = update_borrow_attribution_values(&mut obligation, &accounts[1..])?;
+    let (_, close_exceeded) =
+        update_borrow_attribution_values(program_id, &mut obligation, &accounts[1..])?;
     if close_exceeded.is_none() {
         obligation.closeable = false;
     }
@@ -1198,6 +1166,7 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) ->
         .borrows
         .retain(|liquidity| liquidity.borrowed_amount_wads > Decimal::zero());
 
+    realloc_obligation_if_necessary(&obligation, obligation_info)?;
     Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?;
 
     Ok(())
@@ -1211,8 +1180,13 @@ fn process_refresh_obligation(program_id: &Pubkey, accounts: &[AccountInfo]) ->
 /// - the obligation's deposited_value must be refreshed
 /// - the obligation's true_borrowed_value must be refreshed
 ///
-/// Note that this function packs and unpacks deposit reserves.
+/// # Important
+///
+/// This function packs and unpacks deposit reserves.
+/// This means that any [ReserveBorrow] whose data might be processed in this
+/// function needs to be released.
 fn update_borrow_attribution_values(
+    program_id: &Pubkey,
     obligation: &mut Obligation,
     deposit_reserve_infos: &[AccountInfo],
 ) -> Result<(Option<Pubkey>, Option<Pubkey>), ProgramError> {
@@ -1223,7 +1197,7 @@ fn update_borrow_attribution_values(
 
     for collateral in obligation.deposits.iter_mut() {
         let deposit_reserve_info = next_account_info(deposit_infos)?;
-        let mut deposit_reserve = Reserve::unpack(&deposit_reserve_info.data.borrow())?;
+        let mut deposit_reserve = ReserveBorrow::new_mut(program_id, deposit_reserve_info)?;
 
         // sanity check
         if collateral.deposit_reserve != *deposit_reserve_info.key {
@@ -1258,8 +1232,6 @@ fn update_borrow_attribution_values(
         {
             close_exceeded = Some(*deposit_reserve_info.key);
         }
-
-        Reserve::pack(deposit_reserve, &mut deposit_reserve_info.data.borrow_mut())?;
     }
 
     Ok((open_exceeded, close_exceeded))
@@ -1286,13 +1258,16 @@ fn process_deposit_obligation_collateral(
     let user_transfer_authority_info = next_account_info(account_info_iter)?;
     let clock = &Clock::get()?;
     let token_program_id = next_account_info(account_info_iter)?;
-    _refresh_reserve_interest(program_id, deposit_reserve_info, clock)?;
+
+    let mut deposit_reserve = ReserveBorrow::new_mut(program_id, deposit_reserve_info)?;
+
+    _refresh_reserve_interest(&mut deposit_reserve, clock)?;
     _deposit_obligation_collateral(
         program_id,
         collateral_amount,
         source_collateral_info,
         destination_collateral_info,
-        deposit_reserve_info,
+        &mut deposit_reserve,
         obligation_info,
         lending_market_info,
         obligation_owner_info,
@@ -1300,9 +1275,8 @@ fn process_deposit_obligation_collateral(
         clock,
         token_program_id,
     )?;
-    let mut reserve = Box::new(Reserve::unpack(&deposit_reserve_info.data.borrow())?);
-    reserve.last_update.mark_stale();
-    Reserve::pack(*reserve, &mut deposit_reserve_info.data.borrow_mut())?;
+
+    deposit_reserve.last_update.mark_stale();
     Ok(())
 }
 
@@ -1312,7 +1286,7 @@ fn _deposit_obligation_collateral<'a>(
     collateral_amount: u64,
     source_collateral_info: &AccountInfo<'a>,
     destination_collateral_info: &AccountInfo<'a>,
-    deposit_reserve_info: &AccountInfo<'a>,
+    deposit_reserve: &mut ReserveBorrow,
     obligation_info: &AccountInfo<'a>,
     lending_market_info: &AccountInfo<'a>,
     obligation_owner_info: &AccountInfo<'a>,
@@ -1330,11 +1304,6 @@ fn _deposit_obligation_collateral<'a>(
         return Err(LendingError::InvalidTokenProgram.into());
     }
 
-    let deposit_reserve = Box::new(Reserve::unpack(&deposit_reserve_info.data.borrow())?);
-    if deposit_reserve_info.owner != program_id {
-        msg!("Deposit reserve provided is not owned by the lending program");
-        return Err(LendingError::InvalidAccountOwner.into());
-    }
     if &deposit_reserve.lending_market != lending_market_info.key {
         msg!("Deposit reserve lending market does not match the lending market provided");
         return Err(LendingError::InvalidAccountInput.into());
@@ -1372,11 +1341,24 @@ fn _deposit_obligation_collateral<'a>(
         return Err(LendingError::InvalidSigner.into());
     }
 
-    obligation
-        .find_or_add_collateral_to_deposits(*deposit_reserve_info.key)?
-        .deposit(collateral_amount)?;
+    let collateral = obligation.find_or_add_collateral_to_deposits(deposit_reserve.key())?;
+    collateral.deposit(collateral_amount)?;
+
+    // liq. mining
+    let new_share = collateral.deposited_amount;
+    obligation.user_reward_managers.set_share(
+        deposit_reserve.key(),
+        PositionKind::Deposit,
+        &mut deposit_reserve.deposits_pool_reward_manager,
+        new_share,
+        clock,
+    )?;
+
     obligation.last_update.mark_stale();
+
+    realloc_obligation_if_necessary(&obligation, obligation_info)?;
     Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?;
+
     spl_token_transfer(TokenTransferParams {
         source: source_collateral_info.clone(),
         destination: destination_collateral_info.clone(),
@@ -1416,13 +1398,15 @@ fn process_deposit_reserve_liquidity_and_obligation_collateral(
     let clock = &Clock::get()?;
     let token_program_id = next_account_info(account_info_iter)?;
 
-    _refresh_reserve_interest(program_id, reserve_info, clock)?;
+    let mut reserve = ReserveBorrow::new_mut(program_id, reserve_info)?;
+
+    _refresh_reserve_interest(&mut reserve, clock)?;
     let collateral_amount = _deposit_reserve_liquidity(
         program_id,
         liquidity_amount,
         source_liquidity_info,
         user_collateral_info,
-        reserve_info,
+        &mut reserve,
         reserve_liquidity_supply_info,
         reserve_collateral_mint_info,
         lending_market_info,
@@ -1431,13 +1415,13 @@ fn process_deposit_reserve_liquidity_and_obligation_collateral(
         clock,
         token_program_id,
     )?;
-    _refresh_reserve_interest(program_id, reserve_info, clock)?;
+    _refresh_reserve_interest(&mut reserve, clock)?;
     _deposit_obligation_collateral(
         program_id,
         collateral_amount,
         user_collateral_info,
         destination_collateral_info,
-        reserve_info,
+        &mut reserve,
         obligation_info,
         lending_market_info,
         obligation_owner_info,
@@ -1445,11 +1429,9 @@ fn process_deposit_reserve_liquidity_and_obligation_collateral(
         clock,
         token_program_id,
     )?;
+
     // mark the reserve as stale to make sure no weird bugs happen
-    let mut reserve = Box::new(Reserve::unpack(&reserve_info.data.borrow())?);
     reserve.last_update.mark_stale();
-    Reserve::pack(*reserve, &mut reserve_info.data.borrow_mut())?;
-
     Ok(())
 }
 
@@ -1474,12 +1456,15 @@ fn process_withdraw_obligation_collateral(
     let obligation_owner_info = next_account_info(account_info_iter)?;
     let clock = &Clock::get()?;
     let token_program_id = next_account_info(account_info_iter)?;
+
+    let mut withdraw_reserve = ReserveBorrow::new_mut(program_id, withdraw_reserve_info)?;
+
     _withdraw_obligation_collateral(
         program_id,
         collateral_amount,
         source_collateral_info,
         destination_collateral_info,
-        withdraw_reserve_info,
+        &mut withdraw_reserve,
         obligation_info,
         lending_market_info,
         lending_market_authority_info,
@@ -1489,6 +1474,7 @@ fn process_withdraw_obligation_collateral(
         false,
         &accounts[8..],
     )?;
+
     Ok(())
 }
 
@@ -1498,7 +1484,7 @@ fn _withdraw_obligation_collateral<'a>(
     collateral_amount: u64,
     source_collateral_info: &AccountInfo<'a>,
     destination_collateral_info: &AccountInfo<'a>,
-    withdraw_reserve_info: &AccountInfo<'a>,
+    withdraw_reserve: &mut ReserveBorrow,
     obligation_info: &AccountInfo<'a>,
     lending_market_info: &AccountInfo<'a>,
     lending_market_authority_info: &AccountInfo<'a>,
@@ -1518,11 +1504,6 @@ fn _withdraw_obligation_collateral<'a>(
         return Err(LendingError::InvalidTokenProgram.into());
     }
 
-    let withdraw_reserve = Box::new(Reserve::unpack(&withdraw_reserve_info.data.borrow())?);
-    if withdraw_reserve_info.owner != program_id {
-        msg!("Withdraw reserve provided is not owned by the lending program");
-        return Err(LendingError::InvalidAccountOwner.into());
-    }
     if &withdraw_reserve.lending_market != lending_market_info.key {
         msg!("Withdraw reserve lending market does not match the lending market provided");
         return Err(LendingError::InvalidAccountInput.into());
@@ -1563,7 +1544,7 @@ fn _withdraw_obligation_collateral<'a>(
     }
 
     let (collateral, collateral_index) =
-        obligation.find_collateral_in_deposits(*withdraw_reserve_info.key)?;
+        obligation.find_collateral_in_deposits(withdraw_reserve.key())?;
     if collateral.deposited_amount == 0 {
         msg!("Collateral deposited amount is zero");
         return Err(LendingError::ObligationCollateralEmpty.into());
@@ -1616,7 +1597,7 @@ fn _withdraw_obligation_collateral<'a>(
         u64::MAX
     };
 
-    let max_withdraw_amount = obligation.max_withdraw_amount(collateral, &withdraw_reserve)?;
+    let max_withdraw_amount = obligation.max_withdraw_amount(collateral, withdraw_reserve)?;
     let withdraw_amount = min(
         collateral_amount,
         min(max_withdraw_amount, max_outflow_collateral_amount),
@@ -1640,8 +1621,10 @@ fn _withdraw_obligation_collateral<'a>(
         .market_value
         .saturating_sub(withdraw_value);
 
-    let (open_exceeded, _) =
-        update_borrow_attribution_values(&mut obligation, deposit_reserve_infos)?;
+    let (open_exceeded, _) = withdraw_reserve.while_released(|| {
+        update_borrow_attribution_values(program_id, &mut obligation, deposit_reserve_infos)
+    })?;
+
     if let Some(reserve_pubkey) = open_exceeded {
         msg!(
             "Open borrow attribution limit exceeded for reserve {:?}",
@@ -1652,9 +1635,20 @@ fn _withdraw_obligation_collateral<'a>(
 
     // obligation.withdraw must be called after updating borrow attribution values, since we can
     // lose information if an entire deposit is removed, making the former calculation incorrect
-    obligation.withdraw(withdraw_amount, collateral_index)?;
+    let new_share = obligation.withdraw(withdraw_amount, collateral_index)?;
+
+    // liq. mining
+    obligation.user_reward_managers.set_share(
+        withdraw_reserve.key(),
+        PositionKind::Deposit,
+        &mut withdraw_reserve.deposits_pool_reward_manager,
+        new_share,
+        clock,
+    )?;
+
     obligation.last_update.mark_stale();
 
+    realloc_obligation_if_necessary(&obligation, obligation_info)?;
     Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?;
 
     spl_token_transfer(TokenTransferParams {
@@ -1702,11 +1696,8 @@ fn process_borrow_obligation_liquidity(
         return Err(LendingError::InvalidTokenProgram.into());
     }
 
-    let mut borrow_reserve = Box::new(Reserve::unpack(&borrow_reserve_info.data.borrow())?);
-    if borrow_reserve_info.owner != program_id {
-        msg!("Borrow reserve provided is not owned by the lending program");
-        return Err(LendingError::InvalidAccountOwner.into());
-    }
+    let mut borrow_reserve = ReserveBorrow::new_mut(program_id, borrow_reserve_info)?;
+
     if &borrow_reserve.lending_market != lending_market_info.key {
         msg!("Borrow reserve lending market does not match the lending market provided");
         return Err(LendingError::InvalidAccountInput.into());
@@ -1892,15 +1883,29 @@ fn process_borrow_obligation_liquidity(
         .unweighted_borrowed_value
         .try_add(borrow_reserve.market_value(borrow_amount)?)?;
 
-    Reserve::pack(*borrow_reserve, &mut borrow_reserve_info.data.borrow_mut())?;
-
     let obligation_liquidity = obligation
         .find_or_add_liquidity_to_borrows(*borrow_reserve_info.key, cumulative_borrow_rate_wads)?;
 
     obligation_liquidity.borrow(borrow_amount)?;
+
+    // liq. mining
+    let new_share = obligation_liquidity.liability_shares()?;
+    obligation.user_reward_managers.set_share(
+        borrow_reserve.key(),
+        PositionKind::Borrow,
+        &mut borrow_reserve.borrows_pool_reward_manager,
+        new_share,
+        clock,
+    )?;
+
     obligation.last_update.mark_stale();
 
-    let (open_exceeded, _) = update_borrow_attribution_values(&mut obligation, &accounts[9..])?;
+    // because [update_borrow_attribution_values] takes reference to the data
+    // we need to drop our borrow
+    borrow_reserve.commit();
+
+    let (open_exceeded, _) =
+        update_borrow_attribution_values(program_id, &mut obligation, &accounts[9..])?;
     if let Some(reserve_pubkey) = open_exceeded {
         msg!(
             "Open borrow attribution limit exceeded for reserve {:?}",
@@ -1914,6 +1919,7 @@ fn process_borrow_obligation_liquidity(
         next_account_info(account_info_iter)?;
     }
 
+    realloc_obligation_if_necessary(&obligation, obligation_info)?;
     Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?;
 
     let mut owner_fee = borrow_fee;
@@ -1986,12 +1992,10 @@ fn process_repay_obligation_liquidity(
         return Err(LendingError::InvalidTokenProgram.into());
     }
 
-    _refresh_reserve_interest(program_id, repay_reserve_info, clock)?;
-    let mut repay_reserve = Box::new(Reserve::unpack(&repay_reserve_info.data.borrow())?);
-    if repay_reserve_info.owner != program_id {
-        msg!("Repay reserve provided is not owned by the lending program");
-        return Err(LendingError::InvalidAccountOwner.into());
-    }
+    let mut repay_reserve = ReserveBorrow::new_mut(program_id, repay_reserve_info)?;
+
+    _refresh_reserve_interest(&mut repay_reserve, clock)?;
+
     if &repay_reserve.lending_market != lending_market_info.key {
         msg!("Repay reserve lending market does not match the lending market provided");
         return Err(LendingError::InvalidAccountInput.into());
@@ -2041,10 +2045,20 @@ fn process_repay_obligation_liquidity(
 
     repay_reserve.liquidity.repay(repay_amount, settle_amount)?;
     repay_reserve.last_update.mark_stale();
-    Reserve::pack(*repay_reserve, &mut repay_reserve_info.data.borrow_mut())?;
 
-    obligation.repay(settle_amount, liquidity_index)?;
+    let new_share = obligation.repay(settle_amount, liquidity_index)?;
     obligation.last_update.mark_stale();
+
+    // liq. mining
+    obligation.user_reward_managers.set_share(
+        repay_reserve.key(),
+        PositionKind::Borrow,
+        &mut repay_reserve.borrows_pool_reward_manager,
+        new_share,
+        clock,
+    )?;
+
+    realloc_obligation_if_necessary(&obligation, obligation_info)?;
     Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?;
 
     spl_token_transfer(TokenTransferParams {
@@ -2059,15 +2073,22 @@ fn process_repay_obligation_liquidity(
     Ok(())
 }
 
+/// Because repay and withdraw reserve can match we cannot have both of them
+/// mutably borrowed at the same time.
+///
+/// This function assumes that both reserves are given in read only state and
+/// will mutably borrow them inside the function in a safe fashion.
+///
+/// When the function returns the reserves are in a read only state again.
 #[allow(clippy::too_many_arguments)]
 fn _liquidate_obligation<'a>(
     program_id: &Pubkey,
     liquidity_amount: u64,
     source_liquidity_info: &AccountInfo<'a>,
     destination_collateral_info: &AccountInfo<'a>,
-    repay_reserve_info: &AccountInfo<'a>,
+    repay_reserve: &mut ReserveBorrow,
     repay_reserve_liquidity_supply_info: &AccountInfo<'a>,
-    withdraw_reserve_info: &AccountInfo<'a>,
+    withdraw_reserve: &mut ReserveBorrow,
     withdraw_reserve_collateral_supply_info: &AccountInfo<'a>,
     obligation_info: &AccountInfo<'a>,
     lending_market_info: &AccountInfo<'a>,
@@ -2086,11 +2107,6 @@ fn _liquidate_obligation<'a>(
         return Err(LendingError::InvalidTokenProgram.into());
     }
 
-    let mut repay_reserve = Box::new(Reserve::unpack(&repay_reserve_info.data.borrow())?);
-    if repay_reserve_info.owner != program_id {
-        msg!("Repay reserve provided is not owned by the lending program");
-        return Err(LendingError::InvalidAccountOwner.into());
-    }
     if &repay_reserve.lending_market != lending_market_info.key {
         msg!("Repay reserve lending market does not match the lending market provided");
         return Err(LendingError::InvalidAccountInput.into());
@@ -2114,11 +2130,6 @@ fn _liquidate_obligation<'a>(
         return Err(LendingError::ReserveStale.into());
     }
 
-    let mut withdraw_reserve = Box::new(Reserve::unpack(&withdraw_reserve_info.data.borrow())?);
-    if withdraw_reserve_info.owner != program_id {
-        msg!("Withdraw reserve provided is not owned by the lending program");
-        return Err(LendingError::InvalidAccountOwner.into());
-    }
     if &withdraw_reserve.lending_market != lending_market_info.key {
         msg!("Withdraw reserve lending market does not match the lending market provided");
         return Err(LendingError::InvalidAccountInput.into());
@@ -2174,8 +2185,7 @@ fn _liquidate_obligation<'a>(
         }
     }
 
-    let (liquidity, liquidity_index) =
-        obligation.find_liquidity_in_borrows(*repay_reserve_info.key)?;
+    let (liquidity, liquidity_index) = obligation.find_liquidity_in_borrows(repay_reserve.key())?;
     if liquidity.market_value == Decimal::zero() {
         msg!("Obligation borrow value is zero");
         return Err(LendingError::ObligationLiquidityEmpty.into());
@@ -2186,7 +2196,7 @@ fn _liquidate_obligation<'a>(
     }
 
     let (collateral, collateral_index) =
-        obligation.find_collateral_in_deposits(*withdraw_reserve_info.key)?;
+        obligation.find_collateral_in_deposits(withdraw_reserve.key())?;
     if collateral.market_value == Decimal::zero() {
         msg!("Obligation deposit value is zero");
         return Err(LendingError::ObligationCollateralEmpty.into());
@@ -2227,28 +2237,69 @@ fn _liquidate_obligation<'a>(
         return Err(LendingError::LiquidationTooSmall.into());
     }
 
-    repay_reserve.liquidity.repay(repay_amount, settle_amount)?;
-    repay_reserve.last_update.mark_stale();
-    Reserve::pack(*repay_reserve, &mut repay_reserve_info.data.borrow_mut())?;
-
-    // if there is a full withdraw here (which can happen on a full liquidation), then the borrow
-    // attribution value needs to be updated on the reserve. note that we can't depend on
-    // refresh_obligation to update this correctly because the ObligationCollateral object will be
-    // deleted after this call.
-    if withdraw_amount == collateral.deposited_amount {
-        withdraw_reserve.attributed_borrow_value = withdraw_reserve
-            .attributed_borrow_value
-            .saturating_sub(collateral.market_value);
+    let collateral_deposited_amount = collateral.deposited_amount;
+    let collateral_market_value = collateral.market_value;
 
-        Reserve::pack(
-            *withdraw_reserve,
-            &mut withdraw_reserve_info.data.borrow_mut(),
+    {
+        // we need to update the repay reserve but in order to do that we need to
+        // release the withdraw reserve first
+        withdraw_reserve.release()?;
+        repay_reserve.acquire_reload_mut()?;
+
+        repay_reserve.liquidity.repay(repay_amount, settle_amount)?;
+        repay_reserve.last_update.mark_stale();
+        let new_share = obligation.repay(settle_amount, liquidity_index)?;
+
+        // liq. mining
+        obligation.user_reward_managers.set_share(
+            repay_reserve.key(),
+            PositionKind::Borrow,
+            &mut repay_reserve.borrows_pool_reward_manager,
+            new_share,
+            clock,
         )?;
+
+        repay_reserve.release()?;
+
+        // both reserves released now
+    }
+
+    {
+        // after releasing repay we write into the withdraw reserve
+        withdraw_reserve.acquire_reload_mut()?;
+        if withdraw_amount == collateral_deposited_amount {
+            // if there is a full withdraw here (which can happen on a full liquidation), then the borrow
+            // attribution value needs to be updated on the reserve. note that we can't depend on
+            // refresh_obligation to update this correctly because the ObligationCollateral object will be
+            // deleted after this call.
+
+            withdraw_reserve.attributed_borrow_value = withdraw_reserve
+                .attributed_borrow_value
+                .saturating_sub(collateral_market_value);
+        }
+
+        let new_share = obligation.withdraw(withdraw_amount, collateral_index)?;
+
+        // liq. mining
+        obligation.user_reward_managers.set_share(
+            withdraw_reserve.key(),
+            PositionKind::Deposit,
+            &mut withdraw_reserve.deposits_pool_reward_manager,
+            new_share,
+            clock,
+        )?;
+        withdraw_reserve.release()?;
+
+        // both reserves released now
     }
 
-    obligation.repay(settle_amount, liquidity_index)?;
-    obligation.withdraw(withdraw_amount, collateral_index)?;
+    // and both reserves are again read only
+    withdraw_reserve.acquire_reload()?;
+    repay_reserve.acquire_reload()?;
+
     obligation.last_update.mark_stale();
+
+    realloc_obligation_if_necessary(&obligation, obligation_info)?;
     Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?;
 
     spl_token_transfer(TokenTransferParams {
@@ -2301,14 +2352,17 @@ fn process_liquidate_obligation_and_redeem_reserve_collateral(
     let token_program_id = next_account_info(account_info_iter)?;
     let clock = &Clock::get()?;
 
+    let mut repay_reserve = ReserveBorrow::new(program_id, repay_reserve_info)?;
+    let mut withdraw_reserve = ReserveBorrow::new(program_id, withdraw_reserve_info)?;
+
     let (withdrawn_collateral_amount, bonus) = _liquidate_obligation(
         program_id,
         liquidity_amount,
         source_liquidity_info,
         destination_collateral_info,
-        repay_reserve_info,
+        &mut repay_reserve,
         repay_reserve_liquidity_supply_info,
-        withdraw_reserve_info,
+        &mut withdraw_reserve,
         withdraw_reserve_collateral_supply_info,
         obligation_info,
         lending_market_info,
@@ -2317,9 +2371,10 @@ fn process_liquidate_obligation_and_redeem_reserve_collateral(
         clock,
         token_program_id,
     )?;
+    drop(repay_reserve);
 
-    _refresh_reserve_interest(program_id, withdraw_reserve_info, clock)?;
-    let withdraw_reserve = Box::new(Reserve::unpack(&withdraw_reserve_info.data.borrow())?);
+    withdraw_reserve.acquire_reload_mut()?;
+    _refresh_reserve_interest(&mut withdraw_reserve, clock)?;
     let collateral_exchange_rate = withdraw_reserve.collateral_exchange_rate()?;
     let max_redeemable_collateral = collateral_exchange_rate
         .liquidity_to_collateral(withdraw_reserve.liquidity.available_amount)?;
@@ -2331,7 +2386,7 @@ fn process_liquidate_obligation_and_redeem_reserve_collateral(
             withdraw_collateral_amount,
             destination_collateral_info,
             destination_liquidity_info,
-            withdraw_reserve_info,
+            &mut withdraw_reserve,
             withdraw_reserve_collateral_mint_info,
             withdraw_reserve_liquidity_supply_info,
             lending_market_info,
@@ -2341,7 +2396,6 @@ fn process_liquidate_obligation_and_redeem_reserve_collateral(
             token_program_id,
             false,
         )?;
-        let withdraw_reserve = Box::new(Reserve::unpack(&withdraw_reserve_info.data.borrow())?);
         if &withdraw_reserve.config.fee_receiver != withdraw_reserve_liquidity_fee_receiver_info.key
         {
             msg!("Withdraw reserve liquidity fee receiver does not match the reserve liquidity fee receiver provided");
@@ -2384,12 +2438,14 @@ fn process_withdraw_obligation_collateral_and_redeem_reserve_liquidity(
     let clock = &Clock::get()?;
     let token_program_id = next_account_info(account_info_iter)?;
 
+    let mut reserve = ReserveBorrow::new_mut(program_id, reserve_info)?;
+
     let liquidity_amount = _withdraw_obligation_collateral(
         program_id,
         collateral_amount,
         reserve_collateral_info,
         user_collateral_info,
-        reserve_info,
+        &mut reserve,
         obligation_info,
         lending_market_info,
         lending_market_authority_info,
@@ -2405,7 +2461,7 @@ fn process_withdraw_obligation_collateral_and_redeem_reserve_liquidity(
         liquidity_amount,
         user_collateral_info,
         user_liquidity_info,
-        reserve_info,
+        &mut reserve,
         reserve_collateral_mint_info,
         reserve_liquidity_supply_info,
         lending_market_info,
@@ -2415,6 +2471,7 @@ fn process_withdraw_obligation_collateral_and_redeem_reserve_liquidity(
         token_program_id,
         true,
     )?;
+
     Ok(())
 }
 
@@ -2435,15 +2492,8 @@ fn process_update_reserve_config(
     let pyth_price_info = next_account_info(account_info_iter)?;
     let switchboard_feed_info = next_account_info(account_info_iter)?;
 
-    let mut reserve = Box::new(Reserve::unpack(&reserve_info.data.borrow())?);
-    if reserve_info.owner != program_id {
-        msg!(
-            "Reserve provided is not owned by the lending program {} != {}",
-            &reserve_info.owner.to_string(),
-            &program_id.to_string(),
-        );
-        return Err(LendingError::InvalidAccountOwner.into());
-    }
+    let mut reserve = ReserveBorrow::new_mut(program_id, reserve_info)?;
+
     if &reserve.lending_market != lending_market_info.key {
         msg!("Reserve lending market does not match the lending market provided");
         return Err(LendingError::InvalidAccountInput.into());
@@ -2555,7 +2605,6 @@ fn process_update_reserve_config(
     }
 
     reserve.last_update.mark_stale();
-    Reserve::pack(*reserve, &mut reserve_info.data.borrow_mut())?;
     Ok(())
 }
 
@@ -2570,15 +2619,7 @@ fn process_redeem_fees(program_id: &Pubkey, accounts: &[AccountInfo]) -> Program
     let token_program_id = next_account_info(account_info_iter)?;
     let clock = &Clock::get()?;
 
-    let mut reserve = Box::new(Reserve::unpack(&reserve_info.data.borrow())?);
-    if reserve_info.owner != program_id {
-        msg!(
-            "Reserve provided is not owned by the lending program {} != {}",
-            &reserve_info.owner.to_string(),
-            &program_id.to_string(),
-        );
-        return Err(LendingError::InvalidAccountOwner.into());
-    }
+    let mut reserve = ReserveBorrow::new_mut(program_id, reserve_info)?;
 
     if &reserve.config.fee_receiver != reserve_liquidity_fee_receiver_info.key {
         msg!("Reserve liquidity fee receiver does not match the reserve liquidity fee receiver provided");
@@ -2626,7 +2667,6 @@ fn process_redeem_fees(program_id: &Pubkey, accounts: &[AccountInfo]) -> Program
 
     reserve.liquidity.redeem_fees(withdraw_amount)?;
     reserve.last_update.mark_stale();
-    Reserve::pack(*reserve, &mut reserve_info.data.borrow_mut())?;
 
     spl_token_transfer(TokenTransferParams {
         source: reserve_supply_liquidity_info.clone(),
@@ -2655,18 +2695,21 @@ fn process_flash_borrow_reserve_liquidity(
     let token_program_id = next_account_info(account_info_iter)?;
     let clock = Clock::get()?;
 
-    _refresh_reserve_interest(program_id, reserve_info, &clock)?;
+    let mut reserve = ReserveBorrow::new_mut(program_id, reserve_info)?;
+
+    _refresh_reserve_interest(&mut reserve, &clock)?;
     _flash_borrow_reserve_liquidity(
         program_id,
         liquidity_amount,
         source_liquidity_info,
         destination_liquidity_info,
-        reserve_info,
+        &mut reserve,
         lending_market_info,
         lending_market_authority_info,
         sysvar_info,
         token_program_id,
     )?;
+
     Ok(())
 }
 
@@ -2676,7 +2719,7 @@ fn _flash_borrow_reserve_liquidity<'a>(
     liquidity_amount: u64,
     source_liquidity_info: &AccountInfo<'a>,
     destination_liquidity_info: &AccountInfo<'a>,
-    reserve_info: &AccountInfo<'a>,
+    reserve: &mut ReserveBorrow,
     lending_market_info: &AccountInfo<'a>,
     lending_market_authority_info: &AccountInfo<'a>,
     sysvar_info: &AccountInfo<'a>,
@@ -2691,11 +2734,7 @@ fn _flash_borrow_reserve_liquidity<'a>(
         msg!("Lending market token program does not match the token program provided");
         return Err(LendingError::InvalidTokenProgram.into());
     }
-    let mut reserve = Box::new(Reserve::unpack(&reserve_info.data.borrow())?);
-    if reserve_info.owner != program_id {
-        msg!("Reserve provided is not owned by the lending program");
-        return Err(LendingError::InvalidAccountOwner.into());
-    }
+
     if &reserve.lending_market != lending_market_info.key {
         msg!("Reserve lending market does not match the lending market provided");
         return Err(LendingError::InvalidAccountInput.into());
@@ -2774,7 +2813,7 @@ fn _flash_borrow_reserve_liquidity<'a>(
                     msg!("Multiple flash repays not allowed");
                     return Err(LendingError::MultipleFlashBorrows.into());
                 }
-                if ixn.accounts[4].pubkey != *reserve_info.key {
+                if ixn.accounts[4].pubkey != reserve.key() {
                     msg!("Invalid reserve account on flash repay");
                     return Err(LendingError::InvalidFlashRepay.into());
                 }
@@ -2804,7 +2843,6 @@ fn _flash_borrow_reserve_liquidity<'a>(
 
     reserve.liquidity.borrow(Decimal::from(liquidity_amount))?;
     reserve.last_update.mark_stale();
-    Reserve::pack(*reserve, &mut reserve_info.data.borrow_mut())?;
 
     spl_token_transfer(TokenTransferParams {
         source: source_liquidity_info.clone(),
@@ -2835,6 +2873,8 @@ fn process_flash_repay_reserve_liquidity(
     let sysvar_info = next_account_info(account_info_iter)?;
     let token_program_id = next_account_info(account_info_iter)?;
 
+    let mut reserve = ReserveBorrow::new_mut(program_id, reserve_info)?;
+
     _flash_repay_reserve_liquidity(
         program_id,
         liquidity_amount,
@@ -2843,12 +2883,13 @@ fn process_flash_repay_reserve_liquidity(
         destination_liquidity_info,
         reserve_liquidity_fee_receiver_info,
         host_fee_receiver_info,
-        reserve_info,
+        &mut reserve,
         lending_market_info,
         user_transfer_authority_info,
         sysvar_info,
         token_program_id,
     )?;
+
     Ok(())
 }
 
@@ -2861,7 +2902,7 @@ fn _flash_repay_reserve_liquidity<'a>(
     destination_liquidity_info: &AccountInfo<'a>,
     reserve_liquidity_fee_receiver_info: &AccountInfo<'a>,
     host_fee_receiver_info: &AccountInfo<'a>,
-    reserve_info: &AccountInfo<'a>,
+    reserve: &mut ReserveBorrow,
     lending_market_info: &AccountInfo<'a>,
     user_transfer_authority_info: &AccountInfo<'a>,
     sysvar_info: &AccountInfo<'a>,
@@ -2876,11 +2917,7 @@ fn _flash_repay_reserve_liquidity<'a>(
         msg!("Lending market token program does not match the token program provided");
         return Err(LendingError::InvalidTokenProgram.into());
     }
-    let mut reserve = Box::new(Reserve::unpack(&reserve_info.data.borrow())?);
-    if reserve_info.owner != program_id {
-        msg!("Reserve provided is not owned by the lending program");
-        return Err(LendingError::InvalidAccountOwner.into());
-    }
+
     if &reserve.lending_market != lending_market_info.key {
         msg!("Reserve lending market does not match the lending market provided");
         return Err(LendingError::InvalidAccountInput.into());
@@ -2939,7 +2976,7 @@ fn _flash_repay_reserve_liquidity<'a>(
             liquidity_amount: borrow_liquidity_amount,
         } => {
             // re-check everything here out of paranoia
-            if ixn.accounts[2].pubkey != *reserve_info.key {
+            if ixn.accounts[2].pubkey != reserve.key() {
                 msg!("Invalid reserve account on flash repay");
                 return Err(LendingError::InvalidFlashRepay.into());
             }
@@ -2959,7 +2996,6 @@ fn _flash_repay_reserve_liquidity<'a>(
         .liquidity
         .repay(flash_loan_amount, flash_loan_amount_decimal)?;
     reserve.last_update.mark_stale();
-    Reserve::pack(*reserve, &mut reserve_info.data.borrow_mut())?;
 
     spl_token_transfer(TokenTransferParams {
         source: source_liquidity_info.clone(),
@@ -3024,11 +3060,8 @@ fn process_forgive_debt(
         return Err(LendingError::InvalidSigner.into());
     }
 
-    let mut reserve = Box::new(Reserve::unpack(&reserve_info.data.borrow())?);
-    if reserve_info.owner != program_id {
-        msg!("Reserve provided is not owned by the lending program");
-        return Err(LendingError::InvalidAccountOwner.into());
-    }
+    let mut reserve = ReserveBorrow::new_mut(program_id, reserve_info)?;
+
     if &reserve.lending_market != lending_market_info.key {
         msg!("Reserve lending market does not match the lending market provided");
         return Err(LendingError::InvalidAccountInput.into());
@@ -3077,10 +3110,20 @@ fn process_forgive_debt(
 
     reserve.liquidity.forgive_debt(forgive_amount)?;
     reserve.last_update.mark_stale();
-    Reserve::pack(*reserve, &mut reserve_info.data.borrow_mut())?;
 
-    obligation.repay(forgive_amount, liquidity_index)?;
+    let new_share = obligation.repay(forgive_amount, liquidity_index)?;
     obligation.last_update.mark_stale();
+
+    // liq. mining
+    obligation.user_reward_managers.set_share(
+        reserve.key(),
+        PositionKind::Borrow,
+        &mut reserve.borrows_pool_reward_manager,
+        new_share,
+        &Clock::get()?,
+    )?;
+
+    realloc_obligation_if_necessary(&obligation, obligation_info)?;
     Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?;
 
     Ok(())
@@ -3184,11 +3227,7 @@ pub fn process_set_obligation_closeability_status(
         return Err(LendingError::InvalidAccountOwner.into());
     }
 
-    let reserve = Reserve::unpack(&reserve_info.data.borrow())?;
-    if reserve_info.owner != program_id {
-        msg!("Reserve provided is not owned by the lending program");
-        return Err(LendingError::InvalidAccountOwner.into());
-    }
+    let reserve = ReserveBorrow::new(program_id, reserve_info)?;
     if &reserve.lending_market != lending_market_info.key {
         msg!("Reserve lending market does not match the lending market provided");
         return Err(LendingError::InvalidAccountInput.into());
@@ -3240,6 +3279,7 @@ pub fn process_set_obligation_closeability_status(
 
     obligation.closeable = closeable;
 
+    realloc_obligation_if_necessary(&obligation, obligation_info)?;
     Obligation::pack(obligation, &mut obligation_info.data.borrow_mut())?;
 
     Ok(())
@@ -3270,12 +3310,8 @@ pub fn process_donate_to_reserve(
         return Err(LendingError::InvalidTokenProgram.into());
     }
 
-    if reserve_info.owner != program_id {
-        msg!("Lending market provided is not owned by the lending program");
-        return Err(LendingError::InvalidAccountOwner.into());
-    }
+    let mut reserve = ReserveBorrow::new_mut(program_id, reserve_info)?;
 
-    let mut reserve = Box::new(Reserve::unpack(&reserve_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());
@@ -3297,7 +3333,7 @@ pub fn process_donate_to_reserve(
         return Err(LendingError::InvalidAccountInput.into());
     }
 
-    _refresh_reserve_interest(program_id, reserve_info, clock)?;
+    _refresh_reserve_interest(&mut reserve, clock)?;
 
     reserve.liquidity.donate(liquidity_amount)?;
     spl_token_transfer(TokenTransferParams {
@@ -3310,8 +3346,6 @@ pub fn process_donate_to_reserve(
     })?;
 
     reserve.last_update.mark_stale();
-    Reserve::pack(*reserve, &mut reserve_info.data.borrow_mut())?;
-
     Ok(())
 }
 
@@ -3542,6 +3576,44 @@ fn is_cpi_call(
     Ok(false)
 }
 
+/// Calls realloc on the obligation if the packed size is larger than the
+/// underlying buffer.
+///
+/// # Important
+///
+/// The off-chain client is responsible for making sure the obligation has
+/// enough rent to be still rent-exempt.
+fn realloc_obligation_if_necessary(
+    obligation: &Obligation,
+    obligation_info: &AccountInfo<'_>,
+) -> ProgramResult {
+    let expected_size = obligation.size_in_bytes_when_packed();
+
+    if expected_size <= obligation_info.data_len() {
+        return Ok(());
+    }
+
+    let current_rent = obligation_info.lamports();
+    let new_rent = Rent::get()?.minimum_balance(expected_size);
+
+    if let Some(extra_rent) = new_rent.checked_sub(current_rent) {
+        msg!("Obligation is missing {} lamports in rent", extra_rent);
+        return Err(ProgramError::AccountNotRentExempt);
+    }
+
+    // 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;
+    obligation_info.realloc(expected_size, zero_init)?;
+
+    Ok(())
+}
+
 struct TokenInitializeMintParams<'a: 'b, 'b> {
     mint: AccountInfo<'a>,
     rent: AccountInfo<'a>,
diff --git a/token-lending/program/src/processor/account_borrow.rs b/token-lending/program/src/processor/account_borrow.rs
new file mode 100644
index 00000000000..d3412b39262
--- /dev/null
+++ b/token-lending/program/src/processor/account_borrow.rs
@@ -0,0 +1,216 @@
+//! # Why do we wrap account data?
+//!
+//! Previous version of borrow-lending implementation unpacked and then packed
+//! the account data several times in a single ix to avoid errors of overwriting
+//! data written by other functions.
+//! However, this was still fragile as all function calls between unpack and
+//! pack would have to be checked to ensure they do not write to the same data.
+//!
+//! Instead we now have a convention that data access is created in the
+//! `process_*` functions and are passed as a reference to other functions.
+//!
+//! This structure guarantees at runtime that the double write error does not
+//! occur while avoiding the cost of unpacking and packing the data.
+
+use crate::{error::LendingError, state::Reserve};
+use solana_program::{
+    account_info::AccountInfo, entrypoint::ProgramResult, msg, program_error::ProgramError,
+    program_pack::Pack, pubkey::Pubkey,
+};
+
+use std::ops::{Deref, DerefMut};
+use std::result::Result;
+
+/// Wraps around a [Reserve] data and provides runtime borrow semantics.
+///
+/// Is either in state of
+/// - `release`. The underlying data is not borrowed at all and can be read and
+///    written to by other holders of the account info.
+/// - `Ref`. The underlying data is borrowed as immutable and can be read by
+///    other holders of the account info but not written to.
+/// - `RefMut`. The underlying data is borrowed as mutable and can be read and
+///    written only via this borrow.
+///
+/// # Persistence
+///
+/// The data is written to the underlying account buffer when the borrow is
+/// done mutably with either [Self::new_mut] or [Self::acquire_reload_mut].
+/// The write happens on [Self::release] or in any function that calls it and on
+/// [drop].
+pub(crate) struct ReserveBorrow<'a, 'info> {
+    info: &'a AccountInfo<'info>,
+    guard: ReserveDataGuard<'a, 'info>,
+}
+
+enum ReserveDataGuard<'a, 'info> {
+    Released,
+    Ref(
+        #[allow(dead_code)] std::cell::Ref<'a, &'info mut [u8]>,
+        Box<Reserve>,
+    ),
+    RefMut(std::cell::RefMut<'a, &'info mut [u8]>, Box<Reserve>),
+}
+
+enum ReserveDataGuardKind {
+    Release,
+    Ref,
+    RefMut,
+}
+
+impl Drop for ReserveBorrow<'_, '_> {
+    fn drop(&mut self) {
+        if let Err(e) = self.release() {
+            msg!("Failed to release reserve data");
+            panic!("{}", e);
+        }
+    }
+}
+
+impl Deref for ReserveBorrow<'_, '_> {
+    type Target = Box<Reserve>;
+
+    fn deref(&self) -> &Self::Target {
+        match &self.guard {
+            ReserveDataGuard::Ref(_, inner) => inner,
+            ReserveDataGuard::RefMut(_, inner) => inner,
+            ReserveDataGuard::Released => panic!("Reserve data has been released"),
+        }
+    }
+}
+
+impl DerefMut for ReserveBorrow<'_, '_> {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        match &mut self.guard {
+            ReserveDataGuard::RefMut(_, inner) => inner,
+            ReserveDataGuard::Ref(_, _) => panic!("Reserve data is not mutable"),
+            ReserveDataGuard::Released => panic!("Reserve data has been released"),
+        }
+    }
+}
+
+impl<'a, 'info> ReserveBorrow<'a, 'info> {
+    /// Creates a new `Ref` guard over the data.
+    ///
+    /// Many readers can exist at the same time if no writer is present,
+    /// otherwise panics.
+    pub(crate) fn new(
+        program_id: &Pubkey,
+        info: &'a AccountInfo<'info>,
+    ) -> Result<Self, ProgramError> {
+        if info.owner != program_id {
+            msg!("Reserve provided is not owned by the lending program");
+            return Err(LendingError::InvalidAccountOwner.into());
+        }
+
+        let data = info.data.borrow();
+        let reserve = Box::new(Reserve::unpack(&data)?);
+        let guard = ReserveDataGuard::Ref(data, reserve);
+
+        Ok(Self { guard, info })
+    }
+
+    /// Creates a new `RefMut` guard over the data.
+    ///
+    /// Only one writer can exist at a time and no readers, otherwise panics.
+    pub(crate) fn new_mut(
+        program_id: &Pubkey,
+        info: &'a AccountInfo<'info>,
+    ) -> Result<Self, ProgramError> {
+        if info.owner != program_id {
+            msg!("Reserve provided is not owned by the lending program");
+            return Err(LendingError::InvalidAccountOwner.into());
+        }
+
+        let data = info.data.borrow_mut();
+        let reserve = Box::new(Reserve::unpack(&data)?);
+        let guard = ReserveDataGuard::RefMut(data, reserve);
+
+        Ok(Self { guard, info })
+    }
+
+    pub(crate) fn key(&self) -> Pubkey {
+        *self.info.key
+    }
+
+    /// Explicit version of [drop]ping that panics if the data is not guarded
+    /// as `RefMut`.
+    pub(crate) fn commit(self) {
+        if let ReserveDataGuard::RefMut(_, _) = self.guard {
+            // drop self
+        } else {
+            panic!("Cannot commit a non mutable borrow");
+        }
+    }
+
+    /// Releases the guard over the data.
+    ///
+    /// If the data was guarded as `RefMut`, it will be packed back to the
+    /// account.
+    pub(crate) fn release(&mut self) -> ProgramResult {
+        let prev_guard = std::mem::replace(&mut self.guard, ReserveDataGuard::Released);
+
+        if let ReserveDataGuard::RefMut(mut data, inner) = prev_guard {
+            Reserve::pack(*inner, &mut data)?;
+        }
+
+        Ok(())
+    }
+
+    /// Calls [Self::release] and then creates a new `RefMut` guard.
+    pub(crate) fn acquire_reload_mut(&mut self) -> ProgramResult {
+        self.release()?;
+
+        let data_ref = self.info.data.borrow_mut();
+        let inner = Reserve::unpack(&data_ref)?;
+        self.guard = ReserveDataGuard::RefMut(data_ref, Box::new(inner));
+
+        Ok(())
+    }
+
+    /// Calls [Self::release] and then creates a new `RefMut` guard.
+    pub(crate) fn acquire_reload(&mut self) -> ProgramResult {
+        self.release()?;
+
+        let data_ref = self.info.data.borrow();
+        let inner = Reserve::unpack(&data_ref)?;
+        self.guard = ReserveDataGuard::Ref(data_ref, Box::new(inner));
+
+        Ok(())
+    }
+
+    /// Releases the guard, calls the given function and returns the guard to
+    /// the same state it was before the call.
+    pub(crate) fn while_released<T>(
+        &mut self,
+        f: impl FnOnce() -> Result<T, ProgramError>,
+    ) -> Result<T, ProgramError> {
+        let prev_guard = ReserveDataGuardKind::from(&self.guard);
+        self.release()?;
+
+        let res = f();
+
+        match prev_guard {
+            ReserveDataGuardKind::Ref => {
+                self.acquire_reload()?;
+            }
+            ReserveDataGuardKind::RefMut => {
+                self.acquire_reload_mut()?;
+            }
+            ReserveDataGuardKind::Release => {
+                // already released
+            }
+        }
+
+        res
+    }
+}
+
+impl From<&'_ ReserveDataGuard<'_, '_>> for ReserveDataGuardKind {
+    fn from(guard: &'_ ReserveDataGuard) -> Self {
+        match guard {
+            ReserveDataGuard::Released => ReserveDataGuardKind::Release,
+            ReserveDataGuard::Ref(_, _) => ReserveDataGuardKind::Ref,
+            ReserveDataGuard::RefMut(_, _) => ReserveDataGuardKind::RefMut,
+        }
+    }
+}
diff --git a/token-lending/program/src/processor/liquidity_mining.rs b/token-lending/program/src/processor/liquidity_mining.rs
index 8a2ebf3f9b2..df616720468 100644
--- a/token-lending/program/src/processor/liquidity_mining.rs
+++ b/token-lending/program/src/processor/liquidity_mining.rs
@@ -27,12 +27,11 @@ pub(crate) mod upgrade_reserve;
 
 use solana_program::program_pack::Pack;
 use solana_program::{account_info::AccountInfo, msg, program_error::ProgramError, pubkey::Pubkey};
-use solend_sdk::{
-    error::LendingError,
-    state::{LendingMarket, Reserve},
-};
+use solend_sdk::{error::LendingError, state::LendingMarket};
 use spl_token::state::Account as TokenAccount;
 
+use super::ReserveBorrow;
+
 /// Unpacks a spl_token [TokenAccount].
 fn unpack_token_account(data: &[u8]) -> Result<TokenAccount, LendingError> {
     TokenAccount::unpack(data).map_err(|_| LendingError::InvalidTokenAccount)
@@ -70,15 +69,15 @@ fn reward_vault_authority_seeds<'keys>(
 ///
 /// * ✅ `lending_market_owner_info` is a signer
 /// * ✅ `lending_market_owner_info` matches `lending_market_info`
-fn check_and_unpack_pool_reward_accounts_for_admin_ixs<'info>(
+fn check_and_unpack_pool_reward_accounts_for_admin_ixs<'a, 'info>(
     program_id: &Pubkey,
-    reserve_info: &AccountInfo<'info>,
+    reserve_info: &'a 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<(LendingMarket, Box<Reserve>), ProgramError> {
+) -> Result<(LendingMarket, ReserveBorrow<'a, 'info>), ProgramError> {
     let (lending_market, reserve) = check_and_unpack_pool_reward_accounts(
         program_id,
         reserve_info,
@@ -110,19 +109,15 @@ fn check_and_unpack_pool_reward_accounts_for_admin_ixs<'info>(
 /// * ✅ `lending_market_info` unpacks
 /// * ✅ `token_program_info` matches `lending_market_info`
 /// * ✅ `reward_mint_info` belongs to the token program
-fn check_and_unpack_pool_reward_accounts<'info>(
+fn check_and_unpack_pool_reward_accounts<'a, 'info>(
     program_id: &Pubkey,
-    reserve_info: &AccountInfo<'info>,
+    reserve_info: &'a AccountInfo<'info>,
     reward_mint_info: &AccountInfo<'info>,
     reward_authority_info: &AccountInfo<'info>,
     lending_market_info: &AccountInfo<'info>,
     token_program_info: &AccountInfo<'info>,
-) -> Result<(LendingMarket, Box<Reserve>), 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())?);
+) -> Result<(LendingMarket, ReserveBorrow<'a, 'info>), ProgramError> {
+    let reserve = ReserveBorrow::new_mut(program_id, reserve_info)?;
 
     if lending_market_info.owner != program_id {
         msg!("Lending market provided is not owned by the lending program");
diff --git a/token-lending/program/src/processor/liquidity_mining/add_pool_reward.rs b/token-lending/program/src/processor/liquidity_mining/add_pool_reward.rs
index 6f7fe6e5db9..0ea199c0f6a 100644
--- a/token-lending/program/src/processor/liquidity_mining/add_pool_reward.rs
+++ b/token-lending/program/src/processor/liquidity_mining/add_pool_reward.rs
@@ -6,7 +6,6 @@ use crate::processor::{
     assert_rent_exempt, spl_token_init_account, spl_token_transfer, TokenInitializeAccountParams,
     TokenTransferParams,
 };
-use solana_program::program_pack::Pack;
 use solana_program::{
     account_info::{next_account_info, AccountInfo},
     clock::Clock,
@@ -17,12 +16,11 @@ use solana_program::{
     rent::Rent,
     sysvar::Sysvar,
 };
-use solend_sdk::{
-    error::LendingError,
-    state::{PositionKind, Reserve},
-};
+use solend_sdk::{error::LendingError, state::PositionKind};
 
-use super::{check_and_unpack_pool_reward_accounts_for_admin_ixs, unpack_token_account};
+use super::{
+    check_and_unpack_pool_reward_accounts_for_admin_ixs, unpack_token_account, ReserveBorrow,
+};
 
 /// Use [Self::from_unchecked_iter] to validate the accounts except for
 /// * `reward_token_vault_info`
@@ -32,7 +30,7 @@ struct AddPoolRewardAccounts<'a, 'info> {
     /// ✅ unpacks
     /// ✅ belongs to `lending_market_info`
     /// ✅ is writable
-    reserve_info: &'a AccountInfo<'info>,
+    _reserve_info: &'a AccountInfo<'info>,
     /// ✅ belongs to the token program
     reward_mint_info: &'a AccountInfo<'info>,
     /// ✅ belongs to the token program
@@ -62,7 +60,7 @@ struct AddPoolRewardAccounts<'a, 'info> {
     /// ✅ matches `lending_market_info`
     token_program_info: &'a AccountInfo<'info>,
 
-    reserve: Box<Reserve>,
+    reserve: ReserveBorrow<'a, 'info>,
 }
 
 /// # Effects
@@ -71,7 +69,6 @@ struct AddPoolRewardAccounts<'a, 'info> {
 ///    `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(
     program_id: &Pubkey,
     position_kind: PositionKind,
@@ -119,13 +116,6 @@ pub(crate) fn process(
             clock,
         )?;
 
-    // 3.
-
-    Reserve::pack(
-        *accounts.reserve,
-        &mut accounts.reserve_info.data.borrow_mut(),
-    )?;
-
     Ok(())
 }
 
@@ -193,7 +183,7 @@ impl<'a, 'info> AddPoolRewardAccounts<'a, 'info> {
         }
 
         Ok(Self {
-            reserve_info,
+            _reserve_info: reserve_info,
             reward_mint_info,
             reward_token_source_info,
             reward_authority_info,
diff --git a/token-lending/program/src/processor/liquidity_mining/cancel_pool_reward.rs b/token-lending/program/src/processor/liquidity_mining/cancel_pool_reward.rs
index 875947f1fb2..1a28ed2020b 100644
--- a/token-lending/program/src/processor/liquidity_mining/cancel_pool_reward.rs
+++ b/token-lending/program/src/processor/liquidity_mining/cancel_pool_reward.rs
@@ -2,7 +2,6 @@ use crate::processor::liquidity_mining::{
     check_and_unpack_pool_reward_accounts_for_admin_ixs, unpack_token_account,
 };
 use crate::processor::{spl_token_transfer, TokenTransferParams};
-use solana_program::program_pack::Pack;
 use solana_program::sysvar::Sysvar;
 use solana_program::{
     account_info::{next_account_info, AccountInfo},
@@ -12,12 +11,9 @@ use solana_program::{
     program_error::ProgramError,
     pubkey::Pubkey,
 };
-use solend_sdk::{
-    error::LendingError,
-    state::{PositionKind, Reserve},
-};
+use solend_sdk::{error::LendingError, state::PositionKind};
 
-use super::reward_vault_authority_seeds;
+use super::{reward_vault_authority_seeds, ReserveBorrow};
 
 /// Use [Self::from_unchecked_iter] to validate the accounts.
 struct CancelPoolRewardAccounts<'a, 'info> {
@@ -25,7 +21,7 @@ struct CancelPoolRewardAccounts<'a, 'info> {
     /// ✅ unpacks
     /// ✅ belongs to `lending_market_info`
     /// ✅ is writable
-    reserve_info: &'a AccountInfo<'info>,
+    _reserve_info: &'a AccountInfo<'info>,
     /// ✅ belongs to the token program
     reward_mint_info: &'a AccountInfo<'info>,
     /// ✅ belongs to the token program
@@ -46,14 +42,13 @@ struct CancelPoolRewardAccounts<'a, 'info> {
     /// ✅ matches `lending_market_info`
     token_program_info: &'a AccountInfo<'info>,
 
-    reserve: Box<Reserve>,
+    reserve: ReserveBorrow<'a, 'info>,
 }
 
 /// # 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(
     program_id: &Pubkey,
     position_kind: PositionKind,
@@ -84,19 +79,12 @@ pub(crate) fn process(
         authority: accounts.reward_authority_info.clone(),
         authority_signer_seeds: &reward_vault_authority_seeds(
             accounts.lending_market_info.key,
-            accounts.reserve_info.key,
+            &accounts.reserve.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(())
 }
 
@@ -151,7 +139,7 @@ impl<'a, 'info> CancelPoolRewardAccounts<'a, 'info> {
         }
 
         Ok(Self {
-            reserve_info,
+            _reserve_info: reserve_info,
             reward_mint_info,
             reward_token_destination_info,
             reward_authority_info,
diff --git a/token-lending/program/src/processor/liquidity_mining/claim_user_reward.rs b/token-lending/program/src/processor/liquidity_mining/claim_user_reward.rs
index edd8f6653b1..9564a90caad 100644
--- a/token-lending/program/src/processor/liquidity_mining/claim_user_reward.rs
+++ b/token-lending/program/src/processor/liquidity_mining/claim_user_reward.rs
@@ -1,5 +1,15 @@
-use crate::processor::{spl_token_transfer, TokenTransferParams};
-use solana_program::program_pack::Pack;
+//! Permission-less way to claim allocated user liquidity mining rewards.
+//!
+//! # Migration
+//!
+//! Prior to version @2.1.0 there was no concept of liq. mining.
+//! That means user shares are going to be 0 even if they have a borrow or
+//! deposit.
+//! This ix can be used to start tracking obligation's rewards.
+
+use crate::processor::{
+    realloc_obligation_if_necessary, spl_token_transfer, ReserveBorrow, TokenTransferParams,
+};
 use solana_program::{
     account_info::{next_account_info, AccountInfo},
     clock::Clock,
@@ -9,8 +19,8 @@ use solana_program::{
     pubkey::Pubkey,
     sysvar::Sysvar,
 };
-use solend_sdk::state::{CreatingNewUserRewardManager, Obligation};
-use solend_sdk::{error::LendingError, state::Reserve};
+use solend_sdk::error::LendingError;
+use solend_sdk::state::{Obligation, PositionKind};
 
 use super::{
     check_and_unpack_pool_reward_accounts, reward_vault_authority_seeds, unpack_token_account,
@@ -32,7 +42,7 @@ struct ClaimUserReward<'a, 'info> {
     /// ✅ unpacks
     /// ✅ belongs to `lending_market_info`
     /// ✅ is writable
-    reserve_info: &'a AccountInfo<'info>,
+    _reserve_info: &'a AccountInfo<'info>,
     /// ✅ belongs to the token program
     reward_mint_info: &'a AccountInfo<'info>,
     /// ✅ seed of `lending_market_info`, `reserve_info`, `reward_mint_info`
@@ -50,12 +60,12 @@ struct ClaimUserReward<'a, 'info> {
     token_program_info: &'a AccountInfo<'info>,
 
     obligation: Box<Obligation>,
-    reserve: Box<Reserve>,
+    reserve: ReserveBorrow<'a, 'info>,
 }
 
 /// # Effects
 ///
-/// 1. Updates the user reward manager with the pool reward manager and accrues rewards
+/// 1. Finds the [UserRewardManager] for the reserve and obligation.
 /// 2. Withdraws all eligible rewards from [UserRewardManager].
 ///    Eligible rewards are those that match the vault and user has earned any.
 /// 3. Transfers the withdrawn rewards to the user's token account.
@@ -64,29 +74,66 @@ pub(crate) fn process(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramR
     let clock = &Clock::get()?;
 
     let mut accounts = ClaimUserReward::from_unchecked_iter(program_id, &mut accounts.iter())?;
+    let reserve_key = accounts.reserve.key();
 
     // 1.
 
-    let position_kind = accounts
-        .obligation
-        .find_position_kind(*accounts.reserve_info.key)?;
+    let position_kind = accounts.obligation.find_position_kind(reserve_key)?;
 
     let Some(user_reward_manager) = accounts
         .obligation
-        .find_user_reward_manager_mut(*accounts.reserve_info.key)
+        .user_reward_managers
+        .find_mut(reserve_key, position_kind)
     else {
-        // Let's not error if a user has no rewards to claim for this reserve.
-        // Having this ix idempotent makes cranking easier.
+        // We've checked that the obligation associates this reserve but it's
+        // not in the user reward managers yet.
+        // This means that the obligation hasn't been migrated to track the
+        // pool reward manager.
+        //
+        // We'll upgrade it here.
+
+        let reserve_key = accounts.reserve.key();
+
+        let (pool_reward_manager, migrated_share) = match position_kind {
+            PositionKind::Borrow => {
+                let share = accounts
+                    .obligation
+                    .find_liquidity_in_borrows(reserve_key)?
+                    .0
+                    .liability_shares()?;
+
+                (&mut accounts.reserve.borrows_pool_reward_manager, share)
+            }
+            PositionKind::Deposit => {
+                let share = accounts
+                    .obligation
+                    .find_collateral_in_deposits(reserve_key)?
+                    .0
+                    .deposited_amount;
+
+                (&mut accounts.reserve.deposits_pool_reward_manager, share)
+            }
+        };
+
+        accounts.obligation.user_reward_managers.set_share(
+            reserve_key,
+            position_kind,
+            pool_reward_manager,
+            migrated_share,
+            clock,
+        )?;
+
+        realloc_obligation_if_necessary(&accounts.obligation, accounts.obligation_info)?;
+        Obligation::pack(
+            *accounts.obligation,
+            &mut accounts.obligation_info.data.borrow_mut(),
+        )?;
+
         return Ok(());
     };
 
     let pool_reward_manager = accounts.reserve.pool_reward_manager_mut(position_kind);
 
-    // Syncs the pool reward manager with the user manager and accrues rewards.
-    // If we wanted to optimize CU usage then we could make a dedicated update
-    // function only for claiming rewards to avoid iterating twice over the rewards.
-    user_reward_manager.update(pool_reward_manager, clock, CreatingNewUserRewardManager::No)?;
-
     // 2.
 
     let total_reward_amount = user_reward_manager.claim_rewards(
@@ -104,7 +151,7 @@ pub(crate) fn process(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramR
         authority: accounts.reward_authority_info.clone(),
         authority_signer_seeds: &reward_vault_authority_seeds(
             accounts.lending_market_info.key,
-            accounts.reserve_info.key,
+            &reserve_key,
             accounts.reward_mint_info.key,
         ),
         token_program: accounts.token_program_info.clone(),
@@ -112,15 +159,13 @@ pub(crate) fn process(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramR
 
     // 4.
 
+    realloc_obligation_if_necessary(&accounts.obligation, accounts.obligation_info)?;
     Obligation::pack(
         *accounts.obligation,
         &mut accounts.obligation_info.data.borrow_mut(),
     )?;
 
-    Reserve::pack(
-        *accounts.reserve,
-        &mut accounts.reserve_info.data.borrow_mut(),
-    )?;
+    // reserve is packed on drop
 
     Ok(())
 }
@@ -215,7 +260,7 @@ impl<'a, 'info> ClaimUserReward<'a, 'info> {
         Ok(Self {
             obligation_info,
             obligation_owner_token_account_info,
-            reserve_info,
+            _reserve_info: reserve_info,
             reward_mint_info,
             reward_authority_info,
             reward_token_vault_info,
diff --git a/token-lending/program/src/processor/liquidity_mining/close_pool_reward.rs b/token-lending/program/src/processor/liquidity_mining/close_pool_reward.rs
index 34d6bf4a769..a0b29a15afb 100644
--- a/token-lending/program/src/processor/liquidity_mining/close_pool_reward.rs
+++ b/token-lending/program/src/processor/liquidity_mining/close_pool_reward.rs
@@ -5,7 +5,6 @@
 //!
 //! The claim ix is permission-less and therefore it can be cranked.
 
-use solana_program::program_pack::Pack;
 use solana_program::{
     account_info::{next_account_info, AccountInfo},
     entrypoint::ProgramResult,
@@ -13,10 +12,7 @@ use solana_program::{
     program_error::ProgramError,
     pubkey::Pubkey,
 };
-use solend_sdk::{
-    error::LendingError,
-    state::{PositionKind, Reserve},
-};
+use solend_sdk::{error::LendingError, state::PositionKind};
 use spl_token::state::Account as TokenAccount;
 
 use crate::processor::{
@@ -25,7 +21,7 @@ use crate::processor::{
 
 use super::{
     check_and_unpack_pool_reward_accounts_for_admin_ixs, reward_vault_authority_seeds,
-    unpack_token_account,
+    unpack_token_account, ReserveBorrow,
 };
 
 /// Use [Self::from_unchecked_iter] to validate the accounts.
@@ -57,7 +53,7 @@ struct ClosePoolRewardAccounts<'a, 'info> {
     /// ✅ matches `lending_market_info`
     token_program_info: &'a AccountInfo<'info>,
 
-    reserve: Box<Reserve>,
+    reserve: ReserveBorrow<'a, 'info>,
     reward_token_vault: TokenAccount,
 }
 
@@ -66,7 +62,6 @@ struct ClosePoolRewardAccounts<'a, 'info> {
 /// 1. Closes reward in the [Reserve] account if all users have claimed.
 /// 2. Transfers dust to the `reward_token_destination` account.
 /// 3. Closes reward vault token account.
-/// 3. Packs all changes into account buffers.
 pub(crate) fn process(
     program_id: &Pubkey,
     position_kind: PositionKind,
@@ -116,13 +111,6 @@ pub(crate) fn process(
         token_program: accounts.token_program_info.clone(),
     })?;
 
-    // 4.
-
-    Reserve::pack(
-        *accounts.reserve,
-        &mut accounts.reserve_info.data.borrow_mut(),
-    )?;
-
     Ok(())
 }
 
diff --git a/token-lending/program/tests/attributed_borrows.rs b/token-lending/program/tests/attributed_borrows.rs
index 39a7bafd28d..26517e051af 100644
--- a/token-lending/program/tests/attributed_borrows.rs
+++ b/token-lending/program/tests/attributed_borrows.rs
@@ -4,6 +4,7 @@ use crate::solend_program_test::custom_scenario;
 
 use crate::solend_program_test::User;
 
+use pretty_assertions::assert_eq;
 use solend_program::math::TryDiv;
 
 use solana_sdk::instruction::InstructionError;
@@ -12,6 +13,7 @@ use solend_program::math::TryAdd;
 use solend_program::state::LastUpdate;
 use solend_program::state::Reserve;
 use solend_sdk::error::LendingError;
+use solend_sdk::state::PoolRewardManager;
 use solend_sdk::state::ReserveLiquidity;
 
 use crate::solend_program_test::ObligationArgs;
@@ -314,7 +316,7 @@ async fn test_calculations() {
     assert_eq!(
         err,
         TransactionError::InstructionError(
-            1,
+            2, // ix 0 is CU budget, ix 1 is transfer to obligation for realloc, ix 2 is borrow
             InstructionError::Custom(LendingError::BorrowAttributionLimitExceeded as u32)
         )
     );
@@ -355,6 +357,7 @@ async fn test_calculations() {
     {
         let usdc_reserve = reserves[0].account.clone();
         let usdc_reserve_post = test.load_account::<Reserve>(reserves[0].pubkey).await;
+
         let expected_usdc_reserve_post = Reserve {
             last_update: LastUpdate {
                 slot: 1001,
@@ -382,6 +385,20 @@ async fn test_calculations() {
                 attributed_borrow_limit_open: 120,
                 ..usdc_reserve.config
             },
+            borrows_pool_reward_manager: Box::new(PoolRewardManager {
+                total_shares: {
+                    assert!(
+                        usdc_reserve.borrows_pool_reward_manager.total_shares
+                            < usdc_reserve_post
+                                .account
+                                .borrows_pool_reward_manager
+                                .total_shares
+                    );
+
+                    120_000_000
+                },
+                ..*usdc_reserve.borrows_pool_reward_manager
+            }),
             ..usdc_reserve
         };
         assert_eq!(usdc_reserve_post.account, expected_usdc_reserve_post);
diff --git a/token-lending/program/tests/borrow_obligation_liquidity.rs b/token-lending/program/tests/borrow_obligation_liquidity.rs
index 881a7bbb90d..02cbba53a6b 100644
--- a/token-lending/program/tests/borrow_obligation_liquidity.rs
+++ b/token-lending/program/tests/borrow_obligation_liquidity.rs
@@ -1,6 +1,7 @@
 #![cfg(feature = "test-bpf")]
 
 use crate::helpers::solend_program_test::*;
+use pretty_assertions::assert_eq;
 use solana_program::pubkey::Pubkey;
 use solana_sdk::signature::Signer;
 
@@ -216,7 +217,20 @@ async fn test_success() {
         usdc_reserve_post,
     );
 
-    let wsol_reserve_post = test.load_account::<Reserve>(wsol_reserve.pubkey).await;
+    let mut wsol_reserve_post = test.load_account::<Reserve>(wsol_reserve.pubkey).await;
+
+    {
+        // let's test liq. mining separately bcs of clock time
+
+        let borrows_manager = &wsol_reserve_post.account.borrows_pool_reward_manager;
+
+        assert_eq!(borrows_manager.total_shares, 4000000400);
+        assert_ne!(borrows_manager.last_update_time_secs, 0);
+
+        wsol_reserve_post.account.borrows_pool_reward_manager =
+            wsol_reserve.account.borrows_pool_reward_manager.clone();
+    }
+
     let expected_wsol_reserve_post = Reserve {
         last_update: LastUpdate {
             slot: 1000,
@@ -238,11 +252,7 @@ async fn test_success() {
         ..wsol_reserve.account
     };
 
-    assert_eq!(
-        wsol_reserve_post.account, expected_wsol_reserve_post,
-        "{:#?} {:#?}",
-        wsol_reserve_post, expected_wsol_reserve_post
-    );
+    assert_eq!(wsol_reserve_post.account, expected_wsol_reserve_post);
 
     let obligation_post = test.load_obligation(obligation.pubkey).await;
     assert_eq!(
@@ -271,10 +281,30 @@ async fn test_success() {
             unweighted_borrowed_value: borrow_value,
             allowed_borrow_value: Decimal::from(50u64),
             unhealthy_borrow_value: Decimal::from(55u64),
+            user_reward_managers: {
+                // clock value remains the same
+                let last_update_time_secs =
+                    obligation.account.user_reward_managers[0].last_update_time_secs;
+
+                UserRewardManagers(vec![
+                    UserRewardManager {
+                        reserve: usdc_reserve.pubkey,
+                        position_kind: PositionKind::Deposit,
+                        share: 100000000,
+                        last_update_time_secs,
+                        rewards: Vec::new(),
+                    },
+                    UserRewardManager {
+                        reserve: wsol_reserve.pubkey,
+                        position_kind: PositionKind::Borrow,
+                        share: 4000000400,
+                        last_update_time_secs,
+                        rewards: Vec::new(),
+                    },
+                ])
+            },
             ..obligation.account
         },
-        "{:#?}",
-        obligation_post.account
     );
 }
 
@@ -379,7 +409,7 @@ async fn test_fail_borrow_over_reserve_borrow_limit() {
     assert_eq!(
         res,
         TransactionError::InstructionError(
-            1,
+            2, // ix 0 is CU budget, ix 1 is obligation realloc, ix 2 is borrow
             InstructionError::Custom(LendingError::InvalidAmount as u32)
         )
     );
@@ -450,7 +480,7 @@ async fn test_fail_reserve_borrow_rate_limit_exceeded() {
         assert_eq!(
             res,
             TransactionError::InstructionError(
-                1,
+                2, // ix 0 is CU budget, ix 1 is obligation realloc, ix 2 is borrow
                 InstructionError::Custom(LendingError::OutflowRateLimitExceeded as u32)
             )
         );
@@ -476,7 +506,7 @@ async fn test_fail_reserve_borrow_rate_limit_exceeded() {
     assert_eq!(
         res,
         TransactionError::InstructionError(
-            1,
+            2, // ix 0 is CU budget, ix 1 is obligation realloc, ix 2 is borrow
             InstructionError::Custom(LendingError::OutflowRateLimitExceeded as u32)
         )
     );
diff --git a/token-lending/program/tests/borrow_weight.rs b/token-lending/program/tests/borrow_weight.rs
index 9cbeadaedff..3e417caf628 100644
--- a/token-lending/program/tests/borrow_weight.rs
+++ b/token-lending/program/tests/borrow_weight.rs
@@ -340,7 +340,7 @@ async fn test_liquidation() {
         assert_eq!(
             res,
             TransactionError::InstructionError(
-                1,
+                2,
                 InstructionError::Custom(LendingError::ObligationHealthy as u32)
             )
         );
diff --git a/token-lending/program/tests/deposit_obligation_collateral.rs b/token-lending/program/tests/deposit_obligation_collateral.rs
index 8992464dcbb..959630a5cd4 100644
--- a/token-lending/program/tests/deposit_obligation_collateral.rs
+++ b/token-lending/program/tests/deposit_obligation_collateral.rs
@@ -9,12 +9,16 @@ use helpers::solend_program_test::{
 };
 use helpers::test_reserve_config;
 
+use pretty_assertions::assert_eq;
 use solana_program::instruction::InstructionError;
 use solana_program_test::*;
 use solana_sdk::signature::Keypair;
 use solana_sdk::transaction::TransactionError;
 use solend_program::math::Decimal;
-use solend_program::state::{LastUpdate, LendingMarket, Obligation, ObligationCollateral, Reserve};
+use solend_program::state::{
+    LastUpdate, LendingMarket, Obligation, ObligationCollateral, PoolRewardManager, PositionKind,
+    Reserve, UserRewardManager, UserRewardManagers,
+};
 
 async fn setup() -> (
     SolendProgramTest,
@@ -47,8 +51,10 @@ async fn test_success() {
 
     let balance_checker = BalanceChecker::start(&mut test, &[&usdc_reserve, &user]).await;
 
+    let deposit_amount = 1_000_000u64;
+
     lending_market
-        .deposit_obligation_collateral(&mut test, &usdc_reserve, &obligation, &user, 1_000_000)
+        .deposit_obligation_collateral(&mut test, &usdc_reserve, &obligation, &user, deposit_amount)
         .await
         .expect("This should succeed");
 
@@ -61,12 +67,12 @@ async fn test_success() {
                 .get_account(&usdc_reserve.account.collateral.mint_pubkey)
                 .unwrap(),
             mint: usdc_reserve.account.collateral.mint_pubkey,
-            diff: -1_000_000,
+            diff: -(deposit_amount as i128),
         },
         TokenBalanceChange {
             token_account: usdc_reserve.account.collateral.supply_pubkey,
             mint: usdc_reserve.account.collateral.mint_pubkey,
-            diff: 1_000_000,
+            diff: deposit_amount as _,
         },
     ]);
 
@@ -77,8 +83,24 @@ async fn test_success() {
     let lending_market_post = test.load_account(lending_market.pubkey).await;
     assert_eq!(lending_market, lending_market_post);
 
-    let usdc_reserve_post = test.load_account(usdc_reserve.pubkey).await;
-    assert_eq!(usdc_reserve, usdc_reserve_post);
+    let usdc_reserve_post = test.load_account::<Reserve>(usdc_reserve.pubkey).await;
+    let last_update_time_secs = usdc_reserve_post
+        .account
+        .deposits_pool_reward_manager
+        .last_update_time_secs;
+    assert_ne!(last_update_time_secs, 0);
+
+    assert_eq!(
+        usdc_reserve_post.account,
+        Reserve {
+            deposits_pool_reward_manager: Box::new(PoolRewardManager {
+                total_shares: deposit_amount,
+                last_update_time_secs,
+                ..*usdc_reserve.account.deposits_pool_reward_manager
+            }),
+            ..usdc_reserve.account
+        }
+    );
 
     let obligation_post = test.load_obligation(obligation.pubkey).await;
     assert_eq!(
@@ -90,10 +112,17 @@ async fn test_success() {
             },
             deposits: vec![ObligationCollateral {
                 deposit_reserve: usdc_reserve.pubkey,
-                deposited_amount: 1_000_000,
+                deposited_amount: deposit_amount,
                 market_value: Decimal::zero(), // this field only gets updated on a refresh
                 attributed_borrow_value: Decimal::zero()
             }],
+            user_reward_managers: UserRewardManagers(vec![UserRewardManager {
+                reserve: usdc_reserve.pubkey,
+                position_kind: PositionKind::Deposit,
+                share: deposit_amount,
+                last_update_time_secs,
+                rewards: Vec::new(),
+            }]),
             ..obligation.account
         }
     );
@@ -110,11 +139,13 @@ async fn test_fail_deposit_too_much() {
         .unwrap()
         .unwrap();
 
+    // ix 0 is CU budget, ix 1 is transfer to obligation for realloc, ix 2 is deposit
+    const EXPECTED_IX: u8 = 2;
     match res {
         // InsufficientFunds
-        TransactionError::InstructionError(1, InstructionError::Custom(1)) => (),
+        TransactionError::InstructionError(EXPECTED_IX, InstructionError::Custom(1)) => (),
         // LendingError::TokenTransferFailed
-        TransactionError::InstructionError(1, InstructionError::Custom(17)) => (),
+        TransactionError::InstructionError(EXPECTED_IX, InstructionError::Custom(17)) => (),
         e => panic!("unexpected error: {:#?}", e),
     };
 }
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 6b6779fc9b9..01fc216d29b 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
@@ -3,6 +3,7 @@
 mod helpers;
 
 use crate::solend_program_test::MintSupplyChange;
+use pretty_assertions::assert_eq;
 use std::collections::HashSet;
 
 use helpers::solend_program_test::{
@@ -14,8 +15,8 @@ use solana_sdk::signature::Keypair;
 
 use solend_program::math::Decimal;
 use solend_program::state::{
-    LastUpdate, LendingMarket, Obligation, ObligationCollateral, Reserve, ReserveCollateral,
-    ReserveLiquidity,
+    LastUpdate, LendingMarket, Obligation, ObligationCollateral, PoolRewardManager, PositionKind,
+    Reserve, ReserveCollateral, ReserveLiquidity, UserRewardManager, UserRewardManagers,
 };
 
 async fn setup() -> (
@@ -44,6 +45,8 @@ async fn test_success() {
 
     let balance_checker = BalanceChecker::start(&mut test, &[&usdc_reserve, &user]).await;
 
+    let deposit_amount = 1_000_000;
+
     // deposit
     lending_market
         .deposit_reserve_liquidity_and_obligation_collateral(
@@ -51,7 +54,7 @@ async fn test_success() {
             &usdc_reserve,
             &obligation,
             &user,
-            1_000_000,
+            deposit_amount,
         )
         .await
         .expect("this should succeed");
@@ -66,31 +69,27 @@ async fn test_success() {
             TokenBalanceChange {
                 token_account: user.get_account(&usdc_mint::id()).unwrap(),
                 mint: usdc_mint::id(),
-                diff: -1_000_000,
+                diff: -(deposit_amount as i128),
             },
             TokenBalanceChange {
                 token_account: usdc_reserve.account.collateral.supply_pubkey,
                 mint: usdc_reserve.account.collateral.mint_pubkey,
-                diff: 1_000_000,
+                diff: deposit_amount as _,
             },
             TokenBalanceChange {
                 token_account: usdc_reserve.account.liquidity.supply_pubkey,
                 mint: usdc_reserve.account.liquidity.mint_pubkey,
-                diff: 1_000_000,
+                diff: deposit_amount as _,
             },
         ]),
-        "{:#?}",
-        token_balance_changes
     );
 
     assert_eq!(
         mint_supply_changes,
         HashSet::from([MintSupplyChange {
             mint: usdc_reserve.account.collateral.mint_pubkey,
-            diff: 1_000_000,
+            diff: deposit_amount as _,
         },]),
-        "{:#?}",
-        mint_supply_changes
     );
 
     // check program state
@@ -100,21 +99,33 @@ async fn test_success() {
     assert_eq!(lending_market.account, lending_market_post.account);
 
     let usdc_reserve_post = test.load_account::<Reserve>(usdc_reserve.pubkey).await;
+    let last_update_time_secs = usdc_reserve_post
+        .account
+        .deposits_pool_reward_manager
+        .last_update_time_secs;
+    assert_ne!(last_update_time_secs, 0);
+
     assert_eq!(
         usdc_reserve_post.account,
         Reserve {
             last_update: LastUpdate {
                 slot: 1001,
-                stale: false,
+                stale: true,
             },
             liquidity: ReserveLiquidity {
-                available_amount: usdc_reserve.account.liquidity.available_amount + 1_000_000,
+                available_amount: usdc_reserve.account.liquidity.available_amount + deposit_amount,
                 ..usdc_reserve.account.liquidity
             },
             collateral: ReserveCollateral {
-                mint_total_supply: usdc_reserve.account.collateral.mint_total_supply + 1_000_000,
+                mint_total_supply: usdc_reserve.account.collateral.mint_total_supply
+                    + deposit_amount,
                 ..usdc_reserve.account.collateral
             },
+            deposits_pool_reward_manager: Box::new(PoolRewardManager {
+                total_shares: deposit_amount,
+                last_update_time_secs,
+                ..*usdc_reserve.account.deposits_pool_reward_manager
+            }),
             ..usdc_reserve.account
         }
     );
@@ -134,6 +145,13 @@ async fn test_success() {
                 attributed_borrow_value: Decimal::zero()
             }]
             .to_vec(),
+            user_reward_managers: UserRewardManagers(vec![UserRewardManager {
+                reserve: usdc_reserve.pubkey,
+                position_kind: PositionKind::Deposit,
+                share: deposit_amount,
+                last_update_time_secs: last_update_time_secs,
+                rewards: Vec::new(),
+            }]),
             ..obligation.account
         }
     );
diff --git a/token-lending/program/tests/forgive_debt.rs b/token-lending/program/tests/forgive_debt.rs
index 1d6360d301f..af1685d0778 100644
--- a/token-lending/program/tests/forgive_debt.rs
+++ b/token-lending/program/tests/forgive_debt.rs
@@ -10,6 +10,7 @@ use solana_sdk::instruction::Instruction;
 use solana_sdk::pubkey::Pubkey;
 use solana_sdk::signer::Signer;
 
+use pretty_assertions::assert_eq;
 use std::collections::HashSet;
 
 use solend_sdk::instruction::LendingInstruction;
@@ -121,7 +122,7 @@ async fn test_forgive_debt_success_easy() {
     assert_eq!(
         err,
         TransactionError::InstructionError(
-            3,
+            4,
             InstructionError::Custom(LendingError::InvalidAccountInput as u32)
         )
     );
@@ -202,6 +203,10 @@ async fn test_forgive_debt_success_easy() {
                     + wsol_reserve.account.liquidity.available_amount,
                 ..wsol_reserve.account.liquidity
             },
+            borrows_pool_reward_manager: Box::new(PoolRewardManager {
+                total_shares: 0, // liquidated everything
+                ..*wsol_reserve.account.borrows_pool_reward_manager.clone()
+            }),
             ..wsol_reserve.account.clone()
         }
     );
@@ -308,7 +313,7 @@ async fn test_forgive_debt_fail_invalid_signer() {
     assert_eq!(
         err,
         TransactionError::InstructionError(
-            3,
+            4,
             InstructionError::Custom(LendingError::InvalidMarketOwner as u32)
         )
     );
diff --git a/token-lending/program/tests/helpers/solend_program_test.rs b/token-lending/program/tests/helpers/solend_program_test.rs
index b7979450c98..323dcdcc550 100644
--- a/token-lending/program/tests/helpers/solend_program_test.rs
+++ b/token-lending/program/tests/helpers/solend_program_test.rs
@@ -58,24 +58,31 @@ 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 INIT_OBLIGATION: u32 = 10_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 BORROW_OBLIGATION_LIQUIDITY: u32 = 180_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 LIQUIDATE_OBLIGATION_AND_REDEEM_RESERVE_COLLATERAL: u32 = 230_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 WITHDRAW_OBLIGATION_COLLATERAL: u32 = 130_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 UPDATE_RESERVE_CONFIG: u32 = 30_014;
     pub(super) const DEPOSIT_RESERVE_LIQUIDITY_AND_OBLIGATION_COLLATERAL: u32 = 130_015;
-    pub(super) const REDEEM: u32 = 130_016;
+    pub(super) const REDEEM: u32 = 90_016;
 }
 
+/// This is at most how many bytes can an obligation grow.
+/// An obligation grows dynamically as needed when new rewards are being tracked.
+/// These tests don't need to care about correctly transferring just the amount
+/// needed, we'll just transfer lamports to cover the rent of the largest
+/// possible obligation there can be.
+const OBLIGATION_EXTRA_SIZE: usize = Obligation::MAX_LEN - Obligation::MIN_LEN;
+
 pub struct SolendProgramTest {
     pub context: ProgramTestContext,
     rent: Rent,
@@ -968,6 +975,11 @@ impl Info<LendingMarket> {
             ComputeBudgetInstruction::set_compute_unit_limit(
                 cu_budgets::DEPOSIT_RESERVE_LIQUIDITY_AND_OBLIGATION_COLLATERAL,
             ),
+            system_instruction::transfer(
+                &test.context.payer.pubkey(),
+                &obligation.pubkey,
+                Rent::minimum_balance(&Rent::default(), OBLIGATION_EXTRA_SIZE),
+            ),
             deposit_reserve_liquidity_and_obligation_collateral(
                 solend_program::id(),
                 liquidity_amount,
@@ -1071,6 +1083,11 @@ impl Info<LendingMarket> {
             ComputeBudgetInstruction::set_compute_unit_limit(
                 cu_budgets::DEPOSIT_OBLIGATION_COLLATERAL,
             ),
+            system_instruction::transfer(
+                &test.context.payer.pubkey(),
+                &obligation.pubkey,
+                Rent::minimum_balance(&Rent::default(), OBLIGATION_EXTRA_SIZE),
+            ),
             deposit_obligation_collateral(
                 solend_program::id(),
                 collateral_amount,
@@ -1166,6 +1183,12 @@ impl Info<LendingMarket> {
             r
         };
 
+        instructions.push(system_instruction::transfer(
+            &test.context.payer.pubkey(),
+            &obligation.pubkey,
+            Rent::minimum_balance(&Rent::default(), OBLIGATION_EXTRA_SIZE),
+        ));
+
         instructions.push(refresh_obligation(
             solend_program::id(),
             obligation.pubkey,
@@ -1219,9 +1242,16 @@ impl Info<LendingMarket> {
             .await;
         test.process_transaction(&refresh_ixs, None).await.unwrap();
 
-        let mut instructions = vec![ComputeBudgetInstruction::set_compute_unit_limit(
-            cu_budgets::BORROW_OBLIGATION_LIQUIDITY,
-        )];
+        let mut instructions = vec![
+            ComputeBudgetInstruction::set_compute_unit_limit(
+                cu_budgets::BORROW_OBLIGATION_LIQUIDITY,
+            ),
+            system_instruction::transfer(
+                &test.context.payer.pubkey(),
+                &obligation.pubkey,
+                Rent::minimum_balance(&Rent::default(), OBLIGATION_EXTRA_SIZE),
+            ),
+        ];
         instructions.push(borrow_obligation_liquidity(
             solend_program::id(),
             liquidity_amount,
@@ -1320,6 +1350,11 @@ impl Info<LendingMarket> {
                 ComputeBudgetInstruction::set_compute_unit_limit(
                     cu_budgets::LIQUIDATE_OBLIGATION_AND_REDEEM_RESERVE_COLLATERAL,
                 ),
+                system_instruction::transfer(
+                    &test.context.payer.pubkey(),
+                    &obligation.pubkey,
+                    Rent::minimum_balance(&Rent::default(), OBLIGATION_EXTRA_SIZE),
+                ),
                 liquidate_obligation_and_redeem_reserve_collateral(
                     solend_program::id(),
                     liquidity_amount,
@@ -1359,6 +1394,11 @@ impl Info<LendingMarket> {
             .build_refresh_instructions(test, obligation, None)
             .await;
 
+        instructions.push(system_instruction::transfer(
+            &test.context.payer.pubkey(),
+            &obligation.pubkey,
+            Rent::minimum_balance(&Rent::default(), OBLIGATION_EXTRA_SIZE),
+        ));
         instructions.push(liquidate_obligation(
             solend_program::id(),
             liquidity_amount,
diff --git a/token-lending/program/tests/init_obligation.rs b/token-lending/program/tests/init_obligation.rs
index 1747113fa15..ecb32f4ac8e 100644
--- a/token-lending/program/tests/init_obligation.rs
+++ b/token-lending/program/tests/init_obligation.rs
@@ -54,7 +54,7 @@ async fn test_success() {
             super_unhealthy_borrow_value: Decimal::zero(),
             borrowing_isolated_asset: false,
             closeable: false,
-            user_reward_managers: Vec::new(),
+            user_reward_managers: Default::default(),
         }
     );
 }
diff --git a/token-lending/program/tests/isolated_tier_assets.rs b/token-lending/program/tests/isolated_tier_assets.rs
index 714e257fa6b..4fd5c411eef 100644
--- a/token-lending/program/tests/isolated_tier_assets.rs
+++ b/token-lending/program/tests/isolated_tier_assets.rs
@@ -1,5 +1,7 @@
 #![cfg(feature = "test-bpf")]
 
+use pretty_assertions::assert_eq;
+
 use crate::solend_program_test::custom_scenario;
 use solend_program::state::ObligationCollateral;
 
@@ -15,7 +17,10 @@ use solend_sdk::math::Decimal;
 
 use solend_program::state::LastUpdate;
 use solend_program::state::ReserveType;
-use solend_program::state::{Obligation, ObligationLiquidity, ReserveConfig};
+use solend_program::state::{
+    Obligation, ObligationLiquidity, PositionKind, ReserveConfig, UserRewardManager,
+    UserRewardManagers,
+};
 
 use solend_sdk::state::ReserveFees;
 mod helpers;
@@ -85,6 +90,10 @@ async fn test_refresh_obligation() {
         .iter()
         .find(|r| r.account.liquidity.mint_pubkey == wsol_mint::id())
         .unwrap();
+    let usdc_reserve = reserves
+        .iter()
+        .find(|r| r.account.liquidity.mint_pubkey == usdc_mint::id())
+        .unwrap();
 
     // borrow isolated tier asset
     lending_market
@@ -106,6 +115,10 @@ async fn test_refresh_obligation() {
 
     let obligation_post = test.load_obligation(obligations[0].pubkey).await;
 
+    let last_update_time_secs =
+        obligation_post.account.user_reward_managers[0].last_update_time_secs;
+    assert_ne!(last_update_time_secs, 0,);
+
     assert_eq!(
         obligation_post.account,
         Obligation {
@@ -127,6 +140,22 @@ async fn test_refresh_obligation() {
             unweighted_borrowed_value: Decimal::from(10u64),
             borrowed_value_upper_bound: Decimal::from(10u64),
             borrowing_isolated_asset: true,
+            user_reward_managers: UserRewardManagers(vec![
+                UserRewardManager {
+                    reserve: usdc_reserve.pubkey,
+                    position_kind: PositionKind::Deposit,
+                    share: 100000000,
+                    last_update_time_secs,
+                    rewards: Vec::new(),
+                },
+                UserRewardManager {
+                    reserve: wsol_reserve.pubkey,
+                    position_kind: PositionKind::Borrow,
+                    share: 1000000000,
+                    last_update_time_secs,
+                    rewards: Vec::new(),
+                },
+            ],),
             ..obligations[0].account.clone()
         }
     );
@@ -287,7 +316,7 @@ async fn borrow_isolated_asset_invalid() {
     assert_eq!(
         err,
         TransactionError::InstructionError(
-            1,
+            2,
             InstructionError::Custom(LendingError::IsolatedTierAssetViolation as u32)
         )
     );
@@ -381,7 +410,7 @@ async fn borrow_regular_asset_invalid() {
     assert_eq!(
         err,
         TransactionError::InstructionError(
-            1,
+            2,
             InstructionError::Custom(LendingError::IsolatedTierAssetViolation as u32)
         )
     );
@@ -485,7 +514,7 @@ async fn invalid_borrow_due_to_reserve_config_change() {
     assert_eq!(
         err,
         TransactionError::InstructionError(
-            1,
+            2,
             InstructionError::Custom(LendingError::IsolatedTierAssetViolation as u32)
         )
     );
diff --git a/token-lending/program/tests/liquidate_obligation.rs b/token-lending/program/tests/liquidate_obligation.rs
index e583f55b702..b58a1fb0384 100644
--- a/token-lending/program/tests/liquidate_obligation.rs
+++ b/token-lending/program/tests/liquidate_obligation.rs
@@ -33,7 +33,7 @@ async fn test_fail_deprecated() {
     assert_eq!(
         res,
         TransactionError::InstructionError(
-            3,
+            5,
             InstructionError::Custom(LendingError::DeprecatedInstruction as u32)
         )
     );
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 75eedf36ffa..0edcdfa1523 100644
--- a/token-lending/program/tests/liquidate_obligation_and_redeem_collateral.rs
+++ b/token-lending/program/tests/liquidate_obligation_and_redeem_collateral.rs
@@ -30,11 +30,13 @@ use solana_sdk::signature::Keypair;
 use solend_program::math::Decimal;
 use solend_program::state::LendingMarket;
 use solend_program::state::Obligation;
+use solend_program::state::PoolRewardManager;
 use solend_program::state::Reserve;
 use solend_program::state::ReserveCollateral;
 use solend_program::state::ReserveLiquidity;
 use solend_program::state::LIQUIDATION_CLOSE_FACTOR;
 
+use pretty_assertions::assert_eq;
 use std::collections::HashSet;
 
 #[tokio::test]
@@ -166,9 +168,18 @@ async fn test_success_new() {
     assert_eq!(lending_market_post.account, lending_market.account);
 
     let usdc_reserve_post = test.load_account::<Reserve>(usdc_reserve.pubkey).await;
+    let expected_usdc_reserve_post_total_shares = usdc_reserve
+        .account
+        .deposits_pool_reward_manager
+        .total_shares
+        - expected_usdc_withdrawn * FRACTIONAL_TO_USDC;
     assert_eq!(
         usdc_reserve_post.account,
         Reserve {
+            last_update: LastUpdate {
+                stale: true,
+                ..usdc_reserve.account.last_update
+            },
             liquidity: ReserveLiquidity {
                 available_amount: usdc_reserve.account.liquidity.available_amount
                     - expected_usdc_withdrawn * FRACTIONAL_TO_USDC,
@@ -180,14 +191,23 @@ async fn test_success_new() {
                 ..usdc_reserve.account.collateral
             },
             attributed_borrow_value: Decimal::from(55000u64),
+            deposits_pool_reward_manager: Box::new(PoolRewardManager {
+                total_shares: expected_usdc_reserve_post_total_shares,
+                ..*usdc_reserve.account.deposits_pool_reward_manager.clone()
+            }),
             ..usdc_reserve.account
         }
     );
 
+    let expected_wsol_reserve_post_total_shares = 8000000000;
     let wsol_reserve_post = test.load_account::<Reserve>(wsol_reserve.pubkey).await;
     assert_eq!(
         wsol_reserve_post.account,
         Reserve {
+            last_update: LastUpdate {
+                stale: true,
+                ..wsol_reserve.account.last_update
+            },
             liquidity: ReserveLiquidity {
                 available_amount: wsol_reserve.account.liquidity.available_amount
                     + expected_borrow_repaid * LAMPORTS_TO_SOL,
@@ -201,10 +221,26 @@ async fn test_success_new() {
                 smoothed_market_price: Decimal::from(5500u64),
                 ..wsol_reserve.account.liquidity
             },
+            borrows_pool_reward_manager: Box::new(PoolRewardManager {
+                total_shares: {
+                    assert_eq!(
+                        wsol_reserve
+                            .account
+                            .borrows_pool_reward_manager
+                            .total_shares,
+                        10000000000
+                    );
+
+                    expected_wsol_reserve_post_total_shares
+                },
+                ..*wsol_reserve.account.borrows_pool_reward_manager.clone()
+            }),
             ..wsol_reserve.account
         }
     );
 
+    let deposit_reserve = usdc_reserve.pubkey;
+    let borrow_reserve = wsol_reserve.pubkey;
     let obligation_post = test.load_obligation(obligation.pubkey).await;
     assert_eq!(
         obligation_post.account,
@@ -214,7 +250,7 @@ async fn test_success_new() {
                 stale: true
             },
             deposits: [ObligationCollateral {
-                deposit_reserve: usdc_reserve.pubkey,
+                deposit_reserve,
                 deposited_amount: (100_000 - expected_usdc_withdrawn) * FRACTIONAL_TO_USDC,
                 market_value: Decimal::from(100_000u64), // old value
                 attributed_borrow_value: obligation_post.account.deposits[0]
@@ -222,7 +258,7 @@ async fn test_success_new() {
             }]
             .to_vec(),
             borrows: [ObligationLiquidity {
-                borrow_reserve: wsol_reserve.pubkey,
+                borrow_reserve,
                 cumulative_borrow_rate_wads: Decimal::one(),
                 borrowed_amount_wads: Decimal::from(10 * LAMPORTS_TO_SOL)
                     .try_sub(Decimal::from(expected_borrow_repaid * LAMPORTS_TO_SOL))
@@ -236,6 +272,21 @@ async fn test_success_new() {
             borrowed_value_upper_bound: Decimal::from(55_000u64),
             allowed_borrow_value: Decimal::from(50_000u64),
             unhealthy_borrow_value: Decimal::from(55_000u64),
+            user_reward_managers: {
+                let mut og = obligation.account.user_reward_managers.clone();
+
+                og.iter_mut()
+                    .find(|m| m.reserve == deposit_reserve)
+                    .unwrap()
+                    .share = expected_usdc_reserve_post_total_shares;
+
+                og.iter_mut()
+                    .find(|m| m.reserve == borrow_reserve)
+                    .unwrap()
+                    .share = expected_wsol_reserve_post_total_shares;
+
+                og
+            },
             ..obligation.account
         }
     );
@@ -322,7 +373,7 @@ async fn test_whitelisting_liquidator() {
     assert_eq!(
         err,
         TransactionError::InstructionError(
-            1,
+            2,
             InstructionError::Custom(LendingError::NotWhitelistedLiquidator as u32)
         )
     );
@@ -648,7 +699,7 @@ async fn test_liquidity_ordering() {
     assert_eq!(
         err,
         TransactionError::InstructionError(
-            1,
+            2,
             InstructionError::Custom(LendingError::InvalidAccountInput as u32)
         )
     );
diff --git a/token-lending/program/tests/outflow_rate_limits.rs b/token-lending/program/tests/outflow_rate_limits.rs
index f42dba03f28..66e47fc7d1d 100644
--- a/token-lending/program/tests/outflow_rate_limits.rs
+++ b/token-lending/program/tests/outflow_rate_limits.rs
@@ -169,7 +169,7 @@ async fn test_outflow_reserve() {
         assert_eq!(
             res,
             TransactionError::InstructionError(
-                1,
+                2,
                 InstructionError::Custom(LendingError::OutflowRateLimitExceeded as u32)
             )
         );
diff --git a/token-lending/program/tests/repay_obligation_liquidity.rs b/token-lending/program/tests/repay_obligation_liquidity.rs
index 989da4fab21..24686f4d4e5 100644
--- a/token-lending/program/tests/repay_obligation_liquidity.rs
+++ b/token-lending/program/tests/repay_obligation_liquidity.rs
@@ -3,6 +3,7 @@
 mod helpers;
 
 use crate::solend_program_test::scenario_1;
+use pretty_assertions::assert_eq;
 use std::collections::HashSet;
 
 use helpers::solend_program_test::{BalanceChecker, TokenBalanceChange};
@@ -14,7 +15,7 @@ use solend_program::math::TryDiv;
 use solend_program::state::{LastUpdate, ObligationLiquidity, ReserveLiquidity, SLOTS_PER_YEAR};
 use solend_program::{
     math::{Decimal, TryAdd, TryMul, TrySub},
-    state::{Obligation, Reserve},
+    state::{Obligation, PoolRewardManager, Reserve},
 };
 
 #[tokio::test]
@@ -73,6 +74,7 @@ async fn test_success() {
         .try_sub(Decimal::from(10 * LAMPORTS_TO_SOL))
         .unwrap();
 
+    let expected_wsol_reserve_post_borrow_total_shares = 47;
     assert_eq!(
         wsol_reserve_post.account,
         Reserve {
@@ -86,11 +88,26 @@ async fn test_success() {
                 cumulative_borrow_rate_wads: new_cumulative_borrow_rate,
                 ..wsol_reserve.account.liquidity
             },
+            borrows_pool_reward_manager: Box::new(PoolRewardManager {
+                total_shares: {
+                    assert_eq!(
+                        wsol_reserve
+                            .account
+                            .borrows_pool_reward_manager
+                            .total_shares,
+                        10 * LAMPORTS_PER_SOL,
+                    );
+
+                    expected_wsol_reserve_post_borrow_total_shares
+                },
+                ..*wsol_reserve.account.borrows_pool_reward_manager
+            }),
             ..wsol_reserve.account
         }
     );
 
     let obligation_post = test.load_obligation(obligation.pubkey).await;
+    let borrow_reserve = wsol_reserve.pubkey;
     assert_eq!(
         obligation_post.account,
         Obligation {
@@ -100,12 +117,22 @@ async fn test_success() {
                 stale: true
             },
             borrows: [ObligationLiquidity {
-                borrow_reserve: wsol_reserve.pubkey,
+                borrow_reserve,
                 cumulative_borrow_rate_wads: new_cumulative_borrow_rate,
                 borrowed_amount_wads: new_borrowed_amount_wads,
                 ..obligation.account.borrows[0]
             }]
             .to_vec(),
+            user_reward_managers: {
+                let mut og = obligation.account.user_reward_managers.clone();
+
+                og.iter_mut()
+                    .find(|m| m.reserve == borrow_reserve)
+                    .unwrap()
+                    .share = expected_wsol_reserve_post_borrow_total_shares;
+
+                og
+            },
             ..obligation.account
         }
     );
diff --git a/token-lending/program/tests/two_prices.rs b/token-lending/program/tests/two_prices.rs
index 463b562fb06..809c7a25219 100644
--- a/token-lending/program/tests/two_prices.rs
+++ b/token-lending/program/tests/two_prices.rs
@@ -144,7 +144,7 @@ async fn test_borrow() {
     assert_eq!(
         err,
         TransactionError::InstructionError(
-            1,
+            2,
             InstructionError::Custom(LendingError::BorrowTooLarge as u32)
         )
     );
@@ -376,7 +376,7 @@ async fn test_liquidation_doesnt_use_smoothed_price() {
     assert_eq!(
         err,
         TransactionError::InstructionError(
-            1,
+            2,
             InstructionError::Custom(LendingError::ObligationHealthy as u32)
         )
     );
@@ -412,7 +412,7 @@ async fn test_liquidation_doesnt_use_smoothed_price() {
     assert_eq!(
         err,
         TransactionError::InstructionError(
-            1,
+            2,
             InstructionError::Custom(LendingError::ObligationHealthy as u32)
         )
     );
diff --git a/token-lending/program/tests/withdraw_obligation_collateral.rs b/token-lending/program/tests/withdraw_obligation_collateral.rs
index faf8073df32..9109d4ea4b3 100644
--- a/token-lending/program/tests/withdraw_obligation_collateral.rs
+++ b/token-lending/program/tests/withdraw_obligation_collateral.rs
@@ -9,7 +9,9 @@ use solend_sdk::math::Decimal;
 
 use solana_program_test::*;
 
+use pretty_assertions::assert_eq;
 use solend_program::state::{LastUpdate, Obligation, ObligationCollateral, Reserve};
+use solend_sdk::state::PoolRewardManager;
 use std::collections::HashSet;
 use std::u64;
 
@@ -21,8 +23,16 @@ async fn test_success_withdraw_fixed_amount() {
     let balance_checker =
         BalanceChecker::start(&mut test, &[&usdc_reserve, &user, &wsol_reserve]).await;
 
+    let withdraw_amount = 1_000_000;
+
     lending_market
-        .withdraw_obligation_collateral(&mut test, &usdc_reserve, &obligation, &user, 1_000_000)
+        .withdraw_obligation_collateral(
+            &mut test,
+            &usdc_reserve,
+            &obligation,
+            &user,
+            withdraw_amount,
+        )
         .await
         .unwrap();
 
@@ -34,21 +44,35 @@ async fn test_success_withdraw_fixed_amount() {
                 .get_account(&usdc_reserve.account.collateral.mint_pubkey)
                 .unwrap(),
             mint: usdc_reserve.account.collateral.mint_pubkey,
-            diff: 1_000_000,
+            diff: withdraw_amount as _,
         },
         TokenBalanceChange {
             token_account: usdc_reserve.account.collateral.supply_pubkey,
             mint: usdc_reserve.account.collateral.mint_pubkey,
-            diff: -1_000_000,
+            diff: -(withdraw_amount as i128),
         },
     ]);
     assert_eq!(balance_changes, expected_balance_changes);
     assert_eq!(mint_supply_changes, HashSet::new());
 
     let usdc_reserve_post = test.load_account::<Reserve>(usdc_reserve.pubkey).await;
-    assert_eq!(usdc_reserve_post.account, usdc_reserve.account);
+    assert_eq!(
+        usdc_reserve_post.account,
+        Reserve {
+            deposits_pool_reward_manager: Box::new(PoolRewardManager {
+                total_shares: usdc_reserve
+                    .account
+                    .deposits_pool_reward_manager
+                    .total_shares
+                    - withdraw_amount,
+                ..*usdc_reserve.account.deposits_pool_reward_manager
+            }),
+            ..usdc_reserve.account
+        }
+    );
 
     let obligation_post = test.load_obligation(obligation.pubkey).await;
+    let deposit_reserve = usdc_reserve.pubkey;
     assert_eq!(
         obligation_post.account,
         Obligation {
@@ -57,13 +81,27 @@ async fn test_success_withdraw_fixed_amount() {
                 stale: true
             },
             deposits: [ObligationCollateral {
-                deposit_reserve: usdc_reserve.pubkey,
-                deposited_amount: 100_000_000_000 - 1_000_000,
+                deposit_reserve,
+                deposited_amount: 100_000_000_000 - withdraw_amount,
                 market_value: Decimal::from(99_999u64),
                 ..obligation.account.deposits[0]
             }]
             .to_vec(),
             deposited_value: Decimal::from(99_999u64),
+            user_reward_managers: {
+                let mut og = obligation.account.user_reward_managers.clone();
+
+                og.iter_mut()
+                    .find(|m| m.reserve == deposit_reserve)
+                    .unwrap()
+                    .share = usdc_reserve
+                    .account
+                    .deposits_pool_reward_manager
+                    .total_shares
+                    - withdraw_amount;
+
+                og
+            },
             ..obligation.account
         }
     );
@@ -111,9 +149,19 @@ async fn test_success_withdraw_max() {
     assert_eq!(mint_supply_changes, HashSet::new());
 
     let usdc_reserve_post = test.load_account::<Reserve>(usdc_reserve.pubkey).await;
-    assert_eq!(usdc_reserve_post.account, usdc_reserve.account);
+    assert_eq!(
+        usdc_reserve_post.account,
+        Reserve {
+            deposits_pool_reward_manager: Box::new(PoolRewardManager {
+                total_shares: expected_remaining_collateral,
+                ..*usdc_reserve.account.deposits_pool_reward_manager
+            }),
+            ..usdc_reserve.account
+        }
+    );
 
     let obligation_post = test.load_obligation(obligation.pubkey).await;
+    let deposit_reserve = usdc_reserve.pubkey;
     assert_eq!(
         obligation_post.account,
         Obligation {
@@ -122,13 +170,23 @@ async fn test_success_withdraw_max() {
                 stale: true
             },
             deposits: [ObligationCollateral {
-                deposit_reserve: usdc_reserve.pubkey,
+                deposit_reserve,
                 deposited_amount: expected_remaining_collateral,
                 market_value: Decimal::from(200u64),
                 ..obligation.account.deposits[0]
             }]
             .to_vec(),
             deposited_value: Decimal::from(200u64),
+            user_reward_managers: {
+                let mut og = obligation.account.user_reward_managers.clone();
+
+                og.iter_mut()
+                    .find(|m| m.reserve == deposit_reserve)
+                    .unwrap()
+                    .share = expected_remaining_collateral;
+
+                og
+            },
             ..obligation.account
         }
     );
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 3cfc66f072f..dc68a9a4d63 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
@@ -7,6 +7,7 @@ use solend_program::math::TryDiv;
 mod helpers;
 
 use crate::solend_program_test::*;
+use pretty_assertions::assert_eq;
 use solend_sdk::math::Decimal;
 use solend_sdk::state::ObligationCollateral;
 use solend_sdk::state::ReserveCollateral;
@@ -126,11 +127,20 @@ async fn test_success() {
 
                 rate_limiter
             },
+            deposits_pool_reward_manager: Box::new(PoolRewardManager {
+                total_shares: usdc_reserve
+                    .account
+                    .deposits_pool_reward_manager
+                    .total_shares
+                    - withdraw_amount as u64,
+                ..*usdc_reserve.account.deposits_pool_reward_manager
+            }),
             ..usdc_reserve.account
         }
     );
 
     let obligation_post = test.load_obligation(obligation.pubkey).await;
+    let deposit_reserve = usdc_reserve.pubkey;
     assert_eq!(
         obligation_post.account,
         Obligation {
@@ -139,13 +149,23 @@ async fn test_success() {
                 stale: true
             },
             deposits: [ObligationCollateral {
-                deposit_reserve: usdc_reserve.pubkey,
+                deposit_reserve,
                 deposited_amount: 200 * FRACTIONAL_TO_USDC,
                 market_value: Decimal::from(200u64),
                 ..obligation.account.deposits[0]
             }]
             .to_vec(),
             deposited_value: Decimal::from(200u64),
+            user_reward_managers: {
+                let mut og = obligation.account.user_reward_managers.clone();
+
+                og.iter_mut()
+                    .find(|m| m.reserve == deposit_reserve)
+                    .unwrap()
+                    .share = 200 * FRACTIONAL_TO_USDC;
+
+                og
+            },
             ..obligation.account
         }
     );
diff --git a/token-lending/sdk/Cargo.toml b/token-lending/sdk/Cargo.toml
index 7ebce7c8d64..0cc1bc180af 100644
--- a/token-lending/sdk/Cargo.toml
+++ b/token-lending/sdk/Cargo.toml
@@ -23,6 +23,7 @@ uint = "=0.9.1"
 assert_matches = "1.5.0"
 base64 = "0.13"
 log = "0.4.14"
+pretty_assertions = "1.4.1"
 proptest = "1.6"
 rand = "0.8.5"
 serde = ">=1.0.140"
diff --git a/token-lending/sdk/src/instruction.rs b/token-lending/sdk/src/instruction.rs
index 68d95c87468..14d03b5926d 100644
--- a/token-lending/sdk/src/instruction.rs
+++ b/token-lending/sdk/src/instruction.rs
@@ -1630,7 +1630,7 @@ pub fn withdraw_obligation_collateral_and_redeem_reserve_collateral(
         AccountMeta::new(destination_liquidity_pubkey, false),
         AccountMeta::new(reserve_collateral_mint_pubkey, false),
         AccountMeta::new(reserve_liquidity_supply_pubkey, false),
-        AccountMeta::new_readonly(obligation_owner_pubkey, true),
+        AccountMeta::new(obligation_owner_pubkey, true),
         AccountMeta::new_readonly(user_transfer_authority_pubkey, true),
         AccountMeta::new_readonly(spl_token::id(), false),
     ];
@@ -1672,7 +1672,7 @@ pub fn withdraw_obligation_collateral(
     let mut accounts = vec![
         AccountMeta::new(source_collateral_pubkey, false),
         AccountMeta::new(destination_collateral_pubkey, false),
-        AccountMeta::new_readonly(withdraw_reserve_pubkey, false),
+        AccountMeta::new(withdraw_reserve_pubkey, false),
         AccountMeta::new(obligation_pubkey, false),
         AccountMeta::new_readonly(lending_market_pubkey, false),
         AccountMeta::new_readonly(lending_market_authority_pubkey, false),
@@ -1790,7 +1790,7 @@ pub fn liquidate_obligation(
             AccountMeta::new(destination_collateral_pubkey, false),
             AccountMeta::new(repay_reserve_pubkey, false),
             AccountMeta::new(repay_reserve_liquidity_supply_pubkey, false),
-            AccountMeta::new_readonly(withdraw_reserve_pubkey, false),
+            AccountMeta::new(withdraw_reserve_pubkey, false),
             AccountMeta::new(withdraw_reserve_collateral_supply_pubkey, false),
             AccountMeta::new(obligation_pubkey, false),
             AccountMeta::new_readonly(lending_market_pubkey, false),
diff --git a/token-lending/sdk/src/state/liquidity_mining.rs b/token-lending/sdk/src/state/liquidity_mining.rs
index 1afc68e4b6f..0d5a05cb3a3 100644
--- a/token-lending/sdk/src/state/liquidity_mining.rs
+++ b/token-lending/sdk/src/state/liquidity_mining.rs
@@ -2,9 +2,13 @@ use super::pack_decimal;
 use crate::{
     error::LendingError,
     math::{Decimal, TryAdd, TryDiv, TryMul, TrySub},
-    state::unpack_decimal,
+    state::{unpack_decimal, PositionKind},
 };
 use arrayref::{array_mut_ref, array_ref, array_refs, mut_array_refs};
+use core::{
+    convert::TryInto,
+    ops::{Deref, DerefMut},
+};
 use solana_program::msg;
 use solana_program::program_pack::{Pack, Sealed};
 use solana_program::{
@@ -12,7 +16,7 @@ use solana_program::{
     program_error::ProgramError,
     pubkey::{Pubkey, PUBKEY_BYTES},
 };
-use std::convert::{TryFrom, TryInto};
+use std::convert::TryFrom;
 
 /// Determines the size of [PoolRewardManager]
 pub const MAX_REWARDS: usize = 50;
@@ -64,7 +68,9 @@ pub enum PoolRewardSlot {
         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,
+        /// That way the packing logic knows whether it's fine to skip the
+        /// packing or not.
+        has_been_just_vacated: bool,
     },
     /// Reward has not been closed yet.
     ///
@@ -118,20 +124,23 @@ pub struct PoolReward {
     pub cumulative_rewards_per_share: Decimal,
 }
 
+/// Wraps over user reward managers and allows mutable access to them while
+/// other obligation fields are borrowed.
+#[derive(Clone, Debug, Default, PartialEq)]
+pub struct UserRewardManagers(pub Vec<UserRewardManager>);
+
 /// Tracks user's LM rewards for a specific pool (reserve.)
-#[derive(Debug, PartialEq, Eq, Default, Clone)]
+#[derive(Debug, PartialEq, Eq, Clone)]
 pub struct UserRewardManager {
-    /// User cannot both borrow and deposit in the same reserve.
-    /// 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
-    /// 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.
+    /// Links this manager to a reserve.
     pub reserve: Pubkey,
+    /// Although a user cannot both borrow and deposit in the same reserve, they
+    /// can deposit, withdraw and then borrow the same reserve.
+    /// Meanwhile they could've accumulated some rewards that'd be lost.
+    ///
+    /// Also, have an explicit distinguish between borrow and deposit doesn't
+    /// suffer from a footgun of misattributing rewards.
+    pub position_kind: PositionKind,
     /// For deposits, this is the amount of collateral token user has in
     /// their obligation deposit.
     ///
@@ -196,7 +205,7 @@ impl PoolRewardManager {
 
         let start_time_secs = start_time_secs.max(clock.unix_timestamp as u64);
 
-        if start_time_secs <= end_time_secs {
+        if start_time_secs >= end_time_secs {
             msg!("Pool reward must end after it starts");
             return Err(LendingError::MathOverflow.into());
         }
@@ -302,7 +311,7 @@ impl PoolRewardManager {
 
         self.pool_rewards[pool_reward_index] = PoolRewardSlot::Vacant {
             last_pool_reward_id: pool_reward.id,
-            has_been_vacated_in_this_tx: true,
+            has_been_just_vacated: true,
         };
 
         Ok(vault)
@@ -360,7 +369,7 @@ impl PoolRewardManager {
 
 /// When creating a new [UserRewardManager] we need to know whether we should
 /// populate it with rewards or not.
-pub enum CreatingNewUserRewardManager {
+enum CreatingNewUserRewardManager {
     /// If we are creating a [UserRewardManager] then we want to populate it.
     Yes,
     /// If we are updating an existing [UserRewardManager] then we don't want
@@ -368,11 +377,96 @@ pub enum CreatingNewUserRewardManager {
     No,
 }
 
+impl UserRewardManagers {
+    /// Returns [UserRewardManager] for the given reserve if any
+    pub fn find_mut(
+        &mut self,
+        reserve: Pubkey,
+        position_kind: PositionKind,
+    ) -> Option<&mut UserRewardManager> {
+        self.0.iter_mut().find(|user_reward_manager| {
+            user_reward_manager.reserve == reserve
+                && user_reward_manager.position_kind == position_kind
+        })
+    }
+
+    /// Updates the [UserRewardManager] for the given reserve.
+    ///
+    /// The caller must make sure that the provided [PoolRewardManager] is valid
+    /// for the given reserve.
+    ///
+    /// If an associated [UserRewardManager] is not found, it will be created.
+    ///
+    /// # Important
+    ///
+    /// Only call this if you're sure that the obligation should be tracking
+    /// rewards for the given reserve.
+    pub fn set_share(
+        &mut self,
+        reserve: Pubkey,
+        position_kind: PositionKind,
+        pool_reward_manager: &mut PoolRewardManager,
+        new_share: u64,
+        clock: &Clock,
+    ) -> Result<(), ProgramError> {
+        let user_reward_manager = if let Some(user_reward_manager) =
+            self.find_mut(reserve, position_kind)
+        {
+            user_reward_manager.update(pool_reward_manager, clock)?;
+            user_reward_manager
+        } else {
+            let mut new_user_reward_manager = UserRewardManager::new(reserve, position_kind, clock);
+            new_user_reward_manager.populate(pool_reward_manager, clock)?;
+            self.0.push(new_user_reward_manager);
+            // SAFETY: we just pushed a new item to the vector so ok to unwrap
+            self.0.last_mut().unwrap()
+        };
+
+        user_reward_manager.set_share(pool_reward_manager, new_share);
+
+        Ok(())
+    }
+}
+
 impl UserRewardManager {
+    /// Creates a new empty [UserRewardManager] for the given reserve.
+    pub fn new(reserve: Pubkey, position_kind: PositionKind, clock: &Clock) -> Self {
+        Self {
+            reserve,
+            last_update_time_secs: clock.unix_timestamp as _,
+            position_kind,
+            share: 0,
+            rewards: Vec::new(),
+        }
+    }
+
+    /// Sets new share value for this manager.
+    fn set_share(&mut self, pool_reward_manager: &mut PoolRewardManager, new_share: u64) {
+        msg!(
+            "For reserve {} there are {} total shares. \
+            User's previous position was at {} and new is at {}",
+            self.reserve,
+            pool_reward_manager.total_shares,
+            self.share,
+            new_share
+        );
+
+        // This works even for migrations.
+        // User's old share is 0 although it shouldn't be bcs they have borrowed
+        // or deposited.
+        // We only now attribute the share to the user which is fine, it's as if
+        // they just now borrowed/deposited.
+        pool_reward_manager.total_shares =
+            pool_reward_manager.total_shares - self.share + new_share;
+
+        self.share = new_share;
+    }
+
     /// Claims all rewards that the user has earned.
     /// Returns how many tokens should be transferred to the user.
     ///
     /// # Note
+    ///
     /// Errors if there is no pool reward with this vault.
     pub fn claim_rewards(
         &mut self,
@@ -380,9 +474,11 @@ impl UserRewardManager {
         vault: Pubkey,
         clock: &Clock,
     ) -> Result<u64, ProgramError> {
+        self.update(pool_reward_manager, clock)?;
+
         let (pool_reward_index, pool_reward) = pool_reward_manager
             .pool_rewards
-            .iter()
+            .iter_mut()
             .enumerate()
             .find_map(move |(index, slot)| match slot {
                 PoolRewardSlot::Occupied(pool_reward) if pool_reward.vault == vault => {
@@ -392,10 +488,15 @@ impl UserRewardManager {
             })
             .ok_or(LendingError::NoPoolRewardMatches)?;
 
-        let Some(user_reward) = self.rewards.iter_mut().find(|user_reward| {
-            user_reward.pool_reward_index == pool_reward_index
-                && user_reward.pool_reward_id == pool_reward.id
-        }) else {
+        let Some((user_reward_index, user_reward)) =
+            self.rewards
+                .iter_mut()
+                .enumerate()
+                .find(|(_, user_reward)| {
+                    user_reward.pool_reward_index == pool_reward_index
+                        && user_reward.pool_reward_id == pool_reward.id
+                })
+        else {
             // User is not tracking this reward, nothing to claim.
             // Let's be graceful and make this a no-op.
             // Prevents failures when multiple parties crank rewards.
@@ -404,24 +505,55 @@ impl UserRewardManager {
 
         let to_claim = user_reward.withdraw_earned_rewards()?;
 
-        if pool_reward.has_ended(clock) {
-            // If pool reward has ended then it will be removed from the user
-            // reward manager in the next update call.
-            //
-            // We could also complicate matters by doing updates in place when
-            // needed to save on CU if necessary.
-            self.update(pool_reward_manager, clock, CreatingNewUserRewardManager::No)?;
+        if pool_reward.has_ended(clock) && user_reward.earned_rewards.try_floor_u64()? == 0 {
+            // This reward won't be used anymore as it ended and the user
+            // claimed all there was to claim.
+            // 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;
         }
 
         Ok(to_claim)
     }
 
+    /// Should be updated before any interaction with rewards.
+    ///
+    /// Invoker must have checked that this [PoolRewardManager] matches the
+    /// [UserRewardManager].
+    pub fn update(
+        &mut self,
+        pool_reward_manager: &mut PoolRewardManager,
+        clock: &Clock,
+    ) -> Result<(), ProgramError> {
+        self.update_(pool_reward_manager, clock, CreatingNewUserRewardManager::No)
+    }
+
+    /// When user borrows/deposits for a new reserve this function copies all
+    /// reserve rewards from the pool manager to the user manager and starts
+    /// accruing rewards.
+    ///
+    /// Invoker must have checked that this [PoolRewardManager] matches the
+    /// [UserRewardManager].
+    pub(crate) fn populate(
+        &mut self,
+        pool_reward_manager: &mut PoolRewardManager,
+        clock: &Clock,
+    ) -> Result<(), ProgramError> {
+        self.update_(
+            pool_reward_manager,
+            clock,
+            CreatingNewUserRewardManager::Yes,
+        )
+    }
+
     /// Should be updated before any interaction with rewards.
     ///
     /// # Assumption
     /// Invoker has checked that this [PoolRewardManager] matches the
     /// [UserRewardManager].
-    pub fn update(
+    fn update_(
         &mut self,
         pool_reward_manager: &mut PoolRewardManager,
         clock: &Clock,
@@ -454,11 +586,11 @@ impl UserRewardManager {
                 .find(|(_, r)| r.pool_reward_index == pool_reward_index);
 
             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 has_ended_for_user = self.last_update_time_secs >= end_time_secs;
 
             match maybe_user_reward {
                 Some((user_reward_index, user_reward))
-                    if has_ended && user_reward.earned_rewards.try_floor_u64()? == 0 =>
+                    if has_ended_for_user && user_reward.earned_rewards.try_floor_u64()? == 0 =>
                 {
                     // Reward period ended and there's nothing to crank.
                     // We can clean up this user reward.
@@ -467,7 +599,7 @@ impl UserRewardManager {
                     self.rewards.swap_remove(user_reward_index);
                     pool_reward.num_user_reward_managers -= 1;
                 }
-                _ if has_ended => {
+                _ if has_ended_for_user => {
                     // reward period over & there are rewards yet to be cracked
                 }
                 Some((_, user_reward)) => {
@@ -484,6 +616,9 @@ impl UserRewardManager {
                     user_reward.cumulative_rewards_per_share =
                         pool_reward.cumulative_rewards_per_share;
                 }
+                None if pool_reward.start_time_secs > curr_unix_timestamp_secs => {
+                    // reward period has not started yet
+                }
                 None => {
                     // user did not yet start accruing rewards
 
@@ -532,7 +667,7 @@ impl PoolReward {
     /// Returns whether the reward has ended.
     pub fn has_ended(&self, clock: &Clock) -> bool {
         let end_time_secs = self.start_time_secs + self.duration_secs as u64;
-        clock.unix_timestamp as u64 > end_time_secs
+        clock.unix_timestamp as u64 >= end_time_secs
     }
 }
 
@@ -556,7 +691,7 @@ impl Default for PoolRewardSlot {
             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,
+            has_been_just_vacated: false,
         }
     }
 }
@@ -660,7 +795,7 @@ impl Pack for PoolRewardManager {
                 PoolRewardSlot::Vacant {
                     last_pool_reward_id: pool_reward_id,
                     // nope, has been vacant since unpack
-                    has_been_vacated_in_this_tx: false,
+                    has_been_just_vacated: false,
                 }
             } else {
                 let raw_pool_reward_tail =
@@ -705,7 +840,7 @@ impl PoolRewardSlot {
         let for_sure_has_not_changed = matches!(
             self,
             Self::Vacant {
-                has_been_vacated_in_this_tx: false,
+                has_been_just_vacated: false,
                 ..
             }
         );
@@ -746,10 +881,11 @@ impl UserRewardManager {
     /// Length of data before [Self::rewards] tail.
     ///
     /// - [Self::reserve]
+    /// - [Self::position_kind]
     /// - [Self::share]
     /// - [Self::last_update_time_secs]
     /// - [Self::rewards] vector length as u8
-    const HEAD_LEN: usize = PUBKEY_BYTES + 8 + 8 + 1;
+    const HEAD_LEN: usize = PUBKEY_BYTES + 1 + 8 + 8 + 1;
 
     /// How many bytes are needed to pack this [UserRewardManager].
     pub(crate) fn size_in_bytes_when_packed(&self) -> usize {
@@ -763,17 +899,25 @@ impl UserRewardManager {
     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![
+        let (
+            dst_reserve,
+            dst_position_kind,
+            dst_share,
+            dst_last_update_time_secs,
+            dst_user_rewards_len,
+        ) = mut_array_refs![
             raw_user_reward_manager,
             PUBKEY_BYTES,
+            1, // position_kind
             8, // share
             8, // last_update_time_secs
             1  // length of rewards array that's next to come
         ];
 
+        dst_reserve.copy_from_slice(self.reserve.as_ref());
+        dst_position_kind.copy_from_slice(&(self.position_kind as u8).to_le_bytes());
         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(
             &({
                 debug_assert!(MAX_REWARDS >= self.rewards.len());
@@ -815,15 +959,23 @@ impl UserRewardManager {
         let raw_user_reward_manager_head = array_ref![input, 0, UserRewardManager::HEAD_LEN];
 
         #[allow(clippy::ptr_offset_with_cast)]
-        let (src_reserve, src_share, src_last_update_time_secs, src_user_rewards_len) = array_refs![
+        let (
+            src_reserve,
+            src_position_kind,
+            src_share,
+            src_last_update_time_secs,
+            src_user_rewards_len,
+        ) = array_refs![
             raw_user_reward_manager_head,
             PUBKEY_BYTES,
+            1, // position_kind
             8, // share
             8, // last_update_time_secs
             1  // length of rewards array that's next to come
         ];
 
         let reserve = Pubkey::new_from_array(*src_reserve);
+        let position_kind = u8::from_le_bytes(*src_position_kind).try_into()?;
         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);
@@ -851,6 +1003,7 @@ impl UserRewardManager {
 
         Ok(Self {
             reserve,
+            position_kind,
             share,
             last_update_time_secs,
             rewards,
@@ -858,15 +1011,44 @@ impl UserRewardManager {
     }
 }
 
+impl Deref for UserRewardManagers {
+    type Target = Vec<UserRewardManager>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+impl DerefMut for UserRewardManagers {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.0
+    }
+}
+
+impl Default for UserRewardManager {
+    fn default() -> Self {
+        Self {
+            reserve: Pubkey::default(),
+            position_kind: PositionKind::Deposit,
+            share: 0,
+            last_update_time_secs: 0,
+            rewards: Vec::new(),
+        }
+    }
+}
+
 #[cfg(test)]
 mod tests {
     //! TODO: Rewrite these tests from their Suilend counterparts.
     //! TODO: Calculate test coverage and add tests for missing branches.
 
     use super::*;
+    use pretty_assertions::assert_eq;
     use proptest::prelude::*;
     use rand::Rng;
 
+    const SECONDS_IN_A_DAY: u64 = 86_400;
+
     fn pool_reward_manager_strategy() -> impl Strategy<Value = PoolRewardManager> {
         (0..1u32).prop_perturb(|_, mut rng| PoolRewardManager::new_rand(&mut rng))
     }
@@ -904,7 +1086,7 @@ mod tests {
         let mut m = PoolRewardManager::default();
         m.pool_rewards[0] = PoolRewardSlot::Vacant {
             last_pool_reward_id: PoolRewardId(69),
-            has_been_vacated_in_this_tx: true,
+            has_been_just_vacated: true,
         };
 
         let mut packed = vec![0u8; PoolRewardManager::LEN];
@@ -915,7 +1097,7 @@ mod tests {
             unpacked.pool_rewards[0],
             PoolRewardSlot::Vacant {
                 last_pool_reward_id: PoolRewardId(69),
-                has_been_vacated_in_this_tx: false,
+                has_been_just_vacated: false,
             }
         );
     }
@@ -932,7 +1114,7 @@ mod tests {
                 pool_reward,
                 PoolRewardSlot::Vacant {
                     last_pool_reward_id: PoolRewardId(0),
-                    has_been_vacated_in_this_tx: false,
+                    has_been_just_vacated: false,
                 }
             )
         });
@@ -949,34 +1131,459 @@ mod tests {
         assert!(required_realloc <= MAX_REALLOC);
     }
 
+    /// This tests replicates calculations from Suilend's
+    /// "test_pool_reward_manager_basic" test.
     #[test]
     fn it_tests_pool_reward_manager_basic() {
-        // TODO: rewrite Suilend "test_pool_reward_manager_basic" test
+        let usdc = Pubkey::new_unique(); // reserve pubkey
+        let slnd_vault = Pubkey::new_unique(); // where rewards are stored
+
+        let mut clock = Clock {
+            unix_timestamp: 0,
+            ..Default::default()
+        };
+
+        let mut pool_reward_manager = PoolRewardManager::default();
+        {
+            // setup pool reward manager with one reward
+
+            pool_reward_manager
+                .add_pool_reward(
+                    slnd_vault,
+                    0,
+                    20 * SECONDS_IN_A_DAY,
+                    100 * 1_000_000,
+                    &clock,
+                )
+                .expect("It adds pool reward");
+            assert_eq!(
+                pool_reward_manager.pool_rewards[0],
+                PoolRewardSlot::Occupied(Box::new(PoolReward {
+                    id: PoolRewardId(1),
+                    vault: slnd_vault,
+                    start_time_secs: 0,
+                    duration_secs: 20 * SECONDS_IN_A_DAY as u32,
+                    total_rewards: 100 * 1_000_000,
+                    cumulative_rewards_per_share: Decimal::zero(),
+                    num_user_reward_managers: 0,
+                }))
+            );
+        }
+
+        let mut user_reward_manager_1 = UserRewardManager::new(usdc, PositionKind::Deposit, &clock);
+        {
+            // setup user reward manager with 100/100 shares
+
+            user_reward_manager_1
+                .populate(&mut pool_reward_manager, &clock)
+                .expect("It populates user reward manager");
+            user_reward_manager_1.set_share(&mut pool_reward_manager, 100);
+        }
+
+        {
+            // 1/4 of the reward time passes
+            clock.unix_timestamp = 5 * SECONDS_IN_A_DAY as i64;
+
+            let claimed_slnd = user_reward_manager_1
+                .claim_rewards(&mut pool_reward_manager, slnd_vault, &clock)
+                .expect("It claims rewards");
+            assert_eq!(claimed_slnd, 25 * 1_000_000);
+        }
+
+        let mut user_reward_manager_2 = UserRewardManager::new(usdc, PositionKind::Deposit, &clock);
+        {
+            // setup user reward manager with 400/500 shares
+
+            user_reward_manager_2
+                .populate(&mut pool_reward_manager, &clock)
+                .expect("It populates user reward manager");
+            user_reward_manager_2.set_share(&mut pool_reward_manager, 400);
+        }
+
+        {
+            // 1/2 of the reward time passes
+            clock.unix_timestamp = 10 * SECONDS_IN_A_DAY as i64;
+
+            let claimed_slnd = user_reward_manager_1
+                .claim_rewards(&mut pool_reward_manager, slnd_vault, &clock)
+                .expect("It claims rewards");
+            assert_eq!(claimed_slnd, 5 * 1_000_000);
+
+            let claimed_slnd = user_reward_manager_2
+                .claim_rewards(&mut pool_reward_manager, slnd_vault, &clock)
+                .expect("It claims rewards");
+            assert_eq!(claimed_slnd, 20 * 1_000_000);
+        }
+
+        {
+            // set both user reward managers to 250/500 shares
+            user_reward_manager_1.set_share(&mut pool_reward_manager, 250);
+            user_reward_manager_2.set_share(&mut pool_reward_manager, 250);
+        }
+
+        {
+            // the reward is finished
+            clock.unix_timestamp = 20 * SECONDS_IN_A_DAY as i64;
+
+            let claimed_slnd = user_reward_manager_1
+                .claim_rewards(&mut pool_reward_manager, slnd_vault, &clock)
+                .expect("It claims rewards");
+            assert_eq!(claimed_slnd, 25 * 1_000_000);
+
+            let claimed_slnd = user_reward_manager_2
+                .claim_rewards(&mut pool_reward_manager, slnd_vault, &clock)
+                .expect("It claims rewards");
+            assert_eq!(claimed_slnd, 25 * 1_000_000);
+        }
     }
 
+    /// This tests replicates calculations from Suilend's
+    /// "test_pool_reward_manager_multiple_rewards" test.
     #[test]
     fn it_tests_pool_reward_manager_multiple_rewards() {
-        // TODO: rewrite Suilend "test_pool_reward_manager_multiple_rewards"
+        let usdc = Pubkey::new_unique(); // reserve pubkey
+        let slnd_vault1 = Pubkey::new_unique(); // where rewards are stored
+        let slnd_vault2 = Pubkey::new_unique(); // where rewards are stored
+
+        let mut clock = Clock {
+            unix_timestamp: 0,
+            ..Default::default()
+        };
+
+        let mut pool_reward_manager = PoolRewardManager::default();
+        {
+            // setup a reward that starts now and lasts for 20 days
+
+            pool_reward_manager
+                .add_pool_reward(
+                    slnd_vault1,
+                    0,
+                    20 * SECONDS_IN_A_DAY,
+                    100 * 1_000_000,
+                    &clock,
+                )
+                .expect("It adds pool reward");
+
+            // and another reward that starts in 10 days and lasts for 10 days
+
+            pool_reward_manager
+                .add_pool_reward(
+                    slnd_vault2,
+                    10 * SECONDS_IN_A_DAY,
+                    20 * SECONDS_IN_A_DAY,
+                    100 * 1_000_000,
+                    &clock,
+                )
+                .expect("It adds pool reward");
+        }
+
+        let mut user_reward_manager_1 = UserRewardManager::new(usdc, PositionKind::Deposit, &clock);
+        {
+            // setup user reward manager with 100/100 shares
+
+            user_reward_manager_1
+                .populate(&mut pool_reward_manager, &clock)
+                .expect("It populates user reward manager");
+            user_reward_manager_1.set_share(&mut pool_reward_manager, 100);
+        }
+
+        clock.unix_timestamp = 15 * SECONDS_IN_A_DAY as i64;
+
+        let mut user_reward_manager_2 = UserRewardManager::new(usdc, PositionKind::Deposit, &clock);
+        {
+            // setup user reward manager with 100/200 shares
+
+            user_reward_manager_2
+                .populate(&mut pool_reward_manager, &clock)
+                .expect("It populates user reward manager");
+            user_reward_manager_2.set_share(&mut pool_reward_manager, 100);
+        }
+
+        {
+            clock.unix_timestamp = 30 * SECONDS_IN_A_DAY as i64;
+
+            let claimed_slnd = user_reward_manager_1
+                .claim_rewards(&mut pool_reward_manager, slnd_vault1, &clock)
+                .expect("It claims rewards");
+            assert_eq!(claimed_slnd, 87_500_000);
+
+            let claimed_slnd = user_reward_manager_1
+                .claim_rewards(&mut pool_reward_manager, slnd_vault2, &clock)
+                .expect("It claims rewards");
+            assert_eq!(claimed_slnd, 75 * 1_000_000);
+
+            let claimed_slnd = user_reward_manager_2
+                .claim_rewards(&mut pool_reward_manager, slnd_vault1, &clock)
+                .expect("It claims rewards");
+            assert_eq!(claimed_slnd, 12_500_000);
+
+            let claimed_slnd = user_reward_manager_2
+                .claim_rewards(&mut pool_reward_manager, slnd_vault2, &clock)
+                .expect("It claims rewards");
+            assert_eq!(claimed_slnd, 25 * 1_000_000);
+        }
     }
 
+    /// This tests replicates calculations from Suilend's
+    /// "test_pool_reward_manager_zero_share" test.
     #[test]
     fn it_tests_pool_reward_zero_share() {
-        // TODO: rewrite Suilend "test_pool_reward_manager_zero_share"
+        let usdc = Pubkey::new_unique(); // reserve pubkey
+        let slnd_vault = Pubkey::new_unique(); // where rewards are stored
+
+        let mut clock = Clock {
+            unix_timestamp: 0,
+            ..Default::default()
+        };
+
+        let mut pool_reward_manager = PoolRewardManager::default();
+        {
+            // setup pool reward manager with one reward
+
+            pool_reward_manager
+                .add_pool_reward(
+                    slnd_vault,
+                    0,
+                    20 * SECONDS_IN_A_DAY,
+                    100 * 1_000_000,
+                    &clock,
+                )
+                .expect("It adds pool reward");
+        }
+
+        clock.unix_timestamp = 10 * SECONDS_IN_A_DAY as i64;
+        let mut user_reward_manager_1 = UserRewardManager::new(usdc, PositionKind::Deposit, &clock);
+        user_reward_manager_1
+            .populate(&mut pool_reward_manager, &clock)
+            .expect("It populates user reward manager");
+        user_reward_manager_1.set_share(&mut pool_reward_manager, 1);
+
+        clock.unix_timestamp = 20 * SECONDS_IN_A_DAY as i64;
+        let claimed_slnd = user_reward_manager_1
+            .claim_rewards(&mut pool_reward_manager, slnd_vault, &clock)
+            .expect("It claims rewards");
+        // 50 usdc is unallocated since there was zero share from 0-10 seconds
+        assert_eq!(claimed_slnd, 50 * 1_000_000);
     }
 
+    /// This tests replicates calculations from Suilend's
+    /// "test_pool_reward_manager_auto_farm" test.
     #[test]
     fn it_tests_pool_reward_manager_auto_farm() {
-        // TODO: rewrite Suilend "test_pool_reward_manager_auto_farm"
+        let usdc = Pubkey::new_unique(); // reserve pubkey
+        let slnd_vault = Pubkey::new_unique(); // where rewards are stored
+
+        let mut clock = Clock {
+            unix_timestamp: 0,
+            ..Default::default()
+        };
+
+        let mut pool_reward_manager = PoolRewardManager::default();
+
+        let mut user_reward_manager_1 = UserRewardManager::new(usdc, PositionKind::Deposit, &clock);
+        user_reward_manager_1
+            .populate(&mut pool_reward_manager, &clock)
+            .expect("It populates user reward manager");
+        user_reward_manager_1.set_share(&mut pool_reward_manager, 1);
+
+        pool_reward_manager
+            .add_pool_reward(
+                slnd_vault,
+                0,
+                20 * SECONDS_IN_A_DAY,
+                100 * 1_000_000,
+                &clock,
+            )
+            .expect("It adds pool reward");
+
+        clock.unix_timestamp = 10 * SECONDS_IN_A_DAY as i64;
+
+        let mut user_reward_manager_2 = UserRewardManager::new(usdc, PositionKind::Deposit, &clock);
+        user_reward_manager_2
+            .populate(&mut pool_reward_manager, &clock)
+            .expect("It populates user reward manager");
+        user_reward_manager_2.set_share(&mut pool_reward_manager, 1);
+
+        {
+            clock.unix_timestamp = 20 * SECONDS_IN_A_DAY as i64;
+
+            let claimed_slnd = user_reward_manager_1
+                .claim_rewards(&mut pool_reward_manager, slnd_vault, &clock)
+                .expect("It claims rewards");
+            assert_eq!(claimed_slnd, 75 * 1_000_000);
+
+            user_reward_manager_2.set_share(&mut pool_reward_manager, 1);
+            let claimed_slnd = user_reward_manager_2
+                .claim_rewards(&mut pool_reward_manager, slnd_vault, &clock)
+                .expect("It claims rewards");
+            assert_eq!(claimed_slnd, 25 * 1_000_000);
+        }
     }
 
+    /// This tests replicates Suilend's "test_add_too_many_pool_rewards" test.
     #[test]
     fn it_tests_add_too_many_pool_rewards() {
-        // TODO: rewrite Suilend "test_add_too_many_pool_rewards"
+        let clock = Clock::default();
+
+        let mut pool_reward_manager = PoolRewardManager::default();
+
+        for _ in 0..MAX_REWARDS {
+            let slnd_vault = Pubkey::new_unique(); // where rewards are stored
+            pool_reward_manager
+                .add_pool_reward(
+                    slnd_vault,
+                    0,
+                    20 * SECONDS_IN_A_DAY,
+                    100 * 1_000_000,
+                    &clock,
+                )
+                .expect("It adds pool reward");
+        }
+
+        pool_reward_manager
+            .add_pool_reward(
+                Pubkey::new_unique(),
+                0,
+                20 * SECONDS_IN_A_DAY,
+                100 * 1_000_000,
+                &clock,
+            )
+            .expect_err("It fails to add pool reward");
+    }
+
+    /// This tests replicates Suilend's
+    /// "test_pool_reward_manager_cancel_and_close" test.
+    #[test]
+    fn it_tests_pool_reward_manager_cancel_and_close() {
+        let usdc = Pubkey::new_unique(); // reserve pubkey
+        let slnd_vault = Pubkey::new_unique(); // where rewards are stored
+
+        let mut clock = Clock {
+            unix_timestamp: 0,
+            ..Default::default()
+        };
+
+        let mut pool_reward_manager = PoolRewardManager::default();
+
+        pool_reward_manager
+            .add_pool_reward(
+                slnd_vault,
+                0,
+                20 * SECONDS_IN_A_DAY,
+                100 * 1_000_000,
+                &clock,
+            )
+            .expect("It adds pool reward");
+
+        let mut user_reward_manager_1 = UserRewardManager::new(usdc, PositionKind::Deposit, &clock);
+        user_reward_manager_1
+            .populate(&mut pool_reward_manager, &clock)
+            .expect("It populates user reward manager");
+        user_reward_manager_1.set_share(&mut pool_reward_manager, 100);
+
+        {
+            clock.unix_timestamp = 10 * SECONDS_IN_A_DAY as i64;
+
+            let (from_vault, unallocated_rewards) = pool_reward_manager
+                .cancel_pool_reward(0, &clock)
+                .expect("It cancels pool reward");
+            assert_eq!(from_vault, slnd_vault);
+            assert_eq!(unallocated_rewards, 50 * 1_000_000);
+        }
+
+        {
+            clock.unix_timestamp = 15 * SECONDS_IN_A_DAY as i64;
+
+            let claimed_slnd = user_reward_manager_1
+                .claim_rewards(&mut pool_reward_manager, slnd_vault, &clock)
+                .expect("It claims rewards");
+            assert_eq!(claimed_slnd, 50 * 1_000_000);
+        }
+
+        let from_vault = pool_reward_manager
+            .close_pool_reward(0)
+            .expect("It closes pool reward");
+        assert_eq!(from_vault, slnd_vault);
     }
 
+    /// This tests replicates Suilend's
+    /// "test_pool_reward_manager_cancel_and_close_regression" test.
     #[test]
     fn it_tests_pool_reward_manager_cancel_and_close_regression() {
-        // TODO: rewrite Suilend "test_pool_reward_manager_cancel_and_close_regression"
+        let usdc = Pubkey::new_unique(); // reserve pubkey
+        let slnd_vault1 = Pubkey::new_unique(); // where rewards are stored
+        let slnd_vault2 = Pubkey::new_unique(); // where rewards are stored
+
+        let mut clock = Clock {
+            unix_timestamp: 0,
+            ..Default::default()
+        };
+
+        let mut pool_reward_manager = PoolRewardManager::default();
+
+        pool_reward_manager
+            .add_pool_reward(
+                slnd_vault1,
+                0,
+                20 * SECONDS_IN_A_DAY,
+                100 * 1_000_000,
+                &clock,
+            )
+            .expect("It adds pool reward");
+
+        pool_reward_manager
+            .add_pool_reward(
+                slnd_vault2,
+                20 * SECONDS_IN_A_DAY,
+                30 * SECONDS_IN_A_DAY,
+                100 * 1_000_000,
+                &clock,
+            )
+            .expect("It adds pool reward");
+
+        let mut user_reward_manager_1 = UserRewardManager::new(usdc, PositionKind::Deposit, &clock);
+        user_reward_manager_1
+            .populate(&mut pool_reward_manager, &clock)
+            .expect("It populates user reward manager");
+        user_reward_manager_1.set_share(&mut pool_reward_manager, 100);
+
+        {
+            clock.unix_timestamp = 10 * SECONDS_IN_A_DAY as i64;
+
+            let (from_vault, unallocated_rewards) = pool_reward_manager
+                .cancel_pool_reward(0, &clock)
+                .expect("It cancels pool reward");
+            assert_eq!(from_vault, slnd_vault1);
+            assert_eq!(unallocated_rewards, 50 * 1_000_000);
+
+            clock.unix_timestamp = 15 * SECONDS_IN_A_DAY as i64;
+            let claim_slnd = user_reward_manager_1
+                .claim_rewards(&mut pool_reward_manager, slnd_vault1, &clock)
+                .expect("It claims rewards");
+            assert_eq!(claim_slnd, 50 * 1_000_000);
+
+            let from_vault = pool_reward_manager
+                .close_pool_reward(0)
+                .expect("It closes pool reward");
+            assert_eq!(from_vault, slnd_vault1);
+        }
+
+        clock.unix_timestamp = 20 * SECONDS_IN_A_DAY as i64;
+
+        let mut user_reward_manager_2 = UserRewardManager::new(usdc, PositionKind::Deposit, &clock);
+        user_reward_manager_2
+            .populate(&mut pool_reward_manager, &clock)
+            .expect("It populates user reward manager");
+        user_reward_manager_2.set_share(&mut pool_reward_manager, 100);
+
+        {
+            clock.unix_timestamp = 30 * SECONDS_IN_A_DAY as i64;
+
+            let claimed_slnd = user_reward_manager_2
+                .claim_rewards(&mut pool_reward_manager, slnd_vault2, &clock)
+                .expect("It claims rewards");
+            assert_eq!(claimed_slnd, 50 * 1_000_000);
+        }
     }
 
     impl PoolRewardManager {
@@ -990,7 +1597,7 @@ mod tests {
                     if is_vacant {
                         PoolRewardSlot::Vacant {
                             last_pool_reward_id: Default::default(),
-                            has_been_vacated_in_this_tx: false,
+                            has_been_just_vacated: false,
                         }
                     } else {
                         PoolRewardSlot::Occupied(Box::new(PoolReward {
@@ -1013,6 +1620,7 @@ mod tests {
             let rewards_len = rng.gen_range(0..MAX_REWARDS);
             Self {
                 reserve: Pubkey::new_unique(),
+                position_kind: rng.gen_range(0..=1u8).try_into().unwrap(),
                 share: rng.gen(),
                 last_update_time_secs: rng.gen(),
                 rewards: std::iter::from_fn(|| {
diff --git a/token-lending/sdk/src/state/obligation.rs b/token-lending/sdk/src/state/obligation.rs
index 94fcd8eccac..58d63715178 100644
--- a/token-lending/sdk/src/state/obligation.rs
+++ b/token-lending/sdk/src/state/obligation.rs
@@ -80,11 +80,11 @@ pub struct Obligation {
     /// # (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<UserRewardManager>,
+    pub user_reward_managers: UserRewardManagers,
 }
 
 /// These are the two foundational user interactions in a borrow-lending protocol.
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
 pub enum PositionKind {
     /// User is providing liquidity.
     Deposit = 0,
@@ -115,26 +115,40 @@ impl Obligation {
         self.borrowed_value.try_div(self.deposited_value)
     }
 
-    /// Repay liquidity and remove it from borrows if zeroed out
-    pub fn repay(&mut self, settle_amount: Decimal, liquidity_index: usize) -> ProgramResult {
+    /// Repay liquidity and remove it from borrows if zeroed out.
+    ///
+    /// Returns current liability shares.
+    pub fn repay(
+        &mut self,
+        settle_amount: Decimal,
+        liquidity_index: usize,
+    ) -> Result<u64, ProgramError> {
         let liquidity = &mut self.borrows[liquidity_index];
         if settle_amount == liquidity.borrowed_amount_wads {
             self.borrows.remove(liquidity_index);
+            Ok(0)
         } else {
             liquidity.repay(settle_amount)?;
+            liquidity.liability_shares()
         }
-        Ok(())
     }
 
-    /// Withdraw collateral and remove it from deposits if zeroed out
-    pub fn withdraw(&mut self, withdraw_amount: u64, collateral_index: usize) -> ProgramResult {
+    /// Withdraw collateral and remove it from deposits if zeroed out.
+    ///
+    /// Returns the new deposited amount.
+    pub fn withdraw(
+        &mut self,
+        withdraw_amount: u64,
+        collateral_index: usize,
+    ) -> Result<u64, ProgramError> {
         let collateral = &mut self.deposits[collateral_index];
         if withdraw_amount == collateral.deposited_amount {
             self.deposits.remove(collateral_index);
+            Ok(0)
         } else {
             collateral.withdraw(withdraw_amount)?;
+            Ok(collateral.deposited_amount)
         }
-        Ok(())
     }
 
     /// calculate the maximum amount of collateral that can be borrowed
@@ -335,16 +349,6 @@ impl Obligation {
         msg!("Reserve not found in obligation");
         Err(LendingError::InvalidAccountInput.into())
     }
-
-    /// Returns [UserRewardManager] for the given reserve
-    pub fn find_user_reward_manager_mut(
-        &mut self,
-        reserve: Pubkey,
-    ) -> Option<&mut UserRewardManager> {
-        self.user_reward_managers
-            .iter_mut()
-            .find(|user_reward_manager| user_reward_manager.reserve == reserve)
-    }
 }
 
 /// Initialize an obligation
@@ -469,6 +473,13 @@ impl ObligationLiquidity {
 
         Ok(())
     }
+
+    /// Calculates shares for liquidity mining.
+    pub fn liability_shares(&self) -> Result<u64, ProgramError> {
+        self.borrowed_amount_wads
+            .try_div(self.cumulative_borrow_rate_wads)?
+            .try_floor_u64()
+    }
 }
 
 const OBLIGATION_COLLATERAL_LEN: usize = 88; // 32 + 8 + 16 + 32
@@ -485,13 +496,18 @@ impl Obligation {
     ///
     /// - [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;
+    pub 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 {
+        if self.user_reward_managers.is_empty() {
+            return OBLIGATION_LEN_V1;
+        }
+
         let mut size = OBLIGATION_LEN_V1 + 1;
 
-        for reward_manager in &self.user_reward_managers {
+        for reward_manager in self.user_reward_managers.iter() {
             size += reward_manager.size_in_bytes_when_packed();
         }
 
@@ -807,7 +823,7 @@ impl 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,
+            user_reward_managers: UserRewardManagers(user_reward_managers),
         })
     }
 }
@@ -871,10 +887,11 @@ mod test {
                 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(rng))
-                        .take(user_reward_managers_len)
-                        .collect()
+                    UserRewardManagers(
+                        std::iter::repeat_with(|| UserRewardManager::new_rand(rng))
+                            .take(user_reward_managers_len)
+                            .collect(),
+                    )
                 },
             }
         }
@@ -958,8 +975,9 @@ mod test {
         fn repay_partial_amounts()(amount in 1..=u64::MAX)(
             repay_amount in Just(WAD as u128 * amount as u128),
             borrowed_amount in (WAD as u128 * amount as u128 + 1)..=MAX_BORROWED,
-        ) -> (u128, u128) {
-            (repay_amount, borrowed_amount)
+            cumulative_borrow_rate in (WAD as u128)..=(WAD as u128 * MAX_COMPOUNDED_INTEREST as u128),
+        ) -> (u128, u128, u128) {
+            (repay_amount, borrowed_amount, cumulative_borrow_rate)
         }
     }
 
@@ -975,19 +993,22 @@ mod test {
     proptest! {
         #[test]
         fn repay_partial(
-            (repay_amount, borrowed_amount) in repay_partial_amounts(),
+            (repay_amount, borrowed_amount, cumulative_borrow_rate) in repay_partial_amounts(),
         ) {
             let borrowed_amount_wads = Decimal::from_scaled_val(borrowed_amount);
             let repay_amount_wads = Decimal::from_scaled_val(repay_amount);
+            let cumulative_borrow_rate_wads = Decimal::from_scaled_val(cumulative_borrow_rate);
             let mut obligation = Obligation {
                 borrows: vec![ObligationLiquidity {
                     borrowed_amount_wads,
+                    cumulative_borrow_rate_wads,
                     ..ObligationLiquidity::default()
                 }],
                 ..Obligation::default()
             };
 
-            obligation.repay(repay_amount_wads, 0)?;
+            let liability_shares = obligation.repay(repay_amount_wads, 0)?;
+            assert_ne!(liability_shares, 0);
             assert!(obligation.borrows[0].borrowed_amount_wads < borrowed_amount_wads);
             assert!(obligation.borrows[0].borrowed_amount_wads > Decimal::zero());
         }
@@ -998,9 +1019,11 @@ mod test {
         ) {
             let borrowed_amount_wads = Decimal::from_scaled_val(borrowed_amount);
             let repay_amount_wads = Decimal::from_scaled_val(repay_amount);
+            let cumulative_borrow_rate_wads = Decimal::one();
             let mut obligation = Obligation {
                 borrows: vec![ObligationLiquidity {
                     borrowed_amount_wads,
+                    cumulative_borrow_rate_wads,
                     ..ObligationLiquidity::default()
                 }],
                 ..Obligation::default()
diff --git a/token-lending/sdk/src/state/rate_limiter.rs b/token-lending/sdk/src/state/rate_limiter.rs
index 55f86774018..142acb31cd1 100644
--- a/token-lending/sdk/src/state/rate_limiter.rs
+++ b/token-lending/sdk/src/state/rate_limiter.rs
@@ -205,7 +205,7 @@ impl Pack for RateLimiter {
 }
 
 #[cfg(test)]
-pub fn rand_rate_limiter() -> RateLimiter {
+pub(crate) fn rand_rate_limiter() -> RateLimiter {
     use rand::Rng;
     let mut rng = rand::thread_rng();
 

From ab5568c7ef78a774ca85f25bb3e2c2ea30ade15b Mon Sep 17 00:00:00 2001
From: vanity mnm <vanity@solend.fi>
Date: Mon, 7 Apr 2025 20:13:42 +0200
Subject: [PATCH 10/14] [Liquidity Mining] Code reorg (9) (#208)

* Moving logic into respective modules and hiding accessors

* Making some accessors public for tests

* Increasing budget limit

* Adding a test to add pool reward

* Adding borrow position kind
---
 .../liquidity_mining/add_pool_reward.rs       |    8 +-
 .../program/tests/add_pool_reward.rs          |  162 +++
 .../tests/borrow_obligation_liquidity.rs      |    5 +-
 .../tests/deposit_obligation_collateral.rs    |    7 +-
 ...rve_liquidity_and_obligation_collateral.rs |    7 +-
 .../tests/helpers/solend_program_test.rs      |   64 +-
 .../program/tests/isolated_tier_assets.rs     |    6 +-
 token-lending/sdk/src/instruction.rs          |   85 ++
 .../sdk/src/state/liquidity_mining.rs         | 1189 +----------------
 .../liquidity_mining/pool_reward_manager.rs   |  609 +++++++++
 .../liquidity_mining/user_reward_manager.rs   |  605 +++++++++
 token-lending/sdk/src/state/obligation.rs     |   11 +-
 12 files changed, 1565 insertions(+), 1193 deletions(-)
 create mode 100644 token-lending/program/tests/add_pool_reward.rs
 create mode 100644 token-lending/sdk/src/state/liquidity_mining/pool_reward_manager.rs
 create mode 100644 token-lending/sdk/src/state/liquidity_mining/user_reward_manager.rs

diff --git a/token-lending/program/src/processor/liquidity_mining/add_pool_reward.rs b/token-lending/program/src/processor/liquidity_mining/add_pool_reward.rs
index 0ea199c0f6a..c0cc3fc3cc5 100644
--- a/token-lending/program/src/processor/liquidity_mining/add_pool_reward.rs
+++ b/token-lending/program/src/processor/liquidity_mining/add_pool_reward.rs
@@ -55,7 +55,7 @@ struct AddPoolRewardAccounts<'a, 'info> {
     /// TBD: do we want to create another signer authority to be able to
     /// delegate reward management to a softer multisig?
     lending_market_owner_info: &'a AccountInfo<'info>,
-    /// ❓ we don't yet whether this is rent info
+    /// ❓ we don't yet know whether this is rent info
     rent_info: &'a AccountInfo<'info>,
     /// ✅ matches `lending_market_info`
     token_program_info: &'a AccountInfo<'info>,
@@ -77,6 +77,8 @@ pub(crate) fn process(
     reward_token_amount: u64,
     accounts: &[AccountInfo],
 ) -> ProgramResult {
+    msg!("Adding {position_kind:?} pool reward from {start_time_secs}s to {end_time_secs}s",);
+
     let clock = &Clock::get()?;
 
     let mut accounts =
@@ -162,10 +164,6 @@ impl<'a, 'info> AddPoolRewardAccounts<'a, 'info> {
             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());
-        }
 
         // check that accounts that should be writable are writable
 
diff --git a/token-lending/program/tests/add_pool_reward.rs b/token-lending/program/tests/add_pool_reward.rs
new file mode 100644
index 00000000000..d2408ccdbc5
--- /dev/null
+++ b/token-lending/program/tests/add_pool_reward.rs
@@ -0,0 +1,162 @@
+#![cfg(feature = "test-bpf")]
+
+mod helpers;
+
+use helpers::solend_program_test::{setup_world, LiqMiningReward};
+use helpers::test_reserve_config;
+
+use pretty_assertions::assert_eq;
+use solana_program_test::*;
+use solana_sdk::signature::{Keypair, Signer};
+use solend_program::{
+    math::Decimal,
+    state::{PoolRewardId, PoolRewardManager, PositionKind, Reserve, UserRewardManager},
+};
+use solend_sdk::state::{PoolReward, PoolRewardSlot, UserReward};
+
+#[tokio::test]
+async fn test_success_for_deposit() {
+    test_success(PositionKind::Deposit).await;
+}
+
+#[tokio::test]
+async fn test_success_for_borrow() {
+    test_success(PositionKind::Borrow).await;
+}
+
+async fn test_success(position_kind: PositionKind) {
+    let (mut test, lending_market, usdc_reserve, wsol_reserve, mut lending_market_owner, user) =
+        setup_world(&test_reserve_config(), &test_reserve_config()).await;
+
+    let reward_mint = test.create_mint_as_test_authority().await;
+    let reward_vault = Keypair::new();
+    let duration_secs = 3_600;
+    let total_rewards = 1_000_000;
+    let current_time = test.get_clock().await.unix_timestamp as u64;
+
+    lending_market
+        .add_pool_reward(
+            &mut test,
+            &usdc_reserve,
+            &mut lending_market_owner,
+            &LiqMiningReward {
+                mint: reward_mint,
+                vault: reward_vault.insecure_clone(),
+            },
+            position_kind,
+            current_time,
+            current_time + duration_secs as u64,
+            total_rewards,
+        )
+        .await
+        .expect("Should add pool reward");
+
+    let obligation = lending_market
+        .init_obligation(&mut test, Keypair::new(), &user)
+        .await
+        .expect("This should succeed");
+
+    let usdc_reserve_post = test.load_account::<Reserve>(usdc_reserve.pubkey).await;
+
+    let expected_reward_manager = Box::new(PoolRewardManager {
+        total_shares: 0,
+        last_update_time_secs: current_time as _,
+        pool_rewards: {
+            let mut og = usdc_reserve_post
+                .account
+                .deposits_pool_reward_manager
+                .pool_rewards
+                .clone();
+
+            og[0] = PoolRewardSlot::Occupied(Box::new(PoolReward {
+                id: PoolRewardId(1),
+                vault: reward_vault.pubkey(),
+                start_time_secs: current_time,
+                duration_secs,
+                total_rewards,
+                num_user_reward_managers: 0,
+                cumulative_rewards_per_share: Decimal::zero(),
+            }));
+
+            og
+        },
+    });
+
+    let expected_share = match position_kind {
+        PositionKind::Deposit => {
+            assert_eq!(
+                usdc_reserve_post.account,
+                Reserve {
+                    deposits_pool_reward_manager: expected_reward_manager,
+                    ..usdc_reserve.clone().account
+                }
+            );
+
+            let deposit_amount = 1_000_000;
+            lending_market
+                .deposit_reserve_liquidity_and_obligation_collateral(
+                    &mut test,
+                    &usdc_reserve,
+                    &obligation,
+                    &user,
+                    deposit_amount,
+                )
+                .await
+                .expect("This should succeed");
+
+            deposit_amount
+        }
+        PositionKind::Borrow => {
+            assert_eq!(
+                usdc_reserve_post.account,
+                Reserve {
+                    borrows_pool_reward_manager: expected_reward_manager,
+                    ..usdc_reserve.clone().account
+                }
+            );
+
+            lending_market
+                .deposit_reserve_liquidity_and_obligation_collateral(
+                    &mut test,
+                    &wsol_reserve,
+                    &obligation,
+                    &user,
+                    420_000_000,
+                )
+                .await
+                .expect("This should succeed");
+
+            lending_market
+                .borrow_obligation_liquidity(
+                    &mut test,
+                    &usdc_reserve,
+                    &obligation,
+                    &user,
+                    None,
+                    690,
+                )
+                .await
+                .unwrap();
+
+            690
+        }
+    };
+
+    let obligation_post = test.load_obligation(obligation.pubkey).await;
+
+    assert_eq!(
+        obligation_post.account.user_reward_managers.last().unwrap(),
+        &UserRewardManager {
+            reserve: usdc_reserve.pubkey,
+            position_kind,
+            share: expected_share,
+            last_update_time_secs: current_time as _,
+            rewards: vec![UserReward {
+                pool_reward_index: 0,
+                pool_reward_id: PoolRewardId(1),
+                earned_rewards: Decimal::zero(),
+                cumulative_rewards_per_share: Decimal::zero(),
+            }],
+        }
+    );
+}
diff --git a/token-lending/program/tests/borrow_obligation_liquidity.rs b/token-lending/program/tests/borrow_obligation_liquidity.rs
index 02cbba53a6b..9774d0d8304 100644
--- a/token-lending/program/tests/borrow_obligation_liquidity.rs
+++ b/token-lending/program/tests/borrow_obligation_liquidity.rs
@@ -286,7 +286,7 @@ async fn test_success() {
                 let last_update_time_secs =
                     obligation.account.user_reward_managers[0].last_update_time_secs;
 
-                UserRewardManagers(vec![
+                vec![
                     UserRewardManager {
                         reserve: usdc_reserve.pubkey,
                         position_kind: PositionKind::Deposit,
@@ -301,7 +301,8 @@ async fn test_success() {
                         last_update_time_secs,
                         rewards: Vec::new(),
                     },
-                ])
+                ]
+                .into()
             },
             ..obligation.account
         },
diff --git a/token-lending/program/tests/deposit_obligation_collateral.rs b/token-lending/program/tests/deposit_obligation_collateral.rs
index 959630a5cd4..7e1d0cb5c6e 100644
--- a/token-lending/program/tests/deposit_obligation_collateral.rs
+++ b/token-lending/program/tests/deposit_obligation_collateral.rs
@@ -17,7 +17,7 @@ use solana_sdk::transaction::TransactionError;
 use solend_program::math::Decimal;
 use solend_program::state::{
     LastUpdate, LendingMarket, Obligation, ObligationCollateral, PoolRewardManager, PositionKind,
-    Reserve, UserRewardManager, UserRewardManagers,
+    Reserve, UserRewardManager,
 };
 
 async fn setup() -> (
@@ -116,13 +116,14 @@ async fn test_success() {
                 market_value: Decimal::zero(), // this field only gets updated on a refresh
                 attributed_borrow_value: Decimal::zero()
             }],
-            user_reward_managers: UserRewardManagers(vec![UserRewardManager {
+            user_reward_managers: vec![UserRewardManager {
                 reserve: usdc_reserve.pubkey,
                 position_kind: PositionKind::Deposit,
                 share: deposit_amount,
                 last_update_time_secs,
                 rewards: Vec::new(),
-            }]),
+            }]
+            .into(),
             ..obligation.account
         }
     );
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 01fc216d29b..16eb199fa96 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
@@ -16,7 +16,7 @@ use solana_sdk::signature::Keypair;
 use solend_program::math::Decimal;
 use solend_program::state::{
     LastUpdate, LendingMarket, Obligation, ObligationCollateral, PoolRewardManager, PositionKind,
-    Reserve, ReserveCollateral, ReserveLiquidity, UserRewardManager, UserRewardManagers,
+    Reserve, ReserveCollateral, ReserveLiquidity, UserRewardManager,
 };
 
 async fn setup() -> (
@@ -145,13 +145,14 @@ async fn test_success() {
                 attributed_borrow_value: Decimal::zero()
             }]
             .to_vec(),
-            user_reward_managers: UserRewardManagers(vec![UserRewardManager {
+            user_reward_managers: vec![UserRewardManager {
                 reserve: usdc_reserve.pubkey,
                 position_kind: PositionKind::Deposit,
                 share: deposit_amount,
                 last_update_time_secs: last_update_time_secs,
                 rewards: Vec::new(),
-            }]),
+            }]
+            .into(),
             ..obligation.account
         }
     );
diff --git a/token-lending/program/tests/helpers/solend_program_test.rs b/token-lending/program/tests/helpers/solend_program_test.rs
index 323dcdcc550..95728089c6b 100644
--- a/token-lending/program/tests/helpers/solend_program_test.rs
+++ b/token-lending/program/tests/helpers/solend_program_test.rs
@@ -65,7 +65,7 @@ mod cu_budgets {
     pub(super) const BORROW_OBLIGATION_LIQUIDITY: u32 = 180_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 = 230_008;
+    pub(super) const LIQUIDATE_OBLIGATION_AND_REDEEM_RESERVE_COLLATERAL: u32 = 250_008;
     pub(super) const WITHDRAW_OBLIGATION_COLLATERAL_AND_REDEEM_RESERVE_COLLATERAL: u32 = 200_009;
     pub(super) const WITHDRAW_OBLIGATION_COLLATERAL: u32 = 130_010;
     pub(super) const INIT_RESERVE: u32 = 90_011;
@@ -74,6 +74,7 @@ mod cu_budgets {
     pub(super) const UPDATE_RESERVE_CONFIG: u32 = 30_014;
     pub(super) const DEPOSIT_RESERVE_LIQUIDITY_AND_OBLIGATION_COLLATERAL: u32 = 130_015;
     pub(super) const REDEEM: u32 = 90_016;
+    pub(super) const ADD_POOL_REWARD: u32 = 80_017;
 }
 
 /// This is at most how many bytes can an obligation grow.
@@ -106,6 +107,11 @@ pub struct Info<T> {
     pub account: T,
 }
 
+pub struct LiqMiningReward {
+    pub mint: Pubkey,
+    pub vault: Keypair,
+}
+
 impl SolendProgramTest {
     pub async fn start_with_test(mut test: ProgramTest) -> Self {
         test.prefer_bpf(false);
@@ -366,6 +372,12 @@ impl SolendProgramTest {
         keypair.pubkey()
     }
 
+    pub async fn create_mint_as_test_authority(&mut self) -> Pubkey {
+        let mint = self.create_mint(&self.authority.pubkey()).await;
+        self.mints.insert(mint, None);
+        mint
+    }
+
     pub async fn create_mint(&mut self, mint_authority: &Pubkey) -> Pubkey {
         let keypair = Keypair::new();
         let rent = self.rent.minimum_balance(Mint::LEN);
@@ -903,6 +915,56 @@ impl Info<LendingMarket> {
             .await
     }
 
+    pub async fn add_pool_reward(
+        &self,
+        test: &mut SolendProgramTest,
+        reserve: &Info<Reserve>,
+        user: &mut User,
+        reward: &LiqMiningReward,
+        position_kind: PositionKind,
+        start_time_secs: u64,
+        end_time_secs: u64,
+        reward_amount: u64,
+    ) -> Result<(), BanksClientError> {
+        let token_account = user.create_token_account(&reward.mint, test).await;
+        test.mint_to(&reward.mint, &token_account.pubkey, reward_amount)
+            .await;
+
+        let instructions = [
+            ComputeBudgetInstruction::set_compute_unit_limit(cu_budgets::ADD_POOL_REWARD),
+            system_instruction::create_account(
+                &test.context.payer.pubkey(),
+                &reward.vault.pubkey(),
+                test.rent.minimum_balance(Token::LEN),
+                spl_token::state::Account::LEN as u64,
+                &spl_token::id(),
+            ),
+            add_pool_reward(
+                solend_program::id(),
+                position_kind,
+                start_time_secs,
+                end_time_secs,
+                reward_amount,
+                reserve.pubkey,
+                reward.mint,
+                token_account.pubkey,
+                find_reward_vault_authority(
+                    &solend_program::id(),
+                    &self.pubkey,
+                    &reserve.pubkey,
+                    &reward.mint,
+                )
+                .0,
+                reward.vault.pubkey(),
+                self.pubkey,
+                user.keypair.pubkey(),
+            ),
+        ];
+
+        test.process_transaction(&instructions, Some(&[&user.keypair, &reward.vault]))
+            .await
+    }
+
     pub async fn donate_to_reserve(
         &self,
         test: &mut SolendProgramTest,
diff --git a/token-lending/program/tests/isolated_tier_assets.rs b/token-lending/program/tests/isolated_tier_assets.rs
index 4fd5c411eef..62a2a5cfaac 100644
--- a/token-lending/program/tests/isolated_tier_assets.rs
+++ b/token-lending/program/tests/isolated_tier_assets.rs
@@ -19,7 +19,6 @@ use solend_program::state::LastUpdate;
 use solend_program::state::ReserveType;
 use solend_program::state::{
     Obligation, ObligationLiquidity, PositionKind, ReserveConfig, UserRewardManager,
-    UserRewardManagers,
 };
 
 use solend_sdk::state::ReserveFees;
@@ -140,7 +139,7 @@ async fn test_refresh_obligation() {
             unweighted_borrowed_value: Decimal::from(10u64),
             borrowed_value_upper_bound: Decimal::from(10u64),
             borrowing_isolated_asset: true,
-            user_reward_managers: UserRewardManagers(vec![
+            user_reward_managers: vec![
                 UserRewardManager {
                     reserve: usdc_reserve.pubkey,
                     position_kind: PositionKind::Deposit,
@@ -155,7 +154,8 @@ async fn test_refresh_obligation() {
                     last_update_time_secs,
                     rewards: Vec::new(),
                 },
-            ],),
+            ]
+            .into(),
             ..obligations[0].account.clone()
         }
     );
diff --git a/token-lending/sdk/src/instruction.rs b/token-lending/sdk/src/instruction.rs
index 14d03b5926d..4a69d5d82b2 100644
--- a/token-lending/sdk/src/instruction.rs
+++ b/token-lending/sdk/src/instruction.rs
@@ -2101,6 +2101,91 @@ pub fn upgrade_reserve_to_v2_1_0(
     }
 }
 
+/// Creates a `AddPoolReward` instruction
+#[allow(clippy::too_many_arguments)]
+pub fn add_pool_reward(
+    program_id: Pubkey,
+    position_kind: PositionKind,
+    start_time_secs: u64,
+    end_time_secs: u64,
+    token_amount: u64,
+    reserve_pubkey: Pubkey,
+    reward_mint_pubkey: Pubkey,
+    source_reward_token_account_pubkey: Pubkey,
+    reward_vault_authority_pubkey: Pubkey,
+    reward_vault_pubkey: Pubkey,
+    lending_market_pubkey: Pubkey,
+    lending_market_owner_pubkey: Pubkey,
+) -> Instruction {
+    Instruction {
+        program_id,
+        accounts: vec![
+            AccountMeta::new(reserve_pubkey, false),
+            AccountMeta::new(reward_mint_pubkey, false),
+            AccountMeta::new(source_reward_token_account_pubkey, false),
+            AccountMeta::new(reward_vault_authority_pubkey, false),
+            AccountMeta::new(reward_vault_pubkey, false),
+            AccountMeta::new_readonly(lending_market_pubkey, false),
+            AccountMeta::new_readonly(lending_market_owner_pubkey, true),
+            AccountMeta::new_readonly(sysvar::rent::id(), false),
+            AccountMeta::new_readonly(spl_token::id(), false),
+        ],
+        data: LendingInstruction::AddPoolReward {
+            position_kind,
+            start_time_secs,
+            end_time_secs,
+            token_amount,
+        }
+        .pack(),
+    }
+}
+
+/// Derives the reward vault authority PDA address.
+pub fn find_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,
+    )
+}
+
+/// Creates a reward vault authority PDA address.
+pub fn create_reward_vault_authority(
+    program_id: &Pubkey,
+    lending_market_key: &Pubkey,
+    reserve_key: &Pubkey,
+    reward_mint_key: &Pubkey,
+    bump: u8,
+) -> Result<Pubkey, solana_program::pubkey::PubkeyError> {
+    Pubkey::create_program_address(
+        &[
+            reward_vault_authority_seeds(lending_market_key, reserve_key, reward_mint_key)
+                .as_slice(),
+            &[&[bump]],
+        ]
+        .concat(),
+        program_id,
+    )
+}
+
+/// Returns seeds to derive the reward vault authority PDA address.
+pub fn reward_vault_authority_seeds<'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(),
+    ]
+}
+
 #[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 0d5a05cb3a3..4698b9a62cf 100644
--- a/token-lending/sdk/src/state/liquidity_mining.rs
+++ b/token-lending/sdk/src/state/liquidity_mining.rs
@@ -1,22 +1,10 @@
-use super::pack_decimal;
-use crate::{
-    error::LendingError,
-    math::{Decimal, TryAdd, TryDiv, TryMul, TrySub},
-    state::{unpack_decimal, PositionKind},
-};
-use arrayref::{array_mut_ref, array_ref, array_refs, mut_array_refs};
-use core::{
-    convert::TryInto,
-    ops::{Deref, DerefMut},
-};
-use solana_program::msg;
-use solana_program::program_pack::{Pack, Sealed};
-use solana_program::{
-    clock::Clock,
-    program_error::ProgramError,
-    pubkey::{Pubkey, PUBKEY_BYTES},
-};
-use std::convert::TryFrom;
+//! Liquidity mining feature built analogous to Suilend's implementation.
+
+pub mod pool_reward_manager;
+pub mod user_reward_manager;
+
+pub use pool_reward_manager::*;
+pub use user_reward_manager::*;
 
 /// Determines the size of [PoolRewardManager]
 pub const MAX_REWARDS: usize = 50;
@@ -24,1113 +12,25 @@ pub 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,
-    /// 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.
-///
-/// # 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 [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);
-
-/// # (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.
-#[derive(Clone, Debug, PartialEq)]
-pub enum PoolRewardSlot {
-    /// New reward can be added to this slot.
-    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.
-        /// That way the packing logic knows whether it's fine to skip the
-        /// packing or not.
-        has_been_just_vacated: bool,
-    },
-    /// Reward has not been closed yet.
-    ///
-    /// We box the [PoolReward] to avoid stack overflow.
-    Occupied(Box<PoolReward>),
-}
-
-/// 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.
-    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.
-    ///
-    /// # Reward cancellation
-    ///
-    /// Is cut short if the reward is cancelled.
-    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,
-    /// 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)`
-    ///
-    /// # (Un)Packing
-    /// We only store 16 most significant digits.
-    pub cumulative_rewards_per_share: Decimal,
-}
-
-/// Wraps over user reward managers and allows mutable access to them while
-/// other obligation fields are borrowed.
-#[derive(Clone, Debug, Default, PartialEq)]
-pub struct UserRewardManagers(pub Vec<UserRewardManager>);
-
-/// Tracks user's LM rewards for a specific pool (reserve.)
-#[derive(Debug, PartialEq, Eq, Clone)]
-pub struct UserRewardManager {
-    /// Links this manager to a reserve.
-    pub reserve: Pubkey,
-    /// Although a user cannot both borrow and deposit in the same reserve, they
-    /// can deposit, withdraw and then borrow the same reserve.
-    /// Meanwhile they could've accumulated some rewards that'd be lost.
-    ///
-    /// Also, have an explicit distinguish between borrow and deposit doesn't
-    /// suffer from a footgun of misattributing rewards.
-    pub position_kind: PositionKind,
-    /// 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 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<UserReward>,
-}
-
-/// Track user rewards for a specific [PoolReward].
-#[derive(Debug, PartialEq, Eq, Default, Clone)]
-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,
-    /// 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,
-}
-
-impl PoolRewardManager {
-    /// Adds a new pool reward.
-    ///
-    /// Will first update itself.
-    ///
-    /// Start time will be set to now if it's in the past.
-    /// Must last at least [MIN_REWARD_PERIOD_SECS].
-    /// The amount of tokens to distribute must be greater than zero.
-    ///
-    /// Will return an error if no slot can be found for the new reward.
-    pub fn add_pool_reward(
-        &mut self,
-        vault: Pubkey,
-        start_time_secs: u64,
-        end_time_secs: u64,
-        reward_token_amount: u64,
-        clock: &Clock,
-    ) -> Result<(), ProgramError> {
-        self.update(clock)?;
-
-        let start_time_secs = start_time_secs.max(clock.unix_timestamp as u64);
-
-        if start_time_secs >= end_time_secs {
-            msg!("Pool reward must end after it starts");
-            return Err(LendingError::MathOverflow.into());
-        }
-
-        let duration_secs: u32 = {
-            // SAFETY: just checked that start time is strictly smaller
-            let d = end_time_secs - start_time_secs;
-            d.try_into().map_err(|_| {
-                msg!("Pool reward duration is too long");
-                LendingError::MathOverflow
-            })?
-        };
-        if MIN_REWARD_PERIOD_SECS > duration_secs as u64 {
-            msg!("Pool reward duration must be at least {MIN_REWARD_PERIOD_SECS} secs");
-            return Err(LendingError::PoolRewardPeriodTooShort.into());
-        }
-
-        if reward_token_amount == 0 {
-            msg!("Pool reward amount must be greater than zero");
-            return Err(LendingError::InvalidAmount.into());
-        }
-
-        let eligible_slot =
-            self.pool_rewards
-                .iter_mut()
-                .enumerate()
-                .find_map(|(slot_index, slot)| match slot {
-                    PoolRewardSlot::Vacant {
-                        last_pool_reward_id: PoolRewardId(id),
-                        ..
-                    } if *id < u32::MAX => Some((slot_index, PoolRewardId(*id + 1))),
-                    _ => None,
-                });
-
-        let Some((slot_index, next_id)) = eligible_slot else {
-            msg!("No vacant slot found for the new pool reward");
-            return Err(LendingError::NoVacantSlotForPoolReward.into());
-        };
-
-        self.pool_rewards[slot_index] = PoolRewardSlot::Occupied(Box::new(PoolReward {
-            id: next_id,
-            vault,
-            start_time_secs,
-            duration_secs,
-            total_rewards: reward_token_amount,
-            num_user_reward_managers: 0,
-            cumulative_rewards_per_share: Decimal::zero(),
-        }));
-
-        Ok(())
-    }
-
-    /// Sets the duration of the pool reward to now.
-    /// Returns the amount of unallocated rewards and the vault they are in.
-    pub fn cancel_pool_reward(
-        &mut self,
-        pool_reward_index: usize,
-        clock: &Clock,
-    ) -> Result<(Pubkey, u64), ProgramError> {
-        self.update(clock)?;
-
-        let Some(PoolRewardSlot::Occupied(pool_reward)) =
-            self.pool_rewards.get_mut(pool_reward_index)
-        else {
-            msg!("Cannot cancel a non-existent pool reward");
-            return Err(ProgramError::InvalidArgument);
-        };
-
-        if pool_reward.has_ended(clock) {
-            msg!("Cannot cancel a pool reward that has already ended");
-            return Err(LendingError::InvalidAccountInput.into());
-        }
-
-        let since_start_secs = clock.unix_timestamp as u64 - pool_reward.start_time_secs;
-        let unlocked_rewards = Decimal::from(pool_reward.total_rewards)
-            .try_mul(Decimal::from(since_start_secs))?
-            .try_div(Decimal::from(pool_reward.duration_secs as u64))?
-            .try_floor_u64()?;
-        let remaining_rewards = pool_reward.total_rewards - unlocked_rewards;
-
-        pool_reward.duration_secs =
-            u32::try_from(since_start_secs).expect("New duration to be strictly shorter");
-
-        Ok((pool_reward.vault, remaining_rewards))
-    }
-
-    /// Closes a pool reward if it has been cancelled before.
-    /// Returns the vault the rewards are in.
-    pub fn close_pool_reward(&mut self, pool_reward_index: usize) -> Result<Pubkey, ProgramError> {
-        let Some(PoolRewardSlot::Occupied(pool_reward)) =
-            self.pool_rewards.get_mut(pool_reward_index)
-        else {
-            msg!("Cannot close a non-existent pool reward");
-            return Err(ProgramError::InvalidArgument);
-        };
-
-        if pool_reward.num_user_reward_managers > 0 {
-            msg!("Cannot close a pool reward with active user reward managers");
-            return Err(LendingError::InvalidAccountInput.into());
-        }
-
-        let vault = pool_reward.vault;
-
-        self.pool_rewards[pool_reward_index] = PoolRewardSlot::Vacant {
-            last_pool_reward_id: pool_reward.id,
-            has_been_just_vacated: true,
-        };
-
-        Ok(vault)
-    }
-
-    /// 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)?;
-
-            // 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))?;
-
-            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(())
-    }
-}
-
-/// When creating a new [UserRewardManager] we need to know whether we should
-/// populate it with rewards or not.
-enum CreatingNewUserRewardManager {
-    /// If we are creating a [UserRewardManager] then we want to populate it.
-    Yes,
-    /// If we are updating an existing [UserRewardManager] then we don't want
-    /// to populate it.
-    No,
-}
-
-impl UserRewardManagers {
-    /// Returns [UserRewardManager] for the given reserve if any
-    pub fn find_mut(
-        &mut self,
-        reserve: Pubkey,
-        position_kind: PositionKind,
-    ) -> Option<&mut UserRewardManager> {
-        self.0.iter_mut().find(|user_reward_manager| {
-            user_reward_manager.reserve == reserve
-                && user_reward_manager.position_kind == position_kind
-        })
-    }
-
-    /// Updates the [UserRewardManager] for the given reserve.
-    ///
-    /// The caller must make sure that the provided [PoolRewardManager] is valid
-    /// for the given reserve.
-    ///
-    /// If an associated [UserRewardManager] is not found, it will be created.
-    ///
-    /// # Important
-    ///
-    /// Only call this if you're sure that the obligation should be tracking
-    /// rewards for the given reserve.
-    pub fn set_share(
-        &mut self,
-        reserve: Pubkey,
-        position_kind: PositionKind,
-        pool_reward_manager: &mut PoolRewardManager,
-        new_share: u64,
-        clock: &Clock,
-    ) -> Result<(), ProgramError> {
-        let user_reward_manager = if let Some(user_reward_manager) =
-            self.find_mut(reserve, position_kind)
-        {
-            user_reward_manager.update(pool_reward_manager, clock)?;
-            user_reward_manager
-        } else {
-            let mut new_user_reward_manager = UserRewardManager::new(reserve, position_kind, clock);
-            new_user_reward_manager.populate(pool_reward_manager, clock)?;
-            self.0.push(new_user_reward_manager);
-            // SAFETY: we just pushed a new item to the vector so ok to unwrap
-            self.0.last_mut().unwrap()
-        };
-
-        user_reward_manager.set_share(pool_reward_manager, new_share);
-
-        Ok(())
-    }
-}
-
-impl UserRewardManager {
-    /// Creates a new empty [UserRewardManager] for the given reserve.
-    pub fn new(reserve: Pubkey, position_kind: PositionKind, clock: &Clock) -> Self {
-        Self {
-            reserve,
-            last_update_time_secs: clock.unix_timestamp as _,
-            position_kind,
-            share: 0,
-            rewards: Vec::new(),
-        }
-    }
-
-    /// Sets new share value for this manager.
-    fn set_share(&mut self, pool_reward_manager: &mut PoolRewardManager, new_share: u64) {
-        msg!(
-            "For reserve {} there are {} total shares. \
-            User's previous position was at {} and new is at {}",
-            self.reserve,
-            pool_reward_manager.total_shares,
-            self.share,
-            new_share
-        );
-
-        // This works even for migrations.
-        // User's old share is 0 although it shouldn't be bcs they have borrowed
-        // or deposited.
-        // We only now attribute the share to the user which is fine, it's as if
-        // they just now borrowed/deposited.
-        pool_reward_manager.total_shares =
-            pool_reward_manager.total_shares - self.share + new_share;
-
-        self.share = new_share;
-    }
-
-    /// Claims all rewards that the user has earned.
-    /// Returns how many tokens should be transferred to the user.
-    ///
-    /// # Note
-    ///
-    /// Errors if there is no pool reward with this vault.
-    pub fn claim_rewards(
-        &mut self,
-        pool_reward_manager: &mut PoolRewardManager,
-        vault: Pubkey,
-        clock: &Clock,
-    ) -> Result<u64, ProgramError> {
-        self.update(pool_reward_manager, clock)?;
-
-        let (pool_reward_index, pool_reward) = pool_reward_manager
-            .pool_rewards
-            .iter_mut()
-            .enumerate()
-            .find_map(move |(index, slot)| match slot {
-                PoolRewardSlot::Occupied(pool_reward) if pool_reward.vault == vault => {
-                    Some((index, pool_reward))
-                }
-                _ => None,
-            })
-            .ok_or(LendingError::NoPoolRewardMatches)?;
-
-        let Some((user_reward_index, user_reward)) =
-            self.rewards
-                .iter_mut()
-                .enumerate()
-                .find(|(_, user_reward)| {
-                    user_reward.pool_reward_index == pool_reward_index
-                        && user_reward.pool_reward_id == pool_reward.id
-                })
-        else {
-            // User is not tracking this reward, nothing to claim.
-            // Let's be graceful and make this a no-op.
-            // Prevents failures when multiple parties crank rewards.
-            return Ok(0);
-        };
-
-        let to_claim = user_reward.withdraw_earned_rewards()?;
-
-        if pool_reward.has_ended(clock) && user_reward.earned_rewards.try_floor_u64()? == 0 {
-            // This reward won't be used anymore as it ended and the user
-            // claimed all there was to claim.
-            // 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;
-        }
-
-        Ok(to_claim)
-    }
-
-    /// Should be updated before any interaction with rewards.
-    ///
-    /// Invoker must have checked that this [PoolRewardManager] matches the
-    /// [UserRewardManager].
-    pub fn update(
-        &mut self,
-        pool_reward_manager: &mut PoolRewardManager,
-        clock: &Clock,
-    ) -> Result<(), ProgramError> {
-        self.update_(pool_reward_manager, clock, CreatingNewUserRewardManager::No)
-    }
-
-    /// When user borrows/deposits for a new reserve this function copies all
-    /// reserve rewards from the pool manager to the user manager and starts
-    /// accruing rewards.
-    ///
-    /// Invoker must have checked that this [PoolRewardManager] matches the
-    /// [UserRewardManager].
-    pub(crate) fn populate(
-        &mut self,
-        pool_reward_manager: &mut PoolRewardManager,
-        clock: &Clock,
-    ) -> Result<(), ProgramError> {
-        self.update_(
-            pool_reward_manager,
-            clock,
-            CreatingNewUserRewardManager::Yes,
-        )
-    }
-
-    /// 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: CreatingNewUserRewardManager,
-    ) -> Result<(), ProgramError> {
-        pool_reward_manager.update(clock)?;
-
-        let curr_unix_timestamp_secs = clock.unix_timestamp as u64;
-
-        if matches!(
-            creating_new_reward_manager,
-            CreatingNewUserRewardManager::No
-        ) && curr_unix_timestamp_secs == self.last_update_time_secs
-        {
-            return Ok(());
-        }
-
-        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 maybe_user_reward = self
-                .rewards
-                .iter_mut()
-                .enumerate()
-                .find(|(_, r)| r.pool_reward_index == pool_reward_index);
-
-            let end_time_secs = pool_reward.start_time_secs + pool_reward.duration_secs as u64;
-            let has_ended_for_user = self.last_update_time_secs >= end_time_secs;
-
-            match maybe_user_reward {
-                Some((user_reward_index, user_reward))
-                    if has_ended_for_user && user_reward.earned_rewards.try_floor_u64()? == 0 =>
-                {
-                    // 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_for_user => {
-                    // reward period over & there are rewards yet to be cracked
-                }
-                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;
-                }
-                None if pool_reward.start_time_secs > curr_unix_timestamp_secs => {
-                    // reward period has not started yet
-                }
-                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
-                        {
-                            pool_reward
-                                .cumulative_rewards_per_share
-                                .try_mul(Decimal::from(self.share))?
-                        } else {
-                            debug_assert!(matches!(
-                                creating_new_reward_manager,
-                                CreatingNewUserRewardManager::Yes
-                            ));
-                            Decimal::zero()
-                        },
-                    };
-
-                    self.rewards.push(new_user_reward);
-                    pool_reward.num_user_reward_managers += 1;
-                }
-            }
-        }
-
-        self.last_update_time_secs = curr_unix_timestamp_secs;
-
-        Ok(())
-    }
-}
-
-impl PoolReward {
-    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;
-
-    /// Returns whether the reward has ended.
-    pub fn has_ended(&self, clock: &Clock) -> bool {
-        let end_time_secs = self.start_time_secs + self.duration_secs as u64;
-        clock.unix_timestamp as u64 >= end_time_secs
-    }
-}
-
-impl PoolRewardId {
-    const LEN: usize = std::mem::size_of::<Self>();
-}
-
-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),
-            // 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_just_vacated: false,
-        }
-    }
-}
-
-impl PoolRewardManager {
-    #[inline(never)]
-    pub(crate) fn unpack_to_box(input: &[u8]) -> Result<Box<Self>, ProgramError> {
-        Ok(Box::new(PoolRewardManager::unpack_from_slice(input)?))
-    }
-}
-
-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());
-
-        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_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];
-
-            match pool_reward_slot {
-                PoolRewardSlot::Vacant {
-                    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,
-                    );
-                }
-            };
-        }
-    }
-
-    #[inline(never)]
-    fn unpack_from_slice(input: &[u8]) -> Result<Self, ProgramError> {
-        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_head = array_ref![input, offset, PoolReward::HEAD_LEN];
-
-            #[allow(clippy::ptr_offset_with_cast)]
-            let (src_id, src_vault) =
-                array_refs![raw_pool_reward_head, PoolRewardId::LEN, PUBKEY_BYTES];
-
-            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_just_vacated: 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,
-                    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)
-    }
-}
-
-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_just_vacated: false,
-                ..
-            }
-        );
-
-        !for_sure_has_not_changed
-    }
-}
-
-impl UserReward {
-    /// - [UserReward::pool_reward_index] truncated to a byte
-    /// - [PoolRewardId]
-    /// - packed [Decimal]
-    /// - packed [Decimal]
-    pub const LEN: usize = 1 + PoolRewardId::LEN + 16 + 16;
-
-    /// Removes all earned rewards from [Self] and returns them.
-    ///
-    /// # Note
-    /// Decimals are truncated to u64, dust is kept.
-    fn withdraw_earned_rewards(&mut self) -> Result<u64, ProgramError> {
-        let reward_amount = self.earned_rewards.try_floor_u64()?;
-
-        if reward_amount > 0 {
-            self.earned_rewards = self.earned_rewards.try_sub(reward_amount.into())?;
-        }
-
-        Ok(reward_amount)
-    }
-}
-
-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.
-    pub const MAX_LEN: usize = Self::HEAD_LEN + MAX_REWARDS * UserReward::LEN;
-
-    /// Length of data before [Self::rewards] tail.
-    ///
-    /// - [Self::reserve]
-    /// - [Self::position_kind]
-    /// - [Self::share]
-    /// - [Self::last_update_time_secs]
-    /// - [Self::rewards] vector length as u8
-    const HEAD_LEN: usize = PUBKEY_BYTES + 1 + 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];
-
-        let (
-            dst_reserve,
-            dst_position_kind,
-            dst_share,
-            dst_last_update_time_secs,
-            dst_user_rewards_len,
-        ) = mut_array_refs![
-            raw_user_reward_manager,
-            PUBKEY_BYTES,
-            1, // position_kind
-            8, // share
-            8, // last_update_time_secs
-            1  // length of rewards array that's next to come
-        ];
-
-        dst_reserve.copy_from_slice(self.reserve.as_ref());
-        dst_position_kind.copy_from_slice(&(self.position_kind as u8).to_le_bytes());
-        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_user_rewards_len.copy_from_slice(
-            &({
-                debug_assert!(MAX_REWARDS >= self.rewards.len());
-                debug_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;
-            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<Self, ProgramError> {
-        #[allow(clippy::ptr_offset_with_cast)]
-        let raw_user_reward_manager_head = array_ref![input, 0, UserRewardManager::HEAD_LEN];
-
-        #[allow(clippy::ptr_offset_with_cast)]
-        let (
-            src_reserve,
-            src_position_kind,
-            src_share,
-            src_last_update_time_secs,
-            src_user_rewards_len,
-        ) = array_refs![
-            raw_user_reward_manager_head,
-            PUBKEY_BYTES,
-            1, // position_kind
-            8, // share
-            8, // last_update_time_secs
-            1  // length of rewards array that's next to come
-        ];
-
-        let reserve = Pubkey::new_from_array(*src_reserve);
-        let position_kind = u8::from_le_bytes(*src_position_kind).try_into()?;
-        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);
-
-        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];
-
-            #[allow(clippy::ptr_offset_with_cast)]
-            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,
-            position_kind,
-            share,
-            last_update_time_secs,
-            rewards,
-        })
-    }
-}
-
-impl Deref for UserRewardManagers {
-    type Target = Vec<UserRewardManager>;
-
-    fn deref(&self) -> &Self::Target {
-        &self.0
-    }
-}
-
-impl DerefMut for UserRewardManagers {
-    fn deref_mut(&mut self) -> &mut Self::Target {
-        &mut self.0
-    }
-}
-
-impl Default for UserRewardManager {
-    fn default() -> Self {
-        Self {
-            reserve: Pubkey::default(),
-            position_kind: PositionKind::Deposit,
-            share: 0,
-            last_update_time_secs: 0,
-            rewards: Vec::new(),
-        }
-    }
-}
-
 #[cfg(test)]
-mod tests {
-    //! TODO: Rewrite these tests from their Suilend counterparts.
+mod suilend_tests {
+    //! These tests were taken from the Suilend's codebase and adapted to
+    //! the new codebase.
+    //!
     //! TODO: Calculate test coverage and add tests for missing branches.
 
-    use super::*;
+    use crate::{
+        math::Decimal,
+        state::{
+            PoolReward, PoolRewardId, PoolRewardManager, PoolRewardSlot, PositionKind,
+            UserRewardManager, MAX_REWARDS,
+        },
+    };
     use pretty_assertions::assert_eq;
-    use proptest::prelude::*;
-    use rand::Rng;
+    use solana_program::{clock::Clock, pubkey::Pubkey};
 
     const SECONDS_IN_A_DAY: u64 = 86_400;
 
-    fn pool_reward_manager_strategy() -> impl Strategy<Value = PoolRewardManager> {
-        (0..1u32).prop_perturb(|_, mut rng| PoolRewardManager::new_rand(&mut rng))
-    }
-
-    fn user_reward_manager_strategy() -> impl Strategy<Value = UserRewardManager> {
-        (0..100u32).prop_perturb(|_, mut rng| UserRewardManager::new_rand(&mut rng))
-    }
-
-    proptest! {
-        #[test]
-        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.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]
-        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_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_just_vacated: 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_just_vacated: false,
-            }
-        );
-    }
-
-    #[test]
-    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());
-
-        // 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),
-                    has_been_just_vacated: false,
-                }
-            )
-        });
-
-        assert!(all_rewards_are_empty);
-    }
-
-    #[test]
-    fn it_fits_reserve_realloc_into_single_ix() {
-        const MAX_REALLOC: usize = solana_program::entrypoint::MAX_PERMITTED_DATA_INCREASE;
-
-        let size_of_discriminant = 1;
-        let required_realloc = size_of_discriminant * PoolRewardManager::LEN;
-        assert!(required_realloc <= MAX_REALLOC);
-    }
-
     /// This tests replicates calculations from Suilend's
     /// "test_pool_reward_manager_basic" test.
     #[test]
@@ -1585,55 +485,4 @@ mod tests {
             assert_eq!(claimed_slnd, 50 * 1_000_000);
         }
     }
-
-    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: Default::default(),
-                            has_been_just_vacated: false,
-                        }
-                    } else {
-                        PoolRewardSlot::Occupied(Box::new(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(),
-                        }))
-                    }
-                }),
-            }
-        }
-    }
-
-    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(),
-                position_kind: rng.gen_range(0..=1u8).try_into().unwrap(),
-                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(),
-            }
-        }
-    }
 }
diff --git a/token-lending/sdk/src/state/liquidity_mining/pool_reward_manager.rs b/token-lending/sdk/src/state/liquidity_mining/pool_reward_manager.rs
new file mode 100644
index 00000000000..ecfc83a9ccb
--- /dev/null
+++ b/token-lending/sdk/src/state/liquidity_mining/pool_reward_manager.rs
@@ -0,0 +1,609 @@
+//! [PoolRewardManager]s are stored in [crate::state::Reserve]s.
+//! They can be either borrow or deposit but the logic is the same, the only
+//! difference is how shares are calculated.
+//!
+//! For borrow managers the shares are "liability" and for deposit
+//! managers the shares are "deposited collateral".
+
+use crate::{
+    error::LendingError,
+    math::{Decimal, TryAdd, TryDiv, TryMul},
+    state::{pack_decimal, unpack_decimal, MAX_REWARDS, MIN_REWARD_PERIOD_SECS},
+};
+use arrayref::{array_mut_ref, array_ref, array_refs, mut_array_refs};
+use core::convert::{TryFrom, TryInto};
+use solana_program::{
+    clock::Clock,
+    msg,
+    program_error::ProgramError,
+    program_pack::{Pack, Sealed},
+    pubkey::{Pubkey, PUBKEY_BYTES},
+};
+
+/// 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,
+    /// 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.
+///
+/// # 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 [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);
+
+/// # (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.
+#[derive(Clone, Debug, PartialEq)]
+pub enum PoolRewardSlot {
+    /// New reward can be added to this slot.
+    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.
+        /// That way the packing logic knows whether it's fine to skip the
+        /// packing or not.
+        has_been_just_vacated: bool,
+    },
+    /// Reward has not been closed yet.
+    ///
+    /// We box the [PoolReward] to avoid stack overflow.
+    Occupied(Box<PoolReward>),
+}
+
+/// 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.
+    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.
+    ///
+    /// # Reward cancellation
+    ///
+    /// Is cut short if the reward is cancelled.
+    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,
+    /// 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)`
+    ///
+    /// # (Un)Packing
+    /// We only store 16 most significant digits.
+    pub cumulative_rewards_per_share: Decimal,
+}
+
+impl PoolRewardManager {
+    /// Adds a new pool reward.
+    ///
+    /// Will first update itself.
+    ///
+    /// Start time will be set to now if it's in the past.
+    /// Must last at least [MIN_REWARD_PERIOD_SECS].
+    /// The amount of tokens to distribute must be greater than zero.
+    ///
+    /// Will return an error if no slot can be found for the new reward.
+    pub fn add_pool_reward(
+        &mut self,
+        vault: Pubkey,
+        start_time_secs: u64,
+        end_time_secs: u64,
+        reward_token_amount: u64,
+        clock: &Clock,
+    ) -> Result<(), ProgramError> {
+        self.update(clock)?;
+
+        let start_time_secs = start_time_secs.max(clock.unix_timestamp as u64);
+
+        if start_time_secs >= end_time_secs {
+            msg!("Pool reward must end after it starts");
+            return Err(LendingError::PoolRewardPeriodTooShort.into());
+        }
+
+        let duration_secs: u32 = {
+            // SAFETY: just checked that start time is strictly smaller
+            let d = end_time_secs - start_time_secs;
+            d.try_into().map_err(|_| {
+                msg!("Pool reward duration is too long");
+                LendingError::MathOverflow
+            })?
+        };
+        if MIN_REWARD_PERIOD_SECS > duration_secs as u64 {
+            msg!("Pool reward duration must be at least {MIN_REWARD_PERIOD_SECS} secs");
+            return Err(LendingError::PoolRewardPeriodTooShort.into());
+        }
+
+        if reward_token_amount == 0 {
+            msg!("Pool reward amount must be greater than zero");
+            return Err(LendingError::InvalidAmount.into());
+        }
+
+        let eligible_slot =
+            self.pool_rewards
+                .iter_mut()
+                .enumerate()
+                .find_map(|(slot_index, slot)| match slot {
+                    PoolRewardSlot::Vacant {
+                        last_pool_reward_id: PoolRewardId(id),
+                        ..
+                    } if *id < u32::MAX => Some((slot_index, PoolRewardId(*id + 1))),
+                    _ => None,
+                });
+
+        let Some((slot_index, next_id)) = eligible_slot else {
+            msg!("No vacant slot found for the new pool reward");
+            return Err(LendingError::NoVacantSlotForPoolReward.into());
+        };
+
+        self.pool_rewards[slot_index] = PoolRewardSlot::Occupied(Box::new(PoolReward {
+            id: next_id,
+            vault,
+            start_time_secs,
+            duration_secs,
+            total_rewards: reward_token_amount,
+            num_user_reward_managers: 0,
+            cumulative_rewards_per_share: Decimal::zero(),
+        }));
+
+        Ok(())
+    }
+
+    /// Sets the duration of the pool reward to now.
+    /// Returns the amount of unallocated rewards and the vault they are in.
+    pub fn cancel_pool_reward(
+        &mut self,
+        pool_reward_index: usize,
+        clock: &Clock,
+    ) -> Result<(Pubkey, u64), ProgramError> {
+        self.update(clock)?;
+
+        let Some(PoolRewardSlot::Occupied(pool_reward)) =
+            self.pool_rewards.get_mut(pool_reward_index)
+        else {
+            msg!("Cannot cancel a non-existent pool reward");
+            return Err(ProgramError::InvalidArgument);
+        };
+
+        if pool_reward.has_ended(clock) {
+            msg!("Cannot cancel a pool reward that has already ended");
+            return Err(LendingError::InvalidAccountInput.into());
+        }
+
+        let since_start_secs = clock.unix_timestamp as u64 - pool_reward.start_time_secs;
+        let unlocked_rewards = Decimal::from(pool_reward.total_rewards)
+            .try_mul(Decimal::from(since_start_secs))?
+            .try_div(Decimal::from(pool_reward.duration_secs as u64))?
+            .try_floor_u64()?;
+        let remaining_rewards = pool_reward.total_rewards - unlocked_rewards;
+
+        pool_reward.duration_secs =
+            u32::try_from(since_start_secs).expect("New duration to be strictly shorter");
+
+        Ok((pool_reward.vault, remaining_rewards))
+    }
+
+    /// Closes a pool reward if it has been cancelled before.
+    /// Returns the vault the rewards are in.
+    pub fn close_pool_reward(&mut self, pool_reward_index: usize) -> Result<Pubkey, ProgramError> {
+        let Some(PoolRewardSlot::Occupied(pool_reward)) =
+            self.pool_rewards.get_mut(pool_reward_index)
+        else {
+            msg!("Cannot close a non-existent pool reward");
+            return Err(ProgramError::InvalidArgument);
+        };
+
+        if pool_reward.num_user_reward_managers > 0 {
+            msg!("Cannot close a pool reward with active user reward managers");
+            return Err(LendingError::InvalidAccountInput.into());
+        }
+
+        let vault = pool_reward.vault;
+
+        self.pool_rewards[pool_reward_index] = PoolRewardSlot::Vacant {
+            last_pool_reward_id: pool_reward.id,
+            has_been_just_vacated: true,
+        };
+
+        Ok(vault)
+    }
+}
+
+impl PoolRewardManager {
+    /// Should be updated before any interaction with rewards.
+    pub(crate) 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)?;
+
+            // 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))?;
+
+            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(())
+    }
+}
+
+impl PoolReward {
+    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;
+
+    /// Returns whether the reward has ended.
+    pub(crate) fn has_ended(&self, clock: &Clock) -> bool {
+        let end_time_secs = self.start_time_secs + self.duration_secs as u64;
+        clock.unix_timestamp as u64 >= end_time_secs
+    }
+}
+
+impl PoolRewardId {
+    pub(crate) const LEN: usize = std::mem::size_of::<Self>();
+}
+
+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),
+            // 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_just_vacated: false,
+        }
+    }
+}
+
+impl PoolRewardManager {
+    #[inline(never)]
+    pub(crate) fn unpack_to_box(input: &[u8]) -> Result<Box<Self>, ProgramError> {
+        Ok(Box::new(PoolRewardManager::unpack_from_slice(input)?))
+    }
+}
+
+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());
+
+        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_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];
+
+            match pool_reward_slot {
+                PoolRewardSlot::Vacant {
+                    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,
+                    );
+                }
+            };
+        }
+    }
+
+    #[inline(never)]
+    fn unpack_from_slice(input: &[u8]) -> Result<Self, ProgramError> {
+        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_head = array_ref![input, offset, PoolReward::HEAD_LEN];
+
+            #[allow(clippy::ptr_offset_with_cast)]
+            let (src_id, src_vault) =
+                array_refs![raw_pool_reward_head, PoolRewardId::LEN, PUBKEY_BYTES];
+
+            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_just_vacated: 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,
+                    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)
+    }
+}
+
+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_just_vacated: false,
+                ..
+            }
+        );
+
+        !for_sure_has_not_changed
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use proptest::prelude::*;
+
+    impl PoolRewardManager {
+        pub(crate) fn new_rand(rng: &mut impl rand::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: Default::default(),
+                            has_been_just_vacated: false,
+                        }
+                    } else {
+                        PoolRewardSlot::Occupied(Box::new(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(),
+                        }))
+                    }
+                }),
+            }
+        }
+    }
+
+    #[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_just_vacated: 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_just_vacated: false,
+            }
+        );
+    }
+
+    #[test]
+    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());
+
+        // 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),
+                    has_been_just_vacated: false,
+                }
+            )
+        });
+
+        assert!(all_rewards_are_empty);
+    }
+
+    #[test]
+    fn it_fits_reserve_realloc_into_single_ix() {
+        const MAX_REALLOC: usize = solana_program::entrypoint::MAX_PERMITTED_DATA_INCREASE;
+
+        let size_of_discriminant = 1;
+        let required_realloc = size_of_discriminant * PoolRewardManager::LEN;
+        assert!(required_realloc <= MAX_REALLOC);
+    }
+
+    fn pool_reward_manager_strategy() -> impl Strategy<Value = PoolRewardManager> {
+        (0..1u32).prop_perturb(|_, mut rng| PoolRewardManager::new_rand(&mut rng))
+    }
+
+    proptest! {
+        #[test]
+        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.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);
+            }
+        }
+    }
+}
diff --git a/token-lending/sdk/src/state/liquidity_mining/user_reward_manager.rs b/token-lending/sdk/src/state/liquidity_mining/user_reward_manager.rs
new file mode 100644
index 00000000000..b94dba18755
--- /dev/null
+++ b/token-lending/sdk/src/state/liquidity_mining/user_reward_manager.rs
@@ -0,0 +1,605 @@
+//! [UserRewardManager]s are stored in [crate::state::Obligation]s for each
+//! reserve the user has borrowed from or deposited into at the current time or
+//! in the past.
+
+use crate::{
+    error::LendingError,
+    math::{Decimal, TryAdd, TryMul, TrySub},
+    state::{
+        pack_decimal, unpack_decimal, PoolRewardId, PoolRewardManager, PoolRewardSlot,
+        PositionKind, MAX_REWARDS,
+    },
+};
+use arrayref::{array_mut_ref, array_ref, array_refs, mut_array_refs};
+use core::{
+    convert::TryInto,
+    ops::{Deref, DerefMut},
+};
+use solana_program::{
+    clock::Clock,
+    msg,
+    program_error::ProgramError,
+    pubkey::{Pubkey, PUBKEY_BYTES},
+};
+
+/// Wraps over user reward managers and allows mutable access to them while
+/// other obligation fields are borrowed.
+#[derive(Clone, Debug, Default, PartialEq)]
+pub struct UserRewardManagers(Vec<UserRewardManager>);
+
+/// Tracks user's LM rewards for a specific pool (reserve.)
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub struct UserRewardManager {
+    /// Links this manager to a reserve.
+    pub reserve: Pubkey,
+    /// Although a user cannot both borrow and deposit in the same reserve, they
+    /// can deposit, withdraw and then borrow the same reserve.
+    /// Meanwhile they could've accumulated some rewards that'd be lost.
+    ///
+    /// Also, have an explicit distinguish between borrow and deposit doesn't
+    /// suffer from a footgun of misattributing rewards.
+    pub position_kind: PositionKind,
+    /// 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 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<UserReward>,
+}
+
+/// Track user rewards for a specific [PoolReward].
+#[derive(Debug, PartialEq, Eq, Default, Clone)]
+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,
+    /// 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,
+}
+
+/// When creating a new [UserRewardManager] we need to know whether we should
+/// populate it with rewards or not.
+enum CreatingNewUserRewardManager {
+    /// If we are creating a [UserRewardManager] then we want to populate it.
+    Yes,
+    /// If we are updating an existing [UserRewardManager] then we don't want
+    /// to populate it.
+    No,
+}
+
+impl UserRewardManagers {
+    /// Returns [UserRewardManager] for the given reserve if any
+    pub fn find_mut(
+        &mut self,
+        reserve: Pubkey,
+        position_kind: PositionKind,
+    ) -> Option<&mut UserRewardManager> {
+        self.0.iter_mut().find(|user_reward_manager| {
+            user_reward_manager.reserve == reserve
+                && user_reward_manager.position_kind == position_kind
+        })
+    }
+
+    /// Updates the [UserRewardManager] for the given reserve.
+    ///
+    /// The caller must make sure that the provided [PoolRewardManager] is valid
+    /// for the given reserve.
+    ///
+    /// If an associated [UserRewardManager] is not found, it will be created.
+    ///
+    /// # Important
+    ///
+    /// Only call this if you're sure that the obligation should be tracking
+    /// rewards for the given reserve.
+    pub fn set_share(
+        &mut self,
+        reserve: Pubkey,
+        position_kind: PositionKind,
+        pool_reward_manager: &mut PoolRewardManager,
+        new_share: u64,
+        clock: &Clock,
+    ) -> Result<(), ProgramError> {
+        let user_reward_manager = if let Some(user_reward_manager) =
+            self.find_mut(reserve, position_kind)
+        {
+            user_reward_manager.update(
+                pool_reward_manager,
+                clock,
+                CreatingNewUserRewardManager::No,
+            )?;
+            user_reward_manager
+        } else {
+            let mut new_user_reward_manager = UserRewardManager::new(reserve, position_kind, clock);
+            new_user_reward_manager.populate(pool_reward_manager, clock)?;
+            self.0.push(new_user_reward_manager);
+            // SAFETY: we just pushed a new item to the vector so ok to unwrap
+            self.0.last_mut().unwrap()
+        };
+
+        user_reward_manager.set_share(pool_reward_manager, new_share);
+
+        Ok(())
+    }
+}
+
+impl UserRewardManager {
+    /// Claims all rewards that the user has earned.
+    /// Returns how many tokens should be transferred to the user.
+    ///
+    /// # Note
+    ///
+    /// Errors if there is no pool reward with this vault.
+    pub fn claim_rewards(
+        &mut self,
+        pool_reward_manager: &mut PoolRewardManager,
+        vault: Pubkey,
+        clock: &Clock,
+    ) -> Result<u64, ProgramError> {
+        self.update(pool_reward_manager, clock, CreatingNewUserRewardManager::No)?;
+
+        let (pool_reward_index, pool_reward) = pool_reward_manager
+            .pool_rewards
+            .iter_mut()
+            .enumerate()
+            .find_map(move |(index, slot)| match slot {
+                PoolRewardSlot::Occupied(pool_reward) if pool_reward.vault == vault => {
+                    Some((index, pool_reward))
+                }
+                _ => None,
+            })
+            .ok_or(LendingError::NoPoolRewardMatches)?;
+
+        let Some((user_reward_index, user_reward)) =
+            self.rewards
+                .iter_mut()
+                .enumerate()
+                .find(|(_, user_reward)| {
+                    user_reward.pool_reward_index == pool_reward_index
+                        && user_reward.pool_reward_id == pool_reward.id
+                })
+        else {
+            // User is not tracking this reward, nothing to claim.
+            // Let's be graceful and make this a no-op.
+            // Prevents failures when multiple parties crank rewards.
+            return Ok(0);
+        };
+
+        let to_claim = user_reward.withdraw_earned_rewards()?;
+
+        if pool_reward.has_ended(clock) && user_reward.earned_rewards.try_floor_u64()? == 0 {
+            // This reward won't be used anymore as it ended and the user
+            // claimed all there was to claim.
+            // 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;
+        }
+
+        Ok(to_claim)
+    }
+}
+
+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.
+    pub(crate) const MAX_LEN: usize = Self::HEAD_LEN + MAX_REWARDS * UserReward::LEN;
+
+    /// Length of data before [Self::rewards] tail.
+    ///
+    /// - [Self::reserve]
+    /// - [Self::position_kind]
+    /// - [Self::share]
+    /// - [Self::last_update_time_secs]
+    /// - [Self::rewards] vector length as u8
+    const HEAD_LEN: usize = PUBKEY_BYTES + 1 + 8 + 8 + 1;
+
+    /// Creates a new empty [UserRewardManager] for the given reserve.
+    pub(crate) fn new(reserve: Pubkey, position_kind: PositionKind, clock: &Clock) -> Self {
+        Self {
+            reserve,
+            last_update_time_secs: clock.unix_timestamp as _,
+            position_kind,
+            share: 0,
+            rewards: Vec::new(),
+        }
+    }
+
+    /// Sets new share value for this manager.
+    pub(crate) fn set_share(
+        &mut self,
+        pool_reward_manager: &mut PoolRewardManager,
+        new_share: u64,
+    ) {
+        msg!(
+            "For reserve {} there are {} total shares. \
+            User's previous position was at {} and new is at {}",
+            self.reserve,
+            pool_reward_manager.total_shares,
+            self.share,
+            new_share
+        );
+
+        // This works even for migrations.
+        // User's old share is 0 although it shouldn't be bcs they have borrowed
+        // or deposited.
+        // We only now attribute the share to the user which is fine, it's as if
+        // they just now borrowed/deposited.
+        pool_reward_manager.total_shares =
+            pool_reward_manager.total_shares - self.share + new_share;
+
+        self.share = new_share;
+    }
+
+    /// When user borrows/deposits for a new reserve this function copies all
+    /// reserve rewards from the pool manager to the user manager and starts
+    /// accruing rewards.
+    ///
+    /// Invoker must have checked that this [PoolRewardManager] matches the
+    /// [UserRewardManager].
+    pub(crate) fn populate(
+        &mut self,
+        pool_reward_manager: &mut PoolRewardManager,
+        clock: &Clock,
+    ) -> Result<(), ProgramError> {
+        self.update(
+            pool_reward_manager,
+            clock,
+            CreatingNewUserRewardManager::Yes,
+        )
+    }
+
+    /// 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: CreatingNewUserRewardManager,
+    ) -> Result<(), ProgramError> {
+        pool_reward_manager.update(clock)?;
+
+        let curr_unix_timestamp_secs = clock.unix_timestamp as u64;
+
+        if matches!(
+            creating_new_reward_manager,
+            CreatingNewUserRewardManager::No
+        ) && curr_unix_timestamp_secs == self.last_update_time_secs
+        {
+            return Ok(());
+        }
+
+        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 maybe_user_reward = self
+                .rewards
+                .iter_mut()
+                .enumerate()
+                .find(|(_, r)| r.pool_reward_index == pool_reward_index);
+
+            let end_time_secs = pool_reward.start_time_secs + pool_reward.duration_secs as u64;
+            let has_ended_for_user = self.last_update_time_secs >= end_time_secs;
+
+            match maybe_user_reward {
+                Some((user_reward_index, user_reward))
+                    if has_ended_for_user && user_reward.earned_rewards.try_floor_u64()? == 0 =>
+                {
+                    // 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_for_user => {
+                    // reward period over & there are rewards yet to be cracked
+                }
+                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;
+                }
+                None if pool_reward.start_time_secs > curr_unix_timestamp_secs => {
+                    // reward period has not started yet
+                }
+                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
+                        {
+                            pool_reward
+                                .cumulative_rewards_per_share
+                                .try_mul(Decimal::from(self.share))?
+                        } else {
+                            debug_assert!(matches!(
+                                creating_new_reward_manager,
+                                CreatingNewUserRewardManager::Yes
+                            ));
+                            Decimal::zero()
+                        },
+                    };
+
+                    self.rewards.push(new_user_reward);
+                    pool_reward.num_user_reward_managers += 1;
+                }
+            }
+        }
+
+        self.last_update_time_secs = curr_unix_timestamp_secs;
+
+        Ok(())
+    }
+
+    /// 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];
+
+        let (
+            dst_reserve,
+            dst_position_kind,
+            dst_share,
+            dst_last_update_time_secs,
+            dst_user_rewards_len,
+        ) = mut_array_refs![
+            raw_user_reward_manager,
+            PUBKEY_BYTES,
+            1, // position_kind
+            8, // share
+            8, // last_update_time_secs
+            1  // length of rewards array that's next to come
+        ];
+
+        dst_reserve.copy_from_slice(self.reserve.as_ref());
+        dst_position_kind.copy_from_slice(&(self.position_kind as u8).to_le_bytes());
+        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_user_rewards_len.copy_from_slice(
+            &({
+                debug_assert!(MAX_REWARDS >= self.rewards.len());
+                debug_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;
+            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<Self, ProgramError> {
+        #[allow(clippy::ptr_offset_with_cast)]
+        let raw_user_reward_manager_head = array_ref![input, 0, UserRewardManager::HEAD_LEN];
+
+        #[allow(clippy::ptr_offset_with_cast)]
+        let (
+            src_reserve,
+            src_position_kind,
+            src_share,
+            src_last_update_time_secs,
+            src_user_rewards_len,
+        ) = array_refs![
+            raw_user_reward_manager_head,
+            PUBKEY_BYTES,
+            1, // position_kind
+            8, // share
+            8, // last_update_time_secs
+            1  // length of rewards array that's next to come
+        ];
+
+        let reserve = Pubkey::new_from_array(*src_reserve);
+        let position_kind = u8::from_le_bytes(*src_position_kind).try_into()?;
+        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);
+
+        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];
+
+            #[allow(clippy::ptr_offset_with_cast)]
+            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,
+            position_kind,
+            share,
+            last_update_time_secs,
+            rewards,
+        })
+    }
+}
+
+impl UserReward {
+    /// - [UserReward::pool_reward_index] truncated to a byte
+    /// - [PoolRewardId]
+    /// - packed [Decimal]
+    /// - packed [Decimal]
+    const LEN: usize = 1 + PoolRewardId::LEN + 16 + 16;
+
+    /// Removes all earned rewards from [Self] and returns them.
+    ///
+    /// # Note
+    /// Decimals are truncated to u64, dust is kept.
+    fn withdraw_earned_rewards(&mut self) -> Result<u64, ProgramError> {
+        let reward_amount = self.earned_rewards.try_floor_u64()?;
+
+        if reward_amount > 0 {
+            self.earned_rewards = self.earned_rewards.try_sub(reward_amount.into())?;
+        }
+
+        Ok(reward_amount)
+    }
+}
+
+impl From<Vec<UserRewardManager>> for UserRewardManagers {
+    fn from(user_reward_managers: Vec<UserRewardManager>) -> Self {
+        Self(user_reward_managers)
+    }
+}
+
+impl From<UserRewardManagers> for Vec<UserRewardManager> {
+    fn from(user_reward_managers: UserRewardManagers) -> Self {
+        user_reward_managers.0
+    }
+}
+
+impl Deref for UserRewardManagers {
+    type Target = Vec<UserRewardManager>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+impl DerefMut for UserRewardManagers {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.0
+    }
+}
+
+impl Default for UserRewardManager {
+    fn default() -> Self {
+        Self {
+            reserve: Pubkey::default(),
+            position_kind: PositionKind::Deposit,
+            share: 0,
+            last_update_time_secs: 0,
+            rewards: Vec::new(),
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use proptest::prelude::*;
+
+    impl UserRewardManager {
+        pub(crate) fn new_rand(rng: &mut impl rand::Rng) -> Self {
+            let rewards_len = rng.gen_range(0..MAX_REWARDS);
+            Self {
+                reserve: Pubkey::new_unique(),
+                position_kind: rng.gen_range(0..=1u8).try_into().unwrap(),
+                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(),
+            }
+        }
+    }
+
+    fn user_reward_manager_strategy() -> impl Strategy<Value = UserRewardManager> {
+        (0..100u32).prop_perturb(|_, mut rng| UserRewardManager::new_rand(&mut rng))
+    }
+
+    proptest! {
+        #[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);
+        }
+    }
+}
diff --git a/token-lending/sdk/src/state/obligation.rs b/token-lending/sdk/src/state/obligation.rs
index 58d63715178..b2661d7bf08 100644
--- a/token-lending/sdk/src/state/obligation.rs
+++ b/token-lending/sdk/src/state/obligation.rs
@@ -823,7 +823,7 @@ impl 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: UserRewardManagers(user_reward_managers),
+            user_reward_managers: user_reward_managers.into(),
         })
     }
 }
@@ -887,11 +887,10 @@ mod test {
                 closeable: rng.gen(),
                 user_reward_managers: {
                     let user_reward_managers_len = rng.gen_range(0..=MAX_OBLIGATION_RESERVES);
-                    UserRewardManagers(
-                        std::iter::repeat_with(|| UserRewardManager::new_rand(rng))
-                            .take(user_reward_managers_len)
-                            .collect(),
-                    )
+                    std::iter::repeat_with(|| UserRewardManager::new_rand(rng))
+                        .take(user_reward_managers_len)
+                        .collect::<Vec<_>>()
+                        .into()
                 },
             }
         }

From 8037676a7b2d17ec2e3615571681f33de56f45eb Mon Sep 17 00:00:00 2001
From: vanity mnm <vanity@solend.fi>
Date: Thu, 10 Apr 2025 11:14:34 +0200
Subject: [PATCH 11/14] [Liquidity Mining] BPF tests for closing and canceling
 rewards (10)  (#209)

* Adding test for cancelling reward

* Testing closing pool reward

* Removing outdated TODO

* Fixing clippy issues
---
 token-lending/program/src/processor.rs        |  20 ++-
 .../program/src/processor/liquidity_mining.rs |  77 ++++------
 .../liquidity_mining/add_pool_reward.rs       |  31 +++-
 .../liquidity_mining/cancel_pool_reward.rs    |  48 ++++--
 .../liquidity_mining/claim_user_reward.rs     |  48 ++++--
 .../liquidity_mining/close_pool_reward.rs     |  65 +++++---
 .../program/tests/add_pool_reward.rs          |  12 +-
 .../program/tests/cancel_pool_reward.rs       | 145 ++++++++++++++++++
 .../program/tests/close_pool_reward.rs        | 126 +++++++++++++++
 .../tests/helpers/solend_program_test.rs      | 105 +++++++++++--
 token-lending/sdk/src/instruction.rs          | 141 ++++++++++++++---
 .../sdk/src/state/liquidity_mining.rs         |  12 +-
 12 files changed, 684 insertions(+), 146 deletions(-)
 create mode 100644 token-lending/program/tests/cancel_pool_reward.rs
 create mode 100644 token-lending/program/tests/close_pool_reward.rs

diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs
index 55e58f04587..93c6bbf3734 100644
--- a/token-lending/program/src/processor.rs
+++ b/token-lending/program/src/processor.rs
@@ -208,6 +208,7 @@ pub fn process_instruction(
             process_donate_to_reserve(program_id, liquidity_amount, accounts)
         }
         LendingInstruction::AddPoolReward {
+            reward_authority_bump,
             position_kind,
             start_time_secs,
             end_time_secs,
@@ -216,6 +217,7 @@ pub fn process_instruction(
             msg!("Instruction: Add Pool Reward");
             liquidity_mining::add_pool_reward::process(
                 program_id,
+                reward_authority_bump,
                 position_kind,
                 start_time_secs,
                 end_time_secs,
@@ -224,32 +226,42 @@ pub fn process_instruction(
             )
         }
         LendingInstruction::CancelPoolReward {
+            reward_authority_bump,
             position_kind,
             pool_reward_index,
         } => {
             msg!("Instruction: Cancel Pool Reward");
             liquidity_mining::cancel_pool_reward::process(
                 program_id,
+                reward_authority_bump,
                 position_kind,
-                pool_reward_index,
+                pool_reward_index as _,
                 accounts,
             )
         }
         LendingInstruction::ClosePoolReward {
+            reward_authority_bump,
             position_kind,
             pool_reward_index,
         } => {
             msg!("Instruction: Close Pool Reward");
             liquidity_mining::close_pool_reward::process(
                 program_id,
+                reward_authority_bump,
                 position_kind,
-                pool_reward_index,
+                pool_reward_index as _,
                 accounts,
             )
         }
-        LendingInstruction::ClaimReward => {
+        LendingInstruction::ClaimReward {
+            reward_authority_bump,
+        } => {
             msg!("Instruction: Claim Reward");
-            liquidity_mining::claim_user_reward::process(program_id, accounts)
+            liquidity_mining::claim_user_reward::process(
+                program_id,
+                reward_authority_bump,
+                accounts,
+            )
         }
 
         // temporary ix for upgrade
diff --git a/token-lending/program/src/processor/liquidity_mining.rs b/token-lending/program/src/processor/liquidity_mining.rs
index df616720468..8c697ebb4ea 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] (TODO: add bpf tests)
-//! - [cancel_pool_reward] (TODO: add bpf tests)
-//! - [close_pool_reward] (TODO: add bpf tests)
+//! - [add_pool_reward]
+//! - [cancel_pool_reward]
+//! - [close_pool_reward]
 //!
 //! There is an ix related to migration:
 //! - [upgrade_reserve] (TODO: add bpf tests)
@@ -27,42 +27,27 @@ pub(crate) mod upgrade_reserve;
 
 use solana_program::program_pack::Pack;
 use solana_program::{account_info::AccountInfo, msg, program_error::ProgramError, pubkey::Pubkey};
+use solend_sdk::instruction::create_reward_vault_authority;
 use solend_sdk::{error::LendingError, state::LendingMarket};
 use spl_token::state::Account as TokenAccount;
 
 use super::ReserveBorrow;
+struct Bumps {
+    reward_authority: u8,
+}
 
 /// Unpacks a spl_token [TokenAccount].
 fn unpack_token_account(data: &[u8]) -> Result<TokenAccount, LendingError> {
     TokenAccount::unpack(data).map_err(|_| LendingError::InvalidTokenAccount)
 }
 
-/// Derives the reward vault authority PDA address.
-///
-/// TODO: Accept a bump seed to avoid recalculating it.
-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<'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(),
-    ]
+/// Named args for [check_and_unpack_pool_reward_accounts]
+struct CheckAndUnpackPoolRewardAccounts<'a, 'info> {
+    reserve_info: &'a AccountInfo<'info>,
+    reward_mint_info: &'a AccountInfo<'info>,
+    reward_authority_info: &'a AccountInfo<'info>,
+    lending_market_info: &'a AccountInfo<'info>,
+    token_program_info: &'a AccountInfo<'info>,
 }
 
 /// Does all the checks of [check_and_unpack_pool_reward_accounts] and additionally:
@@ -71,21 +56,11 @@ fn reward_vault_authority_seeds<'keys>(
 /// * ✅ `lending_market_owner_info` matches `lending_market_info`
 fn check_and_unpack_pool_reward_accounts_for_admin_ixs<'a, 'info>(
     program_id: &Pubkey,
-    reserve_info: &'a AccountInfo<'info>,
-    reward_mint_info: &AccountInfo<'info>,
-    reward_authority_info: &AccountInfo<'info>,
-    lending_market_info: &AccountInfo<'info>,
+    bumps: Bumps,
+    accs: CheckAndUnpackPoolRewardAccounts<'a, 'info>,
     lending_market_owner_info: &AccountInfo<'info>,
-    token_program_info: &AccountInfo<'info>,
 ) -> Result<(LendingMarket, ReserveBorrow<'a, 'info>), ProgramError> {
-    let (lending_market, reserve) = check_and_unpack_pool_reward_accounts(
-        program_id,
-        reserve_info,
-        reward_mint_info,
-        reward_authority_info,
-        lending_market_info,
-        token_program_info,
-    )?;
+    let (lending_market, reserve) = check_and_unpack_pool_reward_accounts(program_id, bumps, accs)?;
 
     if lending_market.owner != *lending_market_owner_info.key {
         msg!("Lending market owner does not match the lending market owner provided");
@@ -111,11 +86,14 @@ fn check_and_unpack_pool_reward_accounts_for_admin_ixs<'a, 'info>(
 /// * ✅ `reward_mint_info` belongs to the token program
 fn check_and_unpack_pool_reward_accounts<'a, 'info>(
     program_id: &Pubkey,
-    reserve_info: &'a AccountInfo<'info>,
-    reward_mint_info: &AccountInfo<'info>,
-    reward_authority_info: &AccountInfo<'info>,
-    lending_market_info: &AccountInfo<'info>,
-    token_program_info: &AccountInfo<'info>,
+    bumps: Bumps,
+    CheckAndUnpackPoolRewardAccounts {
+        reserve_info,
+        reward_mint_info,
+        reward_authority_info,
+        lending_market_info,
+        token_program_info,
+    }: CheckAndUnpackPoolRewardAccounts<'a, 'info>,
 ) -> Result<(LendingMarket, ReserveBorrow<'a, 'info>), ProgramError> {
     let reserve = ReserveBorrow::new_mut(program_id, reserve_info)?;
 
@@ -140,12 +118,13 @@ fn check_and_unpack_pool_reward_accounts<'a, 'info>(
         return Err(LendingError::InvalidTokenOwner.into());
     }
 
-    let (expected_reward_vault_authority, _bump_seed) = reward_vault_authority(
+    let expected_reward_vault_authority = create_reward_vault_authority(
         program_id,
         lending_market_info.key,
         reserve_info.key,
         reward_mint_info.key,
-    );
+        bumps.reward_authority,
+    )?;
     if expected_reward_vault_authority != *reward_authority_info.key {
         msg!("Reward vault authority does not match the expected value");
         return Err(LendingError::InvalidAccountInput.into());
diff --git a/token-lending/program/src/processor/liquidity_mining/add_pool_reward.rs b/token-lending/program/src/processor/liquidity_mining/add_pool_reward.rs
index c0cc3fc3cc5..abcb03d6e45 100644
--- a/token-lending/program/src/processor/liquidity_mining/add_pool_reward.rs
+++ b/token-lending/program/src/processor/liquidity_mining/add_pool_reward.rs
@@ -1,6 +1,10 @@
 //! Adds a new pool reward to a reserve.
 //!
 //! Each pool reward has a unique vault that holds the reward tokens.
+//! This vault account must be created for the token program before calling this
+//! ix.
+//! In this ix we initialize the account as token account and transfer the
+//! reward tokens to it.
 
 use crate::processor::{
     assert_rent_exempt, spl_token_init_account, spl_token_transfer, TokenInitializeAccountParams,
@@ -19,7 +23,8 @@ use solana_program::{
 use solend_sdk::{error::LendingError, state::PositionKind};
 
 use super::{
-    check_and_unpack_pool_reward_accounts_for_admin_ixs, unpack_token_account, ReserveBorrow,
+    check_and_unpack_pool_reward_accounts_for_admin_ixs, unpack_token_account, Bumps,
+    CheckAndUnpackPoolRewardAccounts, ReserveBorrow,
 };
 
 /// Use [Self::from_unchecked_iter] to validate the accounts except for
@@ -71,6 +76,7 @@ struct AddPoolRewardAccounts<'a, 'info> {
 /// 2. Finds an empty slot in the [Reserve]'s LM reward vector and adds it there.
 pub(crate) fn process(
     program_id: &Pubkey,
+    reward_authority_bump: u8,
     position_kind: PositionKind,
     start_time_secs: u64,
     end_time_secs: u64,
@@ -81,8 +87,13 @@ pub(crate) fn process(
 
     let clock = &Clock::get()?;
 
-    let mut accounts =
-        AddPoolRewardAccounts::from_unchecked_iter(program_id, &mut accounts.iter())?;
+    let mut accounts = AddPoolRewardAccounts::from_unchecked_iter(
+        program_id,
+        Bumps {
+            reward_authority: reward_authority_bump,
+        },
+        &mut accounts.iter(),
+    )?;
 
     // 1.
 
@@ -124,6 +135,7 @@ pub(crate) fn process(
 impl<'a, 'info> AddPoolRewardAccounts<'a, 'info> {
     fn from_unchecked_iter(
         program_id: &Pubkey,
+        bumps: Bumps,
         iter: &mut impl Iterator<Item = &'a AccountInfo<'info>>,
     ) -> Result<AddPoolRewardAccounts<'a, 'info>, ProgramError> {
         let reserve_info = next_account_info(iter)?;
@@ -138,12 +150,15 @@ impl<'a, 'info> AddPoolRewardAccounts<'a, 'info> {
 
         let (_, reserve) = check_and_unpack_pool_reward_accounts_for_admin_ixs(
             program_id,
-            reserve_info,
-            reward_mint_info,
-            reward_authority_info,
-            lending_market_info,
+            bumps,
+            CheckAndUnpackPoolRewardAccounts {
+                reserve_info,
+                reward_mint_info,
+                reward_authority_info,
+                lending_market_info,
+                token_program_info,
+            },
             lending_market_owner_info,
-            token_program_info,
         )?;
 
         if reward_token_source_info.owner != token_program_info.key {
diff --git a/token-lending/program/src/processor/liquidity_mining/cancel_pool_reward.rs b/token-lending/program/src/processor/liquidity_mining/cancel_pool_reward.rs
index 1a28ed2020b..12677690448 100644
--- a/token-lending/program/src/processor/liquidity_mining/cancel_pool_reward.rs
+++ b/token-lending/program/src/processor/liquidity_mining/cancel_pool_reward.rs
@@ -1,3 +1,9 @@
+//! Cancel a pool reward.
+//!
+//! This ix sets the end time of the pool reward to now are returns any
+//! unallocated rewards to the admin.
+//! Users will still be able to claim rewards.
+
 use crate::processor::liquidity_mining::{
     check_and_unpack_pool_reward_accounts_for_admin_ixs, unpack_token_account,
 };
@@ -11,9 +17,10 @@ use solana_program::{
     program_error::ProgramError,
     pubkey::Pubkey,
 };
+use solend_sdk::instruction::reward_vault_authority_seeds;
 use solend_sdk::{error::LendingError, state::PositionKind};
 
-use super::{reward_vault_authority_seeds, ReserveBorrow};
+use super::{Bumps, CheckAndUnpackPoolRewardAccounts, ReserveBorrow};
 
 /// Use [Self::from_unchecked_iter] to validate the accounts.
 struct CancelPoolRewardAccounts<'a, 'info> {
@@ -51,12 +58,18 @@ struct CancelPoolRewardAccounts<'a, 'info> {
 /// 2. Transfers any unallocated rewards to the `reward_token_destination` account.
 pub(crate) fn process(
     program_id: &Pubkey,
+    reward_authority_bump: u8,
     position_kind: PositionKind,
     pool_reward_index: usize,
     accounts: &[AccountInfo],
 ) -> ProgramResult {
-    let mut accounts =
-        CancelPoolRewardAccounts::from_unchecked_iter(program_id, &mut accounts.iter())?;
+    let mut accounts = CancelPoolRewardAccounts::from_unchecked_iter(
+        program_id,
+        Bumps {
+            reward_authority: reward_authority_bump,
+        },
+        &mut accounts.iter(),
+    )?;
 
     // 1.
 
@@ -77,11 +90,16 @@ pub(crate) fn process(
         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.key(),
-            accounts.reward_mint_info.key,
-        ),
+        authority_signer_seeds: &[
+            reward_vault_authority_seeds(
+                accounts.lending_market_info.key,
+                &accounts.reserve.key(),
+                accounts.reward_mint_info.key,
+            )
+            .as_slice(),
+            &[&[reward_authority_bump]],
+        ]
+        .concat(),
         token_program: accounts.token_program_info.clone(),
     })?;
 
@@ -91,6 +109,7 @@ pub(crate) fn process(
 impl<'a, 'info> CancelPoolRewardAccounts<'a, 'info> {
     fn from_unchecked_iter(
         program_id: &Pubkey,
+        bump: Bumps,
         iter: &mut impl Iterator<Item = &'a AccountInfo<'info>>,
     ) -> Result<CancelPoolRewardAccounts<'a, 'info>, ProgramError> {
         let reserve_info = next_account_info(iter)?;
@@ -104,12 +123,15 @@ impl<'a, 'info> CancelPoolRewardAccounts<'a, 'info> {
 
         let (_, reserve) = check_and_unpack_pool_reward_accounts_for_admin_ixs(
             program_id,
-            reserve_info,
-            reward_mint_info,
-            reward_authority_info,
-            lending_market_info,
+            bump,
+            CheckAndUnpackPoolRewardAccounts {
+                reserve_info,
+                reward_mint_info,
+                reward_authority_info,
+                lending_market_info,
+                token_program_info,
+            },
             lending_market_owner_info,
-            token_program_info,
         )?;
 
         if reward_token_destination_info.owner != token_program_info.key {
diff --git a/token-lending/program/src/processor/liquidity_mining/claim_user_reward.rs b/token-lending/program/src/processor/liquidity_mining/claim_user_reward.rs
index 9564a90caad..49d079c2287 100644
--- a/token-lending/program/src/processor/liquidity_mining/claim_user_reward.rs
+++ b/token-lending/program/src/processor/liquidity_mining/claim_user_reward.rs
@@ -19,11 +19,12 @@ use solana_program::{
     pubkey::Pubkey,
     sysvar::Sysvar,
 };
-use solend_sdk::error::LendingError;
 use solend_sdk::state::{Obligation, PositionKind};
+use solend_sdk::{error::LendingError, instruction::reward_vault_authority_seeds};
 
 use super::{
-    check_and_unpack_pool_reward_accounts, reward_vault_authority_seeds, unpack_token_account,
+    check_and_unpack_pool_reward_accounts, unpack_token_account, Bumps,
+    CheckAndUnpackPoolRewardAccounts,
 };
 
 /// Use [Self::from_unchecked_iter] to validate the accounts.
@@ -70,10 +71,20 @@ struct ClaimUserReward<'a, 'info> {
 ///    Eligible rewards are those that match the vault and user has earned any.
 /// 3. Transfers the withdrawn rewards to the user's token account.
 /// 4. Packs all changes into account buffers for [Obligation] and [Reserve].
-pub(crate) fn process(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
+pub(crate) fn process(
+    program_id: &Pubkey,
+    reward_authority_bump: u8,
+    accounts: &[AccountInfo],
+) -> ProgramResult {
     let clock = &Clock::get()?;
 
-    let mut accounts = ClaimUserReward::from_unchecked_iter(program_id, &mut accounts.iter())?;
+    let mut accounts = ClaimUserReward::from_unchecked_iter(
+        program_id,
+        Bumps {
+            reward_authority: reward_authority_bump,
+        },
+        &mut accounts.iter(),
+    )?;
     let reserve_key = accounts.reserve.key();
 
     // 1.
@@ -149,11 +160,16 @@ pub(crate) fn process(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramR
         destination: accounts.obligation_owner_token_account_info.clone(),
         amount: total_reward_amount,
         authority: accounts.reward_authority_info.clone(),
-        authority_signer_seeds: &reward_vault_authority_seeds(
-            accounts.lending_market_info.key,
-            &reserve_key,
-            accounts.reward_mint_info.key,
-        ),
+        authority_signer_seeds: &[
+            reward_vault_authority_seeds(
+                accounts.lending_market_info.key,
+                &accounts.reserve.key(),
+                accounts.reward_mint_info.key,
+            )
+            .as_slice(),
+            &[&[reward_authority_bump]],
+        ]
+        .concat(),
         token_program: accounts.token_program_info.clone(),
     })?;
 
@@ -173,6 +189,7 @@ pub(crate) fn process(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramR
 impl<'a, 'info> ClaimUserReward<'a, 'info> {
     fn from_unchecked_iter(
         program_id: &Pubkey,
+        bumps: Bumps,
         iter: &mut impl Iterator<Item = &'a AccountInfo<'info>>,
     ) -> Result<ClaimUserReward<'a, 'info>, ProgramError> {
         let obligation_info = next_account_info(iter)?;
@@ -186,11 +203,14 @@ impl<'a, 'info> ClaimUserReward<'a, 'info> {
 
         let (_, reserve) = check_and_unpack_pool_reward_accounts(
             program_id,
-            reserve_info,
-            reward_mint_info,
-            reward_authority_info,
-            lending_market_info,
-            token_program_info,
+            bumps,
+            CheckAndUnpackPoolRewardAccounts {
+                reserve_info,
+                reward_mint_info,
+                reward_authority_info,
+                lending_market_info,
+                token_program_info,
+            },
         )?;
 
         if obligation_info.owner != program_id {
diff --git a/token-lending/program/src/processor/liquidity_mining/close_pool_reward.rs b/token-lending/program/src/processor/liquidity_mining/close_pool_reward.rs
index a0b29a15afb..cb0309e58ff 100644
--- a/token-lending/program/src/processor/liquidity_mining/close_pool_reward.rs
+++ b/token-lending/program/src/processor/liquidity_mining/close_pool_reward.rs
@@ -12,7 +12,9 @@ use solana_program::{
     program_error::ProgramError,
     pubkey::Pubkey,
 };
-use solend_sdk::{error::LendingError, state::PositionKind};
+use solend_sdk::{
+    error::LendingError, instruction::reward_vault_authority_seeds, state::PositionKind,
+};
 use spl_token::state::Account as TokenAccount;
 
 use crate::processor::{
@@ -20,8 +22,8 @@ use crate::processor::{
 };
 
 use super::{
-    check_and_unpack_pool_reward_accounts_for_admin_ixs, reward_vault_authority_seeds,
-    unpack_token_account, ReserveBorrow,
+    check_and_unpack_pool_reward_accounts_for_admin_ixs, unpack_token_account, Bumps,
+    CheckAndUnpackPoolRewardAccounts, ReserveBorrow,
 };
 
 /// Use [Self::from_unchecked_iter] to validate the accounts.
@@ -64,12 +66,18 @@ struct ClosePoolRewardAccounts<'a, 'info> {
 /// 3. Closes reward vault token account.
 pub(crate) fn process(
     program_id: &Pubkey,
+    reward_authority_bump: u8,
     position_kind: PositionKind,
     pool_reward_index: usize,
     accounts: &[AccountInfo],
 ) -> ProgramResult {
-    let mut accounts =
-        ClosePoolRewardAccounts::from_unchecked_iter(program_id, &mut accounts.iter())?;
+    let mut accounts = ClosePoolRewardAccounts::from_unchecked_iter(
+        program_id,
+        Bumps {
+            reward_authority: reward_authority_bump,
+        },
+        &mut accounts.iter(),
+    )?;
 
     // 1.
 
@@ -84,30 +92,43 @@ pub(crate) fn process(
 
     // 2.
 
+    let bump_seed = [reward_authority_bump];
+    let signer_seeds = [
+        reward_vault_authority_seeds(
+            accounts.lending_market_info.key,
+            accounts.reserve_info.key,
+            accounts.reward_mint_info.key,
+        )
+        .as_slice(),
+        &[&bump_seed],
+    ]
+    .concat();
+
+    msg!(
+        "Transferring {} reward tokens to {}",
+        accounts.reward_token_vault.amount,
+        accounts.reward_token_destination_info.key
+    );
     spl_token_transfer(TokenTransferParams {
         source: accounts.reward_token_vault_info.clone(),
         destination: accounts.reward_token_destination_info.clone(),
         amount: accounts.reward_token_vault.amount,
         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,
-        ),
+        authority_signer_seeds: &signer_seeds,
         token_program: accounts.token_program_info.clone(),
     })?;
 
     // 3.
 
+    msg!(
+        "Closing reward token vault {}",
+        accounts.reward_token_vault_info.key
+    );
     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,
-        ),
+        authority_signer_seeds: &signer_seeds,
         token_program: accounts.token_program_info.clone(),
     })?;
 
@@ -117,6 +138,7 @@ pub(crate) fn process(
 impl<'a, 'info> ClosePoolRewardAccounts<'a, 'info> {
     fn from_unchecked_iter(
         program_id: &Pubkey,
+        bumps: Bumps,
         iter: &mut impl Iterator<Item = &'a AccountInfo<'info>>,
     ) -> Result<ClosePoolRewardAccounts<'a, 'info>, ProgramError> {
         let reserve_info = next_account_info(iter)?;
@@ -130,12 +152,15 @@ impl<'a, 'info> ClosePoolRewardAccounts<'a, 'info> {
 
         let (_, reserve) = check_and_unpack_pool_reward_accounts_for_admin_ixs(
             program_id,
-            reserve_info,
-            reward_mint_info,
-            reward_authority_info,
-            lending_market_info,
+            bumps,
+            CheckAndUnpackPoolRewardAccounts {
+                reserve_info,
+                reward_mint_info,
+                reward_authority_info,
+                lending_market_info,
+                token_program_info,
+            },
             lending_market_owner_info,
-            token_program_info,
         )?;
 
         if reward_token_destination_info.owner != token_program_info.key {
diff --git a/token-lending/program/tests/add_pool_reward.rs b/token-lending/program/tests/add_pool_reward.rs
index d2408ccdbc5..3e9f6f8351b 100644
--- a/token-lending/program/tests/add_pool_reward.rs
+++ b/token-lending/program/tests/add_pool_reward.rs
@@ -15,16 +15,16 @@ use solend_program::{
 use solend_sdk::state::{PoolReward, PoolRewardSlot, UserReward};
 
 #[tokio::test]
-async fn test_success_for_deposit() {
-    test_success(PositionKind::Deposit).await;
+async fn test_add_pool_reward_for_deposit() {
+    test_(PositionKind::Deposit).await;
 }
 
 #[tokio::test]
-async fn test_success_for_borrow() {
-    test_success(PositionKind::Borrow).await;
+async fn test_add_pool_reward_for_borrow() {
+    test_(PositionKind::Borrow).await;
 }
 
-async fn test_success(position_kind: PositionKind) {
+async fn test_(position_kind: PositionKind) {
     let (mut test, lending_market, usdc_reserve, wsol_reserve, mut lending_market_owner, user) =
         setup_world(&test_reserve_config(), &test_reserve_config()).await;
 
@@ -62,7 +62,7 @@ async fn test_success(position_kind: PositionKind) {
         total_shares: 0,
         last_update_time_secs: current_time as _,
         pool_rewards: {
-            let mut og = usdc_reserve_post
+            let mut og = usdc_reserve
                 .account
                 .deposits_pool_reward_manager
                 .pool_rewards
diff --git a/token-lending/program/tests/cancel_pool_reward.rs b/token-lending/program/tests/cancel_pool_reward.rs
new file mode 100644
index 00000000000..7bd5b1caca3
--- /dev/null
+++ b/token-lending/program/tests/cancel_pool_reward.rs
@@ -0,0 +1,145 @@
+#![cfg(feature = "test-bpf")]
+
+mod helpers;
+
+use std::collections::HashSet;
+
+use helpers::solend_program_test::{
+    setup_world, BalanceChecker, LiqMiningReward, TokenAccount, TokenBalanceChange,
+};
+use helpers::test_reserve_config;
+
+use pretty_assertions::assert_eq;
+use solana_program_test::*;
+use solana_sdk::signature::{Keypair, Signer};
+use solend_program::{
+    math::Decimal,
+    state::{PoolRewardId, PoolRewardManager, PositionKind, Reserve},
+};
+use solend_sdk::state::{PoolReward, PoolRewardSlot};
+
+#[tokio::test]
+async fn test_cancel_pool_reward_for_deposit() {
+    test_(PositionKind::Deposit).await;
+}
+
+#[tokio::test]
+async fn test_cancel_pool_reward_for_borrow() {
+    test_(PositionKind::Borrow).await;
+}
+
+async fn test_(position_kind: PositionKind) {
+    let (mut test, lending_market, usdc_reserve, _, mut lending_market_owner, _) =
+        setup_world(&test_reserve_config(), &test_reserve_config()).await;
+    let mut clock = test.get_clock().await;
+
+    let reward_mint = test.create_mint_as_test_authority().await;
+    let reward_vault = Keypair::new();
+    let duration_secs = 3_600;
+    let total_rewards = 1_000_000;
+    let initial_time = clock.unix_timestamp as u64;
+    let reward = LiqMiningReward {
+        mint: reward_mint,
+        vault: reward_vault.insecure_clone(),
+    };
+
+    lending_market
+        .add_pool_reward(
+            &mut test,
+            &usdc_reserve,
+            &mut lending_market_owner,
+            &reward,
+            position_kind,
+            initial_time,
+            initial_time + duration_secs as u64,
+            total_rewards,
+        )
+        .await
+        .expect("Should add pool reward");
+
+    let balance_checker = BalanceChecker::start(
+        &mut test,
+        &[&TokenAccount(reward.vault.pubkey()), &lending_market_owner],
+    )
+    .await;
+
+    clock.unix_timestamp += duration_secs as i64 / 2;
+    test.context.set_sysvar(&clock);
+    let time_when_cancelling = clock.unix_timestamp as u64;
+
+    let pool_reward_index = 0;
+    lending_market
+        .cancel_pool_reward(
+            &mut test,
+            &usdc_reserve,
+            &mut lending_market_owner,
+            &reward,
+            position_kind,
+            pool_reward_index,
+        )
+        .await
+        .expect("Should cancel pool reward");
+
+    let (balance_changes, _) = balance_checker.find_balance_changes(&mut test).await;
+
+    let expected_balance_changes = HashSet::from([
+        TokenBalanceChange {
+            token_account: reward.vault.pubkey(),
+            mint: reward.mint,
+            diff: -(total_rewards as i128) / 2,
+        },
+        TokenBalanceChange {
+            token_account: lending_market_owner.get_account(&reward.mint).unwrap(),
+            mint: reward.mint,
+            diff: (total_rewards as i128) / 2,
+        },
+    ]);
+    assert_eq!(balance_changes, expected_balance_changes);
+
+    let usdc_reserve_post = test.load_account::<Reserve>(usdc_reserve.pubkey).await;
+
+    let expected_reward_manager = Box::new(PoolRewardManager {
+        total_shares: 0,
+        last_update_time_secs: time_when_cancelling as _,
+        pool_rewards: {
+            let mut og = usdc_reserve
+                .account
+                .deposits_pool_reward_manager
+                .pool_rewards
+                .clone();
+
+            og[0] = PoolRewardSlot::Occupied(Box::new(PoolReward {
+                id: PoolRewardId(1),
+                vault: reward_vault.pubkey(),
+                start_time_secs: initial_time,
+                duration_secs: duration_secs / 2,
+                total_rewards,
+                num_user_reward_managers: 0,
+                cumulative_rewards_per_share: Decimal::zero(),
+            }));
+
+            og
+        },
+    });
+
+    match position_kind {
+        PositionKind::Deposit => {
+            assert_eq!(
+                usdc_reserve_post.account,
+                Reserve {
+                    deposits_pool_reward_manager: expected_reward_manager,
+                    ..usdc_reserve.clone().account
+                }
+            );
+        }
+        PositionKind::Borrow => {
+            assert_eq!(
+                usdc_reserve_post.account,
+                Reserve {
+                    borrows_pool_reward_manager: expected_reward_manager,
+                    ..usdc_reserve.clone().account
+                }
+            );
+        }
+    }
+}
diff --git a/token-lending/program/tests/close_pool_reward.rs b/token-lending/program/tests/close_pool_reward.rs
new file mode 100644
index 00000000000..155af89836a
--- /dev/null
+++ b/token-lending/program/tests/close_pool_reward.rs
@@ -0,0 +1,126 @@
+#![cfg(feature = "test-bpf")]
+
+mod helpers;
+
+use std::collections::HashSet;
+
+use helpers::solend_program_test::{
+    setup_world, BalanceChecker, LiqMiningReward, TokenBalanceChange,
+};
+use helpers::test_reserve_config;
+
+use pretty_assertions::assert_eq;
+use solana_program_test::*;
+use solana_sdk::signature::Keypair;
+use solend_program::state::{PoolRewardId, PoolRewardManager, PositionKind, Reserve};
+use solend_sdk::state::PoolRewardSlot;
+
+#[tokio::test]
+async fn test_close_pool_reward_for_deposit() {
+    test_(PositionKind::Deposit).await;
+}
+
+#[tokio::test]
+async fn test_close_pool_reward_for_borrow() {
+    test_(PositionKind::Borrow).await;
+}
+
+async fn test_(position_kind: PositionKind) {
+    let (mut test, lending_market, usdc_reserve, _, mut lending_market_owner, _) =
+        setup_world(&test_reserve_config(), &test_reserve_config()).await;
+    let mut clock = test.get_clock().await;
+
+    let reward_mint = test.create_mint_as_test_authority().await;
+    let reward_vault = Keypair::new();
+    let duration_secs = 3_600;
+    let total_rewards = 1_000_000;
+    let initial_time = clock.unix_timestamp as u64;
+    let reward = LiqMiningReward {
+        mint: reward_mint,
+        vault: reward_vault.insecure_clone(),
+    };
+
+    lending_market
+        .add_pool_reward(
+            &mut test,
+            &usdc_reserve,
+            &mut lending_market_owner,
+            &reward,
+            position_kind,
+            initial_time,
+            initial_time + duration_secs as u64,
+            total_rewards,
+        )
+        .await
+        .expect("Should add pool reward");
+
+    let balance_checker = BalanceChecker::start(&mut test, &[&lending_market_owner]).await;
+
+    // doesn't matter when we close as long as there are no obligations
+    clock.unix_timestamp += 1;
+    test.context.set_sysvar(&clock);
+
+    let pool_reward_index = 0;
+    lending_market
+        .close_pool_reward(
+            &mut test,
+            &usdc_reserve,
+            &mut lending_market_owner,
+            &reward,
+            position_kind,
+            pool_reward_index,
+        )
+        .await
+        .expect("Should close pool reward");
+
+    let (balance_changes, _) = balance_checker.find_balance_changes(&mut test).await;
+
+    let expected_balance_changes = HashSet::from([TokenBalanceChange {
+        token_account: lending_market_owner.get_account(&reward.mint).unwrap(),
+        mint: reward.mint,
+        diff: total_rewards as _,
+    }]);
+    assert_eq!(balance_changes, expected_balance_changes);
+
+    let usdc_reserve_post = test.load_account::<Reserve>(usdc_reserve.pubkey).await;
+
+    let expected_reward_manager = Box::new(PoolRewardManager {
+        total_shares: 0,
+        last_update_time_secs: initial_time as _,
+        pool_rewards: {
+            let mut og = usdc_reserve
+                .account
+                .deposits_pool_reward_manager
+                .pool_rewards
+                .clone();
+
+            og[0] = PoolRewardSlot::Vacant {
+                last_pool_reward_id: PoolRewardId(1),
+                has_been_just_vacated: false,
+            };
+
+            og
+        },
+    });
+
+    match position_kind {
+        PositionKind::Deposit => {
+            assert_eq!(
+                usdc_reserve_post.account,
+                Reserve {
+                    deposits_pool_reward_manager: expected_reward_manager,
+                    ..usdc_reserve.clone().account
+                }
+            );
+        }
+        PositionKind::Borrow => {
+            assert_eq!(
+                usdc_reserve_post.account,
+                Reserve {
+                    borrows_pool_reward_manager: expected_reward_manager,
+                    ..usdc_reserve.clone().account
+                }
+            );
+        }
+    }
+}
diff --git a/token-lending/program/tests/helpers/solend_program_test.rs b/token-lending/program/tests/helpers/solend_program_test.rs
index 95728089c6b..d50e4ea6e51 100644
--- a/token-lending/program/tests/helpers/solend_program_test.rs
+++ b/token-lending/program/tests/helpers/solend_program_test.rs
@@ -75,6 +75,8 @@ mod cu_budgets {
     pub(super) const DEPOSIT_RESERVE_LIQUIDITY_AND_OBLIGATION_COLLATERAL: u32 = 130_015;
     pub(super) const REDEEM: u32 = 90_016;
     pub(super) const ADD_POOL_REWARD: u32 = 80_017;
+    pub(super) const CANCEL_POOL_REWARD: u32 = 80_018;
+    pub(super) const CLOSE_POOL_REWARD: u32 = 80_019;
 }
 
 /// This is at most how many bytes can an obligation grow.
@@ -919,17 +921,26 @@ impl Info<LendingMarket> {
         &self,
         test: &mut SolendProgramTest,
         reserve: &Info<Reserve>,
-        user: &mut User,
+        lending_market_owner: &mut User,
         reward: &LiqMiningReward,
         position_kind: PositionKind,
         start_time_secs: u64,
         end_time_secs: u64,
         reward_amount: u64,
     ) -> Result<(), BanksClientError> {
-        let token_account = user.create_token_account(&reward.mint, test).await;
+        let token_account = lending_market_owner
+            .create_token_account(&reward.mint, test)
+            .await;
         test.mint_to(&reward.mint, &token_account.pubkey, reward_amount)
             .await;
 
+        let (reward_authority_pda, reward_authority_bump) = find_reward_vault_authority(
+            &solend_program::id(),
+            &self.pubkey,
+            &reserve.pubkey,
+            &reward.mint,
+        );
+
         let instructions = [
             ComputeBudgetInstruction::set_compute_unit_limit(cu_budgets::ADD_POOL_REWARD),
             system_instruction::create_account(
@@ -941,6 +952,7 @@ impl Info<LendingMarket> {
             ),
             add_pool_reward(
                 solend_program::id(),
+                reward_authority_bump,
                 position_kind,
                 start_time_secs,
                 end_time_secs,
@@ -948,20 +960,91 @@ impl Info<LendingMarket> {
                 reserve.pubkey,
                 reward.mint,
                 token_account.pubkey,
-                find_reward_vault_authority(
-                    &solend_program::id(),
-                    &self.pubkey,
-                    &reserve.pubkey,
-                    &reward.mint,
-                )
-                .0,
+                reward_authority_pda,
                 reward.vault.pubkey(),
                 self.pubkey,
-                user.keypair.pubkey(),
+                lending_market_owner.keypair.pubkey(),
             ),
         ];
 
-        test.process_transaction(&instructions, Some(&[&user.keypair, &reward.vault]))
+        test.process_transaction(
+            &instructions,
+            Some(&[&lending_market_owner.keypair, &reward.vault]),
+        )
+        .await
+    }
+
+    pub async fn cancel_pool_reward(
+        &self,
+        test: &mut SolendProgramTest,
+        reserve: &Info<Reserve>,
+        lending_market_owner: &mut User,
+        reward: &LiqMiningReward,
+        position_kind: PositionKind,
+        pool_reward_index: u64,
+    ) -> Result<(), BanksClientError> {
+        let (reward_authority_pda, reward_authority_bump) = find_reward_vault_authority(
+            &solend_program::id(),
+            &self.pubkey,
+            &reserve.pubkey,
+            &reward.mint,
+        );
+
+        let instructions = [
+            ComputeBudgetInstruction::set_compute_unit_limit(cu_budgets::CANCEL_POOL_REWARD),
+            cancel_pool_reward(
+                solend_program::id(),
+                reward_authority_bump,
+                position_kind,
+                pool_reward_index,
+                reserve.pubkey,
+                reward.mint,
+                lending_market_owner.get_account(&reward.mint).unwrap(),
+                reward_authority_pda,
+                reward.vault.pubkey(),
+                self.pubkey,
+                lending_market_owner.keypair.pubkey(),
+            ),
+        ];
+
+        test.process_transaction(&instructions, Some(&[&lending_market_owner.keypair]))
+            .await
+    }
+
+    pub async fn close_pool_reward(
+        &self,
+        test: &mut SolendProgramTest,
+        reserve: &Info<Reserve>,
+        lending_market_owner: &mut User,
+        reward: &LiqMiningReward,
+        position_kind: PositionKind,
+        pool_reward_index: u64,
+    ) -> Result<(), BanksClientError> {
+        let (reward_authority_pda, reward_authority_bump) = find_reward_vault_authority(
+            &solend_program::id(),
+            &self.pubkey,
+            &reserve.pubkey,
+            &reward.mint,
+        );
+
+        let instructions = [
+            ComputeBudgetInstruction::set_compute_unit_limit(cu_budgets::CLOSE_POOL_REWARD),
+            close_pool_reward(
+                solend_program::id(),
+                reward_authority_bump,
+                position_kind,
+                pool_reward_index,
+                reserve.pubkey,
+                reward.mint,
+                lending_market_owner.get_account(&reward.mint).unwrap(),
+                reward_authority_pda,
+                reward.vault.pubkey(),
+                self.pubkey,
+                lending_market_owner.keypair.pubkey(),
+            ),
+        ];
+
+        test.process_transaction(&instructions, Some(&[&lending_market_owner.keypair]))
             .await
     }
 
diff --git a/token-lending/sdk/src/instruction.rs b/token-lending/sdk/src/instruction.rs
index 4a69d5d82b2..5b3e9d034ec 100644
--- a/token-lending/sdk/src/instruction.rs
+++ b/token-lending/sdk/src/instruction.rs
@@ -550,6 +550,8 @@ pub enum LendingInstruction {
     ///    `[]` Rent sysvar.
     ///    `[]` Token program.
     AddPoolReward {
+        /// The bump seed of the reward authority.
+        reward_authority_bump: u8,
         /// Whether this reward applies to deposits or borrows
         position_kind: PositionKind,
         /// If in the past according to the Clock sysvar then started immediately.
@@ -580,10 +582,12 @@ pub enum LendingInstruction {
     ///    `[signer]` Lending market owner.
     ///    `[]` Token program.
     ClosePoolReward {
+        /// The bump seed of the reward authority.
+        reward_authority_bump: u8,
         /// Whether this reward applies to deposits or borrows
         position_kind: PositionKind,
         /// Identifies a reward within a reserve's deposits/borrows rewards.
-        pool_reward_index: usize,
+        pool_reward_index: u64,
     },
 
     // 27
@@ -606,10 +610,12 @@ pub enum LendingInstruction {
     ///    `[signer]` Lending market owner.
     ///    `[]` Token program.
     CancelPoolReward {
+        /// The bump seed of the reward authority.
+        reward_authority_bump: u8,
         /// Whether this reward applies to deposits or borrows
         position_kind: PositionKind,
         /// Identifies a reward within a reserve's deposits/borrows rewards.
-        pool_reward_index: usize,
+        pool_reward_index: u64,
     },
 
     /// 28
@@ -629,7 +635,10 @@ pub enum LendingInstruction {
     ///   `[writable]` Reward vault token account.
     ///   `[]` Lending market account.
     ///   `[]` Token program.
-    ClaimReward,
+    ClaimReward {
+        /// The bump seed of the reward authority.
+        reward_authority_bump: u8,
+    },
 
     // 255
     /// UpgradeReserveToV2_1_0
@@ -901,11 +910,13 @@ impl LendingInstruction {
                 Self::DonateToReserve { liquidity_amount }
             }
             25 => {
+                let (reward_authority_bump, rest) = Self::unpack_u8(rest)?;
                 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)?;
                 Self::AddPoolReward {
+                    reward_authority_bump,
                     position_kind,
                     start_time_secs,
                     end_time_secs,
@@ -913,22 +924,31 @@ impl LendingInstruction {
                 }
             }
             26 => {
+                let (reward_authority_bump, rest) = Self::unpack_u8(rest)?;
                 let (position_kind, rest) = Self::unpack_try_from_u8(rest)?;
                 let (pool_reward_index, _rest) = Self::unpack_u64(rest)?;
                 Self::ClosePoolReward {
+                    reward_authority_bump,
                     position_kind,
                     pool_reward_index: pool_reward_index as _,
                 }
             }
             27 => {
+                let (reward_authority_bump, rest) = Self::unpack_u8(rest)?;
                 let (position_kind, rest) = Self::unpack_try_from_u8(rest)?;
                 let (pool_reward_index, _rest) = Self::unpack_u64(rest)?;
                 Self::CancelPoolReward {
+                    reward_authority_bump,
                     position_kind,
                     pool_reward_index: pool_reward_index as _,
                 }
             }
-            28 => Self::ClaimReward,
+            28 => {
+                let (reward_authority_bump, _rest) = Self::unpack_u8(rest)?;
+                Self::ClaimReward {
+                    reward_authority_bump,
+                }
+            }
             255 => Self::UpgradeReserveToV2_1_0,
             _ => {
                 msg!("Instruction cannot be unpacked");
@@ -1239,35 +1259,44 @@ impl LendingInstruction {
                 buf.extend_from_slice(&liquidity_amount.to_le_bytes());
             }
             Self::AddPoolReward {
+                reward_authority_bump,
                 position_kind,
                 start_time_secs,
                 end_time_secs,
                 token_amount,
             } => {
                 buf.push(25);
+                buf.extend_from_slice(&reward_authority_bump.to_le_bytes());
                 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 {
+                reward_authority_bump,
                 position_kind,
                 pool_reward_index,
             } => {
                 buf.push(26);
+                buf.extend_from_slice(&reward_authority_bump.to_le_bytes());
                 buf.extend_from_slice(&(position_kind as u8).to_le_bytes());
                 buf.extend_from_slice(&pool_reward_index.to_le_bytes());
             }
             Self::CancelPoolReward {
+                reward_authority_bump,
                 position_kind,
                 pool_reward_index,
             } => {
                 buf.push(27);
+                buf.extend_from_slice(&reward_authority_bump.to_le_bytes());
                 buf.extend_from_slice(&(position_kind as u8).to_le_bytes());
                 buf.extend_from_slice(&pool_reward_index.to_le_bytes());
             }
-            Self::ClaimReward => {
+            Self::ClaimReward {
+                reward_authority_bump,
+            } => {
                 buf.push(28);
+                buf.extend_from_slice(&reward_authority_bump.to_le_bytes());
             }
             Self::UpgradeReserveToV2_1_0 => {
                 buf.push(255);
@@ -2105,32 +2134,34 @@ pub fn upgrade_reserve_to_v2_1_0(
 #[allow(clippy::too_many_arguments)]
 pub fn add_pool_reward(
     program_id: Pubkey,
+    reward_authority_bump: u8,
     position_kind: PositionKind,
     start_time_secs: u64,
     end_time_secs: u64,
     token_amount: u64,
-    reserve_pubkey: Pubkey,
-    reward_mint_pubkey: Pubkey,
-    source_reward_token_account_pubkey: Pubkey,
-    reward_vault_authority_pubkey: Pubkey,
-    reward_vault_pubkey: Pubkey,
-    lending_market_pubkey: Pubkey,
-    lending_market_owner_pubkey: Pubkey,
+    reserve: Pubkey,
+    reward_mint: Pubkey,
+    source_reward_token_account: Pubkey,
+    reward_vault_authority: Pubkey,
+    reward_vault: Pubkey,
+    lending_market: Pubkey,
+    lending_market_owner: Pubkey,
 ) -> Instruction {
     Instruction {
         program_id,
         accounts: vec![
-            AccountMeta::new(reserve_pubkey, false),
-            AccountMeta::new(reward_mint_pubkey, false),
-            AccountMeta::new(source_reward_token_account_pubkey, false),
-            AccountMeta::new(reward_vault_authority_pubkey, false),
-            AccountMeta::new(reward_vault_pubkey, false),
-            AccountMeta::new_readonly(lending_market_pubkey, false),
-            AccountMeta::new_readonly(lending_market_owner_pubkey, true),
+            AccountMeta::new(reserve, false),
+            AccountMeta::new_readonly(reward_mint, false),
+            AccountMeta::new(source_reward_token_account, false),
+            AccountMeta::new_readonly(reward_vault_authority, false),
+            AccountMeta::new(reward_vault, false),
+            AccountMeta::new_readonly(lending_market, false),
+            AccountMeta::new_readonly(lending_market_owner, true),
             AccountMeta::new_readonly(sysvar::rent::id(), false),
             AccountMeta::new_readonly(spl_token::id(), false),
         ],
         data: LendingInstruction::AddPoolReward {
+            reward_authority_bump,
             position_kind,
             start_time_secs,
             end_time_secs,
@@ -2140,6 +2171,78 @@ pub fn add_pool_reward(
     }
 }
 
+/// Creates a `CancelPoolReward` instruction
+#[allow(clippy::too_many_arguments)]
+pub fn cancel_pool_reward(
+    program_id: Pubkey,
+    reward_authority_bump: u8,
+    position_kind: PositionKind,
+    pool_reward_index: u64,
+    reserve: Pubkey,
+    reward_mint: Pubkey,
+    destination_reward_token_account: Pubkey,
+    reward_vault_authority: Pubkey,
+    reward_vault: Pubkey,
+    lending_market: Pubkey,
+    lending_market_owner: Pubkey,
+) -> Instruction {
+    Instruction {
+        program_id,
+        accounts: vec![
+            AccountMeta::new(reserve, false),
+            AccountMeta::new_readonly(reward_mint, false),
+            AccountMeta::new(destination_reward_token_account, false),
+            AccountMeta::new_readonly(reward_vault_authority, false),
+            AccountMeta::new(reward_vault, false),
+            AccountMeta::new_readonly(lending_market, false),
+            AccountMeta::new_readonly(lending_market_owner, true),
+            AccountMeta::new_readonly(spl_token::id(), false),
+        ],
+        data: LendingInstruction::CancelPoolReward {
+            reward_authority_bump,
+            position_kind,
+            pool_reward_index,
+        }
+        .pack(),
+    }
+}
+
+/// Creates a `ClosePoolReward` instruction
+#[allow(clippy::too_many_arguments)]
+pub fn close_pool_reward(
+    program_id: Pubkey,
+    reward_authority_bump: u8,
+    position_kind: PositionKind,
+    pool_reward_index: u64,
+    reserve: Pubkey,
+    reward_mint: Pubkey,
+    destination_reward_token_account: Pubkey,
+    reward_vault_authority: Pubkey,
+    reward_vault: Pubkey,
+    lending_market: Pubkey,
+    lending_market_owner: Pubkey,
+) -> Instruction {
+    Instruction {
+        program_id,
+        accounts: vec![
+            AccountMeta::new(reserve, false),
+            AccountMeta::new_readonly(reward_mint, false),
+            AccountMeta::new(destination_reward_token_account, false),
+            AccountMeta::new_readonly(reward_vault_authority, false),
+            AccountMeta::new(reward_vault, false),
+            AccountMeta::new_readonly(lending_market, false),
+            AccountMeta::new(lending_market_owner, true),
+            AccountMeta::new_readonly(spl_token::id(), false),
+        ],
+        data: LendingInstruction::ClosePoolReward {
+            reward_authority_bump,
+            position_kind,
+            pool_reward_index,
+        }
+        .pack(),
+    }
+}
+
 /// Derives the reward vault authority PDA address.
 pub fn find_reward_vault_authority(
     program_id: &Pubkey,
diff --git a/token-lending/sdk/src/state/liquidity_mining.rs b/token-lending/sdk/src/state/liquidity_mining.rs
index 4698b9a62cf..fe561e78d91 100644
--- a/token-lending/sdk/src/state/liquidity_mining.rs
+++ b/token-lending/sdk/src/state/liquidity_mining.rs
@@ -6,8 +6,16 @@ pub mod user_reward_manager;
 pub use pool_reward_manager::*;
 pub use user_reward_manager::*;
 
-/// Determines the size of [PoolRewardManager]
-pub const MAX_REWARDS: usize = 50;
+/// Determines the size of [PoolRewardManager].
+///
+/// On Suilend this is 50.
+/// However, Sui dynamic object model let's us store more data easily.
+/// In Save we're storing the data on the reserve and this means packing and
+/// unpacking it frequently which negatively impacts CU limits.
+///
+/// In Save, if we want to add new rewards we will crank old ones to make space
+/// in the reserve.
+pub const MAX_REWARDS: usize = 30;
 
 /// Cannot create a reward shorter than this.
 pub const MIN_REWARD_PERIOD_SECS: u64 = 3_600;

From d361638bc5386cb0286f44b88cc9d073778098e4 Mon Sep 17 00:00:00 2001
From: vanity mnm <vanity@solend.fi>
Date: Thu, 10 Apr 2025 16:33:41 +0200
Subject: [PATCH 12/14] [Liquidity Mining] BPF tests for claiming reward (11) 
 (#210)

* Adds tests for claiming a reward ix

* Fixing clippy complaint
---
 token-lending/program/src/processor.rs        |   2 +
 .../program/src/processor/liquidity_mining.rs |   4 +-
 .../liquidity_mining/claim_user_reward.rs     | 119 ++--
 .../program/tests/add_pool_reward.rs          |  14 +-
 .../program/tests/cancel_pool_reward.rs       |  17 +-
 .../program/tests/claim_pool_reward.rs        | 526 ++++++++++++++++++
 .../program/tests/close_pool_reward.rs        |  12 +-
 .../tests/helpers/solend_program_test.rs      |  46 ++
 token-lending/sdk/src/instruction.rs          |  59 +-
 .../sdk/src/state/liquidity_mining.rs         |   2 -
 .../liquidity_mining/pool_reward_manager.rs   |   1 -
 token-lending/sdk/src/state/obligation.rs     |  28 +-
 token-lending/sdk/src/state/reserve.rs        |   8 +
 token-lending/tests/liquidity-mining.ts       |   5 +-
 14 files changed, 734 insertions(+), 109 deletions(-)
 create mode 100644 token-lending/program/tests/claim_pool_reward.rs

diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs
index 93c6bbf3734..8390619dca1 100644
--- a/token-lending/program/src/processor.rs
+++ b/token-lending/program/src/processor.rs
@@ -255,11 +255,13 @@ pub fn process_instruction(
         }
         LendingInstruction::ClaimReward {
             reward_authority_bump,
+            position_kind,
         } => {
             msg!("Instruction: Claim Reward");
             liquidity_mining::claim_user_reward::process(
                 program_id,
                 reward_authority_bump,
+                position_kind,
                 accounts,
             )
         }
diff --git a/token-lending/program/src/processor/liquidity_mining.rs b/token-lending/program/src/processor/liquidity_mining.rs
index 8c697ebb4ea..7e27bf0bd3c 100644
--- a/token-lending/program/src/processor/liquidity_mining.rs
+++ b/token-lending/program/src/processor/liquidity_mining.rs
@@ -12,10 +12,10 @@
 //! - [close_pool_reward]
 //!
 //! There is an ix related to migration:
-//! - [upgrade_reserve] (TODO: add bpf tests)
+//! - [upgrade_reserve] (has anchor integration test)
 //!
 //! There is one user ix:
-//! - [claim_user_reward] (TODO: add bpf tests)
+//! - [claim_user_reward]
 //!
 //! [suilend-lm]: https://github.com/solendprotocol/suilend/blob/dc53150416f352053ac3acbb320ee143409c4a5d/contracts/suilend/sources/liquidity_mining.move#L2
 
diff --git a/token-lending/program/src/processor/liquidity_mining/claim_user_reward.rs b/token-lending/program/src/processor/liquidity_mining/claim_user_reward.rs
index 49d079c2287..45dd377e619 100644
--- a/token-lending/program/src/processor/liquidity_mining/claim_user_reward.rs
+++ b/token-lending/program/src/processor/liquidity_mining/claim_user_reward.rs
@@ -74,6 +74,7 @@ struct ClaimUserReward<'a, 'info> {
 pub(crate) fn process(
     program_id: &Pubkey,
     reward_authority_bump: u8,
+    position_kind: PositionKind,
     accounts: &[AccountInfo],
 ) -> ProgramResult {
     let clock = &Clock::get()?;
@@ -89,13 +90,57 @@ pub(crate) fn process(
 
     // 1.
 
-    let position_kind = accounts.obligation.find_position_kind(reserve_key)?;
+    let pool_reward_manager = accounts.reserve.pool_reward_manager_mut(position_kind);
 
-    let Some(user_reward_manager) = accounts
+    if let Some(user_reward_manager) = accounts
         .obligation
         .user_reward_managers
         .find_mut(reserve_key, position_kind)
-    else {
+    {
+        msg!(
+            "Found user reward manager that was last updated at {} and has {}/{} shares",
+            user_reward_manager.last_update_time_secs,
+            user_reward_manager.share,
+            pool_reward_manager.total_shares
+        );
+
+        // 2.
+
+        let total_reward_amount = user_reward_manager.claim_rewards(
+            pool_reward_manager,
+            *accounts.reward_token_vault_info.key,
+            clock,
+        )?;
+
+        // 3.
+
+        if total_reward_amount > 0 {
+            spl_token_transfer(TokenTransferParams {
+                source: accounts.reward_token_vault_info.clone(),
+                destination: accounts.obligation_owner_token_account_info.clone(),
+                amount: total_reward_amount,
+                authority: accounts.reward_authority_info.clone(),
+                authority_signer_seeds: &[
+                    reward_vault_authority_seeds(
+                        accounts.lending_market_info.key,
+                        &accounts.reserve.key(),
+                        accounts.reward_mint_info.key,
+                    )
+                    .as_slice(),
+                    &[&[reward_authority_bump]],
+                ]
+                .concat(),
+                token_program: accounts.token_program_info.clone(),
+            })?;
+        }
+    } else {
+        let expected_position_kind = accounts.obligation.find_position_kind(reserve_key)?;
+
+        if expected_position_kind != position_kind {
+            msg!("Obligation does not have {:?} for reserve", position_kind);
+            return Err(LendingError::InvalidAccountInput.into());
+        }
+
         // We've checked that the obligation associates this reserve but it's
         // not in the user reward managers yet.
         // This means that the obligation hasn't been migrated to track the
@@ -103,29 +148,27 @@ pub(crate) fn process(
         //
         // We'll upgrade it here.
 
-        let reserve_key = accounts.reserve.key();
-
-        let (pool_reward_manager, migrated_share) = match position_kind {
-            PositionKind::Borrow => {
-                let share = accounts
-                    .obligation
-                    .find_liquidity_in_borrows(reserve_key)?
-                    .0
-                    .liability_shares()?;
-
-                (&mut accounts.reserve.borrows_pool_reward_manager, share)
-            }
+        let migrated_share = match position_kind {
+            PositionKind::Borrow => accounts
+                .obligation
+                .find_liquidity_in_borrows(reserve_key)?
+                .0
+                .liability_shares()?,
             PositionKind::Deposit => {
-                let share = accounts
+                accounts
                     .obligation
                     .find_collateral_in_deposits(reserve_key)?
                     .0
-                    .deposited_amount;
-
-                (&mut accounts.reserve.deposits_pool_reward_manager, share)
+                    .deposited_amount
             }
         };
 
+        msg!(
+            "Migrating obligation to track pool reward manager with share of {}/{}",
+            migrated_share,
+            pool_reward_manager.total_shares
+        );
+
         accounts.obligation.user_reward_managers.set_share(
             reserve_key,
             position_kind,
@@ -133,46 +176,8 @@ pub(crate) fn process(
             migrated_share,
             clock,
         )?;
-
-        realloc_obligation_if_necessary(&accounts.obligation, accounts.obligation_info)?;
-        Obligation::pack(
-            *accounts.obligation,
-            &mut accounts.obligation_info.data.borrow_mut(),
-        )?;
-
-        return Ok(());
     };
 
-    let pool_reward_manager = accounts.reserve.pool_reward_manager_mut(position_kind);
-
-    // 2.
-
-    let total_reward_amount = user_reward_manager.claim_rewards(
-        pool_reward_manager,
-        *accounts.reward_token_vault_info.key,
-        clock,
-    )?;
-
-    // 3.
-
-    spl_token_transfer(TokenTransferParams {
-        source: accounts.reward_token_vault_info.clone(),
-        destination: accounts.obligation_owner_token_account_info.clone(),
-        amount: total_reward_amount,
-        authority: accounts.reward_authority_info.clone(),
-        authority_signer_seeds: &[
-            reward_vault_authority_seeds(
-                accounts.lending_market_info.key,
-                &accounts.reserve.key(),
-                accounts.reward_mint_info.key,
-            )
-            .as_slice(),
-            &[&[reward_authority_bump]],
-        ]
-        .concat(),
-        token_program: accounts.token_program_info.clone(),
-    })?;
-
     // 4.
 
     realloc_obligation_if_necessary(&accounts.obligation, accounts.obligation_info)?;
diff --git a/token-lending/program/tests/add_pool_reward.rs b/token-lending/program/tests/add_pool_reward.rs
index 3e9f6f8351b..09407066fb4 100644
--- a/token-lending/program/tests/add_pool_reward.rs
+++ b/token-lending/program/tests/add_pool_reward.rs
@@ -54,7 +54,7 @@ async fn test_(position_kind: PositionKind) {
     let obligation = lending_market
         .init_obligation(&mut test, Keypair::new(), &user)
         .await
-        .expect("This should succeed");
+        .expect("Should init obligation");
 
     let usdc_reserve_post = test.load_account::<Reserve>(usdc_reserve.pubkey).await;
 
@@ -62,11 +62,7 @@ async fn test_(position_kind: PositionKind) {
         total_shares: 0,
         last_update_time_secs: current_time as _,
         pool_rewards: {
-            let mut og = usdc_reserve
-                .account
-                .deposits_pool_reward_manager
-                .pool_rewards
-                .clone();
+            let mut og = PoolRewardManager::default().pool_rewards;
 
             og[0] = PoolRewardSlot::Occupied(Box::new(PoolReward {
                 id: PoolRewardId(1),
@@ -102,7 +98,7 @@ async fn test_(position_kind: PositionKind) {
                     deposit_amount,
                 )
                 .await
-                .expect("This should succeed");
+                .expect("Should deposit $USDC");
 
             deposit_amount
         }
@@ -124,7 +120,7 @@ async fn test_(position_kind: PositionKind) {
                     420_000_000,
                 )
                 .await
-                .expect("This should succeed");
+                .expect("Should borrow $wSOL");
 
             lending_market
                 .borrow_obligation_liquidity(
@@ -136,7 +132,7 @@ async fn test_(position_kind: PositionKind) {
                     690,
                 )
                 .await
-                .unwrap();
+                .expect("Should borrow $USDC");
 
             690
         }
diff --git a/token-lending/program/tests/cancel_pool_reward.rs b/token-lending/program/tests/cancel_pool_reward.rs
index 7bd5b1caca3..fa09068aca4 100644
--- a/token-lending/program/tests/cancel_pool_reward.rs
+++ b/token-lending/program/tests/cancel_pool_reward.rs
@@ -31,13 +31,12 @@ async fn test_cancel_pool_reward_for_borrow() {
 async fn test_(position_kind: PositionKind) {
     let (mut test, lending_market, usdc_reserve, _, mut lending_market_owner, _) =
         setup_world(&test_reserve_config(), &test_reserve_config()).await;
-    let mut clock = test.get_clock().await;
 
     let reward_mint = test.create_mint_as_test_authority().await;
     let reward_vault = Keypair::new();
     let duration_secs = 3_600;
     let total_rewards = 1_000_000;
-    let initial_time = clock.unix_timestamp as u64;
+    let initial_time = test.get_clock().await.unix_timestamp as u64;
     let reward = LiqMiningReward {
         mint: reward_mint,
         vault: reward_vault.insecure_clone(),
@@ -63,9 +62,9 @@ async fn test_(position_kind: PositionKind) {
     )
     .await;
 
-    clock.unix_timestamp += duration_secs as i64 / 2;
-    test.context.set_sysvar(&clock);
-    let time_when_cancelling = clock.unix_timestamp as u64;
+    let current_time = test
+        .advance_clock_by_slots_and_secs(1, duration_secs as u64 / 2)
+        .await;
 
     let pool_reward_index = 0;
     lending_market
@@ -100,13 +99,9 @@ async fn test_(position_kind: PositionKind) {
 
     let expected_reward_manager = Box::new(PoolRewardManager {
         total_shares: 0,
-        last_update_time_secs: time_when_cancelling as _,
+        last_update_time_secs: current_time,
         pool_rewards: {
-            let mut og = usdc_reserve
-                .account
-                .deposits_pool_reward_manager
-                .pool_rewards
-                .clone();
+            let mut og = PoolRewardManager::default().pool_rewards;
 
             og[0] = PoolRewardSlot::Occupied(Box::new(PoolReward {
                 id: PoolRewardId(1),
diff --git a/token-lending/program/tests/claim_pool_reward.rs b/token-lending/program/tests/claim_pool_reward.rs
new file mode 100644
index 00000000000..f48322796df
--- /dev/null
+++ b/token-lending/program/tests/claim_pool_reward.rs
@@ -0,0 +1,526 @@
+#![cfg(feature = "test-bpf")]
+
+mod helpers;
+
+use std::collections::HashSet;
+
+use helpers::solend_program_test::{
+    setup_world, BalanceChecker, LiqMiningReward, TokenAccount, TokenBalanceChange,
+};
+use helpers::test_reserve_config;
+
+use pretty_assertions::assert_eq;
+use solana_program_test::*;
+use solana_sdk::account::Account;
+use solana_sdk::instruction::InstructionError;
+use solana_sdk::signature::{Keypair, Signer};
+use solana_sdk::transaction::TransactionError;
+use solend_program::{
+    math::Decimal,
+    state::{PoolRewardId, PoolRewardManager, PositionKind, Reserve, UserRewardManager},
+};
+use solend_sdk::error::LendingError;
+use solend_sdk::math::TryMul;
+use solend_sdk::state::{Obligation, PoolReward, PoolRewardSlot, UserReward};
+
+#[tokio::test]
+async fn test_claim_pool_reward_for_deposit() {
+    test_(PositionKind::Deposit).await;
+}
+
+#[tokio::test]
+async fn test_claim_pool_reward_for_borrow() {
+    test_(PositionKind::Borrow).await;
+}
+
+async fn test_(position_kind: PositionKind) {
+    let (mut test, lending_market, usdc_reserve, wsol_reserve, mut lending_market_owner, mut user) =
+        setup_world(&test_reserve_config(), &test_reserve_config()).await;
+
+    let reward_mint = test.create_mint_as_test_authority().await;
+    let reward_vault = Keypair::new();
+    let duration_secs = 3_600;
+    let total_rewards = 1_000_000;
+    let current_time = test.get_clock().await.unix_timestamp as u64;
+    let initial_time = current_time;
+    let reward = LiqMiningReward {
+        mint: reward_mint,
+        vault: reward_vault.insecure_clone(),
+    };
+
+    lending_market
+        .add_pool_reward(
+            &mut test,
+            &usdc_reserve,
+            &mut lending_market_owner,
+            &reward,
+            position_kind,
+            current_time,
+            current_time + duration_secs as u64,
+            total_rewards,
+        )
+        .await
+        .expect("Should add pool reward");
+
+    let obligation = lending_market
+        .init_obligation(&mut test, Keypair::new(), &user)
+        .await
+        .expect("Should init obligation");
+
+    let expected_share = match position_kind {
+        PositionKind::Deposit => {
+            let deposit_amount = 1_000_000;
+            lending_market
+                .deposit_reserve_liquidity_and_obligation_collateral(
+                    &mut test,
+                    &usdc_reserve,
+                    &obligation,
+                    &user,
+                    deposit_amount,
+                )
+                .await
+                .expect("Should deposit $USDC");
+
+            deposit_amount
+        }
+        PositionKind::Borrow => {
+            lending_market
+                .deposit_reserve_liquidity_and_obligation_collateral(
+                    &mut test,
+                    &wsol_reserve,
+                    &obligation,
+                    &user,
+                    420_000_000,
+                )
+                .await
+                .expect("Should deposit $wSOL");
+
+            lending_market
+                .borrow_obligation_liquidity(
+                    &mut test,
+                    &usdc_reserve,
+                    &obligation,
+                    &user,
+                    None,
+                    690,
+                )
+                .await
+                .expect("Should borrow $USDC");
+
+            690
+        }
+    };
+
+    let current_time = test
+        .advance_clock_by_slots_and_secs(1, duration_secs as u64 / 2)
+        .await;
+
+    // user must have a token account to deposit rewards into ahead of time
+    user.create_token_account(&reward.mint, &mut test).await;
+
+    let balance_checker =
+        BalanceChecker::start(&mut test, &[&TokenAccount(reward.vault.pubkey()), &user]).await;
+
+    lending_market
+        .claim_pool_reward(
+            &mut test,
+            &obligation,
+            &usdc_reserve,
+            &user,
+            &reward,
+            position_kind,
+        )
+        .await
+        .expect("Should claim reward");
+
+    let (balance_changes, _) = balance_checker.find_balance_changes(&mut test).await;
+
+    let diff = (total_rewards as i128) / 2
+        - match position_kind {
+            PositionKind::Deposit => 0,
+            PositionKind::Borrow => 1, // integer division rounds down
+        };
+    let expected_balance_changes = HashSet::from([
+        TokenBalanceChange {
+            token_account: user.get_account(&reward.mint).unwrap(),
+            mint: reward.mint,
+            diff,
+        },
+        TokenBalanceChange {
+            token_account: reward.vault.pubkey(),
+            mint: reward.mint,
+            diff: -diff,
+        },
+    ]);
+    assert_eq!(balance_changes, expected_balance_changes);
+
+    let usdc_reserve_post = test.load_account::<Reserve>(usdc_reserve.pubkey).await;
+
+    let cumulative_rewards_per_share = match position_kind {
+        PositionKind::Deposit => Decimal::from_scaled_val(500000000000000000),
+        PositionKind::Borrow => Decimal::from_scaled_val(724637681159420289855),
+    };
+
+    let expected_reward_manager = PoolRewardManager {
+        total_shares: expected_share,
+        last_update_time_secs: current_time,
+        pool_rewards: {
+            let mut og = PoolRewardManager::default().pool_rewards;
+
+            og[0] = PoolRewardSlot::Occupied(Box::new(PoolReward {
+                id: PoolRewardId(1),
+                vault: reward_vault.pubkey(),
+                start_time_secs: initial_time,
+                duration_secs,
+                total_rewards,
+                num_user_reward_managers: 1,
+                cumulative_rewards_per_share,
+            }));
+
+            og
+        },
+    };
+
+    assert_eq!(
+        usdc_reserve_post.account.pool_reward_manager(position_kind),
+        &expected_reward_manager
+    );
+
+    let obligation_post = test.load_obligation(obligation.pubkey).await;
+
+    let earned_rewards = match position_kind {
+        PositionKind::Deposit => {
+            // on deposit there's no division involved and so it ends up being
+            // nice whole number
+            Decimal::zero()
+        }
+        PositionKind::Borrow => {
+            // on borrow we have some precision loss and so the one extra
+            // _almost_ token stays in the user's account
+            Decimal::from_scaled_val(999999999999999950)
+        }
+    };
+    // we don't withdraw fractions of a token but keep them around for future claims
+    assert_eq!(earned_rewards.try_floor_u64().unwrap(), 0);
+
+    assert_eq!(
+        obligation_post.account.user_reward_managers.last().unwrap(),
+        &UserRewardManager {
+            reserve: usdc_reserve.pubkey,
+            position_kind,
+            share: expected_share,
+            last_update_time_secs: current_time,
+            rewards: vec![UserReward {
+                pool_reward_index: 0,
+                pool_reward_id: PoolRewardId(1),
+                earned_rewards,
+                cumulative_rewards_per_share
+            }],
+        }
+    );
+
+    // move time forward so that all rewards can be claimed
+
+    let current_time = test
+        .advance_clock_by_slots_and_secs(1, duration_secs as _)
+        .await;
+
+    lending_market
+        .claim_pool_reward(
+            &mut test,
+            &obligation,
+            &usdc_reserve,
+            &user,
+            &reward,
+            position_kind,
+        )
+        .await
+        .expect("Should claim reward");
+
+    // reserve should have no user reward managers
+
+    let usdc_reserve_final = test.load_account::<Reserve>(usdc_reserve.pubkey).await;
+    let pool_reward_manager = usdc_reserve_final
+        .account
+        .pool_reward_manager(position_kind);
+
+    assert_eq!(pool_reward_manager.last_update_time_secs, current_time);
+
+    assert_eq!(
+        pool_reward_manager.pool_rewards[0],
+        PoolRewardSlot::Occupied(Box::new(PoolReward {
+            id: PoolRewardId(1),
+            vault: reward_vault.pubkey(),
+            start_time_secs: initial_time,
+            duration_secs,
+            total_rewards,
+            num_user_reward_managers: 0,
+            cumulative_rewards_per_share: cumulative_rewards_per_share
+                .try_mul(Decimal::from(2u64))
+                .unwrap()
+        }))
+    );
+
+    // obligation should no longer track this reward
+
+    let obligation_final = test.load_obligation(obligation.pubkey).await;
+
+    assert_eq!(
+        obligation_final
+            .account
+            .user_reward_managers
+            .last()
+            .unwrap()
+            .rewards,
+        vec![],
+    );
+}
+
+#[tokio::test]
+async fn test_cannot_claim_into_wrong_destination() {
+    let (mut test, lending_market, usdc_reserve, _, mut lending_market_owner, user) =
+        setup_world(&test_reserve_config(), &test_reserve_config()).await;
+
+    let reward_mint = test.create_mint_as_test_authority().await;
+    let reward_vault = Keypair::new();
+    let duration_secs = 3_600;
+    let total_rewards = 1_000_000;
+    let initial_time = test.get_clock().await.unix_timestamp as u64;
+    let reward = LiqMiningReward {
+        mint: reward_mint,
+        vault: reward_vault.insecure_clone(),
+    };
+
+    lending_market
+        .add_pool_reward(
+            &mut test,
+            &usdc_reserve,
+            &mut lending_market_owner,
+            &reward,
+            PositionKind::Deposit,
+            initial_time,
+            initial_time + duration_secs as u64,
+            total_rewards,
+        )
+        .await
+        .expect("Should add pool reward");
+
+    let obligation = lending_market
+        .init_obligation(&mut test, Keypair::new(), &user)
+        .await
+        .expect("Should init obligation");
+
+    lending_market
+        .deposit_reserve_liquidity_and_obligation_collateral(
+            &mut test,
+            &usdc_reserve,
+            &obligation,
+            &user,
+            1,
+        )
+        .await
+        .expect("Should deposit $USDC");
+
+    // let's use a token account of a wrong user
+    lending_market_owner
+        .create_token_account(&reward.mint, &mut test)
+        .await;
+
+    let err = lending_market
+        .claim_pool_reward(
+            &mut test,
+            &obligation,
+            &usdc_reserve,
+            &lending_market_owner, // ! wrong
+            &reward,
+            PositionKind::Deposit,
+        )
+        .await
+        .expect_err("Cannot steal user reward");
+
+    assert_eq!(
+        err.unwrap(),
+        TransactionError::InstructionError(
+            1,
+            InstructionError::Custom(LendingError::InvalidAccountInput as _)
+        )
+    );
+}
+
+#[tokio::test]
+async fn test_migrate_obligation() {
+    let (mut test, lending_market, usdc_reserve, _, mut lending_market_owner, mut user) =
+        setup_world(&test_reserve_config(), &test_reserve_config()).await;
+
+    let obligation = lending_market
+        .init_obligation(&mut test, Keypair::new(), &user)
+        .await
+        .expect("Should init obligation");
+
+    lending_market
+        .deposit_reserve_liquidity_and_obligation_collateral(
+            &mut test,
+            &usdc_reserve,
+            &obligation,
+            &user,
+            1,
+        )
+        .await
+        .expect("Should deposit $USDC");
+
+    {
+        // The call above set up the obligation with a user reward manager.
+        // We'll now truncate the liq. mining data to simulate an obligation in
+        // the old format.
+        // However, that will leave the reserve in an invalid state as it will
+        // have already the user shares set up.
+        // That's ok, let's just ignore that in this test.
+
+        let mut new_raw_obligation = Account {
+            data: vec![0; Obligation::MIN_LEN],
+            ..test
+                .context
+                .banks_client
+                .get_account(obligation.pubkey)
+                .await
+                .expect("Should access obligation account")
+                .expect("Obligation account should exist")
+        };
+
+        Obligation::pack(
+            {
+                let mut obligation = test.load_obligation(obligation.pubkey).await;
+                obligation.account.user_reward_managers.clear();
+                obligation.account
+            },
+            &mut new_raw_obligation.data,
+        )
+        .expect("Should pack obligation");
+
+        test.context
+            .set_account(&obligation.pubkey, &new_raw_obligation.into());
+    }
+
+    let reward_mint = test.create_mint_as_test_authority().await;
+    let reward_vault = Keypair::new();
+    let duration_secs = 3_600;
+    let total_rewards = 1_000_000;
+    let initial_time = test.get_clock().await.unix_timestamp as u64;
+    let reward = LiqMiningReward {
+        mint: reward_mint,
+        vault: reward_vault.insecure_clone(),
+    };
+
+    lending_market
+        .add_pool_reward(
+            &mut test,
+            &usdc_reserve,
+            &mut lending_market_owner,
+            &reward,
+            PositionKind::Deposit,
+            initial_time,
+            initial_time + duration_secs as u64,
+            total_rewards,
+        )
+        .await
+        .expect("Should add pool reward");
+
+    let current_time = test
+        .advance_clock_by_slots_and_secs(1, duration_secs as u64 / 2)
+        .await;
+
+    // user must have a token account to deposit rewards into ahead of time
+    user.create_token_account(&reward.mint, &mut test).await;
+
+    let balance_checker = BalanceChecker::start(&mut test, &[&user]).await;
+
+    // At this point the user did not have any shares in the obligation and so
+    // they cannot claim anything.
+    // However, we migrate the obligation so that next time they claim they do
+    // get something.
+
+    lending_market
+        .claim_pool_reward(
+            &mut test,
+            &obligation,
+            &usdc_reserve,
+            &user,
+            &reward,
+            PositionKind::Deposit,
+        )
+        .await
+        .expect("Should claim reward");
+
+    let usdc_reserve_post = test.load_account::<Reserve>(usdc_reserve.pubkey).await;
+
+    assert_eq!(
+        usdc_reserve_post
+            .account
+            .deposits_pool_reward_manager
+            .total_shares,
+        2 // 1 from the old obligation and 1 from the new one
+    );
+
+    let obligation_post = test.load_obligation(obligation.pubkey).await;
+
+    assert_eq!(
+        obligation_post.account.user_reward_managers[0],
+        UserRewardManager {
+            reserve: usdc_reserve.pubkey,
+            position_kind: PositionKind::Deposit,
+            share: 1,
+            last_update_time_secs: current_time,
+            rewards: vec![UserReward {
+                pool_reward_index: 0,
+                pool_reward_id: PoolRewardId(1),
+                earned_rewards: Decimal::zero(),
+                cumulative_rewards_per_share: Decimal::from_scaled_val(500000_000000000000000000),
+            }],
+        }
+    );
+
+    let (balance_changes, _) = balance_checker.find_balance_changes(&mut test).await;
+    assert_eq!(balance_changes, HashSet::new());
+
+    let current_time = test.advance_clock_by_slots_and_secs(1, duration_secs).await;
+
+    // now they should be able to claim rewards
+
+    lending_market
+        .claim_pool_reward(
+            &mut test,
+            &obligation,
+            &usdc_reserve,
+            &user,
+            &reward,
+            PositionKind::Deposit,
+        )
+        .await
+        .expect("Should claim reward");
+
+    let obligation_final = test.load_obligation(obligation.pubkey).await;
+
+    assert_eq!(
+        obligation_final.account.user_reward_managers[0],
+        UserRewardManager {
+            reserve: usdc_reserve.pubkey,
+            position_kind: PositionKind::Deposit,
+            share: 1,
+            last_update_time_secs: current_time,
+            rewards: vec![],
+        }
+    );
+
+    let (balance_changes, _) = balance_checker.find_balance_changes(&mut test).await;
+    assert_eq!(
+        balance_changes,
+        HashSet::from([TokenBalanceChange {
+            token_account: user.get_account(&reward.mint).unwrap(),
+            mint: reward.mint,
+            // There are 2 shares and we're accruing rewards for half the time.
+            // There are 2 shares bcs we reset the obligation and "register" it
+            // a second time in this test.
+            diff: (total_rewards as i128) / 4,
+        }])
+    );
+}
diff --git a/token-lending/program/tests/close_pool_reward.rs b/token-lending/program/tests/close_pool_reward.rs
index 155af89836a..1493d8a8e8f 100644
--- a/token-lending/program/tests/close_pool_reward.rs
+++ b/token-lending/program/tests/close_pool_reward.rs
@@ -28,13 +28,12 @@ async fn test_close_pool_reward_for_borrow() {
 async fn test_(position_kind: PositionKind) {
     let (mut test, lending_market, usdc_reserve, _, mut lending_market_owner, _) =
         setup_world(&test_reserve_config(), &test_reserve_config()).await;
-    let mut clock = test.get_clock().await;
 
     let reward_mint = test.create_mint_as_test_authority().await;
     let reward_vault = Keypair::new();
     let duration_secs = 3_600;
     let total_rewards = 1_000_000;
-    let initial_time = clock.unix_timestamp as u64;
+    let initial_time = test.get_clock().await.unix_timestamp as u64;
     let reward = LiqMiningReward {
         mint: reward_mint,
         vault: reward_vault.insecure_clone(),
@@ -57,8 +56,7 @@ async fn test_(position_kind: PositionKind) {
     let balance_checker = BalanceChecker::start(&mut test, &[&lending_market_owner]).await;
 
     // doesn't matter when we close as long as there are no obligations
-    clock.unix_timestamp += 1;
-    test.context.set_sysvar(&clock);
+    test.advance_clock_by_slots_and_secs(1, 1).await;
 
     let pool_reward_index = 0;
     lending_market
@@ -88,11 +86,7 @@ async fn test_(position_kind: PositionKind) {
         total_shares: 0,
         last_update_time_secs: initial_time as _,
         pool_rewards: {
-            let mut og = usdc_reserve
-                .account
-                .deposits_pool_reward_manager
-                .pool_rewards
-                .clone();
+            let mut og = PoolRewardManager::default().pool_rewards;
 
             og[0] = PoolRewardSlot::Vacant {
                 last_pool_reward_id: PoolRewardId(1),
diff --git a/token-lending/program/tests/helpers/solend_program_test.rs b/token-lending/program/tests/helpers/solend_program_test.rs
index d50e4ea6e51..1f45cabdbbf 100644
--- a/token-lending/program/tests/helpers/solend_program_test.rs
+++ b/token-lending/program/tests/helpers/solend_program_test.rs
@@ -77,6 +77,7 @@ mod cu_budgets {
     pub(super) const ADD_POOL_REWARD: u32 = 80_017;
     pub(super) const CANCEL_POOL_REWARD: u32 = 80_018;
     pub(super) const CLOSE_POOL_REWARD: u32 = 80_019;
+    pub(super) const CLAIM_POOL_REWARD: u32 = 80_020;
 }
 
 /// This is at most how many bytes can an obligation grow.
@@ -337,6 +338,16 @@ impl SolendProgramTest {
             .await
     }
 
+    /// Returns the new clock unix timestamp
+    pub async fn advance_clock_by_slots_and_secs(&mut self, slots: u64, secs: u64) -> u64 {
+        self.advance_clock_by_slots(slots).await;
+        let mut clock = self.get_clock().await;
+        clock.unix_timestamp += secs as i64;
+        self.context.set_sysvar(&clock);
+
+        clock.unix_timestamp as u64
+    }
+
     /// Advances clock by x slots. note that transactions don't automatically increment the slot
     /// value in Clock, so this function must be explicitly called whenever you want time to move
     /// forward.
@@ -1048,6 +1059,41 @@ impl Info<LendingMarket> {
             .await
     }
 
+    pub async fn claim_pool_reward(
+        &self,
+        test: &mut SolendProgramTest,
+        obligation: &Info<Obligation>,
+        reserve: &Info<Reserve>,
+        obligation_owner: &User,
+        reward: &LiqMiningReward,
+        position_kind: PositionKind,
+    ) -> Result<(), BanksClientError> {
+        let (reward_authority_pda, reward_authority_bump) = find_reward_vault_authority(
+            &solend_program::id(),
+            &self.pubkey,
+            &reserve.pubkey,
+            &reward.mint,
+        );
+
+        let instructions = [
+            ComputeBudgetInstruction::set_compute_unit_limit(cu_budgets::CLAIM_POOL_REWARD),
+            claim_pool_reward(
+                solend_program::id(),
+                reward_authority_bump,
+                position_kind,
+                obligation.pubkey,
+                obligation_owner.get_account(&reward.mint).unwrap(),
+                reserve.pubkey,
+                reward.mint,
+                reward_authority_pda,
+                reward.vault.pubkey(),
+                self.pubkey,
+            ),
+        ];
+
+        test.process_transaction(&instructions, None).await
+    }
+
     pub async fn donate_to_reserve(
         &self,
         test: &mut SolendProgramTest,
diff --git a/token-lending/sdk/src/instruction.rs b/token-lending/sdk/src/instruction.rs
index 5b3e9d034ec..e7e4a251b68 100644
--- a/token-lending/sdk/src/instruction.rs
+++ b/token-lending/sdk/src/instruction.rs
@@ -621,10 +621,10 @@ pub enum LendingInstruction {
     /// 28
     /// ClaimReward
     ///
-    /// * User can claim rewards from their obligation.
+    /// * Permissionless claim of rewards from an obligation.
     ///
     ///   `[writable]` Obligation account.
-    ///   `[writable]` Obligation owner reward receiving token account.
+    ///   `[writable]` Obligation owner's token account that receives reward.
     ///   `[writable]` Reserve account.
     ///   `[]` Reward mint.
     ///   `[]` Derived reserve pool reward authority. Seed:
@@ -638,6 +638,10 @@ pub enum LendingInstruction {
     ClaimReward {
         /// The bump seed of the reward authority.
         reward_authority_bump: u8,
+        /// Even though an obligation can either deposit or borrow the same
+        /// reserve, the obligation's rewards can hold rewards for both.
+        /// It's therefore necessary to specify which kind of reward to claim.
+        position_kind: PositionKind,
     },
 
     // 255
@@ -944,9 +948,11 @@ impl LendingInstruction {
                 }
             }
             28 => {
-                let (reward_authority_bump, _rest) = Self::unpack_u8(rest)?;
+                let (reward_authority_bump, rest) = Self::unpack_u8(rest)?;
+                let (position_kind, _rest) = Self::unpack_try_from_u8(rest)?;
                 Self::ClaimReward {
                     reward_authority_bump,
+                    position_kind,
                 }
             }
             255 => Self::UpgradeReserveToV2_1_0,
@@ -1294,9 +1300,11 @@ impl LendingInstruction {
             }
             Self::ClaimReward {
                 reward_authority_bump,
+                position_kind,
             } => {
                 buf.push(28);
                 buf.extend_from_slice(&reward_authority_bump.to_le_bytes());
+                buf.extend_from_slice(&(position_kind as u8).to_le_bytes());
             }
             Self::UpgradeReserveToV2_1_0 => {
                 buf.push(255);
@@ -2243,6 +2251,51 @@ pub fn close_pool_reward(
     }
 }
 
+///   `[writable]` Obligation account.
+///   `[writable]` Obligation owner's token account that receives reward.
+///   `[writable]` Reserve account.
+///   `[]` Reward mint.
+///   `[]` 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.
+///   `[]` Token program.
+#[allow(clippy::too_many_arguments)]
+pub fn claim_pool_reward(
+    program_id: Pubkey,
+    reward_authority_bump: u8,
+    position_kind: PositionKind,
+    obligation: Pubkey,
+    obligation_owner_token_account_for_reward: Pubkey,
+    reserve: Pubkey,
+    reward_mint: Pubkey,
+    reward_vault_authority: Pubkey,
+    reward_vault: Pubkey,
+    lending_market: Pubkey,
+) -> Instruction {
+    Instruction {
+        program_id,
+        accounts: vec![
+            AccountMeta::new(obligation, false),
+            AccountMeta::new(obligation_owner_token_account_for_reward, false),
+            AccountMeta::new(reserve, false),
+            AccountMeta::new_readonly(reward_mint, false),
+            AccountMeta::new_readonly(reward_vault_authority, false),
+            AccountMeta::new(reward_vault, false),
+            AccountMeta::new_readonly(lending_market, false),
+            AccountMeta::new_readonly(spl_token::id(), false),
+        ],
+        data: LendingInstruction::ClaimReward {
+            reward_authority_bump,
+            position_kind,
+        }
+        .pack(),
+    }
+}
+
 /// Derives the reward vault authority PDA address.
 pub fn find_reward_vault_authority(
     program_id: &Pubkey,
diff --git a/token-lending/sdk/src/state/liquidity_mining.rs b/token-lending/sdk/src/state/liquidity_mining.rs
index fe561e78d91..16886e522b7 100644
--- a/token-lending/sdk/src/state/liquidity_mining.rs
+++ b/token-lending/sdk/src/state/liquidity_mining.rs
@@ -24,8 +24,6 @@ pub const MIN_REWARD_PERIOD_SECS: u64 = 3_600;
 mod suilend_tests {
     //! These tests were taken from the Suilend's codebase and adapted to
     //! the new codebase.
-    //!
-    //! TODO: Calculate test coverage and add tests for missing branches.
 
     use crate::{
         math::Decimal,
diff --git a/token-lending/sdk/src/state/liquidity_mining/pool_reward_manager.rs b/token-lending/sdk/src/state/liquidity_mining/pool_reward_manager.rs
index ecfc83a9ccb..b255b88a7e6 100644
--- a/token-lending/sdk/src/state/liquidity_mining/pool_reward_manager.rs
+++ b/token-lending/sdk/src/state/liquidity_mining/pool_reward_manager.rs
@@ -415,7 +415,6 @@ impl Pack for PoolRewardManager {
                     *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,
diff --git a/token-lending/sdk/src/state/obligation.rs b/token-lending/sdk/src/state/obligation.rs
index b2661d7bf08..b1c85e6e560 100644
--- a/token-lending/sdk/src/state/obligation.rs
+++ b/token-lending/sdk/src/state/obligation.rs
@@ -485,11 +485,11 @@ 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
+const OBLIGATION_LEN_V2_0_2: 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 Obligation {
     /// Obligation with no Liquidity Mining Rewards
-    pub const MIN_LEN: usize = OBLIGATION_LEN_V1;
+    pub const MIN_LEN: usize = OBLIGATION_LEN_V2_0_2;
 
     /// Maximum account size for obligation.
     /// Scenario in which all reserves have all associated rewards filled.
@@ -502,10 +502,10 @@ impl Obligation {
     /// How many bytes are needed to pack this [UserRewardManager].
     pub fn size_in_bytes_when_packed(&self) -> usize {
         if self.user_reward_managers.is_empty() {
-            return OBLIGATION_LEN_V1;
+            return OBLIGATION_LEN_V2_0_2;
         }
 
-        let mut size = OBLIGATION_LEN_V1 + 1;
+        let mut size = OBLIGATION_LEN_V2_0_2 + 1;
 
         for reward_manager in self.user_reward_managers.iter() {
             size += reward_manager.size_in_bytes_when_packed();
@@ -554,7 +554,7 @@ impl Obligation {
 
     /// Since @v2.1.0 we pack vec of user reward managers
     pub fn pack_into_slice(&self, dst: &mut [u8]) {
-        let output = array_mut_ref![dst, 0, OBLIGATION_LEN_V1];
+        let output = array_mut_ref![dst, 0, OBLIGATION_LEN_V2_0_2];
         #[allow(clippy::ptr_offset_with_cast)]
         let (
             discriminator,
@@ -666,17 +666,17 @@ impl Obligation {
             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;
+            dst[OBLIGATION_LEN_V2_0_2] = user_reward_managers_len;
 
-            let mut offset = OBLIGATION_LEN_V1 + 1;
+            let mut offset = OBLIGATION_LEN_V2_0_2 + 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 {
+        } else if dst.len() > OBLIGATION_LEN_V2_0_2 {
             // set the length to 0 if obligation was resized before
 
-            dst[OBLIGATION_LEN_V1] = 0;
+            dst[OBLIGATION_LEN_V2_0_2] = 0;
         };
 
         // Any data after offset is garbage, but we don't zero it out bcs
@@ -686,7 +686,7 @@ impl Obligation {
     /// Unpacks a byte buffer into an [Obligation].
     /// Since @v2.1.0 we unpack vector of user reward managers
     pub fn unpack_from_slice(src: &[u8]) -> Result<Self, ProgramError> {
-        let input = array_ref![src, 0, OBLIGATION_LEN_V1];
+        let input = array_ref![src, 0, OBLIGATION_LEN_V2_0_2];
         #[allow(clippy::ptr_offset_with_cast)]
         let (
             discriminator,
@@ -738,7 +738,7 @@ impl Obligation {
             }
             Err(LendingError::AccountNotMigrated) => {
                 // We're migrating the account from v2.0.2 to v2.1.0.
-                debug_assert_eq!(OBLIGATION_LEN_V1, input.len());
+                debug_assert_eq!(OBLIGATION_LEN_V2_0_2, input.len());
 
                 AccountDiscriminator::Obligation
             }
@@ -788,11 +788,11 @@ impl Obligation {
             offset += OBLIGATION_LIQUIDITY_LEN;
         }
 
-        let user_reward_managers = match src.get(OBLIGATION_LEN_V1) {
+        let user_reward_managers = match src.get(OBLIGATION_LEN_V2_0_2) {
             Some(len @ 1..) => {
                 let mut user_reward_managers = Vec::with_capacity(*len as _);
 
-                let mut offset = OBLIGATION_LEN_V1 + 1;
+                let mut offset = OBLIGATION_LEN_V2_0_2 + 1;
                 for _ in 0..*len {
                     let user_reward_manager = UserRewardManager::unpack_from_slice(&src[offset..])?;
                     offset += user_reward_manager.size_in_bytes_when_packed();
diff --git a/token-lending/sdk/src/state/reserve.rs b/token-lending/sdk/src/state/reserve.rs
index d2f4def8193..7f59f39384a 100644
--- a/token-lending/sdk/src/state/reserve.rs
+++ b/token-lending/sdk/src/state/reserve.rs
@@ -594,6 +594,14 @@ impl Reserve {
         ))
     }
 
+    /// Returns the pool reward manager for the given position kind
+    pub fn pool_reward_manager(&self, position_kind: PositionKind) -> &PoolRewardManager {
+        match position_kind {
+            PositionKind::Borrow => &self.borrows_pool_reward_manager,
+            PositionKind::Deposit => &self.deposits_pool_reward_manager,
+        }
+    }
+
     /// Returns the pool reward manager for the given position kind
     pub fn pool_reward_manager_mut(
         &mut self,
diff --git a/token-lending/tests/liquidity-mining.ts b/token-lending/tests/liquidity-mining.ts
index d3c988d6961..d8cd3aa2c8c 100644
--- a/token-lending/tests/liquidity-mining.ts
+++ b/token-lending/tests/liquidity-mining.ts
@@ -1,4 +1,7 @@
 /**
+ * Temporary test to showcase that reserve upgrades work with CLI.
+ * We'll delete this once all reserves are upgraded.
+ *
  * $ anchor test --provider.cluster localnet --detach
  */
 
@@ -57,7 +60,7 @@ describe("liquidity mining", () => {
       .getProvider()
       .connection.getAccountInfo(new PublicKey(TEST_RESERVE_FOR_UPGRADE));
 
-    expect(reserveAfter.data.length).to.eq(8651); // new version data length
+    expect(reserveAfter.data.length).to.eq(5451); // new version data length
     const expectedRentAfter = await anchor
       .getProvider()
       .connection.getMinimumBalanceForRentExemption(reserveAfter.data.length);

From 0a04ecf8395900e2def37218871d98165ae6044f Mon Sep 17 00:00:00 2001
From: vanity <vanity@solend.fi>
Date: Sun, 13 Apr 2025 16:48:48 +0200
Subject: [PATCH 13/14] Improving code coverage of account checks

---
 token-lending/program/src/processor.rs        |   2 +-
 .../program/src/processor/account_borrow.rs   |  17 +
 .../program/src/processor/liquidity_mining.rs | 462 +++++++++++++++++-
 .../program/tests/refresh_obligation.rs       |   2 +-
 token-lending/sdk/src/state/lending_market.rs |   4 +-
 .../liquidity_mining/user_reward_manager.rs   |   2 +-
 token-lending/sdk/src/state/obligation.rs     |  10 +-
 7 files changed, 488 insertions(+), 11 deletions(-)

diff --git a/token-lending/program/src/processor.rs b/token-lending/program/src/processor.rs
index 8390619dca1..71f1a115e60 100644
--- a/token-lending/program/src/processor.rs
+++ b/token-lending/program/src/processor.rs
@@ -3601,7 +3601,7 @@ fn realloc_obligation_if_necessary(
     obligation: &Obligation,
     obligation_info: &AccountInfo<'_>,
 ) -> ProgramResult {
-    let expected_size = obligation.size_in_bytes_when_packed();
+    let expected_size = obligation.get_packed_len();
 
     if expected_size <= obligation_info.data_len() {
         return Ok(());
diff --git a/token-lending/program/src/processor/account_borrow.rs b/token-lending/program/src/processor/account_borrow.rs
index d3412b39262..2d812af5250 100644
--- a/token-lending/program/src/processor/account_borrow.rs
+++ b/token-lending/program/src/processor/account_borrow.rs
@@ -18,6 +18,7 @@ use solana_program::{
     program_pack::Pack, pubkey::Pubkey,
 };
 
+use std::fmt::Debug;
 use std::ops::{Deref, DerefMut};
 use std::result::Result;
 
@@ -214,3 +215,19 @@ impl From<&'_ ReserveDataGuard<'_, '_>> for ReserveDataGuardKind {
         }
     }
 }
+
+impl Debug for ReserveBorrow<'_, '_> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        let mut dbg = f.debug_struct("ReserveBorrow");
+        match &self.guard {
+            ReserveDataGuard::Released => dbg.field("variant", &"Released"),
+            ReserveDataGuard::Ref(_, reserve) => {
+                dbg.field("variant", &"Ref").field("reserve", &reserve)
+            }
+            ReserveDataGuard::RefMut(_, reserve) => {
+                dbg.field("variant", &"RefMut").field("reserve", &reserve)
+            }
+        }
+        .finish()
+    }
+}
diff --git a/token-lending/program/src/processor/liquidity_mining.rs b/token-lending/program/src/processor/liquidity_mining.rs
index 7e27bf0bd3c..6f2bfb2a94c 100644
--- a/token-lending/program/src/processor/liquidity_mining.rs
+++ b/token-lending/program/src/processor/liquidity_mining.rs
@@ -79,11 +79,11 @@ fn check_and_unpack_pool_reward_accounts_for_admin_ixs<'a, 'info>(
 /// * ✅ `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
 /// * ✅ `token_program_info` matches `lending_market_info`
 /// * ✅ `reward_mint_info` belongs to the token program
+/// * ✅ `reward_authority_info` is seed of `lending_market_info`, `reserve_info`, `reward_mint_info`
 fn check_and_unpack_pool_reward_accounts<'a, 'info>(
     program_id: &Pubkey,
     bumps: Bumps,
@@ -132,3 +132,463 @@ fn check_and_unpack_pool_reward_accounts<'a, 'info>(
 
     Ok((lending_market, reserve))
 }
+
+#[cfg(test)]
+mod tests {
+    //! For each ✅ in [check_and_unpack_pool_reward_accounts] and
+    //! [check_and_unpack_pool_reward_accounts_for_admin_ixs] there is a test
+    //! that expects a failure if that conditions is not met.
+
+    use solana_program::system_program;
+    use solend_sdk::{
+        instruction::find_reward_vault_authority,
+        state::{discriminator::AccountDiscriminator, Reserve},
+    };
+    use spl_token::state::Mint;
+
+    use super::*;
+
+    #[test]
+    fn test_check_and_unpack_pool_reward_accounts_ok() {
+        let (account_info_builders, bumps) = CheckAndUnpackPoolRewardAccountInfoBuilders::new();
+
+        account_info_builders
+            .check_and_unpack_pool_reward_accounts(crate::id(), bumps)
+            .expect("Should succeed");
+    }
+
+    /// ❌ `reserve_info` belongs to this program
+    #[test]
+    fn test_fails_if_reserve_info_does_not_belong_to_program() {
+        let (mut account_info_builders, bumps) = CheckAndUnpackPoolRewardAccountInfoBuilders::new();
+        account_info_builders.reserve.owner = Pubkey::new_unique();
+
+        account_info_builders
+            .check_and_unpack_pool_reward_accounts(crate::id(), bumps)
+            .expect_err("Should fail");
+    }
+
+    /// ❌ `reserve_info` unpacks
+    #[test]
+    fn test_fails_if_reserve_info_does_not_unpack() {
+        let (mut account_info_builders, bumps) = CheckAndUnpackPoolRewardAccountInfoBuilders::new();
+        account_info_builders.reserve.data = vec![0; Reserve::get_packed_len() - 1];
+
+        account_info_builders
+            .check_and_unpack_pool_reward_accounts(crate::id(), bumps)
+            .expect_err("Should fail");
+    }
+
+    /// ❌ `reserve_info` belongs to `lending_market_info`
+    #[test]
+    fn test_fails_if_reserve_info_does_not_belong_to_lending_market() {
+        let (mut account_info_builders, bumps) = CheckAndUnpackPoolRewardAccountInfoBuilders::new();
+
+        account_info_builders.reserve = AccountInfoBuilder::from(Reserve {
+            discriminator: AccountDiscriminator::Reserve,
+            lending_market: Pubkey::new_unique(),
+            ..Default::default()
+        });
+
+        account_info_builders
+            .check_and_unpack_pool_reward_accounts(crate::id(), bumps)
+            .expect_err("Should fail");
+    }
+
+    ///  ❌ `lending_market_info` belongs to this program
+    #[test]
+    fn test_fails_if_lending_market_info_does_not_belong_to_program() {
+        let (mut account_info_builders, bumps) = CheckAndUnpackPoolRewardAccountInfoBuilders::new();
+        account_info_builders.lending_market.owner = Pubkey::new_unique();
+
+        account_info_builders
+            .check_and_unpack_pool_reward_accounts(crate::id(), bumps)
+            .expect_err("Should fail");
+    }
+
+    ///  ❌ `lending_market_info` unpacks
+    #[test]
+    fn test_fails_if_lending_market_info_does_not_unpack() {
+        let (mut account_info_builders, bumps) = CheckAndUnpackPoolRewardAccountInfoBuilders::new();
+        account_info_builders.lending_market.data = vec![0; LendingMarket::get_packed_len() - 1];
+
+        account_info_builders
+            .check_and_unpack_pool_reward_accounts(crate::id(), bumps)
+            .expect_err("Should fail");
+    }
+
+    ///  ❌ `token_program_info` matches `lending_market_info`
+    #[test]
+    fn test_fails_if_token_program_info_does_not_match_lending_market() {
+        let (mut account_info_builders, bumps) = CheckAndUnpackPoolRewardAccountInfoBuilders::new();
+
+        account_info_builders.lending_market = AccountInfoBuilder::from(LendingMarket {
+            discriminator: AccountDiscriminator::LendingMarket,
+            token_program_id: Pubkey::new_unique(),
+            ..Default::default()
+        });
+
+        account_info_builders
+            .check_and_unpack_pool_reward_accounts(crate::id(), bumps)
+            .expect_err("Should fail");
+    }
+
+    ///  ❌ `reward_mint_info` belongs to the token program
+    #[test]
+    fn test_fails_if_reward_mint_info_does_not_belong_to_token_program() {
+        let (mut account_info_builders, bumps) = CheckAndUnpackPoolRewardAccountInfoBuilders::new();
+        account_info_builders.mint.owner = Pubkey::new_unique();
+
+        account_info_builders
+            .check_and_unpack_pool_reward_accounts(crate::id(), bumps)
+            .expect_err("Should fail");
+    }
+
+    /// ❌ `reward_authority_info` is seed of `lending_market_info`, `reserve_info`, `reward_mint_info`
+    #[test]
+    fn test_fails_if_reward_authority_info_is_not_seed() {
+        let (mut account_info_builders, og_bumps) =
+            CheckAndUnpackPoolRewardAccountInfoBuilders::new();
+        let og_reward_authority = account_info_builders.reward_authority.key;
+
+        // wrong lending market
+
+        let (new_reward_authority, new_reward_authority_bump) = find_reward_vault_authority(
+            &crate::id(),
+            &Pubkey::new_unique(),
+            &account_info_builders.reserve.key,
+            &account_info_builders.mint.key,
+        );
+        account_info_builders.reward_authority.key = new_reward_authority;
+        account_info_builders
+            .clone()
+            .check_and_unpack_pool_reward_accounts(
+                crate::id(),
+                Bumps {
+                    reward_authority: new_reward_authority_bump,
+                },
+            )
+            .expect_err("Should fail");
+
+        // wrong reserve
+
+        let (new_reward_authority, new_reward_authority_bump) = find_reward_vault_authority(
+            &crate::id(),
+            &account_info_builders.lending_market.key,
+            &Pubkey::new_unique(),
+            &account_info_builders.mint.key,
+        );
+        account_info_builders.reward_authority.key = new_reward_authority;
+        account_info_builders
+            .clone()
+            .check_and_unpack_pool_reward_accounts(
+                crate::id(),
+                Bumps {
+                    reward_authority: new_reward_authority_bump,
+                },
+            )
+            .expect_err("Should fail");
+
+        // wrong mint
+
+        let (new_reward_authority, new_reward_authority_bump) = find_reward_vault_authority(
+            &crate::id(),
+            &account_info_builders.lending_market.key,
+            &account_info_builders.reserve.key,
+            &Pubkey::new_unique(),
+        );
+        account_info_builders.reward_authority.key = new_reward_authority;
+        account_info_builders
+            .clone()
+            .check_and_unpack_pool_reward_accounts(
+                crate::id(),
+                Bumps {
+                    reward_authority: new_reward_authority_bump,
+                },
+            )
+            .expect_err("Should fail");
+
+        // wrong bump
+
+        account_info_builders.reward_authority.key = og_reward_authority;
+        account_info_builders
+            .clone()
+            .check_and_unpack_pool_reward_accounts(
+                crate::id(),
+                Bumps {
+                    reward_authority: og_bumps.reward_authority.wrapping_add(1),
+                },
+            )
+            .expect_err("Should fail");
+    }
+
+    #[test]
+    fn test_check_and_unpack_pool_reward_accounts_for_admin_ixs_ok() {
+        let (account_info_builders, bumps) = CheckAndUnpackPoolRewardAccountInfoBuilders::new();
+
+        account_info_builders
+            .check_and_unpack_pool_reward_accounts_for_admin_ixs(crate::id(), bumps)
+            .expect("Should succeed");
+    }
+
+    /// ❌ `lending_market_owner_info` is a signer
+    #[test]
+    fn test_fails_if_lending_market_owner_info_is_not_signer() {
+        let (mut account_info_builders, bumps) = CheckAndUnpackPoolRewardAccountInfoBuilders::new();
+        account_info_builders.lending_market_owner.is_signer = false;
+
+        account_info_builders
+            .check_and_unpack_pool_reward_accounts_for_admin_ixs(crate::id(), bumps)
+            .expect_err("Should fail");
+    }
+
+    /// ❌ `lending_market_owner_info` matches `lending_market_info`
+    #[test]
+    fn test_fails_if_lending_market_owner_info_does_not_match_lending_market() {
+        let (mut account_info_builders, bumps) = CheckAndUnpackPoolRewardAccountInfoBuilders::new();
+        account_info_builders.lending_market_owner.key = Pubkey::new_unique();
+
+        account_info_builders
+            .check_and_unpack_pool_reward_accounts_for_admin_ixs(crate::id(), bumps)
+            .expect_err("Should fail");
+    }
+
+    #[derive(Clone)]
+    struct CheckAndUnpackPoolRewardAccountInfoBuilders {
+        lending_market: AccountInfoBuilder,
+        lending_market_owner: AccountInfoBuilder,
+        mint: AccountInfoBuilder,
+        reserve: AccountInfoBuilder,
+        reward_authority: AccountInfoBuilder,
+        token_program: AccountInfoBuilder,
+    }
+
+    #[derive(Clone)]
+    struct AccountInfoBuilder {
+        key: Pubkey,
+        lamports: u64,
+        data: Vec<u8>,
+        owner: Pubkey,
+        rent_epoch: u64,
+        is_signer: bool,
+        is_writable: bool,
+        is_executable: bool,
+    }
+
+    impl CheckAndUnpackPoolRewardAccountInfoBuilders {
+        fn new() -> (Self, Bumps) {
+            let token_program = AccountInfoBuilder::new_token_program();
+            let lending_market_owner = AccountInfoBuilder::new_lending_market_owner();
+            let lending_market: AccountInfoBuilder = AccountInfoBuilder::from(LendingMarket {
+                discriminator: AccountDiscriminator::LendingMarket,
+                token_program_id: token_program.key,
+                owner: lending_market_owner.key,
+                ..Default::default()
+            });
+            let mint = AccountInfoBuilder::from(Mint {
+                is_initialized: true,
+                ..Default::default()
+            });
+            let reserve = AccountInfoBuilder::from(Reserve {
+                discriminator: AccountDiscriminator::Reserve,
+                lending_market: lending_market.key,
+                ..Default::default()
+            });
+            let (reward_authority, bumps) = AccountInfoBuilder::new_reward_authority(
+                &lending_market.key,
+                &reserve.key,
+                &mint.key,
+            );
+
+            (
+                Self {
+                    lending_market_owner,
+                    lending_market,
+                    mint,
+                    reserve,
+                    reward_authority,
+                    token_program,
+                },
+                bumps,
+            )
+        }
+
+        fn check_and_unpack_pool_reward_accounts(
+            mut self,
+            program_id: Pubkey,
+            bumps: Bumps,
+        ) -> Result<(), ProgramError> {
+            let lending_market_info = self.lending_market.as_account_info();
+            let mint_info = self.mint.as_account_info();
+            let reserve_info = self.reserve.as_account_info();
+            let reward_authority_info = self.reward_authority.as_account_info();
+            let token_program_info = self.token_program.as_account_info();
+
+            check_and_unpack_pool_reward_accounts(
+                &program_id,
+                bumps,
+                CheckAndUnpackPoolRewardAccounts {
+                    lending_market_info: &lending_market_info,
+                    reserve_info: &reserve_info,
+                    reward_authority_info: &reward_authority_info,
+                    reward_mint_info: &mint_info,
+                    token_program_info: &token_program_info,
+                },
+            )
+            .map(drop)
+        }
+
+        fn check_and_unpack_pool_reward_accounts_for_admin_ixs(
+            mut self,
+            program_id: Pubkey,
+            bumps: Bumps,
+        ) -> Result<(), ProgramError> {
+            let lending_market_info = self.lending_market.as_account_info();
+            let mint_info = self.mint.as_account_info();
+            let reserve_info = self.reserve.as_account_info();
+            let reward_authority_info = self.reward_authority.as_account_info();
+            let token_program_info = self.token_program.as_account_info();
+            let lending_market_owner_info = self.lending_market_owner.as_account_info();
+
+            check_and_unpack_pool_reward_accounts_for_admin_ixs(
+                &program_id,
+                bumps,
+                CheckAndUnpackPoolRewardAccounts {
+                    lending_market_info: &lending_market_info,
+                    reserve_info: &reserve_info,
+                    reward_authority_info: &reward_authority_info,
+                    reward_mint_info: &mint_info,
+                    token_program_info: &token_program_info,
+                },
+                &lending_market_owner_info,
+            )
+            .map(drop)
+        }
+    }
+
+    impl From<LendingMarket> for AccountInfoBuilder {
+        fn from(lending_market: LendingMarket) -> Self {
+            let mut data = vec![0; LendingMarket::get_packed_len()];
+            LendingMarket::pack(lending_market, &mut data).unwrap();
+
+            Self {
+                key: Pubkey::new_unique(),
+                lamports: 1,
+                data,
+                owner: crate::id(),
+                rent_epoch: 0,
+                is_signer: false,
+                is_writable: false,
+                is_executable: false,
+            }
+        }
+    }
+
+    impl From<Mint> for AccountInfoBuilder {
+        fn from(mint: Mint) -> Self {
+            let mut data = vec![0; Mint::get_packed_len()];
+            Mint::pack(mint, &mut data).unwrap();
+
+            Self {
+                key: Pubkey::new_unique(),
+                lamports: 1,
+                data,
+                owner: spl_token::id(),
+                rent_epoch: 0,
+                is_signer: false,
+                is_writable: false,
+                is_executable: false,
+            }
+        }
+    }
+
+    impl From<Reserve> for AccountInfoBuilder {
+        fn from(reserve: Reserve) -> Self {
+            let mut data = vec![0; Reserve::get_packed_len()];
+            Reserve::pack(reserve, &mut data).unwrap();
+
+            Self {
+                key: Pubkey::new_unique(),
+                lamports: 1,
+                data,
+                owner: crate::id(),
+                rent_epoch: 0,
+                is_signer: false,
+                is_writable: false,
+                is_executable: false,
+            }
+        }
+    }
+
+    impl AccountInfoBuilder {
+        fn as_account_info(&mut self) -> AccountInfo {
+            AccountInfo::new(
+                &self.key,
+                self.is_signer,
+                self.is_writable,
+                &mut self.lamports,
+                &mut self.data,
+                &self.owner,
+                self.is_executable,
+                self.rent_epoch,
+            )
+        }
+
+        fn new_token_program() -> Self {
+            Self {
+                key: spl_token::id(),
+                lamports: 0,
+                data: vec![],
+                owner: system_program::id(),
+                rent_epoch: 0,
+                is_signer: false,
+                is_writable: false,
+                is_executable: true,
+            }
+        }
+
+        fn new_reward_authority(
+            lending_market_key: &Pubkey,
+            reserve_key: &Pubkey,
+            reward_mint_key: &Pubkey,
+        ) -> (Self, Bumps) {
+            let (key, bump) = find_reward_vault_authority(
+                &crate::id(),
+                lending_market_key,
+                reserve_key,
+                reward_mint_key,
+            );
+
+            let s = Self {
+                key,
+                lamports: 0,
+                data: vec![],
+                owner: system_program::id(),
+                rent_epoch: 0,
+                is_signer: false,
+                is_writable: false,
+                is_executable: false,
+            };
+
+            (
+                s,
+                Bumps {
+                    reward_authority: bump,
+                },
+            )
+        }
+
+        fn new_lending_market_owner() -> Self {
+            Self {
+                key: Pubkey::new_unique(),
+                lamports: 0,
+                data: vec![],
+                owner: system_program::id(),
+                rent_epoch: 0,
+                is_signer: true,
+                is_writable: false,
+                is_executable: false,
+            }
+        }
+    }
+}
diff --git a/token-lending/program/tests/refresh_obligation.rs b/token-lending/program/tests/refresh_obligation.rs
index 6c3397a897f..ac64044de4a 100644
--- a/token-lending/program/tests/refresh_obligation.rs
+++ b/token-lending/program/tests/refresh_obligation.rs
@@ -544,7 +544,7 @@ async fn test_normalize_obligation() {
         ..Obligation::default()
     };
 
-    let mut packed_obligation = vec![0; obligation.size_in_bytes_when_packed()];
+    let mut packed_obligation = vec![0; obligation.get_packed_len()];
     obligation.pack_into_slice(&mut packed_obligation);
     test.add_packed(
         obligation_pubkey,
diff --git a/token-lending/sdk/src/state/lending_market.rs b/token-lending/sdk/src/state/lending_market.rs
index 3185a69caa7..2d699eaa39a 100644
--- a/token-lending/sdk/src/state/lending_market.rs
+++ b/token-lending/sdk/src/state/lending_market.rs
@@ -223,12 +223,12 @@ impl Pack for LendingMarket {
 }
 
 #[cfg(test)]
-mod test {
+pub(crate) mod test {
     use super::*;
     use rand::Rng;
 
     impl LendingMarket {
-        fn new_rand(rng: &mut impl Rng) -> Self {
+        pub(crate) fn new_rand(rng: &mut impl Rng) -> Self {
             Self {
                 discriminator: AccountDiscriminator::LendingMarket,
                 bump_seed: rng.gen(),
diff --git a/token-lending/sdk/src/state/liquidity_mining/user_reward_manager.rs b/token-lending/sdk/src/state/liquidity_mining/user_reward_manager.rs
index b94dba18755..558bd09a936 100644
--- a/token-lending/sdk/src/state/liquidity_mining/user_reward_manager.rs
+++ b/token-lending/sdk/src/state/liquidity_mining/user_reward_manager.rs
@@ -379,7 +379,7 @@ impl UserRewardManager {
     }
 
     /// How many bytes are needed to pack this [UserRewardManager].
-    pub(crate) fn size_in_bytes_when_packed(&self) -> usize {
+    pub(crate) fn get_packed_len(&self) -> usize {
         Self::HEAD_LEN + self.rewards.len() * UserReward::LEN
     }
 
diff --git a/token-lending/sdk/src/state/obligation.rs b/token-lending/sdk/src/state/obligation.rs
index b1c85e6e560..c88a95ccb59 100644
--- a/token-lending/sdk/src/state/obligation.rs
+++ b/token-lending/sdk/src/state/obligation.rs
@@ -499,8 +499,8 @@ impl Obligation {
     pub 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 {
+    /// How many bytes are needed to pack this [Obligation].
+    pub fn get_packed_len(&self) -> usize {
         if self.user_reward_managers.is_empty() {
             return OBLIGATION_LEN_V2_0_2;
         }
@@ -508,7 +508,7 @@ impl Obligation {
         let mut size = OBLIGATION_LEN_V2_0_2 + 1;
 
         for reward_manager in self.user_reward_managers.iter() {
-            size += reward_manager.size_in_bytes_when_packed();
+            size += reward_manager.get_packed_len();
         }
 
         size
@@ -671,7 +671,7 @@ impl Obligation {
             let mut offset = OBLIGATION_LEN_V2_0_2 + 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();
+                offset += user_reward_manager.get_packed_len();
             }
         } else if dst.len() > OBLIGATION_LEN_V2_0_2 {
             // set the length to 0 if obligation was resized before
@@ -795,7 +795,7 @@ impl Obligation {
                 let mut offset = OBLIGATION_LEN_V2_0_2 + 1;
                 for _ in 0..*len {
                     let user_reward_manager = UserRewardManager::unpack_from_slice(&src[offset..])?;
-                    offset += user_reward_manager.size_in_bytes_when_packed();
+                    offset += user_reward_manager.get_packed_len();
                     user_reward_managers.push(user_reward_manager);
                 }
 

From 9c615e0db31a3841a51ca90722252f37bb9d26f2 Mon Sep 17 00:00:00 2001
From: vanity <vanity@solend.fi>
Date: Tue, 15 Apr 2025 19:07:43 +0200
Subject: [PATCH 14/14] Documenting liquidity mining

---
 token-lending/LIQUIDITY_MINING.md             | 217 ++++++++++++++++++
 .../liquidity_mining/cancel_pool_reward.rs    |   2 +-
 .../liquidity_mining/pool_reward_manager.rs   |   4 +-
 3 files changed, 220 insertions(+), 3 deletions(-)
 create mode 100644 token-lending/LIQUIDITY_MINING.md

diff --git a/token-lending/LIQUIDITY_MINING.md b/token-lending/LIQUIDITY_MINING.md
new file mode 100644
index 00000000000..5fe3325abeb
--- /dev/null
+++ b/token-lending/LIQUIDITY_MINING.md
@@ -0,0 +1,217 @@
+# Liquidity Mining
+
+## Overview
+
+The liquidity mining feature models the same feature implemented in [Suilend][suilend-lm].
+In a gist we track deposits and borrows for each reserve in two structures: pool reward manager that exist on each reserve and user reward manager that exist on linked obligations.
+
+Deposits increase total pool shares and user shares by the exact amount of collateral token deposited into an obligation.
+Collateral token that is _not_ deposited into any obligation does not count toward the total pool shares.
+
+Conversely, withdraws decrease total pool shares and user shares by the exact amount of collateral token withdrawn from an obligation.
+
+Similarly, borrows increase total pool shares and user shares.
+However, the amount of shares is determined by "liability shares".
+Liability shares are calculated for an obligation as `(borrow_amount / cumulative_borrow_rate)`.
+
+Conversely, repays decrease total pool shares and user shares.
+
+When a user deposits, withdraws, borrows or repays, we calculate their effective shares and then set them rather than incrementing/decrementing them.
+
+An obligation can also be liquidated which is a process of repaying and withdrawing.
+This adequately updates the pool reward manager and user reward manager deposit shares for the withdraw reserve and liability shares for the repay reserve.
+
+An obligation's debt can also be forgiven.
+This is an act of repaying and the liability shares are updated accordingly.
+
+## Differences to Suilend
+
+In Suilend a reserve can have at most 50 rewards.
+However, Sui dynamic object model let's us store more data easily.
+In Save we're storing the data on the reserve and this means packing and
+unpacking it frequently which negatively impacts CU limits.
+We lower the number of rewards to 30.
+In Save, if we want to add new rewards we will crank old ones to make space
+in the reserve if there isn't any.
+
+In Suilend we 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 an extra packed decimal (16 bytes) on each reserve's pool reward (30).
+
+## New ixs
+
+There's a common concept of reward vault and reward vault authority across the ixs.
+A reward vault is a token account that stores reward tokens for a specific pool reward.
+A reward vault authority is a PDA that is used to sign CPIs into the token program for the reward vault.
+
+```rust
+// the seeds for the reward vault authority
+[
+    b"RewardVaultAuthority",
+    lending_market_key,
+    reserve_key,
+    reward_mint_key,
+]
+```
+
+> TBD: Should we use the reward vault token account pubkey instead to create a 1-1 relationship between the authority and the vault?
+> What will be easier for the clients to use?
+
+### `add_pool_reward`
+
+Admin only ix that adds a new pool reward to a reserve's reward manager, either a deposit or a borrow one.
+This ix will fail if all slots are occupied.
+
+There's a minimum reward period of 1 hour, no short rewards are allowed.
+
+Each pool reward has a unique vault that holds the reward tokens.
+This vault account must be created for the token program before calling this ix.
+In this ix we initialize the account as token account and transfer the reward tokens to it from the admin's token account.
+
+### `cancel_pool_reward`
+
+This ix sets the end time of the pool reward to "now" and returns any unallocated rewards to the admin.
+Users will still be able to claim rewards they accrued until this point.
+
+### `claim_pool_reward`
+
+Permission-less way to claim allocated user liquidity mining rewards.
+
+It finds the UserRewardManager for the reserve and obligation and withdraws
+all eligible rewards from it.
+The eligible rewards are then transferred to the obligation owners's token account.
+
+Anyone can call this ix which is useful for cranking.
+
+Alternatively, if the obligation is not yet migrated, this does the migration for the obligation as well.
+See [Migrations](#migrations) section for more details.
+
+### `close_pool_reward`
+
+Closes a pool reward, making its slot vacant and ready for a new reward.
+
+Before closing a pool reward that pool reward must first be cancelled and all rewards must be claimed by the users.
+
+### `upgrade_reserve`
+
+Temporary ix to upgrade a reserve to LM feature added in @v2.0.2.
+Fails if reserve was not sized as @v2.0.2 (ie. has been upgraded or created with @v2.1.00).
+
+Until this ix is called for a Reserve account, all other ixs that try to unpack the Reserve will fail due to size mismatch.
+
+## Changes
+
+This section is partly relevant also to client implementations.
+There are breaking changes introduced with this version.
+
+### First byte of each account is discriminator
+
+In @v2.0.2 the first byte of any _initialized_ account was set to the program version, ie. `0x01`.
+Once any account is mutably packed in @v2.1.0, the first byte will be set to the account discriminator:
+
+```rust
+/// 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 = 2,
+    /// [crate::state::Reserve]
+    Reserve = 3,
+    /// [crate::state::Obligation]
+    Obligation = 4,
+}
+```
+
+### Obligations need more rent
+
+Because obligations track user rewards that depend on the number of reserves and the rewards that those reserves have, we now dynamically reallocate size of obligation accounts.
+
+This means that sometimes obligations will need more rent than before and this rent must be (`system_program`) transferred to the obligation account before any interaction with the borrow-lending program.
+
+The calculation for an upper-bound of an obligation size from which rent-exempt balance is calculated:
+
+```math
+\overline{s} = 1301 + \sum_{i=0}^{n} 50 + 37 * m_{i}
+```
+
+Where $`n`$ is the number of reserves in the obligation and $`m_{i}`$ is the number of pool rewards in obligation's reserve manager $`i`$.
+
+This is an upper-bound because some of those pool rewards might be over and therefore wouldn't be copied to the obligation.
+
+The particular obligation's reserve manager depends on whether the obligation is a borrow or deposit obligation.
+
+### Reserve size increased
+
+Migrated reserve accounts are sized at 5451 bytes.
+
+### CUs increased for all reserve/obligation related ixs
+
+We increase the reserve size and the obligation size which costs more compute when (un)packing.
+Additionally, we now write to the reserve account on withdrawal to update the total shares.
+
+All this means more CUs are needed for ixs to succeed.
+Additionally, the CUs increase linearly with the number of rewards in each involved reserve.
+
+> TBD: Let's review together the limits used in the present client implementation.
+
+### Reserve account in processor is protected by runtime borrow checker
+
+In @v2.0.2 access to reserve account followed a pattern of unpacking an immutable reference to a cloned memory location, working with it and then mutably packing it back to the original location.
+This introduced extra up(pack)ing operations and was prone to double spend bugs.
+
+In this version we're leveraging the `solana_program` framework's usage of `Cell` container.
+We keep a `Ref`/`RefMut` around in a wrapper struct along with the unpacked reserve struct and automatically pack it back to the original location when `RefMut` is dropped.
+This way we guarantee at runtime that only one mutable reference to the reserve exists at any time.
+
+## Migrations
+
+### `Reserve`
+
+There's a CLI command for `UpgradeReserveToV2_1_0` ix to permission-lessly upgrade a reserve account.
+Once upgraded any subsequent calls to this ix for the specific reserve will fail.
+The upgrade requires 4832 extra bytes which amounts to ~0.035 $SOL.
+Some reserves have extra rent and won't require the full amount.
+The `UpgradeReserveToV2_1_0` ix can be delete as soon as all reserves are migrated.
+
+### `Obligation`
+
+To start tracking rewards for an obligation we need to set its shares to the appropriate amount.
+They are at 0 before the obligation is fully migrated.
+
+We can call `claim_pool_reward` ix to do this, or any deposit/withdraw/repay/borrow ix.
+
+> Prior to version @2.1.0 there was no concept of liq. mining.
+> That means user shares are going to be 0 even if they have a borrow or deposit.
+> This ix can be used to start tracking obligation's rewards.
+
+The obligation will be reallocated if it needs more space to add extra rewards.
+Client must ensure that the obligation has enough rent-exempt balance.
+All obligations would benefit from a extra airdropped rent about `1 + 50 * obligation_reserves` lamports.
+
+### `LendingMarket`
+
+The lending market account is not changed in this version except for the first byte discriminator.
+
+A lending market will be automatically upgraded on the first mutable ix.
+
+## Outstanding work
+
+- [ ] Review feature parity with Suilend
+- [ ] Consider changing the reward vault authority seed
+- [ ] Consider having another admin account to manage the rewards
+- [ ] Consider spending some rent to the obligations from the reclaimed merkle-tree reward distributor
+- [ ] Discuss CU limits with the Save client team
+
+<!-- List of References -->
+
+[suilend-lm]: https://github.com/solendprotocol/suilend/blob/dc53150416f352053ac3acbb320ee143409c4a5d/contracts/suilend/sources/liquidity_mining.move#L2
diff --git a/token-lending/program/src/processor/liquidity_mining/cancel_pool_reward.rs b/token-lending/program/src/processor/liquidity_mining/cancel_pool_reward.rs
index 12677690448..29f1ab9ee7b 100644
--- a/token-lending/program/src/processor/liquidity_mining/cancel_pool_reward.rs
+++ b/token-lending/program/src/processor/liquidity_mining/cancel_pool_reward.rs
@@ -1,6 +1,6 @@
 //! Cancel a pool reward.
 //!
-//! This ix sets the end time of the pool reward to now are returns any
+//! This ix sets the end time of the pool reward to now and returns any
 //! unallocated rewards to the admin.
 //! Users will still be able to claim rewards.
 
diff --git a/token-lending/sdk/src/state/liquidity_mining/pool_reward_manager.rs b/token-lending/sdk/src/state/liquidity_mining/pool_reward_manager.rs
index b255b88a7e6..370287c687c 100644
--- a/token-lending/sdk/src/state/liquidity_mining/pool_reward_manager.rs
+++ b/token-lending/sdk/src/state/liquidity_mining/pool_reward_manager.rs
@@ -83,8 +83,8 @@ pub enum PoolRewardSlot {
 /// 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].
+/// However, this can be calculated dynamically which avoids storing an extra
+/// packed [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