Configurable interest rate for 0% utilization (#848)

This commit is contained in:
Christian Kamm 2024-01-15 12:45:00 +01:00 committed by GitHub
parent 637b43fec4
commit 18729cf04c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 224 additions and 13 deletions

View File

@ -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
]
}
}

View File

@ -51,6 +51,7 @@ pub fn token_edit(
maint_weight_shift_abort: bool,
set_fallback_oracle: bool,
deposit_limit_opt: Option<u64>,
zero_util_rate: Option<f32>,
) -> 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

View File

@ -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())?;

View File

@ -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)

View File

@ -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<u64>,
zero_util_rate_opt: Option<f32>,
) -> 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(())
}

View File

@ -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::<Bank>(),
@ -220,7 +239,8 @@ const_assert_eq!(
+ 16 * 3
+ 32
+ 8
+ 1968
+ 16
+ 1952
);
const_assert_eq!(size_of::<Bank>(), 3064);
const_assert_eq!(size_of::<Bank>() % 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::<f64>()
};
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);
}
}

View File

@ -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,
}
}

View File

@ -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));

View File

@ -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,

View File

@ -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 {

View File

@ -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
]
}
}