Skip to content

Commit 95a4e55

Browse files
committed
0xripleys outflow limits (#125)
Use a sliding window rate limiter to limit borrows and withdraws at the lending pool owner's discretion.
1 parent 5a53f75 commit 95a4e55

18 files changed

+985
-109
lines changed

token-lending/cli/src/main.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use lending_state::SolendState;
22
use solana_client::rpc_config::RpcSendTransactionConfig;
33
use solana_sdk::{commitment_config::CommitmentLevel, compute_budget::ComputeBudgetInstruction};
4+
use solend_program::state::{RateLimiterConfig, SLOTS_PER_YEAR};
45
use solend_sdk::{
56
instruction::{
67
liquidate_obligation_and_redeem_reserve_collateral, redeem_reserve_collateral,
@@ -1675,6 +1676,10 @@ fn command_update_reserve(
16751676
&[update_reserve_config(
16761677
config.lending_program_id,
16771678
reserve.config,
1679+
RateLimiterConfig {
1680+
window_duration: SLOTS_PER_YEAR / 365,
1681+
max_outflow: u64::MAX,
1682+
},
16781683
reserve_pubkey,
16791684
lending_market_pubkey,
16801685
lending_market_owner_keypair.pubkey(),

token-lending/program/src/processor.rs

Lines changed: 81 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ use solana_program::{
3030
Sysvar,
3131
},
3232
};
33+
use solend_sdk::state::{RateLimiter, RateLimiterConfig};
3334
use solend_sdk::{switchboard_v2_devnet, switchboard_v2_mainnet};
3435
use spl_token::state::Mint;
3536
use std::{cmp::min, result::Result};
@@ -53,9 +54,17 @@ pub fn process_instruction(
5354
msg!("Instruction: Init Lending Market");
5455
process_init_lending_market(program_id, owner, quote_currency, accounts)
5556
}
56-
LendingInstruction::SetLendingMarketOwner { new_owner } => {
57+
LendingInstruction::SetLendingMarketOwnerAndConfig {
58+
new_owner,
59+
rate_limiter_config,
60+
} => {
5761
msg!("Instruction: Set Lending Market Owner");
58-
process_set_lending_market_owner(program_id, new_owner, accounts)
62+
process_set_lending_market_owner_and_config(
63+
program_id,
64+
new_owner,
65+
rate_limiter_config,
66+
accounts,
67+
)
5968
}
6069
LendingInstruction::InitReserve {
6170
liquidity_amount,
@@ -128,9 +137,12 @@ pub fn process_instruction(
128137
accounts,
129138
)
130139
}
131-
LendingInstruction::UpdateReserveConfig { config } => {
140+
LendingInstruction::UpdateReserveConfig {
141+
config,
142+
rate_limiter_config,
143+
} => {
132144
msg!("Instruction: UpdateReserveConfig");
133-
process_update_reserve_config(program_id, config, accounts)
145+
process_update_reserve_config(program_id, config, rate_limiter_config, accounts)
134146
}
135147
LendingInstruction::LiquidateObligationAndRedeemReserveCollateral { liquidity_amount } => {
136148
msg!("Instruction: Liquidate Obligation and Redeem Reserve Collateral");
@@ -190,16 +202,18 @@ fn process_init_lending_market(
190202
token_program_id: *token_program_id.key,
191203
oracle_program_id: *oracle_program_id.key,
192204
switchboard_oracle_program_id: *switchboard_oracle_program_id.key,
205+
current_slot: Clock::get()?.slot,
193206
});
194207
LendingMarket::pack(lending_market, &mut lending_market_info.data.borrow_mut())?;
195208

196209
Ok(())
197210
}
198211

199212
#[inline(never)] // avoid stack frame limit
200-
fn process_set_lending_market_owner(
213+
fn process_set_lending_market_owner_and_config(
201214
program_id: &Pubkey,
202215
new_owner: Pubkey,
216+
rate_limiter_config: RateLimiterConfig,
203217
accounts: &[AccountInfo],
204218
) -> ProgramResult {
205219
let account_info_iter = &mut accounts.iter();
@@ -221,6 +235,11 @@ fn process_set_lending_market_owner(
221235
}
222236

223237
lending_market.owner = new_owner;
238+
239+
if rate_limiter_config != lending_market.rate_limiter.config {
240+
lending_market.rate_limiter = RateLimiter::new(rate_limiter_config, Clock::get()?.slot);
241+
}
242+
224243
LendingMarket::pack(lending_market, &mut lending_market_info.data.borrow_mut())?;
225244

226245
Ok(())
@@ -347,6 +366,7 @@ fn process_init_reserve(
347366
supply_pubkey: *reserve_collateral_supply_info.key,
348367
}),
349368
config,
369+
rate_limiter_config: RateLimiterConfig::default(),
350370
});
351371

352372
let collateral_amount = reserve.deposit_liquidity(liquidity_amount)?;
@@ -659,7 +679,6 @@ fn process_redeem_reserve_collateral(
659679
}
660680
let token_program_id = next_account_info(account_info_iter)?;
661681

662-
_refresh_reserve_interest(program_id, reserve_info, clock)?;
663682
_redeem_reserve_collateral(
664683
program_id,
665684
collateral_amount,
@@ -673,6 +692,7 @@ fn process_redeem_reserve_collateral(
673692
user_transfer_authority_info,
674693
clock,
675694
token_program_id,
695+
true,
676696
)?;
677697
let mut reserve = Reserve::unpack(&reserve_info.data.borrow())?;
678698
reserve.last_update.mark_stale();
@@ -695,8 +715,9 @@ fn _redeem_reserve_collateral<'a>(
695715
user_transfer_authority_info: &AccountInfo<'a>,
696716
clock: &Clock,
697717
token_program_id: &AccountInfo<'a>,
718+
check_rate_limits: bool,
698719
) -> Result<u64, ProgramError> {
699-
let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?;
720+
let mut lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?;
700721
if lending_market_info.owner != program_id {
701722
msg!("Lending market provided is not owned by the lending program");
702723
return Err(LendingError::InvalidAccountOwner.into());
@@ -750,8 +771,31 @@ fn _redeem_reserve_collateral<'a>(
750771
}
751772

752773
let liquidity_amount = reserve.redeem_collateral(collateral_amount)?;
774+
775+
if check_rate_limits {
776+
lending_market
777+
.rate_limiter
778+
.update(
779+
clock.slot,
780+
reserve.market_value(Decimal::from(liquidity_amount))?,
781+
)
782+
.map_err(|err| {
783+
msg!("Market outflow limit exceeded! Please try again later.");
784+
err
785+
})?;
786+
787+
reserve
788+
.rate_limiter
789+
.update(clock.slot, Decimal::from(liquidity_amount))
790+
.map_err(|err| {
791+
msg!("Reserve outflow limit exceeded! Please try again later.");
792+
err
793+
})?;
794+
}
795+
753796
reserve.last_update.mark_stale();
754797
Reserve::pack(reserve, &mut reserve_info.data.borrow_mut())?;
798+
LendingMarket::pack(lending_market, &mut lending_market_info.data.borrow_mut())?;
755799

756800
spl_token_burn(TokenBurnParams {
757801
mint: reserve_collateral_mint_info.clone(),
@@ -1359,7 +1403,7 @@ fn process_borrow_obligation_liquidity(
13591403
}
13601404
let token_program_id = next_account_info(account_info_iter)?;
13611405

1362-
let lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?;
1406+
let mut lending_market = LendingMarket::unpack(&lending_market_info.data.borrow())?;
13631407
if lending_market_info.owner != program_id {
13641408
msg!("Lending market provided is not owned by the lending program");
13651409
return Err(LendingError::InvalidAccountOwner.into());
@@ -1477,6 +1521,27 @@ fn process_borrow_obligation_liquidity(
14771521

14781522
let cumulative_borrow_rate_wads = borrow_reserve.liquidity.cumulative_borrow_rate_wads;
14791523

1524+
// check outflow rate limits
1525+
{
1526+
lending_market
1527+
.rate_limiter
1528+
.update(clock.slot, borrow_reserve.market_value(borrow_amount)?)
1529+
.map_err(|err| {
1530+
msg!("Market outflow limit exceeded! Please try again later.");
1531+
err
1532+
})?;
1533+
1534+
borrow_reserve
1535+
.rate_limiter
1536+
.update(clock.slot, borrow_amount)
1537+
.map_err(|err| {
1538+
msg!("Reserve outflow limit exceeded! Please try again later");
1539+
err
1540+
})?;
1541+
}
1542+
1543+
LendingMarket::pack(lending_market, &mut lending_market_info.data.borrow_mut())?;
1544+
14801545
borrow_reserve.liquidity.borrow(borrow_amount)?;
14811546
borrow_reserve.last_update.mark_stale();
14821547
Reserve::pack(borrow_reserve, &mut borrow_reserve_info.data.borrow_mut())?;
@@ -1885,6 +1950,7 @@ fn process_liquidate_obligation_and_redeem_reserve_collateral(
18851950
user_transfer_authority_info,
18861951
clock,
18871952
token_program_id,
1953+
false,
18881954
)?;
18891955
let withdraw_reserve = Reserve::unpack(&withdraw_reserve_info.data.borrow())?;
18901956
if &withdraw_reserve.config.fee_receiver != withdraw_reserve_liquidity_fee_receiver_info.key
@@ -1959,6 +2025,7 @@ fn process_withdraw_obligation_collateral_and_redeem_reserve_liquidity(
19592025
user_transfer_authority_info,
19602026
clock,
19612027
token_program_id,
2028+
true,
19622029
)?;
19632030
Ok(())
19642031
}
@@ -1967,6 +2034,7 @@ fn process_withdraw_obligation_collateral_and_redeem_reserve_liquidity(
19672034
fn process_update_reserve_config(
19682035
program_id: &Pubkey,
19692036
config: ReserveConfig,
2037+
rate_limiter_config: RateLimiterConfig,
19702038
accounts: &[AccountInfo],
19712039
) -> ProgramResult {
19722040
validate_reserve_config(config)?;
@@ -2024,6 +2092,11 @@ fn process_update_reserve_config(
20242092
return Err(LendingError::InvalidMarketAuthority.into());
20252093
}
20262094

2095+
// if window duration and max outflow are different, then create a new rate limiter instance.
2096+
if rate_limiter_config != reserve.rate_limiter.config {
2097+
reserve.rate_limiter = RateLimiter::new(rate_limiter_config, Clock::get()?.slot);
2098+
}
2099+
20272100
if *pyth_price_info.key != reserve.liquidity.pyth_oracle_pubkey {
20282101
validate_pyth_keys(&lending_market, pyth_product_info, pyth_price_info)?;
20292102
reserve.liquidity.pyth_oracle_pubkey = *pyth_price_info.key;

0 commit comments

Comments
 (0)