From 18729cf04c86daaee73885cf41d0b11202fef450 Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Mon, 15 Jan 2024 12:45:00 +0100 Subject: [PATCH] Configurable interest rate for 0% utilization (#848) --- mango_v4.json | 39 +++++++++- .../mango-v4/src/instructions/token_edit.rs | 11 +++ .../src/instructions/token_register.rs | 4 +- .../instructions/token_register_trustless.rs | 3 +- programs/mango-v4/src/lib.rs | 4 + programs/mango-v4/src/state/bank.rs | 75 ++++++++++++++++-- .../tests/program_test/mango_client.rs | 2 + ts/client/src/accounts/bank.ts | 15 +++- ts/client/src/client.ts | 2 + ts/client/src/clientIxParamBuilder.ts | 4 + ts/client/src/mango_v4.ts | 78 ++++++++++++++++++- 11 files changed, 224 insertions(+), 13 deletions(-) diff --git a/mango_v4.json b/mango_v4.json index 125eb7644..d0e24216a 100644 --- a/mango_v4.json +++ b/mango_v4.json @@ -623,6 +623,10 @@ { "name": "depositLimit", "type": "u64" + }, + { + "name": "zeroUtilRate", + "type": "f32" } ] }, @@ -1021,6 +1025,12 @@ "type": { "option": "u64" } + }, + { + "name": "zeroUtilRateOpt", + "type": { + "option": "f32" + } } ] }, @@ -7137,6 +7147,16 @@ }, { "name": "util0", + "docs": [ + "The unscaled borrow interest curve is defined as continuous piecewise linear with the points:", + "", + "- 0% util: zero_util_rate", + "- util0% util: rate0", + "- util1% util: rate1", + "- 100% util: max_rate", + "", + "The final rate is this unscaled curve multiplied by interest_curve_scaling." + ], "type": { "defined": "I80F48" } @@ -7161,6 +7181,12 @@ }, { "name": "maxRate", + "docs": [ + "the 100% utilization rate", + "", + "This isn't the max_rate, since this still gets scaled by interest_curve_scaling,", + "which is >=1." + ], "type": { "defined": "I80F48" } @@ -7421,12 +7447,23 @@ ], "type": "u64" }, + { + "name": "zeroUtilRate", + "docs": [ + "The unscaled borrow interest curve point for zero utilization.", + "", + "See util0, rate0, util1, rate1, max_rate" + ], + "type": { + "defined": "I80F48" + } + }, { "name": "reserved", "type": { "array": [ "u8", - 1968 + 1952 ] } } diff --git a/programs/mango-v4/src/instructions/token_edit.rs b/programs/mango-v4/src/instructions/token_edit.rs index 4c3cec6f9..073f97c2b 100644 --- a/programs/mango-v4/src/instructions/token_edit.rs +++ b/programs/mango-v4/src/instructions/token_edit.rs @@ -51,6 +51,7 @@ pub fn token_edit( maint_weight_shift_abort: bool, set_fallback_oracle: bool, deposit_limit_opt: Option, + zero_util_rate: Option, ) -> Result<()> { let group = ctx.accounts.group.load()?; @@ -462,6 +463,16 @@ pub fn token_edit( bank.deposit_limit = deposit_limit; require_group_admin = true; } + + if let Some(zero_util_rate) = zero_util_rate { + msg!( + "Zero utilization rate old {:?}, new {:?}", + bank.zero_util_rate, + zero_util_rate + ); + bank.zero_util_rate = I80F48::from_num(zero_util_rate); + require_group_admin = true; + } } // account constraint #1 diff --git a/programs/mango-v4/src/instructions/token_register.rs b/programs/mango-v4/src/instructions/token_register.rs index 3169b0c9b..deaadaf95 100644 --- a/programs/mango-v4/src/instructions/token_register.rs +++ b/programs/mango-v4/src/instructions/token_register.rs @@ -42,6 +42,7 @@ pub fn token_register( interest_target_utilization: f32, group_insurance_fund: bool, deposit_limit: u64, + zero_util_rate: f32, ) -> Result<()> { // Require token 0 to be in the insurance token if token_index == INSURANCE_TOKEN_INDEX { @@ -122,7 +123,8 @@ pub fn token_register( maint_weight_shift_liab_target: I80F48::ZERO, fallback_oracle: ctx.accounts.fallback_oracle.key(), deposit_limit, - reserved: [0; 1968], + zero_util_rate: I80F48::from_num(zero_util_rate), + reserved: [0; 1952], }; let oracle_ref = &AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?; diff --git a/programs/mango-v4/src/instructions/token_register_trustless.rs b/programs/mango-v4/src/instructions/token_register_trustless.rs index d6b53c4f8..77a05da05 100644 --- a/programs/mango-v4/src/instructions/token_register_trustless.rs +++ b/programs/mango-v4/src/instructions/token_register_trustless.rs @@ -104,7 +104,8 @@ pub fn token_register_trustless( maint_weight_shift_liab_target: I80F48::ZERO, fallback_oracle: ctx.accounts.fallback_oracle.key(), deposit_limit: 0, - reserved: [0; 1968], + zero_util_rate: I80F48::ZERO, + reserved: [0; 1952], }; let oracle_ref = &AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?; if let Ok(oracle_price) = bank.oracle_price(&OracleAccountInfos::from_reader(oracle_ref), None) diff --git a/programs/mango-v4/src/lib.rs b/programs/mango-v4/src/lib.rs index 4ca27de12..01a6577ac 100644 --- a/programs/mango-v4/src/lib.rs +++ b/programs/mango-v4/src/lib.rs @@ -155,6 +155,7 @@ pub mod mango_v4 { interest_target_utilization: f32, group_insurance_fund: bool, deposit_limit: u64, + zero_util_rate: f32, ) -> Result<()> { #[cfg(feature = "enable-gpl")] instructions::token_register( @@ -186,6 +187,7 @@ pub mod mango_v4 { interest_target_utilization, group_insurance_fund, deposit_limit, + zero_util_rate, )?; Ok(()) } @@ -239,6 +241,7 @@ pub mod mango_v4 { maint_weight_shift_abort: bool, set_fallback_oracle: bool, deposit_limit_opt: Option, + zero_util_rate_opt: Option, ) -> Result<()> { #[cfg(feature = "enable-gpl")] instructions::token_edit( @@ -279,6 +282,7 @@ pub mod mango_v4 { maint_weight_shift_abort, set_fallback_oracle, deposit_limit_opt, + zero_util_rate_opt, )?; Ok(()) } diff --git a/programs/mango-v4/src/state/bank.rs b/programs/mango-v4/src/state/bank.rs index de8fc90b0..3f2187dcc 100644 --- a/programs/mango-v4/src/state/bank.rs +++ b/programs/mango-v4/src/state/bank.rs @@ -59,10 +59,24 @@ pub struct Bank { pub avg_utilization: I80F48, pub adjustment_factor: I80F48, + + /// The unscaled borrow interest curve is defined as continuous piecewise linear with the points: + /// + /// - 0% util: zero_util_rate + /// - util0% util: rate0 + /// - util1% util: rate1 + /// - 100% util: max_rate + /// + /// The final rate is this unscaled curve multiplied by interest_curve_scaling. pub util0: I80F48, pub rate0: I80F48, pub util1: I80F48, pub rate1: I80F48, + + /// the 100% utilization rate + /// + /// This isn't the max_rate, since this still gets scaled by interest_curve_scaling, + /// which is >=1. pub max_rate: I80F48, // TODO: add ix/logic to regular send this to DAO @@ -182,8 +196,13 @@ pub struct Bank { /// zero means none, in token native pub deposit_limit: u64, + /// The unscaled borrow interest curve point for zero utilization. + /// + /// See util0, rate0, util1, rate1, max_rate + pub zero_util_rate: I80F48, + #[derivative(Debug = "ignore")] - pub reserved: [u8; 1968], + pub reserved: [u8; 1952], } const_assert_eq!( size_of::(), @@ -220,7 +239,8 @@ const_assert_eq!( + 16 * 3 + 32 + 8 - + 1968 + + 16 + + 1952 ); const_assert_eq!(size_of::(), 3064); const_assert_eq!(size_of::() % 8, 0); @@ -324,7 +344,8 @@ impl Bank { maint_weight_shift_liab_target: existing_bank.maint_weight_shift_liab_target, fallback_oracle: existing_bank.oracle, deposit_limit: existing_bank.deposit_limit, - reserved: [0; 1968], + zero_util_rate: existing_bank.zero_util_rate, + reserved: [0; 1952], } } @@ -355,6 +376,7 @@ impl Bank { require_gte!(self.maint_weight_shift_duration_inv, 0.0); require_gte!(self.maint_weight_shift_asset_target, 0.0); require_gte!(self.maint_weight_shift_liab_target, 0.0); + require_gte!(self.zero_util_rate, I80F48::ZERO); Ok(()) } @@ -1000,6 +1022,7 @@ impl Bank { pub fn compute_interest_rate(&self, utilization: I80F48) -> I80F48 { Bank::interest_rate_curve_calculator( utilization, + self.zero_util_rate, self.util0, self.rate0, self.util1, @@ -1014,6 +1037,7 @@ impl Bank { #[inline(always)] pub fn interest_rate_curve_calculator( utilization: I80F48, + zero_util_rate: I80F48, util0: I80F48, rate0: I80F48, util1: I80F48, @@ -1025,8 +1049,8 @@ impl Bank { let utilization = utilization.max(I80F48::ZERO).min(I80F48::ONE); let v = if utilization <= util0 { - let slope = rate0 / util0; - slope * utilization + let slope = (rate0 - zero_util_rate) / util0; + zero_util_rate + slope * utilization } else if utilization <= util1 { let extra_util = utilization - util0; let slope = (rate1 - rate0) / (util1 - util0); @@ -1683,4 +1707,45 @@ mod tests { Ok(()) } + + #[test] + fn test_bank_interest_rate_curve() { + let mut bank = Bank::zeroed(); + bank.zero_util_rate = I80F48::from(1); + bank.rate0 = I80F48::from(3); + bank.rate1 = I80F48::from(7); + bank.max_rate = I80F48::from(13); + + bank.util0 = I80F48::from_num(0.5); + bank.util1 = I80F48::from_num(0.75); + + let interest = |v: f64| { + bank.compute_interest_rate(I80F48::from_num(v)) + .to_num::() + }; + let d = |a: f64, b: f64| (a - b).abs(); + + // the points + let eps = 0.0001; + assert!(d(interest(-0.5), 1.0) <= eps); + assert!(d(interest(0.0), 1.0) <= eps); + assert!(d(interest(0.5), 3.0) <= eps); + assert!(d(interest(0.75), 7.0) <= eps); + assert!(d(interest(1.0), 13.0) <= eps); + assert!(d(interest(1.5), 13.0) <= eps); + + // midpoints + assert!(d(interest(0.25), 2.0) <= eps); + assert!(d(interest((0.5 + 0.75) / 2.0), 5.0) <= eps); + assert!(d(interest((0.75 + 1.0) / 2.0), 10.0) <= eps); + + // around the points + let delta = 0.000001; + assert!(d(interest(0.0 + delta), 1.0) <= eps); + assert!(d(interest(0.5 - delta), 3.0) <= eps); + assert!(d(interest(0.5 + delta), 3.0) <= eps); + assert!(d(interest(0.75 - delta), 7.0) <= eps); + assert!(d(interest(0.75 + delta), 7.0) <= eps); + assert!(d(interest(1.0 - delta), 13.0) <= eps); + } } diff --git a/programs/mango-v4/tests/program_test/mango_client.rs b/programs/mango-v4/tests/program_test/mango_client.rs index 7159f2013..c9080d6a7 100644 --- a/programs/mango-v4/tests/program_test/mango_client.rs +++ b/programs/mango-v4/tests/program_test/mango_client.rs @@ -1074,6 +1074,7 @@ impl ClientInstruction for TokenRegisterInstruction { interest_target_utilization: 0.5, group_insurance_fund: true, deposit_limit: 0, + zero_util_rate: 0.0, }; let bank = Pubkey::find_program_address( @@ -1319,6 +1320,7 @@ pub fn token_edit_instruction_default() -> mango_v4::instruction::TokenEdit { maint_weight_shift_abort: false, set_fallback_oracle: false, deposit_limit_opt: None, + zero_util_rate_opt: None, } } diff --git a/ts/client/src/accounts/bank.ts b/ts/client/src/accounts/bank.ts index b46a521ae..bd2fbd206 100644 --- a/ts/client/src/accounts/bank.ts +++ b/ts/client/src/accounts/bank.ts @@ -1,6 +1,7 @@ import { BN } from '@coral-xyz/anchor'; import { utf8 } from '@coral-xyz/anchor/dist/cjs/utils/bytes'; import { PublicKey } from '@solana/web3.js'; +import { format } from 'path'; import { I80F48, I80F48Dto, ONE_I80F48, ZERO_I80F48 } from '../numbers/I80F48'; import { As, toUiDecimals } from '../utils'; import { OracleProvider, isOracleStaleOrUnconfident } from './oracle'; @@ -79,6 +80,7 @@ export class Bank implements BankForHealth { public maintWeightShiftDurationInv: I80F48; public maintWeightShiftAssetTarget: I80F48; public maintWeightShiftLiabTarget: I80F48; + public zeroUtilRate: I80F48; static from( publicKey: PublicKey, @@ -139,6 +141,7 @@ export class Bank implements BankForHealth { maintWeightShiftAssetTarget: I80F48Dto; maintWeightShiftLiabTarget: I80F48Dto; depositLimit: BN; + zeroUtilRate: I80F48Dto; }, ): Bank { return new Bank( @@ -199,6 +202,7 @@ export class Bank implements BankForHealth { obj.maintWeightShiftAssetTarget, obj.maintWeightShiftLiabTarget, obj.depositLimit, + obj.zeroUtilRate, ); } @@ -260,6 +264,7 @@ export class Bank implements BankForHealth { maintWeightShiftAssetTarget: I80F48Dto, maintWeightShiftLiabTarget: I80F48Dto, public depositLimit: BN, + zeroUtilRate: I80F48Dto, ) { this.name = utf8.decode(new Uint8Array(name)).split('\x00')[0]; this.oracleConfig = { @@ -289,6 +294,7 @@ export class Bank implements BankForHealth { this.maintWeightShiftDurationInv = I80F48.from(maintWeightShiftDurationInv); this.maintWeightShiftAssetTarget = I80F48.from(maintWeightShiftAssetTarget); this.maintWeightShiftLiabTarget = I80F48.from(maintWeightShiftLiabTarget); + this.zeroUtilRate = I80F48.from(zeroUtilRate); this._price = undefined; this._uiPrice = undefined; this._oracleLastUpdatedSlot = undefined; @@ -508,13 +514,16 @@ export class Bank implements BankForHealth { return ZERO_I80F48(); } - const utilization = totalBorrows.div(totalDeposits); + const utilization = totalBorrows + .div(totalDeposits) + .max(ZERO_I80F48()) + .min(ONE_I80F48()); const scaling = I80F48.fromNumber( this.interestCurveScaling == 0.0 ? 1.0 : this.interestCurveScaling, ); if (utilization.lt(this.util0)) { - const slope = this.rate0.div(this.util0); - return slope.mul(utilization).mul(scaling); + const slope = this.rate0.sub(this.zeroUtilRate).div(this.util0); + return this.zeroUtilRate.add(slope.mul(utilization)).mul(scaling); } else if (utilization.lt(this.util1)) { const extraUtil = utilization.sub(this.util0); const slope = this.rate1.sub(this.rate0).div(this.util1.sub(this.util0)); diff --git a/ts/client/src/client.ts b/ts/client/src/client.ts index f4ea1339a..9649ed32e 100644 --- a/ts/client/src/client.ts +++ b/ts/client/src/client.ts @@ -458,6 +458,7 @@ export class MangoClient { params.interestTargetUtilization, params.groupInsuranceFund, params.depositLimit, + params.zeroUtilRate, ) .accounts({ group: group.publicKey, @@ -542,6 +543,7 @@ export class MangoClient { params.maintWeightShiftAbort ?? false, params.setFallbackOracle ?? false, params.depositLimit, + params.zeroUtilRate, ) .accounts({ group: group.publicKey, diff --git a/ts/client/src/clientIxParamBuilder.ts b/ts/client/src/clientIxParamBuilder.ts index f4b2b73e7..03d598c11 100644 --- a/ts/client/src/clientIxParamBuilder.ts +++ b/ts/client/src/clientIxParamBuilder.ts @@ -28,6 +28,7 @@ export interface TokenRegisterParams { interestCurveScaling: number; interestTargetUtilization: number; depositLimit: BN; + zeroUtilRate: number; } export const DefaultTokenRegisterParams: TokenRegisterParams = { @@ -66,6 +67,7 @@ export const DefaultTokenRegisterParams: TokenRegisterParams = { interestCurveScaling: 4.0, interestTargetUtilization: 0.5, depositLimit: new BN(0), + zeroUtilRate: 0.0, }; export interface TokenEditParams { @@ -105,6 +107,7 @@ export interface TokenEditParams { maintWeightShiftAbort: boolean | null; setFallbackOracle: boolean | null; depositLimit: BN | null; + zeroUtilRate: number | null; } export const NullTokenEditParams: TokenEditParams = { @@ -144,6 +147,7 @@ export const NullTokenEditParams: TokenEditParams = { maintWeightShiftAbort: null, setFallbackOracle: null, depositLimit: null, + zeroUtilRate: null, }; export interface PerpEditParams { diff --git a/ts/client/src/mango_v4.ts b/ts/client/src/mango_v4.ts index 8d0922d62..bd7b6b90c 100644 --- a/ts/client/src/mango_v4.ts +++ b/ts/client/src/mango_v4.ts @@ -623,6 +623,10 @@ export type MangoV4 = { { "name": "depositLimit", "type": "u64" + }, + { + "name": "zeroUtilRate", + "type": "f32" } ] }, @@ -1021,6 +1025,12 @@ export type MangoV4 = { "type": { "option": "u64" } + }, + { + "name": "zeroUtilRateOpt", + "type": { + "option": "f32" + } } ] }, @@ -7137,6 +7147,16 @@ export type MangoV4 = { }, { "name": "util0", + "docs": [ + "The unscaled borrow interest curve is defined as continuous piecewise linear with the points:", + "", + "- 0% util: zero_util_rate", + "- util0% util: rate0", + "- util1% util: rate1", + "- 100% util: max_rate", + "", + "The final rate is this unscaled curve multiplied by interest_curve_scaling." + ], "type": { "defined": "I80F48" } @@ -7161,6 +7181,12 @@ export type MangoV4 = { }, { "name": "maxRate", + "docs": [ + "the 100% utilization rate", + "", + "This isn't the max_rate, since this still gets scaled by interest_curve_scaling,", + "which is >=1." + ], "type": { "defined": "I80F48" } @@ -7421,12 +7447,23 @@ export type MangoV4 = { ], "type": "u64" }, + { + "name": "zeroUtilRate", + "docs": [ + "The unscaled borrow interest curve point for zero utilization.", + "", + "See util0, rate0, util1, rate1, max_rate" + ], + "type": { + "defined": "I80F48" + } + }, { "name": "reserved", "type": { "array": [ "u8", - 1968 + 1952 ] } } @@ -14346,6 +14383,10 @@ export const IDL: MangoV4 = { { "name": "depositLimit", "type": "u64" + }, + { + "name": "zeroUtilRate", + "type": "f32" } ] }, @@ -14744,6 +14785,12 @@ export const IDL: MangoV4 = { "type": { "option": "u64" } + }, + { + "name": "zeroUtilRateOpt", + "type": { + "option": "f32" + } } ] }, @@ -20860,6 +20907,16 @@ export const IDL: MangoV4 = { }, { "name": "util0", + "docs": [ + "The unscaled borrow interest curve is defined as continuous piecewise linear with the points:", + "", + "- 0% util: zero_util_rate", + "- util0% util: rate0", + "- util1% util: rate1", + "- 100% util: max_rate", + "", + "The final rate is this unscaled curve multiplied by interest_curve_scaling." + ], "type": { "defined": "I80F48" } @@ -20884,6 +20941,12 @@ export const IDL: MangoV4 = { }, { "name": "maxRate", + "docs": [ + "the 100% utilization rate", + "", + "This isn't the max_rate, since this still gets scaled by interest_curve_scaling,", + "which is >=1." + ], "type": { "defined": "I80F48" } @@ -21144,12 +21207,23 @@ export const IDL: MangoV4 = { ], "type": "u64" }, + { + "name": "zeroUtilRate", + "docs": [ + "The unscaled borrow interest curve point for zero utilization.", + "", + "See util0, rate0, util1, rate1, max_rate" + ], + "type": { + "defined": "I80F48" + } + }, { "name": "reserved", "type": { "array": [ "u8", - 1968 + 1952 ] } }