1256 lines
39 KiB
Rust
1256 lines
39 KiB
Rust
#![allow(dead_code)]
|
|
|
|
pub mod flash_loan_receiver;
|
|
pub mod genesis;
|
|
|
|
use assert_matches::*;
|
|
use solana_program::{program_option::COption, program_pack::Pack, pubkey::Pubkey};
|
|
use solana_program_test::*;
|
|
use solana_sdk::{
|
|
account::Account,
|
|
signature::{read_keypair_file, Keypair, Signer},
|
|
system_instruction::create_account,
|
|
transaction::{Transaction, TransactionError},
|
|
};
|
|
use spl_token::{
|
|
instruction::approve,
|
|
state::{Account as Token, AccountState, Mint},
|
|
};
|
|
use spl_token_lending::{
|
|
instruction::{
|
|
borrow_obligation_liquidity, deposit_reserve_liquidity, init_lending_market,
|
|
init_obligation, init_reserve, liquidate_obligation, refresh_reserve,
|
|
},
|
|
math::{Decimal, Rate, TryAdd, TryMul},
|
|
pyth,
|
|
state::{
|
|
InitLendingMarketParams, InitObligationParams, InitReserveParams, LendingMarket,
|
|
NewReserveCollateralParams, NewReserveLiquidityParams, Obligation, ObligationCollateral,
|
|
ObligationLiquidity, Reserve, ReserveCollateral, ReserveConfig, ReserveFees,
|
|
ReserveLiquidity, INITIAL_COLLATERAL_RATIO, PROGRAM_VERSION,
|
|
},
|
|
};
|
|
use std::{convert::TryInto, str::FromStr};
|
|
|
|
pub const QUOTE_CURRENCY: [u8; 32] =
|
|
*b"USD\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";
|
|
|
|
pub const LAMPORTS_TO_SOL: u64 = 1_000_000_000;
|
|
pub const FRACTIONAL_TO_USDC: u64 = 1_000_000;
|
|
|
|
pub const TEST_RESERVE_CONFIG: ReserveConfig = ReserveConfig {
|
|
optimal_utilization_rate: 80,
|
|
loan_to_value_ratio: 50,
|
|
liquidation_bonus: 5,
|
|
liquidation_threshold: 55,
|
|
min_borrow_rate: 0,
|
|
optimal_borrow_rate: 4,
|
|
max_borrow_rate: 30,
|
|
fees: ReserveFees {
|
|
/// 0.00001% (Aave borrow fee)
|
|
borrow_fee_wad: 100_000_000_000,
|
|
/// 0.3% (Aave flash loan fee)
|
|
flash_loan_fee_wad: 3_000_000_000_000_000,
|
|
host_fee_percentage: 20,
|
|
},
|
|
};
|
|
|
|
pub const SOL_PYTH_PRODUCT: &str = "3Mnn2fX6rQyUsyELYms1sBJyChWofzSNRoqYzvgMVz5E";
|
|
pub const SOL_PYTH_PRICE: &str = "J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix";
|
|
|
|
pub const SRM_PYTH_PRODUCT: &str = "6MEwdxe4g1NeAF9u6KDG14anJpFsVEa2cvr5H6iriFZ8";
|
|
pub const SRM_PYTH_PRICE: &str = "992moaMQKs32GKZ9dxi8keyM2bUmbrwBZpK4p2K6X5Vs";
|
|
|
|
pub const USDC_MINT: &str = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
|
|
|
|
trait AddPacked {
|
|
fn add_packable_account<T: Pack>(
|
|
&mut self,
|
|
pubkey: Pubkey,
|
|
amount: u64,
|
|
data: &T,
|
|
owner: &Pubkey,
|
|
);
|
|
}
|
|
|
|
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);
|
|
self.add_account(pubkey, account);
|
|
}
|
|
}
|
|
|
|
pub fn add_lending_market(test: &mut ProgramTest) -> TestLendingMarket {
|
|
let lending_market_pubkey = Pubkey::new_unique();
|
|
let (lending_market_authority, bump_seed) =
|
|
Pubkey::find_program_address(&[lending_market_pubkey.as_ref()], &spl_token_lending::id());
|
|
|
|
let lending_market_owner =
|
|
read_keypair_file("tests/fixtures/lending_market_owner.json").unwrap();
|
|
let oracle_program_id = read_keypair_file("tests/fixtures/oracle_program_id.json")
|
|
.unwrap()
|
|
.pubkey();
|
|
|
|
test.add_packable_account(
|
|
lending_market_pubkey,
|
|
u32::MAX as u64,
|
|
&LendingMarket::new(InitLendingMarketParams {
|
|
bump_seed,
|
|
owner: lending_market_owner.pubkey(),
|
|
quote_currency: QUOTE_CURRENCY,
|
|
token_program_id: spl_token::id(),
|
|
oracle_program_id,
|
|
}),
|
|
&spl_token_lending::id(),
|
|
);
|
|
|
|
TestLendingMarket {
|
|
pubkey: lending_market_pubkey,
|
|
owner: lending_market_owner,
|
|
authority: lending_market_authority,
|
|
quote_currency: QUOTE_CURRENCY,
|
|
oracle_program_id,
|
|
}
|
|
}
|
|
|
|
#[derive(Default)]
|
|
pub struct AddObligationArgs<'a> {
|
|
pub deposits: &'a [(&'a TestReserve, u64)],
|
|
pub borrows: &'a [(&'a TestReserve, u64)],
|
|
pub mark_fresh: bool,
|
|
pub slots_elapsed: u64,
|
|
}
|
|
|
|
pub fn add_obligation(
|
|
test: &mut ProgramTest,
|
|
lending_market: &TestLendingMarket,
|
|
user_accounts_owner: &Keypair,
|
|
args: AddObligationArgs,
|
|
) -> TestObligation {
|
|
let AddObligationArgs {
|
|
deposits,
|
|
borrows,
|
|
mark_fresh,
|
|
slots_elapsed,
|
|
} = args;
|
|
|
|
let obligation_keypair = Keypair::new();
|
|
let obligation_pubkey = obligation_keypair.pubkey();
|
|
|
|
let (obligation_deposits, test_deposits) = deposits
|
|
.iter()
|
|
.map(|(deposit_reserve, collateral_amount)| {
|
|
let mut collateral = ObligationCollateral::new(deposit_reserve.pubkey);
|
|
collateral.deposited_amount = *collateral_amount;
|
|
|
|
(
|
|
collateral,
|
|
TestObligationCollateral {
|
|
obligation_pubkey,
|
|
deposit_reserve: deposit_reserve.pubkey,
|
|
deposited_amount: *collateral_amount,
|
|
},
|
|
)
|
|
})
|
|
.unzip();
|
|
|
|
let (obligation_borrows, test_borrows) = borrows
|
|
.iter()
|
|
.map(|(borrow_reserve, liquidity_amount)| {
|
|
let borrowed_amount_wads = Decimal::from(*liquidity_amount);
|
|
|
|
let mut liquidity = ObligationLiquidity::new(borrow_reserve.pubkey);
|
|
liquidity.borrowed_amount_wads = borrowed_amount_wads;
|
|
|
|
(
|
|
liquidity,
|
|
TestObligationLiquidity {
|
|
obligation_pubkey,
|
|
borrow_reserve: borrow_reserve.pubkey,
|
|
borrowed_amount_wads,
|
|
},
|
|
)
|
|
})
|
|
.unzip();
|
|
|
|
let current_slot = slots_elapsed + 1;
|
|
|
|
let mut obligation = Obligation::new(InitObligationParams {
|
|
// intentionally wrapped to simulate elapsed slots
|
|
current_slot,
|
|
lending_market: lending_market.pubkey,
|
|
owner: user_accounts_owner.pubkey(),
|
|
deposits: obligation_deposits,
|
|
borrows: obligation_borrows,
|
|
});
|
|
|
|
if mark_fresh {
|
|
obligation.last_update.update_slot(current_slot);
|
|
}
|
|
|
|
test.add_packable_account(
|
|
obligation_pubkey,
|
|
u32::MAX as u64,
|
|
&obligation,
|
|
&spl_token_lending::id(),
|
|
);
|
|
|
|
TestObligation {
|
|
pubkey: obligation_pubkey,
|
|
lending_market: lending_market.pubkey,
|
|
owner: user_accounts_owner.pubkey(),
|
|
deposits: test_deposits,
|
|
borrows: test_borrows,
|
|
}
|
|
}
|
|
|
|
#[derive(Default)]
|
|
pub struct AddReserveArgs {
|
|
pub name: String,
|
|
pub config: ReserveConfig,
|
|
pub liquidity_amount: u64,
|
|
pub liquidity_mint_pubkey: Pubkey,
|
|
pub liquidity_mint_decimals: u8,
|
|
pub user_liquidity_amount: u64,
|
|
pub borrow_amount: u64,
|
|
pub initial_borrow_rate: u8,
|
|
pub collateral_amount: u64,
|
|
pub mark_fresh: bool,
|
|
pub slots_elapsed: u64,
|
|
}
|
|
|
|
pub fn add_reserve(
|
|
test: &mut ProgramTest,
|
|
lending_market: &TestLendingMarket,
|
|
oracle: &TestOracle,
|
|
user_accounts_owner: &Keypair,
|
|
args: AddReserveArgs,
|
|
) -> TestReserve {
|
|
let AddReserveArgs {
|
|
name,
|
|
config,
|
|
liquidity_amount,
|
|
liquidity_mint_pubkey,
|
|
liquidity_mint_decimals,
|
|
user_liquidity_amount,
|
|
borrow_amount,
|
|
initial_borrow_rate,
|
|
collateral_amount,
|
|
mark_fresh,
|
|
slots_elapsed,
|
|
} = args;
|
|
|
|
let is_native = if liquidity_mint_pubkey == spl_token::native_mint::id() {
|
|
COption::Some(1)
|
|
} else {
|
|
COption::None
|
|
};
|
|
|
|
let current_slot = slots_elapsed + 1;
|
|
|
|
let collateral_mint_pubkey = Pubkey::new_unique();
|
|
test.add_packable_account(
|
|
collateral_mint_pubkey,
|
|
u32::MAX as u64,
|
|
&Mint {
|
|
is_initialized: true,
|
|
decimals: liquidity_mint_decimals,
|
|
mint_authority: COption::Some(lending_market.authority),
|
|
supply: collateral_amount,
|
|
..Mint::default()
|
|
},
|
|
&spl_token::id(),
|
|
);
|
|
|
|
let collateral_supply_pubkey = Pubkey::new_unique();
|
|
test.add_packable_account(
|
|
collateral_supply_pubkey,
|
|
u32::MAX as u64,
|
|
&Token {
|
|
mint: collateral_mint_pubkey,
|
|
owner: lending_market.authority,
|
|
amount: collateral_amount,
|
|
state: AccountState::Initialized,
|
|
..Token::default()
|
|
},
|
|
&spl_token::id(),
|
|
);
|
|
|
|
let amount = if let COption::Some(rent_reserve) = is_native {
|
|
liquidity_amount + rent_reserve
|
|
} else {
|
|
u32::MAX as u64
|
|
};
|
|
|
|
let liquidity_supply_pubkey = Pubkey::new_unique();
|
|
test.add_packable_account(
|
|
liquidity_supply_pubkey,
|
|
amount,
|
|
&Token {
|
|
mint: liquidity_mint_pubkey,
|
|
owner: lending_market.authority,
|
|
amount: liquidity_amount,
|
|
state: AccountState::Initialized,
|
|
is_native,
|
|
..Token::default()
|
|
},
|
|
&spl_token::id(),
|
|
);
|
|
|
|
let liquidity_fee_receiver_pubkey = Pubkey::new_unique();
|
|
test.add_packable_account(
|
|
liquidity_fee_receiver_pubkey,
|
|
u32::MAX as u64,
|
|
&Token {
|
|
mint: liquidity_mint_pubkey,
|
|
owner: lending_market.owner.pubkey(),
|
|
amount: 0,
|
|
state: AccountState::Initialized,
|
|
..Token::default()
|
|
},
|
|
&spl_token::id(),
|
|
);
|
|
|
|
let liquidity_host_pubkey = Pubkey::new_unique();
|
|
test.add_packable_account(
|
|
liquidity_host_pubkey,
|
|
u32::MAX as u64,
|
|
&Token {
|
|
mint: liquidity_mint_pubkey,
|
|
owner: user_accounts_owner.pubkey(),
|
|
amount: 0,
|
|
state: AccountState::Initialized,
|
|
..Token::default()
|
|
},
|
|
&spl_token::id(),
|
|
);
|
|
|
|
let reserve_keypair = Keypair::new();
|
|
let reserve_pubkey = reserve_keypair.pubkey();
|
|
let mut reserve = Reserve::new(InitReserveParams {
|
|
current_slot,
|
|
lending_market: lending_market.pubkey,
|
|
liquidity: ReserveLiquidity::new(NewReserveLiquidityParams {
|
|
mint_pubkey: liquidity_mint_pubkey,
|
|
mint_decimals: liquidity_mint_decimals,
|
|
supply_pubkey: liquidity_supply_pubkey,
|
|
fee_receiver: liquidity_fee_receiver_pubkey,
|
|
oracle_pubkey: oracle.price_pubkey,
|
|
market_price: oracle.price,
|
|
}),
|
|
collateral: ReserveCollateral::new(NewReserveCollateralParams {
|
|
mint_pubkey: collateral_mint_pubkey,
|
|
supply_pubkey: collateral_supply_pubkey,
|
|
}),
|
|
config,
|
|
});
|
|
reserve.deposit_liquidity(liquidity_amount).unwrap();
|
|
reserve.liquidity.borrow(borrow_amount.into()).unwrap();
|
|
let borrow_rate_multiplier = Rate::one()
|
|
.try_add(Rate::from_percent(initial_borrow_rate))
|
|
.unwrap();
|
|
reserve.liquidity.cumulative_borrow_rate_wads =
|
|
Decimal::one().try_mul(borrow_rate_multiplier).unwrap();
|
|
|
|
if mark_fresh {
|
|
reserve.last_update.update_slot(current_slot);
|
|
}
|
|
|
|
test.add_packable_account(
|
|
reserve_pubkey,
|
|
u32::MAX as u64,
|
|
&reserve,
|
|
&spl_token_lending::id(),
|
|
);
|
|
|
|
let amount = if let COption::Some(rent_reserve) = is_native {
|
|
user_liquidity_amount + rent_reserve
|
|
} else {
|
|
u32::MAX as u64
|
|
};
|
|
|
|
let user_liquidity_pubkey = Pubkey::new_unique();
|
|
test.add_packable_account(
|
|
user_liquidity_pubkey,
|
|
amount,
|
|
&Token {
|
|
mint: liquidity_mint_pubkey,
|
|
owner: user_accounts_owner.pubkey(),
|
|
amount: user_liquidity_amount,
|
|
state: AccountState::Initialized,
|
|
is_native,
|
|
..Token::default()
|
|
},
|
|
&spl_token::id(),
|
|
);
|
|
let user_collateral_pubkey = Pubkey::new_unique();
|
|
test.add_packable_account(
|
|
user_collateral_pubkey,
|
|
u32::MAX as u64,
|
|
&Token {
|
|
mint: collateral_mint_pubkey,
|
|
owner: user_accounts_owner.pubkey(),
|
|
amount: liquidity_amount * INITIAL_COLLATERAL_RATIO,
|
|
state: AccountState::Initialized,
|
|
..Token::default()
|
|
},
|
|
&spl_token::id(),
|
|
);
|
|
|
|
TestReserve {
|
|
name,
|
|
pubkey: reserve_pubkey,
|
|
lending_market_pubkey: lending_market.pubkey,
|
|
config,
|
|
liquidity_mint_pubkey,
|
|
liquidity_mint_decimals,
|
|
liquidity_supply_pubkey,
|
|
liquidity_fee_receiver_pubkey,
|
|
liquidity_host_pubkey,
|
|
liquidity_oracle_pubkey: oracle.price_pubkey,
|
|
collateral_mint_pubkey,
|
|
collateral_supply_pubkey,
|
|
user_liquidity_pubkey,
|
|
user_collateral_pubkey,
|
|
market_price: oracle.price,
|
|
}
|
|
}
|
|
|
|
pub fn add_account_for_program(
|
|
test: &mut ProgramTest,
|
|
program_derived_account: &Pubkey,
|
|
amount: u64,
|
|
mint_pubkey: &Pubkey,
|
|
) -> Pubkey {
|
|
let program_owned_token_account = Keypair::new();
|
|
test.add_packable_account(
|
|
program_owned_token_account.pubkey(),
|
|
u32::MAX as u64,
|
|
&Token {
|
|
mint: *mint_pubkey,
|
|
owner: *program_derived_account,
|
|
amount,
|
|
state: AccountState::Initialized,
|
|
is_native: COption::None,
|
|
..Token::default()
|
|
},
|
|
&spl_token::id(),
|
|
);
|
|
program_owned_token_account.pubkey()
|
|
}
|
|
|
|
pub struct TestLendingMarket {
|
|
pub pubkey: Pubkey,
|
|
pub owner: Keypair,
|
|
pub authority: Pubkey,
|
|
pub quote_currency: [u8; 32],
|
|
pub oracle_program_id: Pubkey,
|
|
}
|
|
|
|
pub struct BorrowArgs<'a> {
|
|
pub liquidity_amount: u64,
|
|
pub obligation: &'a TestObligation,
|
|
pub borrow_reserve: &'a TestReserve,
|
|
pub user_accounts_owner: &'a Keypair,
|
|
}
|
|
|
|
pub struct LiquidateArgs<'a> {
|
|
pub liquidity_amount: u64,
|
|
pub obligation: &'a TestObligation,
|
|
pub repay_reserve: &'a TestReserve,
|
|
pub withdraw_reserve: &'a TestReserve,
|
|
pub user_accounts_owner: &'a Keypair,
|
|
}
|
|
|
|
impl TestLendingMarket {
|
|
pub async fn init(banks_client: &mut BanksClient, payer: &Keypair) -> Self {
|
|
let lending_market_owner =
|
|
read_keypair_file("tests/fixtures/lending_market_owner.json").unwrap();
|
|
let oracle_program_id = read_keypair_file("tests/fixtures/oracle_program_id.json")
|
|
.unwrap()
|
|
.pubkey();
|
|
|
|
let lending_market_keypair = Keypair::new();
|
|
let lending_market_pubkey = lending_market_keypair.pubkey();
|
|
let (lending_market_authority, _bump_seed) = Pubkey::find_program_address(
|
|
&[&lending_market_pubkey.to_bytes()[..32]],
|
|
&spl_token_lending::id(),
|
|
);
|
|
|
|
let rent = banks_client.get_rent().await.unwrap();
|
|
let mut transaction = Transaction::new_with_payer(
|
|
&[
|
|
create_account(
|
|
&payer.pubkey(),
|
|
&lending_market_pubkey,
|
|
rent.minimum_balance(LendingMarket::LEN),
|
|
LendingMarket::LEN as u64,
|
|
&spl_token_lending::id(),
|
|
),
|
|
init_lending_market(
|
|
spl_token_lending::id(),
|
|
lending_market_owner.pubkey(),
|
|
QUOTE_CURRENCY,
|
|
lending_market_pubkey,
|
|
oracle_program_id,
|
|
),
|
|
],
|
|
Some(&payer.pubkey()),
|
|
);
|
|
|
|
let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap();
|
|
transaction.sign(&[&payer, &lending_market_keypair], recent_blockhash);
|
|
assert_matches!(banks_client.process_transaction(transaction).await, Ok(()));
|
|
|
|
TestLendingMarket {
|
|
owner: lending_market_owner,
|
|
pubkey: lending_market_pubkey,
|
|
authority: lending_market_authority,
|
|
quote_currency: QUOTE_CURRENCY,
|
|
oracle_program_id,
|
|
}
|
|
}
|
|
|
|
pub async fn refresh_reserve(
|
|
&self,
|
|
banks_client: &mut BanksClient,
|
|
payer: &Keypair,
|
|
reserve: &TestReserve,
|
|
) {
|
|
let mut transaction = Transaction::new_with_payer(
|
|
&[refresh_reserve(
|
|
spl_token_lending::id(),
|
|
reserve.pubkey,
|
|
reserve.liquidity_oracle_pubkey,
|
|
)],
|
|
Some(&payer.pubkey()),
|
|
);
|
|
|
|
let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap();
|
|
transaction.sign(&[payer], recent_blockhash);
|
|
|
|
assert_matches!(banks_client.process_transaction(transaction).await, Ok(()));
|
|
}
|
|
|
|
pub async fn deposit(
|
|
&self,
|
|
banks_client: &mut BanksClient,
|
|
user_accounts_owner: &Keypair,
|
|
payer: &Keypair,
|
|
reserve: &TestReserve,
|
|
liquidity_amount: u64,
|
|
) {
|
|
let user_transfer_authority = Keypair::new();
|
|
let mut transaction = Transaction::new_with_payer(
|
|
&[
|
|
approve(
|
|
&spl_token::id(),
|
|
&reserve.user_liquidity_pubkey,
|
|
&user_transfer_authority.pubkey(),
|
|
&user_accounts_owner.pubkey(),
|
|
&[],
|
|
liquidity_amount,
|
|
)
|
|
.unwrap(),
|
|
deposit_reserve_liquidity(
|
|
spl_token_lending::id(),
|
|
liquidity_amount,
|
|
reserve.user_liquidity_pubkey,
|
|
reserve.user_collateral_pubkey,
|
|
reserve.pubkey,
|
|
reserve.liquidity_supply_pubkey,
|
|
reserve.collateral_mint_pubkey,
|
|
self.pubkey,
|
|
user_transfer_authority.pubkey(),
|
|
),
|
|
],
|
|
Some(&payer.pubkey()),
|
|
);
|
|
|
|
let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap();
|
|
transaction.sign(
|
|
&[payer, user_accounts_owner, &user_transfer_authority],
|
|
recent_blockhash,
|
|
);
|
|
|
|
assert_matches!(banks_client.process_transaction(transaction).await, Ok(()));
|
|
}
|
|
|
|
pub async fn liquidate(
|
|
&self,
|
|
banks_client: &mut BanksClient,
|
|
payer: &Keypair,
|
|
args: LiquidateArgs<'_>,
|
|
) {
|
|
let LiquidateArgs {
|
|
liquidity_amount,
|
|
obligation,
|
|
repay_reserve,
|
|
withdraw_reserve,
|
|
user_accounts_owner,
|
|
} = args;
|
|
|
|
let user_transfer_authority = Keypair::new();
|
|
let mut transaction = Transaction::new_with_payer(
|
|
&[
|
|
approve(
|
|
&spl_token::id(),
|
|
&repay_reserve.user_liquidity_pubkey,
|
|
&user_transfer_authority.pubkey(),
|
|
&user_accounts_owner.pubkey(),
|
|
&[],
|
|
liquidity_amount,
|
|
)
|
|
.unwrap(),
|
|
liquidate_obligation(
|
|
spl_token_lending::id(),
|
|
liquidity_amount,
|
|
repay_reserve.user_liquidity_pubkey,
|
|
withdraw_reserve.user_collateral_pubkey,
|
|
repay_reserve.pubkey,
|
|
repay_reserve.liquidity_supply_pubkey,
|
|
withdraw_reserve.pubkey,
|
|
withdraw_reserve.collateral_supply_pubkey,
|
|
obligation.pubkey,
|
|
self.pubkey,
|
|
user_transfer_authority.pubkey(),
|
|
),
|
|
],
|
|
Some(&payer.pubkey()),
|
|
);
|
|
|
|
let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap();
|
|
transaction.sign(
|
|
&[&payer, &user_accounts_owner, &user_transfer_authority],
|
|
recent_blockhash,
|
|
);
|
|
assert!(banks_client.process_transaction(transaction).await.is_ok());
|
|
}
|
|
|
|
pub async fn borrow(
|
|
&self,
|
|
banks_client: &mut BanksClient,
|
|
payer: &Keypair,
|
|
args: BorrowArgs<'_>,
|
|
) {
|
|
let BorrowArgs {
|
|
liquidity_amount,
|
|
obligation,
|
|
borrow_reserve,
|
|
user_accounts_owner,
|
|
} = args;
|
|
|
|
let mut transaction = Transaction::new_with_payer(
|
|
&[borrow_obligation_liquidity(
|
|
spl_token_lending::id(),
|
|
liquidity_amount,
|
|
borrow_reserve.liquidity_supply_pubkey,
|
|
borrow_reserve.user_liquidity_pubkey,
|
|
borrow_reserve.pubkey,
|
|
borrow_reserve.liquidity_fee_receiver_pubkey,
|
|
obligation.pubkey,
|
|
self.pubkey,
|
|
obligation.owner,
|
|
Some(borrow_reserve.liquidity_host_pubkey),
|
|
)],
|
|
Some(&payer.pubkey()),
|
|
);
|
|
|
|
let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap();
|
|
transaction.sign(&vec![payer, user_accounts_owner], recent_blockhash);
|
|
|
|
assert_matches!(banks_client.process_transaction(transaction).await, Ok(()));
|
|
}
|
|
|
|
pub async fn get_state(&self, banks_client: &mut BanksClient) -> LendingMarket {
|
|
let lending_market_account: Account = banks_client
|
|
.get_account(self.pubkey)
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
LendingMarket::unpack(&lending_market_account.data[..]).unwrap()
|
|
}
|
|
|
|
pub async fn validate_state(&self, banks_client: &mut BanksClient) {
|
|
let lending_market = self.get_state(banks_client).await;
|
|
assert_eq!(lending_market.version, PROGRAM_VERSION);
|
|
assert_eq!(lending_market.owner, self.owner.pubkey());
|
|
assert_eq!(lending_market.quote_currency, self.quote_currency);
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct TestReserve {
|
|
pub name: String,
|
|
pub pubkey: Pubkey,
|
|
pub lending_market_pubkey: Pubkey,
|
|
pub config: ReserveConfig,
|
|
pub liquidity_mint_pubkey: Pubkey,
|
|
pub liquidity_mint_decimals: u8,
|
|
pub liquidity_supply_pubkey: Pubkey,
|
|
pub liquidity_fee_receiver_pubkey: Pubkey,
|
|
pub liquidity_host_pubkey: Pubkey,
|
|
pub liquidity_oracle_pubkey: Pubkey,
|
|
pub collateral_mint_pubkey: Pubkey,
|
|
pub collateral_supply_pubkey: Pubkey,
|
|
pub user_liquidity_pubkey: Pubkey,
|
|
pub user_collateral_pubkey: Pubkey,
|
|
pub market_price: Decimal,
|
|
}
|
|
|
|
impl TestReserve {
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub async fn init(
|
|
name: String,
|
|
banks_client: &mut BanksClient,
|
|
lending_market: &TestLendingMarket,
|
|
oracle: &TestOracle,
|
|
liquidity_amount: u64,
|
|
config: ReserveConfig,
|
|
liquidity_mint_pubkey: Pubkey,
|
|
user_liquidity_pubkey: Pubkey,
|
|
payer: &Keypair,
|
|
user_accounts_owner: &Keypair,
|
|
) -> Result<Self, TransactionError> {
|
|
let reserve_keypair = Keypair::new();
|
|
let reserve_pubkey = reserve_keypair.pubkey();
|
|
let collateral_mint_keypair = Keypair::new();
|
|
let collateral_supply_keypair = Keypair::new();
|
|
let liquidity_supply_keypair = Keypair::new();
|
|
let liquidity_fee_receiver_keypair = Keypair::new();
|
|
let liquidity_host_keypair = Keypair::new();
|
|
let user_collateral_token_keypair = Keypair::new();
|
|
let user_transfer_authority_keypair = Keypair::new();
|
|
|
|
let liquidity_mint_account = banks_client
|
|
.get_account(liquidity_mint_pubkey)
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
let liquidity_mint = Mint::unpack(&liquidity_mint_account.data[..]).unwrap();
|
|
|
|
let rent = banks_client.get_rent().await.unwrap();
|
|
let mut transaction = Transaction::new_with_payer(
|
|
&[
|
|
approve(
|
|
&spl_token::id(),
|
|
&user_liquidity_pubkey,
|
|
&user_transfer_authority_keypair.pubkey(),
|
|
&user_accounts_owner.pubkey(),
|
|
&[],
|
|
liquidity_amount,
|
|
)
|
|
.unwrap(),
|
|
create_account(
|
|
&payer.pubkey(),
|
|
&collateral_mint_keypair.pubkey(),
|
|
rent.minimum_balance(Mint::LEN),
|
|
Mint::LEN as u64,
|
|
&spl_token::id(),
|
|
),
|
|
create_account(
|
|
&payer.pubkey(),
|
|
&collateral_supply_keypair.pubkey(),
|
|
rent.minimum_balance(Token::LEN),
|
|
Token::LEN as u64,
|
|
&spl_token::id(),
|
|
),
|
|
create_account(
|
|
&payer.pubkey(),
|
|
&liquidity_supply_keypair.pubkey(),
|
|
rent.minimum_balance(Token::LEN),
|
|
Token::LEN as u64,
|
|
&spl_token::id(),
|
|
),
|
|
create_account(
|
|
&payer.pubkey(),
|
|
&liquidity_fee_receiver_keypair.pubkey(),
|
|
rent.minimum_balance(Token::LEN),
|
|
Token::LEN as u64,
|
|
&spl_token::id(),
|
|
),
|
|
create_account(
|
|
&payer.pubkey(),
|
|
&liquidity_host_keypair.pubkey(),
|
|
rent.minimum_balance(Token::LEN),
|
|
Token::LEN as u64,
|
|
&spl_token::id(),
|
|
),
|
|
create_account(
|
|
&payer.pubkey(),
|
|
&user_collateral_token_keypair.pubkey(),
|
|
rent.minimum_balance(Token::LEN),
|
|
Token::LEN as u64,
|
|
&spl_token::id(),
|
|
),
|
|
create_account(
|
|
&payer.pubkey(),
|
|
&reserve_pubkey,
|
|
rent.minimum_balance(Reserve::LEN),
|
|
Reserve::LEN as u64,
|
|
&spl_token_lending::id(),
|
|
),
|
|
init_reserve(
|
|
spl_token_lending::id(),
|
|
liquidity_amount,
|
|
config,
|
|
user_liquidity_pubkey,
|
|
user_collateral_token_keypair.pubkey(),
|
|
reserve_pubkey,
|
|
liquidity_mint_pubkey,
|
|
liquidity_supply_keypair.pubkey(),
|
|
liquidity_fee_receiver_keypair.pubkey(),
|
|
collateral_mint_keypair.pubkey(),
|
|
collateral_supply_keypair.pubkey(),
|
|
oracle.product_pubkey,
|
|
oracle.price_pubkey,
|
|
lending_market.pubkey,
|
|
lending_market.owner.pubkey(),
|
|
user_transfer_authority_keypair.pubkey(),
|
|
),
|
|
],
|
|
Some(&payer.pubkey()),
|
|
);
|
|
|
|
let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap();
|
|
transaction.sign(
|
|
&vec![
|
|
payer,
|
|
user_accounts_owner,
|
|
&reserve_keypair,
|
|
&lending_market.owner,
|
|
&collateral_mint_keypair,
|
|
&collateral_supply_keypair,
|
|
&liquidity_supply_keypair,
|
|
&liquidity_fee_receiver_keypair,
|
|
&liquidity_host_keypair,
|
|
&user_collateral_token_keypair,
|
|
&user_transfer_authority_keypair,
|
|
],
|
|
recent_blockhash,
|
|
);
|
|
|
|
banks_client
|
|
.process_transaction(transaction)
|
|
.await
|
|
.map(|_| Self {
|
|
name,
|
|
pubkey: reserve_pubkey,
|
|
lending_market_pubkey: lending_market.pubkey,
|
|
config,
|
|
liquidity_mint_pubkey,
|
|
liquidity_mint_decimals: liquidity_mint.decimals,
|
|
liquidity_supply_pubkey: liquidity_supply_keypair.pubkey(),
|
|
liquidity_fee_receiver_pubkey: liquidity_fee_receiver_keypair.pubkey(),
|
|
liquidity_host_pubkey: liquidity_host_keypair.pubkey(),
|
|
liquidity_oracle_pubkey: oracle.price_pubkey,
|
|
collateral_mint_pubkey: collateral_mint_keypair.pubkey(),
|
|
collateral_supply_pubkey: collateral_supply_keypair.pubkey(),
|
|
user_liquidity_pubkey,
|
|
user_collateral_pubkey: user_collateral_token_keypair.pubkey(),
|
|
market_price: oracle.price,
|
|
})
|
|
.map_err(|e| e.unwrap())
|
|
}
|
|
|
|
pub async fn get_state(&self, banks_client: &mut BanksClient) -> Reserve {
|
|
let reserve_account: Account = banks_client
|
|
.get_account(self.pubkey)
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
Reserve::unpack(&reserve_account.data[..]).unwrap()
|
|
}
|
|
|
|
pub async fn validate_state(&self, banks_client: &mut BanksClient) {
|
|
let reserve = self.get_state(banks_client).await;
|
|
assert!(reserve.last_update.slot > 0);
|
|
assert_eq!(PROGRAM_VERSION, reserve.version);
|
|
assert_eq!(self.lending_market_pubkey, reserve.lending_market);
|
|
assert_eq!(self.liquidity_mint_pubkey, reserve.liquidity.mint_pubkey);
|
|
assert_eq!(
|
|
self.liquidity_supply_pubkey,
|
|
reserve.liquidity.supply_pubkey
|
|
);
|
|
assert_eq!(self.collateral_mint_pubkey, reserve.collateral.mint_pubkey);
|
|
assert_eq!(
|
|
self.collateral_supply_pubkey,
|
|
reserve.collateral.supply_pubkey
|
|
);
|
|
assert_eq!(self.config, reserve.config);
|
|
|
|
assert_eq!(
|
|
self.liquidity_oracle_pubkey,
|
|
reserve.liquidity.oracle_pubkey
|
|
);
|
|
assert_eq!(
|
|
reserve.liquidity.cumulative_borrow_rate_wads,
|
|
Decimal::one()
|
|
);
|
|
assert_eq!(reserve.liquidity.borrowed_amount_wads, Decimal::zero());
|
|
assert!(reserve.liquidity.available_amount > 0);
|
|
assert!(reserve.collateral.mint_total_supply > 0);
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct TestObligation {
|
|
pub pubkey: Pubkey,
|
|
pub lending_market: Pubkey,
|
|
pub owner: Pubkey,
|
|
pub deposits: Vec<TestObligationCollateral>,
|
|
pub borrows: Vec<TestObligationLiquidity>,
|
|
}
|
|
|
|
impl TestObligation {
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub async fn init(
|
|
banks_client: &mut BanksClient,
|
|
lending_market: &TestLendingMarket,
|
|
user_accounts_owner: &Keypair,
|
|
payer: &Keypair,
|
|
) -> Result<Self, TransactionError> {
|
|
let obligation_keypair = Keypair::new();
|
|
let obligation = TestObligation {
|
|
pubkey: obligation_keypair.pubkey(),
|
|
lending_market: lending_market.pubkey,
|
|
owner: user_accounts_owner.pubkey(),
|
|
deposits: vec![],
|
|
borrows: vec![],
|
|
};
|
|
|
|
let rent = banks_client.get_rent().await.unwrap();
|
|
let mut transaction = Transaction::new_with_payer(
|
|
&[
|
|
create_account(
|
|
&payer.pubkey(),
|
|
&obligation_keypair.pubkey(),
|
|
rent.minimum_balance(Obligation::LEN),
|
|
Obligation::LEN as u64,
|
|
&spl_token_lending::id(),
|
|
),
|
|
init_obligation(
|
|
spl_token_lending::id(),
|
|
obligation.pubkey,
|
|
lending_market.pubkey,
|
|
user_accounts_owner.pubkey(),
|
|
),
|
|
],
|
|
Some(&payer.pubkey()),
|
|
);
|
|
|
|
let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap();
|
|
transaction.sign(
|
|
&vec![payer, &obligation_keypair, user_accounts_owner],
|
|
recent_blockhash,
|
|
);
|
|
|
|
banks_client
|
|
.process_transaction(transaction)
|
|
.await
|
|
.map_err(|e| e.unwrap())?;
|
|
|
|
Ok(obligation)
|
|
}
|
|
|
|
pub async fn get_state(&self, banks_client: &mut BanksClient) -> Obligation {
|
|
let obligation_account: Account = banks_client
|
|
.get_account(self.pubkey)
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
Obligation::unpack(&obligation_account.data[..]).unwrap()
|
|
}
|
|
|
|
pub async fn validate_state(&self, banks_client: &mut BanksClient) {
|
|
let obligation = self.get_state(banks_client).await;
|
|
assert_eq!(obligation.version, PROGRAM_VERSION);
|
|
assert_eq!(obligation.lending_market, self.lending_market);
|
|
assert_eq!(obligation.owner, self.owner);
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct TestObligationCollateral {
|
|
pub obligation_pubkey: Pubkey,
|
|
pub deposit_reserve: Pubkey,
|
|
pub deposited_amount: u64,
|
|
}
|
|
|
|
impl TestObligationCollateral {
|
|
pub async fn get_state(&self, banks_client: &mut BanksClient) -> Obligation {
|
|
let obligation_account: Account = banks_client
|
|
.get_account(self.obligation_pubkey)
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
Obligation::unpack(&obligation_account.data[..]).unwrap()
|
|
}
|
|
|
|
pub async fn validate_state(&self, banks_client: &mut BanksClient) {
|
|
let obligation = self.get_state(banks_client).await;
|
|
assert_eq!(obligation.version, PROGRAM_VERSION);
|
|
|
|
let (collateral, _) = obligation
|
|
.find_collateral_in_deposits(self.deposit_reserve)
|
|
.unwrap();
|
|
assert_eq!(collateral.deposited_amount, self.deposited_amount);
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct TestObligationLiquidity {
|
|
pub obligation_pubkey: Pubkey,
|
|
pub borrow_reserve: Pubkey,
|
|
pub borrowed_amount_wads: Decimal,
|
|
}
|
|
|
|
impl TestObligationLiquidity {
|
|
pub async fn get_state(&self, banks_client: &mut BanksClient) -> Obligation {
|
|
let obligation_account: Account = banks_client
|
|
.get_account(self.obligation_pubkey)
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
Obligation::unpack(&obligation_account.data[..]).unwrap()
|
|
}
|
|
|
|
pub async fn validate_state(&self, banks_client: &mut BanksClient) {
|
|
let obligation = self.get_state(banks_client).await;
|
|
assert_eq!(obligation.version, PROGRAM_VERSION);
|
|
let (liquidity, _) = obligation
|
|
.find_liquidity_in_borrows(self.borrow_reserve)
|
|
.unwrap();
|
|
assert!(liquidity.cumulative_borrow_rate_wads >= Decimal::one());
|
|
assert!(liquidity.borrowed_amount_wads >= self.borrowed_amount_wads);
|
|
}
|
|
}
|
|
|
|
pub struct TestMint {
|
|
pub pubkey: Pubkey,
|
|
pub authority: Keypair,
|
|
pub decimals: u8,
|
|
}
|
|
|
|
pub fn add_usdc_mint(test: &mut ProgramTest) -> TestMint {
|
|
let authority = Keypair::new();
|
|
let pubkey = Pubkey::from_str(USDC_MINT).unwrap();
|
|
let decimals = 6;
|
|
test.add_packable_account(
|
|
pubkey,
|
|
u32::MAX as u64,
|
|
&Mint {
|
|
is_initialized: true,
|
|
mint_authority: COption::Some(authority.pubkey()),
|
|
decimals,
|
|
..Mint::default()
|
|
},
|
|
&spl_token::id(),
|
|
);
|
|
TestMint {
|
|
pubkey,
|
|
authority,
|
|
decimals,
|
|
}
|
|
}
|
|
|
|
pub struct TestOracle {
|
|
pub product_pubkey: Pubkey,
|
|
pub price_pubkey: Pubkey,
|
|
pub price: Decimal,
|
|
}
|
|
|
|
pub fn add_sol_oracle(test: &mut ProgramTest) -> TestOracle {
|
|
add_oracle(
|
|
test,
|
|
Pubkey::from_str(SOL_PYTH_PRODUCT).unwrap(),
|
|
Pubkey::from_str(SOL_PYTH_PRICE).unwrap(),
|
|
// Set SOL price to $20
|
|
Decimal::from(20u64),
|
|
)
|
|
}
|
|
|
|
pub fn add_usdc_oracle(test: &mut ProgramTest) -> TestOracle {
|
|
add_oracle(
|
|
test,
|
|
// Mock with SRM since Pyth doesn't have USDC yet
|
|
Pubkey::from_str(SRM_PYTH_PRODUCT).unwrap(),
|
|
Pubkey::from_str(SRM_PYTH_PRICE).unwrap(),
|
|
// Set USDC price to $1
|
|
Decimal::from(1u64),
|
|
)
|
|
}
|
|
|
|
pub fn add_oracle(
|
|
test: &mut ProgramTest,
|
|
product_pubkey: Pubkey,
|
|
price_pubkey: Pubkey,
|
|
price: Decimal,
|
|
) -> TestOracle {
|
|
let oracle_program_id = read_keypair_file("tests/fixtures/oracle_program_id.json").unwrap();
|
|
|
|
// Add Pyth product account
|
|
test.add_account_with_file_data(
|
|
product_pubkey,
|
|
u32::MAX as u64,
|
|
oracle_program_id.pubkey(),
|
|
&format!("{}.bin", product_pubkey.to_string()),
|
|
);
|
|
|
|
// Add Pyth price account after setting the price
|
|
let filename = &format!("{}.bin", price_pubkey.to_string());
|
|
let mut pyth_price_data = read_file(find_file(filename).unwrap_or_else(|| {
|
|
panic!("Unable to locate {}", filename);
|
|
}));
|
|
|
|
let mut pyth_price = pyth::load_mut::<pyth::Price>(pyth_price_data.as_mut_slice()).unwrap();
|
|
|
|
let decimals = 10u64
|
|
.checked_pow(pyth_price.expo.checked_abs().unwrap().try_into().unwrap())
|
|
.unwrap();
|
|
|
|
pyth_price.valid_slot = 0;
|
|
pyth_price.agg.price = price
|
|
.try_round_u64()
|
|
.unwrap()
|
|
.checked_mul(decimals)
|
|
.unwrap()
|
|
.try_into()
|
|
.unwrap();
|
|
|
|
test.add_account(
|
|
price_pubkey,
|
|
Account {
|
|
lamports: u32::MAX as u64,
|
|
data: pyth_price_data,
|
|
owner: oracle_program_id.pubkey(),
|
|
executable: false,
|
|
rent_epoch: 0,
|
|
},
|
|
);
|
|
|
|
TestOracle {
|
|
product_pubkey,
|
|
price_pubkey,
|
|
price,
|
|
}
|
|
}
|
|
|
|
pub async fn create_and_mint_to_token_account(
|
|
banks_client: &mut BanksClient,
|
|
mint_pubkey: Pubkey,
|
|
mint_authority: Option<&Keypair>,
|
|
payer: &Keypair,
|
|
authority: Pubkey,
|
|
amount: u64,
|
|
) -> Pubkey {
|
|
if let Some(mint_authority) = mint_authority {
|
|
let account_pubkey =
|
|
create_token_account(banks_client, mint_pubkey, &payer, Some(authority), None).await;
|
|
|
|
mint_to(
|
|
banks_client,
|
|
mint_pubkey,
|
|
&payer,
|
|
account_pubkey,
|
|
mint_authority,
|
|
amount,
|
|
)
|
|
.await;
|
|
|
|
account_pubkey
|
|
} else {
|
|
create_token_account(
|
|
banks_client,
|
|
mint_pubkey,
|
|
&payer,
|
|
Some(authority),
|
|
Some(amount),
|
|
)
|
|
.await
|
|
}
|
|
}
|
|
|
|
pub async fn create_token_account(
|
|
banks_client: &mut BanksClient,
|
|
mint_pubkey: Pubkey,
|
|
payer: &Keypair,
|
|
authority: Option<Pubkey>,
|
|
native_amount: Option<u64>,
|
|
) -> Pubkey {
|
|
let token_keypair = Keypair::new();
|
|
let token_pubkey = token_keypair.pubkey();
|
|
let authority_pubkey = authority.unwrap_or_else(|| payer.pubkey());
|
|
|
|
let rent = banks_client.get_rent().await.unwrap();
|
|
let lamports = rent.minimum_balance(Token::LEN) + native_amount.unwrap_or_default();
|
|
let mut transaction = Transaction::new_with_payer(
|
|
&[
|
|
create_account(
|
|
&payer.pubkey(),
|
|
&token_pubkey,
|
|
lamports,
|
|
Token::LEN as u64,
|
|
&spl_token::id(),
|
|
),
|
|
spl_token::instruction::initialize_account(
|
|
&spl_token::id(),
|
|
&token_pubkey,
|
|
&mint_pubkey,
|
|
&authority_pubkey,
|
|
)
|
|
.unwrap(),
|
|
],
|
|
Some(&payer.pubkey()),
|
|
);
|
|
|
|
let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap();
|
|
transaction.sign(&[&payer, &token_keypair], recent_blockhash);
|
|
|
|
assert_matches!(banks_client.process_transaction(transaction).await, Ok(()));
|
|
|
|
token_pubkey
|
|
}
|
|
|
|
pub async fn mint_to(
|
|
banks_client: &mut BanksClient,
|
|
mint_pubkey: Pubkey,
|
|
payer: &Keypair,
|
|
account_pubkey: Pubkey,
|
|
authority: &Keypair,
|
|
amount: u64,
|
|
) {
|
|
let mut transaction = Transaction::new_with_payer(
|
|
&[spl_token::instruction::mint_to(
|
|
&spl_token::id(),
|
|
&mint_pubkey,
|
|
&account_pubkey,
|
|
&authority.pubkey(),
|
|
&[],
|
|
amount,
|
|
)
|
|
.unwrap()],
|
|
Some(&payer.pubkey()),
|
|
);
|
|
|
|
let recent_blockhash = banks_client.get_latest_blockhash().await.unwrap();
|
|
transaction.sign(&[payer, authority], recent_blockhash);
|
|
|
|
assert_matches!(banks_client.process_transaction(transaction).await, Ok(()));
|
|
}
|
|
|
|
pub async fn get_token_balance(banks_client: &mut BanksClient, pubkey: Pubkey) -> u64 {
|
|
let token: Account = banks_client.get_account(pubkey).await.unwrap().unwrap();
|
|
|
|
spl_token::state::Account::unpack(&token.data[..])
|
|
.unwrap()
|
|
.amount
|
|
}
|