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::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>, ) -> Result, 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>, ) -> Result, 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>, ) -> Result, 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>, ) -> Result, 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::(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::(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 { &self, test: &mut SolendProgramTest, reserve: &Info, - 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 { ), add_pool_reward( solend_program::id(), + reward_authority_bump, position_kind, start_time_secs, end_time_secs, @@ -948,20 +960,91 @@ impl Info { 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, + 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, + 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;