diff --git a/programs/drift/src/controller/amm/tests.rs b/programs/drift/src/controller/amm/tests.rs index 44fed8f480..c62e2959d5 100644 --- a/programs/drift/src/controller/amm/tests.rs +++ b/programs/drift/src/controller/amm/tests.rs @@ -513,8 +513,8 @@ fn update_pool_balances_test() { market.pnl_pool.balance_type(), ) .unwrap(); - assert_eq!(pnl_pool_token_amount, 1_650_000_000 + 3); - assert_eq!(amm_fee_pool_token_amount, 16_666_666); + assert_eq!(pnl_pool_token_amount, 3_300_000_000 + 3); + assert_eq!(amm_fee_pool_token_amount, 33333333); // negative fee pool market.amm.total_fee_minus_distributions = -8_008_123_456; @@ -539,7 +539,7 @@ fn update_pool_balances_test() { market.pnl_pool.balance_type(), ) .unwrap(); - assert_eq!(pnl_pool_token_amount, 665678880); + assert_eq!(pnl_pool_token_amount, 2332345547); assert_eq!(amm_fee_pool_token_amount, 0); } diff --git a/programs/drift/src/controller/orders/tests.rs b/programs/drift/src/controller/orders/tests.rs index 18b9fbf48f..13247bbb90 100644 --- a/programs/drift/src/controller/orders/tests.rs +++ b/programs/drift/src/controller/orders/tests.rs @@ -8056,7 +8056,7 @@ pub mod fulfill_spot_order_with_match { ) .unwrap(); - assert_eq!(base_filled, 166666666); + assert_eq!(base_filled, 333333333); } #[test] @@ -8252,7 +8252,7 @@ pub mod fulfill_spot_order_with_match { ) .unwrap(); - assert_eq!(base_filled, 166666660); + assert_eq!(base_filled, 333333330); } #[test] @@ -8450,7 +8450,7 @@ pub mod fulfill_spot_order_with_match { ) .unwrap(); - assert_eq!(base_filled, 166666660); + assert_eq!(base_filled, 333333330); } #[test] @@ -8646,7 +8646,7 @@ pub mod fulfill_spot_order_with_match { ) .unwrap(); - assert_eq!(base_filled, 166666666); + assert_eq!(base_filled, 333333333); } #[test] diff --git a/programs/drift/src/controller/spot_balance/tests.rs b/programs/drift/src/controller/spot_balance/tests.rs index 95a1e46c5e..2ecdc9fb17 100644 --- a/programs/drift/src/controller/spot_balance/tests.rs +++ b/programs/drift/src/controller/spot_balance/tests.rs @@ -18,6 +18,7 @@ use crate::math::constants::{ use crate::math::margin::{ calculate_margin_requirement_and_total_collateral_and_liability_info, MarginRequirementType, }; +use crate::math::spot_balance::calculate_borrow_rate; use crate::math::spot_withdraw::{ calculate_max_borrow_token_amount, calculate_min_deposit_token_amount, calculate_token_utilization_limits, check_withdraw_limits, @@ -269,6 +270,7 @@ fn test_daily_withdraw_limits() { }; sol_spot_market.deposit_balance = 50 * SPOT_BALANCE_PRECISION; sol_spot_market.deposit_token_twap = (500 * SPOT_BALANCE_PRECISION) as u64; + sol_spot_market.optimal_utilization = 1_000_000 as u32; //20% APR sol_spot_market.optimal_borrow_rate = SPOT_RATE_PRECISION_U32 / 5; //20% APR sol_spot_market.max_borrow_rate = SPOT_RATE_PRECISION_U32; //100% APR @@ -345,9 +347,10 @@ fn test_daily_withdraw_limits() { assert_eq!(sol_spot_market.deposit_balance, 50000000000); assert_eq!(sol_spot_market.borrow_balance, 8000000002); assert_eq!(sol_spot_market.borrow_token_twap, 0); + update_spot_market_cumulative_interest(&mut sol_spot_market, None, now + 3655 * 24).unwrap(); - assert_eq!(sol_spot_market.deposit_token_twap, 500072987867); - assert_eq!(sol_spot_market.borrow_token_twap, 80072075950); + assert_eq!(sol_spot_market.deposit_token_twap, 500007120768); + assert_eq!(sol_spot_market.borrow_token_twap, 80006208813); update_spot_balances_and_cumulative_deposits_with_limits( 100000 * 100000, @@ -486,7 +489,7 @@ fn test_check_withdraw_limits() { 0, ) .unwrap(); - assert_eq!(mbt, 600000); + assert_eq!(mbt, 642857); let valid_withdraw = check_withdraw_limits(&spot_market, Some(&user), Some(0)).unwrap(); assert!(valid_withdraw); @@ -575,7 +578,7 @@ fn test_check_withdraw_limits_below_optimal_utilization() { assert_eq!(mdt_dep, 153000000000000); assert_eq!(max_bor, 142800000000000); - assert_eq!(mbt_bor, 142600000000000); + assert_eq!(mbt_bor, 151342857142857); let valid_withdraw = check_withdraw_limits(&sol_spot_market, None, None).unwrap(); assert_eq!(valid_withdraw, true); @@ -623,17 +626,17 @@ fn test_check_withdraw_limits_above_optimal_utilization() { initial_liability_weight: 12 * SPOT_WEIGHT_PRECISION / 10, maintenance_liability_weight: 11 * SPOT_WEIGHT_PRECISION / 10, deposit_balance: 200_000 * SPOT_BALANCE_PRECISION, // 200k sol - borrow_balance: 140_000 * SPOT_BALANCE_PRECISION, + borrow_balance: 155_000 * SPOT_BALANCE_PRECISION, liquidator_fee: LIQUIDATION_FEE_PRECISION / 1000, deposit_token_twap: 204000000000000_u64, borrow_token_twap: 192200000000000_u64, - utilization_twap: 800000, // 80% + utilization_twap: 890000, // 89% status: MarketStatus::Active, ..SpotMarket::default() }; - assert_eq!(sol_spot_market.get_utilization().unwrap(), 838627); + assert_eq!(sol_spot_market.get_utilization().unwrap(), 928480); assert!( sol_spot_market.get_utilization().unwrap() > sol_spot_market.optimal_utilization as u128 ); // below optimal util @@ -670,14 +673,14 @@ fn test_check_withdraw_limits_above_optimal_utilization() { .unwrap(); assert_eq!(deposit_tokens_1, 204000000000000); - assert_eq!(borrow_tokens_1, 171080000000000); + assert_eq!(borrow_tokens_1, 189410000000000); // utilization bands differ from others - assert_eq!(min_dep, 190088888888888); //174571.428571 + assert_eq!(min_dep, 200433862433862); assert_eq!(mdt_dep, 153000000000000); - assert_eq!(max_bor, 183600000000000); - assert_eq!(mbt_bor, 163200000000000); + assert_eq!(max_bor, 192780000000000); + assert_eq!(mbt_bor, 178500000000000); // without passing a user, since borrows are above the built in limit of 80% will fail let valid_withdraw = check_withdraw_limits(&sol_spot_market, None, None).unwrap(); @@ -1215,9 +1218,11 @@ fn check_fee_collection_larger_nums() { update_spot_market_cumulative_interest(&mut spot_market, None, now + 100).unwrap(); - assert_eq!(spot_market.revenue_pool.scaled_balance, 3844266986); - assert_eq!(spot_market.cumulative_deposit_interest, 10000346004); - assert_eq!(spot_market.cumulative_borrow_interest, 10000711270); + let br = calculate_borrow_rate(&spot_market, spot_market.get_utilization().unwrap()).unwrap(); + assert_eq!(br, 20173678); + assert_eq!(spot_market.revenue_pool.scaled_balance, 3457492406); + assert_eq!(spot_market.cumulative_deposit_interest, 10000311188); + assert_eq!(spot_market.cumulative_borrow_interest, 10000639702); assert_eq!(spot_market.last_interest_ts, 100); assert_eq!(spot_market.last_twap_ts, 100); assert_eq!(spot_market.utilization_twap, 624); @@ -1241,19 +1246,19 @@ fn check_fee_collection_larger_nums() { ) .unwrap(); - assert_eq!(deposit_tokens_1, 1000038444799); - assert_eq!(borrow_tokens_1, 540548444855); - assert_eq!(if_tokens_1, 3844399); + assert_eq!(deposit_tokens_1, 1000034576399); + assert_eq!(borrow_tokens_1, 540544576533); + assert_eq!(if_tokens_1, 3457599); update_spot_market_cumulative_interest(&mut spot_market, None, now + 7500).unwrap(); assert_eq!(spot_market.last_interest_ts, 7500); assert_eq!(spot_market.last_twap_ts, 7500); - assert_eq!(spot_market.utilization_twap, 46976); + assert_eq!(spot_market.utilization_twap, 46964); - assert_eq!(spot_market.cumulative_deposit_interest, 10025953120); - assert_eq!(spot_market.cumulative_borrow_interest, 10053351363); - assert_eq!(spot_market.revenue_pool.scaled_balance, 287632341391); + assert_eq!(spot_market.cumulative_deposit_interest, 10023340555); + assert_eq!(spot_market.cumulative_borrow_interest, 10047980763); + assert_eq!(spot_market.revenue_pool.scaled_balance, 258744322775); let deposit_tokens_2 = get_token_amount( spot_market.deposit_balance, @@ -1274,14 +1279,14 @@ fn check_fee_collection_larger_nums() { ) .unwrap(); - assert_eq!(deposit_tokens_2, 1002883690837); - assert_eq!(borrow_tokens_2, 543393694522); - assert_eq!(if_tokens_2, 288378837); + assert_eq!(deposit_tokens_2, 1002593403746); + assert_eq!(borrow_tokens_2, 543103408221); + assert_eq!(if_tokens_2, 259348246); //assert >=0 assert_eq!( (borrow_tokens_2 - borrow_tokens_1) - (deposit_tokens_2 - deposit_tokens_1), - 3629 + 4341 ); update_spot_market_cumulative_interest( @@ -1293,9 +1298,11 @@ fn check_fee_collection_larger_nums() { now = now + 750 + (60 * 60 * 24 * 365); - assert_eq!(spot_market.cumulative_deposit_interest, 120056141117); - assert_eq!(spot_market.cumulative_borrow_interest, 236304445676); - assert_eq!(spot_market.revenue_pool.scaled_balance, 102149084836788); + assert_eq!(spot_market.get_utilization().unwrap(), 961580); + + assert_eq!(spot_market.cumulative_deposit_interest, 108608729074); + assert_eq!(spot_market.cumulative_borrow_interest, 212759822472); + assert_eq!(spot_market.revenue_pool.scaled_balance, 101141669831135); let deposit_tokens_3 = get_token_amount( spot_market.deposit_balance, @@ -1316,24 +1323,24 @@ fn check_fee_collection_larger_nums() { ) .unwrap(); - assert_eq!(deposit_tokens_3, 13231976606113); - assert_eq!(borrow_tokens_3, 12772491593234); - assert_eq!(if_tokens_3, 1226362494413); + assert_eq!(deposit_tokens_3, 11959359729078); + assert_eq!(borrow_tokens_3, 11499881164435); + assert_eq!(if_tokens_3, 1098486821678); - assert_eq!((borrow_tokens_3 - borrow_tokens_2), 12229097898712); - assert_eq!((deposit_tokens_3 - deposit_tokens_2), 12229092915276); + assert_eq!((borrow_tokens_3 - borrow_tokens_2), 10956777756214); + assert_eq!((deposit_tokens_3 - deposit_tokens_2), 10956766325332); // assert >= 0 assert_eq!( (borrow_tokens_3 - borrow_tokens_2) - (deposit_tokens_3 - deposit_tokens_2), - 4_983_436 //$4.98 missing + 11430882 //$11.43 missing ); let mut if_balance_2 = 0; // settle IF pool to 100% utilization boundary // only half of depositors available claim was settled (to protect vault) - assert_eq!(spot_market.revenue_pool.scaled_balance, 102149084836788); + assert_eq!(spot_market.revenue_pool.scaled_balance, 101141669831135); spot_market.insurance_fund.revenue_settle_period = 1; let settle_amount = settle_revenue_to_insurance_fund( deposit_tokens_3 as u64, @@ -1343,15 +1350,15 @@ fn check_fee_collection_larger_nums() { true, ) .unwrap(); - assert_eq!(settle_amount, 229742506020); + assert_eq!(settle_amount, 229739282275); assert_eq!(spot_market.insurance_fund.user_shares, 0); assert_eq!(spot_market.insurance_fund.total_shares, 0); if_balance_2 += settle_amount; - assert_eq!(if_balance_2, 229742506020); - assert_eq!(if_tokens_3 - (settle_amount as u128), 996619988393); // w/ update interest for settle_spot_market_to_if + assert_eq!(if_balance_2, 229739282275); + assert_eq!(if_tokens_3 - (settle_amount as u128), 868747539403); // w/ update interest for settle_spot_market_to_if - assert_eq!(spot_market.revenue_pool.scaled_balance, 83024042298956); - assert_eq!(spot_market.utilization_twap, 965274); + assert_eq!(spot_market.revenue_pool.scaled_balance, 79996002243946); + assert_eq!(spot_market.utilization_twap, 961580); let deposit_tokens_4 = get_token_amount( spot_market.deposit_balance, @@ -1372,8 +1379,66 @@ fn check_fee_collection_larger_nums() { ) .unwrap(); - assert_eq!(deposit_tokens_4 - borrow_tokens_4, 229742506021); - assert_eq!(if_tokens_4, 996833556273); + assert_eq!(deposit_tokens_4 - borrow_tokens_4, 229739282275); + assert_eq!(if_tokens_4, 868870384546); +} + +#[test] +fn test_multi_stage_borrow_rate_curve() { + let mut spot_market = SpotMarket { + market_index: 0, + oracle_source: OracleSource::QuoteAsset, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + cumulative_borrow_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 6, + initial_asset_weight: SPOT_WEIGHT_PRECISION, + maintenance_asset_weight: SPOT_WEIGHT_PRECISION, + deposit_balance: 1_000_000 * SPOT_BALANCE_PRECISION, + borrow_balance: 0, + deposit_token_twap: QUOTE_PRECISION_U64 / 2, + + optimal_utilization: SPOT_UTILIZATION_PRECISION_U32 * 70 / 100, // 70% + optimal_borrow_rate: SPOT_RATE_PRECISION_U32 * 10 / 100, // 10% + max_borrow_rate: SPOT_RATE_PRECISION_U32 * 500 / 100, // 500% + ..SpotMarket::default() + }; + + // Store all rates to verify monotonicity and smoothness later + let mut last_rate = 0_u128; + let mut last_rate_delta = 0_u128; + let mut rate_jumps = 0_u128; + for i in 0..=200 { + let utilization = i * 5_000; // 0.5% increments + + let rate = calculate_borrow_rate(&spot_market, utilization).unwrap(); + println!("Utilization: {} => Rate: {}", utilization, rate); + + if (rate - last_rate) > (last_rate_delta + 1) && last_rate_delta != 0 { + rate_jumps += 1; + } + + println!("rate deltas: {} => {}", (rate - last_rate), last_rate_delta); + + assert!( + (rate - last_rate) >= last_rate_delta.saturating_sub(1), + "Rate of change should not decrease by much" + ); + last_rate_delta = rate - last_rate; + + // Check monotonic increasing + assert!(rate >= last_rate, "Rate should not decrease"); + last_rate = rate; + + if utilization == spot_market.optimal_utilization as u128 { + assert!(((rate as i32) - (spot_market.optimal_borrow_rate as i32)).abs() <= 1) + } + + if utilization == 1000000_u128 { + assert_eq!(rate as u32, spot_market.max_borrow_rate) + } + } + + assert_eq!(rate_jumps, 5); } #[test] diff --git a/programs/drift/src/math/constants.rs b/programs/drift/src/math/constants.rs index ce8c49dc9c..dce049d88b 100644 --- a/programs/drift/src/math/constants.rs +++ b/programs/drift/src/math/constants.rs @@ -220,3 +220,13 @@ pub const MAX_PREDICTION_MARKET_PRICE_U128: u128 = PRICE_PRECISION_U64 as u128; // POOL IDs pub const LST_POOL_ID: u8 = 2; + +// SPOT INTEREST RATE CURVE +pub const INTEREST_RATE_SEGMENT_AND_WEIGHTS: &[(u128, u128)] = &[ + (850_000, 50), + (900_000, 100), + (950_000, 150), + (990_000, 200), + (995_000, 250), + (1_000_000, 250), +]; diff --git a/programs/drift/src/math/orders/tests.rs b/programs/drift/src/math/orders/tests.rs index bfe13744da..ed11e85685 100644 --- a/programs/drift/src/math/orders/tests.rs +++ b/programs/drift/src/math/orders/tests.rs @@ -581,7 +581,7 @@ mod get_max_fill_amounts { let (max_base, max_quote) = get_max_fill_amounts(&user, 0, &base_market, "e_market, true).unwrap(); - assert_eq!(max_base, Some(16666666666)); + assert_eq!(max_base, Some(33333333333)); assert_eq!(max_quote, None); } @@ -704,7 +704,7 @@ mod get_max_fill_amounts { get_max_fill_amounts(&user, 0, &base_market, "e_market, true).unwrap(); assert_eq!(max_base, None); - assert_eq!(max_quote, Some(16666666)); + assert_eq!(max_quote, Some(33333333)); } } diff --git a/programs/drift/src/math/spot_balance.rs b/programs/drift/src/math/spot_balance.rs index 5af43ef48e..c444585421 100644 --- a/programs/drift/src/math/spot_balance.rs +++ b/programs/drift/src/math/spot_balance.rs @@ -2,7 +2,9 @@ use crate::error::{DriftResult, ErrorCode}; use crate::math::casting::Cast; #[cfg(feature = "drift-rs")] use crate::math::constants::PERCENTAGE_PRECISION; -use crate::math::constants::{ONE_YEAR, SPOT_RATE_PRECISION, SPOT_UTILIZATION_PRECISION}; +use crate::math::constants::{ + INTEREST_RATE_SEGMENT_AND_WEIGHTS, ONE_YEAR, SPOT_RATE_PRECISION, SPOT_UTILIZATION_PRECISION, +}; use crate::math::safe_math::{SafeDivFloor, SafeMath}; use crate::state::oracle::{OraclePriceData, StrictOraclePrice}; use crate::state::spot_market::{SpotBalanceType, SpotMarket}; @@ -179,37 +181,51 @@ pub fn calculate_accumulated_interest( #[inline(always)] pub fn calculate_borrow_rate(spot_market: &SpotMarket, utilization: u128) -> DriftResult { - let borrow_rate = if utilization > spot_market.optimal_utilization.cast()? { - let surplus_utilization = utilization.safe_sub(spot_market.optimal_utilization.cast()?)?; + let optimal_util = spot_market.optimal_utilization.cast::()?; + let optimal_rate = spot_market.optimal_borrow_rate.cast::()?; + let max_rate = spot_market.max_borrow_rate.cast::()?; + let min_rate = spot_market.get_min_borrow_rate()?.cast::()?; - let borrow_rate_slope = spot_market - .max_borrow_rate - .cast::()? - .safe_sub(spot_market.optimal_borrow_rate.cast()?)? - .safe_mul(SPOT_UTILIZATION_PRECISION)? - .safe_div( - SPOT_UTILIZATION_PRECISION.safe_sub(spot_market.optimal_utilization.cast()?)?, - )?; - - spot_market.optimal_borrow_rate.cast::()?.safe_add( - surplus_utilization - .safe_mul(borrow_rate_slope)? - .safe_div(SPOT_UTILIZATION_PRECISION)?, - )? - } else { - let borrow_rate_slope = spot_market - .optimal_borrow_rate - .cast::()? - .safe_mul(SPOT_UTILIZATION_PRECISION)? - .safe_div(spot_market.optimal_utilization.cast()?)?; + let weights_divisor = 1000; + let borrow_rate = if utilization <= optimal_util { + let slope = optimal_rate + .safe_mul(SPOT_UTILIZATION_PRECISION)? + .safe_div(optimal_util)?; utilization - .safe_mul(borrow_rate_slope)? + .safe_mul(slope)? .safe_div(SPOT_UTILIZATION_PRECISION)? - } - .max(spot_market.get_min_borrow_rate()?.cast()?); + } else { + let total_extra_rate = max_rate.safe_sub(optimal_rate)?; + + let mut rate = optimal_rate; + let mut prev_util = optimal_util; + + for &(bp, weight) in INTEREST_RATE_SEGMENT_AND_WEIGHTS { + let segment_start = prev_util; + let segment_end = bp.min(SPOT_UTILIZATION_PRECISION); + let segment_range = segment_end.safe_sub(segment_start)?; + let segment_rate_total = total_extra_rate + .safe_mul(weight as u128)? + .safe_div(weights_divisor)?; + + if utilization <= segment_end { + let partial_util = utilization.safe_sub(segment_start)?; + let partial_rate = segment_rate_total + .safe_mul(partial_util)? + .safe_div(segment_range)?; + rate = rate.safe_add(partial_rate)?; + break; + } else { + rate = rate.safe_add(segment_rate_total)?; + prev_util = segment_end; + } + } + + rate + }; - Ok(borrow_rate) + Ok(borrow_rate.max(min_rate)) } #[cfg(feature = "drift-rs")] diff --git a/programs/drift/src/math/spot_withdraw.rs b/programs/drift/src/math/spot_withdraw.rs index 78fdf8f8a4..4e82989e9f 100644 --- a/programs/drift/src/math/spot_withdraw.rs +++ b/programs/drift/src/math/spot_withdraw.rs @@ -39,13 +39,13 @@ pub fn calculate_max_borrow_token_amount( let lesser_deposit_amount = deposit_token_amount.min(deposit_token_twap); let max_borrow_token = if pool_id == 0 { - // main pool between ~15-80% utilization with friction on twap in 10% increments + // main pool between ~30-87.5% utilization with friction on twap in 14% increments withdraw_guard_threshold .max( - (lesser_deposit_amount / 6) - .max(borrow_token_twap.safe_add(lesser_deposit_amount / 10)?) - .min(lesser_deposit_amount.safe_sub(lesser_deposit_amount / 5)?), + (lesser_deposit_amount / 3) + .max(borrow_token_twap.safe_add(lesser_deposit_amount / 7)?) + .min(lesser_deposit_amount.safe_sub(lesser_deposit_amount / 8)?), ) .min(max_token_borrows) } else { diff --git a/programs/drift/src/state/spot_market.rs b/programs/drift/src/state/spot_market.rs index 450f7fd26e..c810e7bc35 100644 --- a/programs/drift/src/state/spot_market.rs +++ b/programs/drift/src/state/spot_market.rs @@ -514,7 +514,7 @@ impl SpotMarket { } pub fn is_healthy_utilization(self) -> DriftResult { - let unhealthy_utilization = 800000; // 80% + let unhealthy_utilization = 900000; // 90% let utilization: u64 = self.get_utilization()?.cast()?; Ok(self.utilization_twap <= unhealthy_utilization && utilization <= unhealthy_utilization) } diff --git a/sdk/src/math/spotBalance.ts b/sdk/src/math/spotBalance.ts index 7c0a456e71..8d3ebfc4c9 100644 --- a/sdk/src/math/spotBalance.ts +++ b/sdk/src/math/spotBalance.ts @@ -382,35 +382,61 @@ export function calculateInterestRate( delta = ZERO, currentUtilization: BN = null ): BN { + // todo: ensure both a delta and current util aren't pass? const utilization = currentUtilization || calculateUtilization(bank, delta); - let interestRate: BN; - if (utilization.gt(new BN(bank.optimalUtilization))) { - const surplusUtilization = utilization.sub(new BN(bank.optimalUtilization)); - const borrowRateSlope = new BN(bank.maxBorrowRate - bank.optimalBorrowRate) - .mul(SPOT_MARKET_UTILIZATION_PRECISION) - .div( - SPOT_MARKET_UTILIZATION_PRECISION.sub(new BN(bank.optimalUtilization)) - ); - interestRate = new BN(bank.optimalBorrowRate).add( - surplusUtilization - .mul(borrowRateSlope) - .div(SPOT_MARKET_UTILIZATION_PRECISION) - ); - } else { - const borrowRateSlope = new BN(bank.optimalBorrowRate) - .mul(SPOT_MARKET_UTILIZATION_PRECISION) - .div(new BN(bank.optimalUtilization)); + const optimalUtil = new BN(bank.optimalUtilization); + const optimalRate = new BN(bank.optimalBorrowRate); + const maxRate = new BN(bank.maxBorrowRate); + const minRate = new BN(bank.minBorrowRate).mul( + PERCENTAGE_PRECISION.divn(200) + ); - interestRate = utilization - .mul(borrowRateSlope) - .div(SPOT_MARKET_UTILIZATION_PRECISION); + const weightsDivisor = new BN(1000); + const segments: [BN, BN][] = [ + [new BN(850_000), new BN(50)], + [new BN(900_000), new BN(100)], + [new BN(950_000), new BN(150)], + [new BN(990_000), new BN(200)], + [new BN(995_000), new BN(250)], + [SPOT_MARKET_UTILIZATION_PRECISION, new BN(250)], + ]; + + let rate: BN; + if (utilization.lte(optimalUtil)) { + // below optimal: linear ramp from 0 to optimalRate + const slope = optimalRate + .mul(SPOT_MARKET_UTILIZATION_PRECISION) + .div(optimalUtil); + rate = utilization.mul(slope).div(SPOT_MARKET_UTILIZATION_PRECISION); + } else { + // above optimal: piecewise segments + const totalExtraRate = maxRate.sub(optimalRate); + + rate = optimalRate.clone(); + let prevUtil = optimalUtil.clone(); + + for (const [bp, weight] of segments) { + const segmentEnd = bp.gt(SPOT_MARKET_UTILIZATION_PRECISION) + ? SPOT_MARKET_UTILIZATION_PRECISION + : bp; + const segmentRange = segmentEnd.sub(prevUtil); + + const segmentRateTotal = totalExtraRate.mul(weight).div(weightsDivisor); + + if (utilization.lte(segmentEnd)) { + const partialUtil = utilization.sub(prevUtil); + const partialRate = segmentRateTotal.mul(partialUtil).div(segmentRange); + rate = rate.add(partialRate); + break; + } else { + rate = rate.add(segmentRateTotal); + prevUtil = segmentEnd; + } + } } - return BN.max( - interestRate, - new BN(bank.minBorrowRate).mul(PERCENTAGE_PRECISION.divn(200)) - ); + return rate.lt(minRate) ? minRate : rate; } export function calculateDepositRate(