Add slippage constraint to borrow instruction (#3423)

* Add slippage limit to instruction

* Add slippage error

* Return error if below slippage limit

* Add tests

* Remove TODO

* Split instruction

* Fix test name

* Test limit case

* Update test

* Revert "Split instruction"

This reverts commit 3eba4942bd51f2402bf112edb514882636f0e401.

# Conflicts:
#	token-lending/program/tests/borrow_obligation_liquidity.rs

* Allow missing slippage_limit

* Fix limits

* Remove assertion
This commit is contained in:
Justin Malčić 2022-08-17 14:58:04 +01:00 committed by GitHub
parent e8bafb4b3f
commit 8d88fb6c88
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 400 additions and 8 deletions

View File

@ -160,6 +160,9 @@ pub enum LendingError {
#[error("Not enough liquidity after flash loan")]
NotEnoughLiquidityAfterFlashLoan,
// 45
/// Lending instruction exceeds desired slippage limit
#[error("Amount smaller than desired slippage limit")]
ExceededSlippage,
}
impl From<LendingError> for ProgramError {

View File

@ -219,7 +219,8 @@ pub enum LendingInstruction {
BorrowObligationLiquidity {
/// Amount of liquidity to borrow - u64::MAX for 100% of borrowing power
liquidity_amount: u64,
// @TODO: slippage constraint - https://git.io/JmV67
/// Minimum amount of liquidity to receive, if borrowing 100% of borrowing power
slippage_limit: u64,
},
// 11
@ -370,8 +371,12 @@ impl LendingInstruction {
Self::WithdrawObligationCollateral { collateral_amount }
}
10 => {
let (liquidity_amount, _rest) = Self::unpack_u64(rest)?;
Self::BorrowObligationLiquidity { liquidity_amount }
let (liquidity_amount, rest) = Self::unpack_u64(rest)?;
let (slippage_limit, _rest) = Self::unpack_u64(rest).unwrap_or((0, &[]));
Self::BorrowObligationLiquidity {
liquidity_amount,
slippage_limit,
}
}
11 => {
let (liquidity_amount, _rest) = Self::unpack_u64(rest)?;
@ -525,9 +530,13 @@ impl LendingInstruction {
buf.push(9);
buf.extend_from_slice(&collateral_amount.to_le_bytes());
}
Self::BorrowObligationLiquidity { liquidity_amount } => {
Self::BorrowObligationLiquidity {
liquidity_amount,
slippage_limit,
} => {
buf.push(10);
buf.extend_from_slice(&liquidity_amount.to_le_bytes());
buf.extend_from_slice(&slippage_limit.to_le_bytes());
}
Self::RepayObligationLiquidity { liquidity_amount } => {
buf.push(11);
@ -860,6 +869,7 @@ pub fn withdraw_obligation_collateral(
pub fn borrow_obligation_liquidity(
program_id: Pubkey,
liquidity_amount: u64,
slippage_limit: Option<u64>,
source_liquidity_pubkey: Pubkey,
destination_liquidity_pubkey: Pubkey,
borrow_reserve_pubkey: Pubkey,
@ -888,10 +898,15 @@ pub fn borrow_obligation_liquidity(
if let Some(host_fee_receiver_pubkey) = host_fee_receiver_pubkey {
accounts.push(AccountMeta::new(host_fee_receiver_pubkey, false));
}
let slippage_limit = slippage_limit.unwrap_or(0);
Instruction {
program_id,
accounts,
data: LendingInstruction::BorrowObligationLiquidity { liquidity_amount }.pack(),
data: LendingInstruction::BorrowObligationLiquidity {
liquidity_amount,
slippage_limit,
}
.pack(),
}
}
@ -1309,6 +1324,7 @@ mod tests {
let instruction = borrow_obligation_liquidity(
program_id,
liquidity_amount,
None,
source_liquidity_pubkey,
destination_liquidity_pubkey,
borrow_reserve_pubkey,
@ -1322,7 +1338,11 @@ mod tests {
assert_eq!(instruction.accounts.len(), 11);
assert_eq!(
instruction.data,
LendingInstruction::BorrowObligationLiquidity { liquidity_amount }.pack()
LendingInstruction::BorrowObligationLiquidity {
liquidity_amount,
slippage_limit: 0
}
.pack()
);
}

View File

@ -83,9 +83,17 @@ pub fn process_instruction(
msg!("Instruction: Withdraw Obligation Collateral");
process_withdraw_obligation_collateral(program_id, collateral_amount, accounts)
}
LendingInstruction::BorrowObligationLiquidity { liquidity_amount } => {
LendingInstruction::BorrowObligationLiquidity {
liquidity_amount,
slippage_limit,
} => {
msg!("Instruction: Borrow Obligation Liquidity");
process_borrow_obligation_liquidity(program_id, liquidity_amount, accounts)
process_borrow_obligation_liquidity(
program_id,
liquidity_amount,
slippage_limit,
accounts,
)
}
LendingInstruction::RepayObligationLiquidity { liquidity_amount } => {
msg!("Instruction: Repay Obligation Liquidity");
@ -1023,6 +1031,7 @@ fn process_withdraw_obligation_collateral(
fn process_borrow_obligation_liquidity(
program_id: &Pubkey,
liquidity_amount: u64,
slippage_limit: u64,
accounts: &[AccountInfo],
) -> ProgramResult {
if liquidity_amount == 0 {
@ -1141,6 +1150,11 @@ fn process_borrow_obligation_liquidity(
return Err(LendingError::BorrowTooSmall.into());
}
if liquidity_amount == u64::MAX && receive_amount < slippage_limit {
msg!("Received liquidity would be smaller than the desired slippage limit");
return Err(LendingError::ExceededSlippage.into());
}
borrow_reserve.liquidity.borrow(borrow_amount)?;
borrow_reserve.last_update.mark_stale();
Reserve::pack(borrow_reserve, &mut borrow_reserve_info.data.borrow_mut())?;

View File

@ -102,6 +102,7 @@ async fn test_borrow_usdc_fixed_amount() {
borrow_obligation_liquidity(
spl_token_lending::id(),
USDC_BORROW_AMOUNT_FRACTIONAL,
None,
usdc_test_reserve.liquidity_supply_pubkey,
usdc_test_reserve.user_liquidity_pubkey,
usdc_test_reserve.pubkey,
@ -249,6 +250,7 @@ async fn test_borrow_sol_max_amount() {
borrow_obligation_liquidity(
spl_token_lending::id(),
u64::MAX,
None,
sol_test_reserve.liquidity_supply_pubkey,
sol_test_reserve.user_liquidity_pubkey,
sol_test_reserve.pubkey,
@ -380,6 +382,7 @@ async fn test_borrow_too_large() {
borrow_obligation_liquidity(
spl_token_lending::id(),
USDC_BORROW_AMOUNT_FRACTIONAL,
None,
usdc_test_reserve.liquidity_supply_pubkey,
usdc_test_reserve.user_liquidity_pubkey,
usdc_test_reserve.pubkey,
@ -408,3 +411,353 @@ async fn test_borrow_too_large() {
)
);
}
#[tokio::test]
async fn test_borrow_max_receive_minimum() {
let mut test = ProgramTest::new(
"spl_token_lending",
spl_token_lending::id(),
processor!(process_instruction),
);
// limit to track compute unit increase
test.set_compute_max_units(60_000);
const FEE_AMOUNT: u64 = 5000;
const HOST_FEE_AMOUNT: u64 = 1000;
const USDC_DEPOSIT_AMOUNT_FRACTIONAL: u64 =
2_000 * FRACTIONAL_TO_USDC * INITIAL_COLLATERAL_RATIO;
const SOL_BORROW_AMOUNT_LAMPORTS: u64 = 50 * LAMPORTS_TO_SOL;
const USDC_RESERVE_COLLATERAL_FRACTIONAL: u64 = 2 * USDC_DEPOSIT_AMOUNT_FRACTIONAL;
const SOL_RESERVE_LIQUIDITY_LAMPORTS: u64 = 2 * SOL_BORROW_AMOUNT_LAMPORTS;
const SLIPPAGE_LIMIT: u64 = SOL_BORROW_AMOUNT_LAMPORTS - FEE_AMOUNT;
let user_accounts_owner = Keypair::new();
let lending_market = add_lending_market(&mut test);
let mut reserve_config = TEST_RESERVE_CONFIG;
reserve_config.loan_to_value_ratio = 50;
let usdc_mint = add_usdc_mint(&mut test);
let usdc_oracle = add_usdc_oracle(&mut test);
let usdc_test_reserve = add_reserve(
&mut test,
&lending_market,
&usdc_oracle,
&user_accounts_owner,
AddReserveArgs {
liquidity_amount: USDC_RESERVE_COLLATERAL_FRACTIONAL,
liquidity_mint_pubkey: usdc_mint.pubkey,
liquidity_mint_decimals: usdc_mint.decimals,
config: reserve_config,
mark_fresh: true,
..AddReserveArgs::default()
},
);
let sol_oracle = add_sol_oracle(&mut test);
let sol_test_reserve = add_reserve(
&mut test,
&lending_market,
&sol_oracle,
&user_accounts_owner,
AddReserveArgs {
liquidity_amount: SOL_RESERVE_LIQUIDITY_LAMPORTS,
liquidity_mint_pubkey: spl_token::native_mint::id(),
liquidity_mint_decimals: 9,
config: reserve_config,
mark_fresh: true,
..AddReserveArgs::default()
},
);
let test_obligation = add_obligation(
&mut test,
&lending_market,
&user_accounts_owner,
AddObligationArgs {
deposits: &[(&usdc_test_reserve, USDC_DEPOSIT_AMOUNT_FRACTIONAL)],
..AddObligationArgs::default()
},
);
let (mut banks_client, payer, recent_blockhash) = test.start().await;
let initial_liquidity_supply =
get_token_balance(&mut banks_client, sol_test_reserve.liquidity_supply_pubkey).await;
let mut transaction = Transaction::new_with_payer(
&[
refresh_obligation(
spl_token_lending::id(),
test_obligation.pubkey,
vec![usdc_test_reserve.pubkey],
),
borrow_obligation_liquidity(
spl_token_lending::id(),
u64::MAX,
Some(SLIPPAGE_LIMIT),
sol_test_reserve.liquidity_supply_pubkey,
sol_test_reserve.user_liquidity_pubkey,
sol_test_reserve.pubkey,
sol_test_reserve.liquidity_fee_receiver_pubkey,
test_obligation.pubkey,
lending_market.pubkey,
test_obligation.owner,
Some(sol_test_reserve.liquidity_host_pubkey),
),
],
Some(&payer.pubkey()),
);
transaction.sign(&[&payer, &user_accounts_owner], recent_blockhash);
assert!(banks_client.process_transaction(transaction).await.is_ok());
let sol_reserve = sol_test_reserve.get_state(&mut banks_client).await;
let obligation = test_obligation.get_state(&mut banks_client).await;
let (total_fee, host_fee) = sol_reserve
.config
.fees
.calculate_borrow_fees(SOL_BORROW_AMOUNT_LAMPORTS.into(), FeeCalculation::Inclusive)
.unwrap();
assert_eq!(total_fee, FEE_AMOUNT);
assert_eq!(host_fee, HOST_FEE_AMOUNT);
let borrow_amount =
get_token_balance(&mut banks_client, sol_test_reserve.user_liquidity_pubkey).await;
assert_eq!(borrow_amount, SOL_BORROW_AMOUNT_LAMPORTS - FEE_AMOUNT);
let liquidity = &obligation.borrows[0];
assert_eq!(
liquidity.borrowed_amount_wads,
Decimal::from(SOL_BORROW_AMOUNT_LAMPORTS)
);
let liquidity_supply =
get_token_balance(&mut banks_client, sol_test_reserve.liquidity_supply_pubkey).await;
assert_eq!(
liquidity_supply,
initial_liquidity_supply - SOL_BORROW_AMOUNT_LAMPORTS
);
let fee_balance = get_token_balance(
&mut banks_client,
sol_test_reserve.liquidity_fee_receiver_pubkey,
)
.await;
assert_eq!(fee_balance, FEE_AMOUNT - HOST_FEE_AMOUNT);
let host_fee_balance =
get_token_balance(&mut banks_client, sol_test_reserve.liquidity_host_pubkey).await;
assert_eq!(host_fee_balance, HOST_FEE_AMOUNT);
}
#[tokio::test]
async fn test_borrow_max_receive_less_than_slippage() {
let mut test = ProgramTest::new(
"spl_token_lending",
spl_token_lending::id(),
processor!(process_instruction),
);
// limit to track compute unit increase
test.set_compute_max_units(60_000);
const FEE_AMOUNT: u64 = 5000;
const USDC_DEPOSIT_AMOUNT_FRACTIONAL: u64 =
2_000 * FRACTIONAL_TO_USDC * INITIAL_COLLATERAL_RATIO;
const SOL_BORROW_AMOUNT_LAMPORTS: u64 = 50 * LAMPORTS_TO_SOL;
const USDC_RESERVE_COLLATERAL_FRACTIONAL: u64 = 2 * USDC_DEPOSIT_AMOUNT_FRACTIONAL;
const SOL_RESERVE_LIQUIDITY_LAMPORTS: u64 = 2 * SOL_BORROW_AMOUNT_LAMPORTS;
const SLIPPAGE_LIMIT: u64 = SOL_BORROW_AMOUNT_LAMPORTS - FEE_AMOUNT + 1;
let user_accounts_owner = Keypair::new();
let lending_market = add_lending_market(&mut test);
let mut reserve_config = TEST_RESERVE_CONFIG;
reserve_config.loan_to_value_ratio = 50;
let usdc_mint = add_usdc_mint(&mut test);
let usdc_oracle = add_usdc_oracle(&mut test);
let usdc_test_reserve = add_reserve(
&mut test,
&lending_market,
&usdc_oracle,
&user_accounts_owner,
AddReserveArgs {
liquidity_amount: USDC_RESERVE_COLLATERAL_FRACTIONAL,
liquidity_mint_pubkey: usdc_mint.pubkey,
liquidity_mint_decimals: usdc_mint.decimals,
config: reserve_config,
mark_fresh: true,
..AddReserveArgs::default()
},
);
let sol_oracle = add_sol_oracle(&mut test);
let sol_test_reserve = add_reserve(
&mut test,
&lending_market,
&sol_oracle,
&user_accounts_owner,
AddReserveArgs {
liquidity_amount: SOL_RESERVE_LIQUIDITY_LAMPORTS,
liquidity_mint_pubkey: spl_token::native_mint::id(),
liquidity_mint_decimals: 9,
config: reserve_config,
mark_fresh: true,
..AddReserveArgs::default()
},
);
let test_obligation = add_obligation(
&mut test,
&lending_market,
&user_accounts_owner,
AddObligationArgs {
deposits: &[(&usdc_test_reserve, USDC_DEPOSIT_AMOUNT_FRACTIONAL)],
..AddObligationArgs::default()
},
);
let (mut banks_client, payer, recent_blockhash) = test.start().await;
let mut transaction = Transaction::new_with_payer(
&[
refresh_obligation(
spl_token_lending::id(),
test_obligation.pubkey,
vec![usdc_test_reserve.pubkey],
),
borrow_obligation_liquidity(
spl_token_lending::id(),
u64::MAX,
Some(SLIPPAGE_LIMIT),
sol_test_reserve.liquidity_supply_pubkey,
sol_test_reserve.user_liquidity_pubkey,
sol_test_reserve.pubkey,
sol_test_reserve.liquidity_fee_receiver_pubkey,
test_obligation.pubkey,
lending_market.pubkey,
test_obligation.owner,
Some(sol_test_reserve.liquidity_host_pubkey),
),
],
Some(&payer.pubkey()),
);
transaction.sign(&[&payer, &user_accounts_owner], recent_blockhash);
assert_eq!(
banks_client
.process_transaction(transaction)
.await
.unwrap_err()
.unwrap(),
TransactionError::InstructionError(
1,
InstructionError::Custom(LendingError::ExceededSlippage as u32)
)
);
}
#[tokio::test]
async fn test_borrow_less_than_max_with_slippage() {
let mut test = ProgramTest::new(
"spl_token_lending",
spl_token_lending::id(),
processor!(process_instruction),
);
const USDC_TOTAL_BORROW_FRACTIONAL: u64 = 1_000 * FRACTIONAL_TO_USDC;
const FEE_AMOUNT: u64 = 100;
const SOL_DEPOSIT_AMOUNT_LAMPORTS: u64 = 100 * LAMPORTS_TO_SOL * INITIAL_COLLATERAL_RATIO;
const USDC_BORROW_AMOUNT_FRACTIONAL: u64 = USDC_TOTAL_BORROW_FRACTIONAL - FEE_AMOUNT;
const SOL_RESERVE_COLLATERAL_LAMPORTS: u64 = 2 * SOL_DEPOSIT_AMOUNT_LAMPORTS;
const USDC_RESERVE_LIQUIDITY_FRACTIONAL: u64 = 2 * USDC_TOTAL_BORROW_FRACTIONAL;
const SLIPPAGE_LIMIT: u64 = u64::MAX;
let user_accounts_owner = Keypair::new();
let lending_market = add_lending_market(&mut test);
let mut reserve_config = TEST_RESERVE_CONFIG;
reserve_config.loan_to_value_ratio = 50;
let sol_oracle = add_sol_oracle(&mut test);
let sol_test_reserve = add_reserve(
&mut test,
&lending_market,
&sol_oracle,
&user_accounts_owner,
AddReserveArgs {
collateral_amount: SOL_RESERVE_COLLATERAL_LAMPORTS,
liquidity_mint_pubkey: spl_token::native_mint::id(),
liquidity_mint_decimals: 9,
config: reserve_config,
mark_fresh: true,
..AddReserveArgs::default()
},
);
let usdc_mint = add_usdc_mint(&mut test);
let usdc_oracle = add_usdc_oracle(&mut test);
let usdc_test_reserve = add_reserve(
&mut test,
&lending_market,
&usdc_oracle,
&user_accounts_owner,
AddReserveArgs {
liquidity_amount: USDC_RESERVE_LIQUIDITY_FRACTIONAL,
liquidity_mint_pubkey: usdc_mint.pubkey,
liquidity_mint_decimals: usdc_mint.decimals,
config: reserve_config,
mark_fresh: true,
..AddReserveArgs::default()
},
);
let test_obligation = add_obligation(
&mut test,
&lending_market,
&user_accounts_owner,
AddObligationArgs {
deposits: &[(&sol_test_reserve, SOL_DEPOSIT_AMOUNT_LAMPORTS)],
..AddObligationArgs::default()
},
);
let (mut banks_client, payer, recent_blockhash) = test.start().await;
let mut transaction = Transaction::new_with_payer(
&[
refresh_obligation(
spl_token_lending::id(),
test_obligation.pubkey,
vec![sol_test_reserve.pubkey],
),
borrow_obligation_liquidity(
spl_token_lending::id(),
USDC_BORROW_AMOUNT_FRACTIONAL,
Some(SLIPPAGE_LIMIT),
usdc_test_reserve.liquidity_supply_pubkey,
usdc_test_reserve.user_liquidity_pubkey,
usdc_test_reserve.pubkey,
usdc_test_reserve.liquidity_fee_receiver_pubkey,
test_obligation.pubkey,
lending_market.pubkey,
test_obligation.owner,
Some(usdc_test_reserve.liquidity_host_pubkey),
),
],
Some(&payer.pubkey()),
);
transaction.sign(&[&payer, &user_accounts_owner], recent_blockhash);
// check that transaction succeeds
banks_client.process_transaction(transaction).await.unwrap();
}

View File

@ -651,6 +651,7 @@ impl TestLendingMarket {
&[borrow_obligation_liquidity(
spl_token_lending::id(),
liquidity_amount,
None,
borrow_reserve.liquidity_supply_pubkey,
borrow_reserve.user_liquidity_pubkey,
borrow_reserve.pubkey,

View File

@ -165,6 +165,7 @@ async fn test_success() {
borrow_obligation_liquidity(
spl_token_lending::id(),
USDC_BORROW_AMOUNT_FRACTIONAL,
None,
usdc_test_reserve.liquidity_supply_pubkey,
usdc_test_reserve.user_liquidity_pubkey,
usdc_test_reserve.pubkey,