Bank: store interest curve scale and target util separately (#755)
Allows for better configurability independent of the previous MAX_RATE concept
This commit is contained in:
parent
8110dd1566
commit
3b28856692
|
@ -596,6 +596,14 @@
|
||||||
{
|
{
|
||||||
"name": "flashLoanSwapFeeRate",
|
"name": "flashLoanSwapFeeRate",
|
||||||
"type": "f32"
|
"type": "f32"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "interestCurveScaling",
|
||||||
|
"type": "f32"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "interestTargetUtilization",
|
||||||
|
"type": "f32"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -930,6 +938,18 @@
|
||||||
"type": {
|
"type": {
|
||||||
"option": "f32"
|
"option": "f32"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "interestCurveScalingOpt",
|
||||||
|
"type": {
|
||||||
|
"option": "f32"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "interestTargetUtilizationOpt",
|
||||||
|
"type": {
|
||||||
|
"option": "f32"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -7033,12 +7053,29 @@
|
||||||
"name": "flashLoanSwapFeeRate",
|
"name": "flashLoanSwapFeeRate",
|
||||||
"type": "f32"
|
"type": "f32"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "interestTargetUtilization",
|
||||||
|
"docs": [
|
||||||
|
"Target utilization: If actual utilization is higher, scale up interest.",
|
||||||
|
"If it's lower, scale down interest (if possible)"
|
||||||
|
],
|
||||||
|
"type": "f32"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "interestCurveScaling",
|
||||||
|
"docs": [
|
||||||
|
"Current interest curve scaling, always >= 1.0",
|
||||||
|
"",
|
||||||
|
"Except when first migrating to having this field, then 0.0"
|
||||||
|
],
|
||||||
|
"type": "f64"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "reserved",
|
"name": "reserved",
|
||||||
"type": {
|
"type": {
|
||||||
"array": [
|
"array": [
|
||||||
"u8",
|
"u8",
|
||||||
2092
|
2080
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11335,6 +11372,56 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "UpdateRateLogV2",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "mangoGroup",
|
||||||
|
"type": "publicKey",
|
||||||
|
"index": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tokenIndex",
|
||||||
|
"type": "u16",
|
||||||
|
"index": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "rate0",
|
||||||
|
"type": "i128",
|
||||||
|
"index": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "util0",
|
||||||
|
"type": "i128",
|
||||||
|
"index": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "rate1",
|
||||||
|
"type": "i128",
|
||||||
|
"index": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "util1",
|
||||||
|
"type": "i128",
|
||||||
|
"index": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "maxRate",
|
||||||
|
"type": "i128",
|
||||||
|
"index": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "curveScaling",
|
||||||
|
"type": "f64",
|
||||||
|
"index": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "targetUtilization",
|
||||||
|
"type": "f32",
|
||||||
|
"index": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "TokenLiqWithTokenLog",
|
"name": "TokenLiqWithTokenLog",
|
||||||
"fields": [
|
"fields": [
|
||||||
|
|
|
@ -42,6 +42,8 @@ pub fn token_edit(
|
||||||
token_conditional_swap_taker_fee_rate_opt: Option<f32>,
|
token_conditional_swap_taker_fee_rate_opt: Option<f32>,
|
||||||
token_conditional_swap_maker_fee_rate_opt: Option<f32>,
|
token_conditional_swap_maker_fee_rate_opt: Option<f32>,
|
||||||
flash_loan_swap_fee_rate_opt: Option<f32>,
|
flash_loan_swap_fee_rate_opt: Option<f32>,
|
||||||
|
interest_curve_scaling_opt: Option<f32>,
|
||||||
|
interest_target_utilization_opt: Option<f32>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let group = ctx.accounts.group.load()?;
|
let group = ctx.accounts.group.load()?;
|
||||||
|
|
||||||
|
@ -339,6 +341,27 @@ pub fn token_edit(
|
||||||
bank.flash_loan_swap_fee_rate = fee_rate;
|
bank.flash_loan_swap_fee_rate = fee_rate;
|
||||||
require_group_admin = true;
|
require_group_admin = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(interest_curve_scaling) = interest_curve_scaling_opt {
|
||||||
|
msg!(
|
||||||
|
"Interest curve scaling old {:?}, new {:?}",
|
||||||
|
bank.interest_curve_scaling,
|
||||||
|
interest_curve_scaling
|
||||||
|
);
|
||||||
|
require_gte!(interest_curve_scaling, 1.0);
|
||||||
|
bank.interest_curve_scaling = interest_curve_scaling.into();
|
||||||
|
require_group_admin = true;
|
||||||
|
}
|
||||||
|
if let Some(interest_target_utilization) = interest_target_utilization_opt {
|
||||||
|
msg!(
|
||||||
|
"Interest target utilization old {:?}, new {:?}",
|
||||||
|
bank.interest_target_utilization,
|
||||||
|
interest_target_utilization
|
||||||
|
);
|
||||||
|
require_gte!(interest_target_utilization, 0.0);
|
||||||
|
bank.interest_target_utilization = interest_target_utilization;
|
||||||
|
require_group_admin = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// account constraint #1
|
// account constraint #1
|
||||||
|
|
|
@ -38,6 +38,8 @@ pub fn token_register(
|
||||||
token_conditional_swap_taker_fee_rate: f32,
|
token_conditional_swap_taker_fee_rate: f32,
|
||||||
token_conditional_swap_maker_fee_rate: f32,
|
token_conditional_swap_maker_fee_rate: f32,
|
||||||
flash_loan_swap_fee_rate: f32,
|
flash_loan_swap_fee_rate: f32,
|
||||||
|
interest_curve_scaling: f32,
|
||||||
|
interest_target_utilization: f32,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
// Require token 0 to be in the insurance token
|
// Require token 0 to be in the insurance token
|
||||||
if token_index == INSURANCE_TOKEN_INDEX {
|
if token_index == INSURANCE_TOKEN_INDEX {
|
||||||
|
@ -108,7 +110,9 @@ pub fn token_register(
|
||||||
token_conditional_swap_taker_fee_rate,
|
token_conditional_swap_taker_fee_rate,
|
||||||
token_conditional_swap_maker_fee_rate,
|
token_conditional_swap_maker_fee_rate,
|
||||||
flash_loan_swap_fee_rate: flash_loan_swap_fee_rate,
|
flash_loan_swap_fee_rate: flash_loan_swap_fee_rate,
|
||||||
reserved: [0; 2092],
|
interest_target_utilization,
|
||||||
|
interest_curve_scaling: interest_curve_scaling.into(),
|
||||||
|
reserved: [0; 2080],
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Ok(oracle_price) =
|
if let Ok(oracle_price) =
|
||||||
|
|
|
@ -46,10 +46,10 @@ pub fn token_register_trustless(
|
||||||
// 10% daily adjustment at 0% or 100% utilization
|
// 10% daily adjustment at 0% or 100% utilization
|
||||||
adjustment_factor: I80F48::from_num(0.004),
|
adjustment_factor: I80F48::from_num(0.004),
|
||||||
util0: I80F48::from_num(0.5),
|
util0: I80F48::from_num(0.5),
|
||||||
rate0: I80F48::from_num(0.072),
|
rate0: I80F48::from_num(0.018),
|
||||||
util1: I80F48::from_num(0.8),
|
util1: I80F48::from_num(0.8),
|
||||||
rate1: I80F48::from_num(0.2),
|
rate1: I80F48::from_num(0.05),
|
||||||
max_rate: I80F48::from_num(2.0),
|
max_rate: I80F48::from_num(0.5),
|
||||||
collected_fees_native: I80F48::ZERO,
|
collected_fees_native: I80F48::ZERO,
|
||||||
loan_origination_fee_rate: I80F48::from_num(0.0005),
|
loan_origination_fee_rate: I80F48::from_num(0.0005),
|
||||||
loan_fee_rate: I80F48::from_num(0.005),
|
loan_fee_rate: I80F48::from_num(0.005),
|
||||||
|
@ -80,9 +80,10 @@ pub fn token_register_trustless(
|
||||||
token_conditional_swap_taker_fee_rate: 0.0005,
|
token_conditional_swap_taker_fee_rate: 0.0005,
|
||||||
token_conditional_swap_maker_fee_rate: 0.0005,
|
token_conditional_swap_maker_fee_rate: 0.0005,
|
||||||
flash_loan_swap_fee_rate: 0.0005,
|
flash_loan_swap_fee_rate: 0.0005,
|
||||||
reserved: [0; 2092],
|
interest_target_utilization: 0.5,
|
||||||
|
interest_curve_scaling: 4.0,
|
||||||
|
reserved: [0; 2080],
|
||||||
};
|
};
|
||||||
require_gt!(bank.max_rate, MINIMUM_MAX_RATE);
|
|
||||||
|
|
||||||
if let Ok(oracle_price) =
|
if let Ok(oracle_price) =
|
||||||
bank.oracle_price(&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?, None)
|
bank.oracle_price(&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?, None)
|
||||||
|
|
|
@ -2,7 +2,7 @@ use anchor_lang::prelude::*;
|
||||||
|
|
||||||
use crate::accounts_ix::*;
|
use crate::accounts_ix::*;
|
||||||
use crate::error::MangoError;
|
use crate::error::MangoError;
|
||||||
use crate::logs::{UpdateIndexLog, UpdateRateLog};
|
use crate::logs::{UpdateIndexLog, UpdateRateLogV2};
|
||||||
use crate::state::HOUR;
|
use crate::state::HOUR;
|
||||||
use crate::{
|
use crate::{
|
||||||
accounts_zerocopy::{AccountInfoRef, LoadMutZeroCopyRef, LoadZeroCopyRef},
|
accounts_zerocopy::{AccountInfoRef, LoadMutZeroCopyRef, LoadZeroCopyRef},
|
||||||
|
@ -140,32 +140,54 @@ pub fn token_update_index_and_rate(ctx: Context<TokenUpdateIndexAndRate>) -> Res
|
||||||
|
|
||||||
// compute optimal rates, and max rate and set them on the bank
|
// compute optimal rates, and max rate and set them on the bank
|
||||||
{
|
{
|
||||||
let some_bank = ctx.remaining_accounts[0].load::<Bank>()?;
|
let mut some_bank = ctx.remaining_accounts[0].load_mut::<Bank>()?;
|
||||||
|
|
||||||
let diff_ts = I80F48::from_num(now_ts - some_bank.bank_rate_last_updated);
|
let diff_ts = I80F48::from_num(now_ts - some_bank.bank_rate_last_updated);
|
||||||
|
|
||||||
// update each hour
|
// update each hour
|
||||||
if diff_ts > HOUR {
|
if diff_ts > HOUR {
|
||||||
let (rate0, rate1, max_rate) = some_bank.compute_rates();
|
// First setup when new parameters are introduced
|
||||||
|
if some_bank.interest_curve_scaling == 0.0 {
|
||||||
|
let old_max_rate = 0.5;
|
||||||
|
some_bank.interest_curve_scaling =
|
||||||
|
some_bank.max_rate.to_num::<f64>() / old_max_rate;
|
||||||
|
some_bank.interest_target_utilization = some_bank.util0.to_num();
|
||||||
|
|
||||||
emit!(UpdateRateLog {
|
let descale_factor = I80F48::from_num(1.0 / some_bank.interest_curve_scaling);
|
||||||
|
some_bank.rate0 *= descale_factor;
|
||||||
|
some_bank.rate1 *= descale_factor;
|
||||||
|
some_bank.max_rate *= descale_factor;
|
||||||
|
}
|
||||||
|
|
||||||
|
some_bank.update_interest_rate_scaling();
|
||||||
|
|
||||||
|
let rate0 = some_bank.rate0;
|
||||||
|
let rate1 = some_bank.rate1;
|
||||||
|
let max_rate = some_bank.max_rate;
|
||||||
|
let scaling = some_bank.interest_curve_scaling;
|
||||||
|
let target_util = some_bank.interest_target_utilization;
|
||||||
|
|
||||||
|
emit!(UpdateRateLogV2 {
|
||||||
mango_group: mint_info.group.key(),
|
mango_group: mint_info.group.key(),
|
||||||
token_index: mint_info.token_index,
|
token_index: mint_info.token_index,
|
||||||
rate0: rate0.to_bits(),
|
rate0: rate0.to_bits(),
|
||||||
|
util0: some_bank.util0.to_bits(),
|
||||||
rate1: rate1.to_bits(),
|
rate1: rate1.to_bits(),
|
||||||
|
util1: some_bank.util1.to_bits(),
|
||||||
max_rate: max_rate.to_bits(),
|
max_rate: max_rate.to_bits(),
|
||||||
|
curve_scaling: some_bank.interest_curve_scaling,
|
||||||
|
target_utilization: some_bank.interest_target_utilization,
|
||||||
});
|
});
|
||||||
|
|
||||||
drop(some_bank);
|
drop(some_bank);
|
||||||
|
|
||||||
msg!("rate0 {}", rate0);
|
// Apply the new parameters to all banks
|
||||||
msg!("rate1 {}", rate1);
|
|
||||||
msg!("max_rate {}", max_rate);
|
|
||||||
|
|
||||||
for ai in ctx.remaining_accounts.iter() {
|
for ai in ctx.remaining_accounts.iter() {
|
||||||
let mut bank = ai.load_mut::<Bank>()?;
|
let mut bank = ai.load_mut::<Bank>()?;
|
||||||
|
|
||||||
bank.bank_rate_last_updated = now_ts;
|
bank.bank_rate_last_updated = now_ts;
|
||||||
|
bank.interest_curve_scaling = scaling;
|
||||||
|
bank.interest_target_utilization = target_util;
|
||||||
bank.rate0 = rate0;
|
bank.rate0 = rate0;
|
||||||
bank.rate1 = rate1;
|
bank.rate1 = rate1;
|
||||||
bank.max_rate = max_rate;
|
bank.max_rate = max_rate;
|
||||||
|
|
|
@ -148,6 +148,8 @@ pub mod mango_v4 {
|
||||||
token_conditional_swap_taker_fee_rate: f32,
|
token_conditional_swap_taker_fee_rate: f32,
|
||||||
token_conditional_swap_maker_fee_rate: f32,
|
token_conditional_swap_maker_fee_rate: f32,
|
||||||
flash_loan_swap_fee_rate: f32,
|
flash_loan_swap_fee_rate: f32,
|
||||||
|
interest_curve_scaling: f32,
|
||||||
|
interest_target_utilization: f32,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
#[cfg(feature = "enable-gpl")]
|
#[cfg(feature = "enable-gpl")]
|
||||||
instructions::token_register(
|
instructions::token_register(
|
||||||
|
@ -175,6 +177,8 @@ pub mod mango_v4 {
|
||||||
token_conditional_swap_taker_fee_rate,
|
token_conditional_swap_taker_fee_rate,
|
||||||
token_conditional_swap_maker_fee_rate,
|
token_conditional_swap_maker_fee_rate,
|
||||||
flash_loan_swap_fee_rate,
|
flash_loan_swap_fee_rate,
|
||||||
|
interest_curve_scaling,
|
||||||
|
interest_target_utilization,
|
||||||
)?;
|
)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -219,6 +223,8 @@ pub mod mango_v4 {
|
||||||
token_conditional_swap_taker_fee_rate_opt: Option<f32>,
|
token_conditional_swap_taker_fee_rate_opt: Option<f32>,
|
||||||
token_conditional_swap_maker_fee_rate_opt: Option<f32>,
|
token_conditional_swap_maker_fee_rate_opt: Option<f32>,
|
||||||
flash_loan_swap_fee_rate_opt: Option<f32>,
|
flash_loan_swap_fee_rate_opt: Option<f32>,
|
||||||
|
interest_curve_scaling_opt: Option<f32>,
|
||||||
|
interest_target_utilization_opt: Option<f32>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
#[cfg(feature = "enable-gpl")]
|
#[cfg(feature = "enable-gpl")]
|
||||||
instructions::token_edit(
|
instructions::token_edit(
|
||||||
|
@ -250,6 +256,8 @@ pub mod mango_v4 {
|
||||||
token_conditional_swap_taker_fee_rate_opt,
|
token_conditional_swap_taker_fee_rate_opt,
|
||||||
token_conditional_swap_maker_fee_rate_opt,
|
token_conditional_swap_maker_fee_rate_opt,
|
||||||
flash_loan_swap_fee_rate_opt,
|
flash_loan_swap_fee_rate_opt,
|
||||||
|
interest_curve_scaling_opt,
|
||||||
|
interest_target_utilization_opt,
|
||||||
)?;
|
)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -300,6 +300,20 @@ pub struct UpdateRateLog {
|
||||||
pub max_rate: i128, // I80F48
|
pub max_rate: i128, // I80F48
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[event]
|
||||||
|
pub struct UpdateRateLogV2 {
|
||||||
|
pub mango_group: Pubkey,
|
||||||
|
pub token_index: u16,
|
||||||
|
// contrary to v1 these do not have curve_scaling factored in!
|
||||||
|
pub rate0: i128, // I80F48
|
||||||
|
pub util0: i128, // I80F48
|
||||||
|
pub rate1: i128, // I80F48
|
||||||
|
pub util1: i128, // I80F48
|
||||||
|
pub max_rate: i128, // I80F48
|
||||||
|
pub curve_scaling: f64,
|
||||||
|
pub target_utilization: f32,
|
||||||
|
}
|
||||||
|
|
||||||
#[event]
|
#[event]
|
||||||
pub struct TokenLiqWithTokenLog {
|
pub struct TokenLiqWithTokenLog {
|
||||||
pub mango_group: Pubkey,
|
pub mango_group: Pubkey,
|
||||||
|
|
|
@ -18,7 +18,6 @@ pub const DAY: i64 = 86400;
|
||||||
pub const DAY_I80F48: I80F48 = I80F48::from_bits(86_400 * I80F48::ONE.to_bits());
|
pub const DAY_I80F48: I80F48 = I80F48::from_bits(86_400 * I80F48::ONE.to_bits());
|
||||||
pub const ONE_BPS: I80F48 = I80F48::from_bits(28147497671);
|
pub const ONE_BPS: I80F48 = I80F48::from_bits(28147497671);
|
||||||
pub const YEAR_I80F48: I80F48 = I80F48::from_bits(31_536_000 * I80F48::ONE.to_bits());
|
pub const YEAR_I80F48: I80F48 = I80F48::from_bits(31_536_000 * I80F48::ONE.to_bits());
|
||||||
pub const MINIMUM_MAX_RATE: I80F48 = I80F48::from_bits(I80F48::ONE.to_bits() / 2);
|
|
||||||
|
|
||||||
#[derive(Derivative)]
|
#[derive(Derivative)]
|
||||||
#[derivative(Debug)]
|
#[derivative(Debug)]
|
||||||
|
@ -148,8 +147,17 @@ pub struct Bank {
|
||||||
|
|
||||||
pub flash_loan_swap_fee_rate: f32,
|
pub flash_loan_swap_fee_rate: f32,
|
||||||
|
|
||||||
|
/// Target utilization: If actual utilization is higher, scale up interest.
|
||||||
|
/// If it's lower, scale down interest (if possible)
|
||||||
|
pub interest_target_utilization: f32,
|
||||||
|
|
||||||
|
/// Current interest curve scaling, always >= 1.0
|
||||||
|
///
|
||||||
|
/// Except when first migrating to having this field, then 0.0
|
||||||
|
pub interest_curve_scaling: f64,
|
||||||
|
|
||||||
#[derivative(Debug = "ignore")]
|
#[derivative(Debug = "ignore")]
|
||||||
pub reserved: [u8; 2092],
|
pub reserved: [u8; 2080],
|
||||||
}
|
}
|
||||||
const_assert_eq!(
|
const_assert_eq!(
|
||||||
size_of::<Bank>(),
|
size_of::<Bank>(),
|
||||||
|
@ -180,8 +188,9 @@ const_assert_eq!(
|
||||||
+ 1
|
+ 1
|
||||||
+ 6
|
+ 6
|
||||||
+ 8
|
+ 8
|
||||||
+ 3 * 4
|
+ 4 * 4
|
||||||
+ 2092
|
+ 8
|
||||||
|
+ 2080
|
||||||
);
|
);
|
||||||
const_assert_eq!(size_of::<Bank>(), 3064);
|
const_assert_eq!(size_of::<Bank>(), 3064);
|
||||||
const_assert_eq!(size_of::<Bank>() % 8, 0);
|
const_assert_eq!(size_of::<Bank>() % 8, 0);
|
||||||
|
@ -211,9 +220,11 @@ impl Bank {
|
||||||
indexed_deposits: I80F48::ZERO,
|
indexed_deposits: I80F48::ZERO,
|
||||||
indexed_borrows: I80F48::ZERO,
|
indexed_borrows: I80F48::ZERO,
|
||||||
collected_fees_native: I80F48::ZERO,
|
collected_fees_native: I80F48::ZERO,
|
||||||
|
fees_withdrawn: 0,
|
||||||
dust: I80F48::ZERO,
|
dust: I80F48::ZERO,
|
||||||
flash_loan_approved_amount: 0,
|
flash_loan_approved_amount: 0,
|
||||||
flash_loan_token_account_initial: u64::MAX,
|
flash_loan_token_account_initial: u64::MAX,
|
||||||
|
net_borrows_in_window: 0,
|
||||||
bump,
|
bump,
|
||||||
bank_num,
|
bank_num,
|
||||||
|
|
||||||
|
@ -245,22 +256,24 @@ impl Bank {
|
||||||
token_index: existing_bank.token_index,
|
token_index: existing_bank.token_index,
|
||||||
mint_decimals: existing_bank.mint_decimals,
|
mint_decimals: existing_bank.mint_decimals,
|
||||||
oracle_config: existing_bank.oracle_config,
|
oracle_config: existing_bank.oracle_config,
|
||||||
stable_price_model: StablePriceModel::default(),
|
stable_price_model: existing_bank.stable_price_model,
|
||||||
min_vault_to_deposits_ratio: existing_bank.min_vault_to_deposits_ratio,
|
min_vault_to_deposits_ratio: existing_bank.min_vault_to_deposits_ratio,
|
||||||
net_borrow_limit_per_window_quote: existing_bank.net_borrow_limit_per_window_quote,
|
net_borrow_limit_per_window_quote: existing_bank.net_borrow_limit_per_window_quote,
|
||||||
net_borrow_limit_window_size_ts: existing_bank.net_borrow_limit_window_size_ts,
|
net_borrow_limit_window_size_ts: existing_bank.net_borrow_limit_window_size_ts,
|
||||||
last_net_borrows_window_start_ts: existing_bank.last_net_borrows_window_start_ts,
|
last_net_borrows_window_start_ts: existing_bank.last_net_borrows_window_start_ts,
|
||||||
net_borrows_in_window: 0,
|
borrow_weight_scale_start_quote: existing_bank.borrow_weight_scale_start_quote,
|
||||||
borrow_weight_scale_start_quote: f64::MAX,
|
deposit_weight_scale_start_quote: existing_bank.deposit_weight_scale_start_quote,
|
||||||
deposit_weight_scale_start_quote: f64::MAX,
|
reduce_only: existing_bank.reduce_only,
|
||||||
reduce_only: 0,
|
force_close: existing_bank.force_close,
|
||||||
force_close: 0,
|
|
||||||
padding: [0; 6],
|
padding: [0; 6],
|
||||||
fees_withdrawn: 0,
|
token_conditional_swap_taker_fee_rate: existing_bank
|
||||||
token_conditional_swap_taker_fee_rate: 0.0,
|
.token_conditional_swap_taker_fee_rate,
|
||||||
token_conditional_swap_maker_fee_rate: 0.0,
|
token_conditional_swap_maker_fee_rate: existing_bank
|
||||||
flash_loan_swap_fee_rate: 0.0,
|
.token_conditional_swap_maker_fee_rate,
|
||||||
reserved: [0; 2092],
|
flash_loan_swap_fee_rate: existing_bank.flash_loan_swap_fee_rate,
|
||||||
|
interest_target_utilization: existing_bank.interest_target_utilization,
|
||||||
|
interest_curve_scaling: existing_bank.interest_curve_scaling,
|
||||||
|
reserved: [0; 2080],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -270,7 +283,7 @@ impl Bank {
|
||||||
require_gte!(self.rate0, I80F48::ZERO);
|
require_gte!(self.rate0, I80F48::ZERO);
|
||||||
require_gte!(self.util1, I80F48::ZERO);
|
require_gte!(self.util1, I80F48::ZERO);
|
||||||
require_gte!(self.rate1, I80F48::ZERO);
|
require_gte!(self.rate1, I80F48::ZERO);
|
||||||
require_gte!(self.max_rate, MINIMUM_MAX_RATE);
|
require_gte!(self.max_rate, I80F48::ZERO);
|
||||||
require_gte!(self.loan_fee_rate, 0.0);
|
require_gte!(self.loan_fee_rate, 0.0);
|
||||||
require_gte!(self.loan_origination_fee_rate, 0.0);
|
require_gte!(self.loan_origination_fee_rate, 0.0);
|
||||||
require_gte!(self.maint_asset_weight, 0.0);
|
require_gte!(self.maint_asset_weight, 0.0);
|
||||||
|
@ -286,6 +299,8 @@ impl Bank {
|
||||||
require_gte!(self.token_conditional_swap_taker_fee_rate, 0.0);
|
require_gte!(self.token_conditional_swap_taker_fee_rate, 0.0);
|
||||||
require_gte!(self.token_conditional_swap_maker_fee_rate, 0.0);
|
require_gte!(self.token_conditional_swap_maker_fee_rate, 0.0);
|
||||||
require_gte!(self.flash_loan_swap_fee_rate, 0.0);
|
require_gte!(self.flash_loan_swap_fee_rate, 0.0);
|
||||||
|
require_gte!(self.interest_curve_scaling, 1.0);
|
||||||
|
require_gte!(self.interest_target_utilization, 0.0);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -795,6 +810,7 @@ impl Bank {
|
||||||
self.util1,
|
self.util1,
|
||||||
self.rate1,
|
self.rate1,
|
||||||
self.max_rate,
|
self.max_rate,
|
||||||
|
self.interest_curve_scaling,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -808,8 +824,9 @@ impl Bank {
|
||||||
util1: I80F48,
|
util1: I80F48,
|
||||||
rate1: I80F48,
|
rate1: I80F48,
|
||||||
max_rate: I80F48,
|
max_rate: I80F48,
|
||||||
|
scaling: f64,
|
||||||
) -> I80F48 {
|
) -> I80F48 {
|
||||||
if utilization <= util0 {
|
let v = if utilization <= util0 {
|
||||||
let slope = rate0 / util0;
|
let slope = rate0 / util0;
|
||||||
slope * utilization
|
slope * utilization
|
||||||
} else if utilization <= util1 {
|
} else if utilization <= util1 {
|
||||||
|
@ -820,6 +837,13 @@ impl Bank {
|
||||||
let extra_util = utilization - util1;
|
let extra_util = utilization - util1;
|
||||||
let slope = (max_rate - rate1) / (I80F48::ONE - util1);
|
let slope = (max_rate - rate1) / (I80F48::ONE - util1);
|
||||||
rate1 + slope * extra_util
|
rate1 + slope * extra_util
|
||||||
|
};
|
||||||
|
|
||||||
|
// scaling will be 0 when it's introduced
|
||||||
|
if scaling == 0.0 {
|
||||||
|
v
|
||||||
|
} else {
|
||||||
|
v * I80F48::from_num(scaling)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -855,34 +879,23 @@ impl Bank {
|
||||||
}
|
}
|
||||||
|
|
||||||
// computes new optimal rates and max rate
|
// computes new optimal rates and max rate
|
||||||
pub fn compute_rates(&self) -> (I80F48, I80F48, I80F48) {
|
pub fn update_interest_rate_scaling(&mut self) {
|
||||||
// interest rate legs 2 and 3 are seen as punitive legs, encouraging utilization to move towards optimal utilization
|
// Interest increases above target_util, decreases below
|
||||||
// lets choose util0 as optimal utilization and 0 to utli0 as the leg where we want the utlization to preferably be
|
let target_util = self.interest_target_utilization as f64;
|
||||||
let optimal_util = self.util0;
|
|
||||||
// use avg_utilization and not instantaneous_utilization so that rates cannot be manipulated easily
|
// use avg_utilization and not instantaneous_utilization so that rates cannot be manipulated easily
|
||||||
let avg_util = self.avg_utilization;
|
let avg_util = self.avg_utilization.to_num::<f64>();
|
||||||
|
|
||||||
// move rates up when utilization is above optimal utilization, and vice versa
|
// move rates up when utilization is above optimal utilization, and vice versa
|
||||||
// util factor is between -1 (avg util = 0) and +1 (avg util = 100%)
|
// util factor is between -1 (avg util = 0) and +1 (avg util = 100%)
|
||||||
let util_factor = if avg_util > optimal_util {
|
let util_factor = if avg_util > target_util {
|
||||||
(avg_util - optimal_util) / (I80F48::ONE - optimal_util)
|
(avg_util - target_util) / (1.0 - target_util)
|
||||||
} else {
|
} else {
|
||||||
(avg_util - optimal_util) / optimal_util
|
(avg_util - target_util) / target_util
|
||||||
};
|
};
|
||||||
let adjustment = I80F48::ONE + self.adjustment_factor * util_factor;
|
let adjustment = 1.0 + self.adjustment_factor.to_num::<f64>() * util_factor;
|
||||||
|
|
||||||
// 1. irrespective of which leg current utilization is in, update all rates
|
self.interest_curve_scaling = (self.interest_curve_scaling * adjustment).max(1.0)
|
||||||
// 2. only update rates as long as new adjusted rates are above MINIMUM_MAX_RATE,
|
|
||||||
// since we don't want to fall to such low rates that it would take a long time to
|
|
||||||
// recover to high rates if utilization suddently increases to a high value
|
|
||||||
if (self.max_rate * adjustment) > MINIMUM_MAX_RATE {
|
|
||||||
(
|
|
||||||
(self.rate0 * adjustment),
|
|
||||||
(self.rate1 * adjustment),
|
|
||||||
(self.max_rate * adjustment),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
(self.rate0, self.rate1, self.max_rate)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn oracle_price(
|
pub fn oracle_price(
|
||||||
|
|
|
@ -997,6 +997,8 @@ impl ClientInstruction for TokenRegisterInstruction {
|
||||||
token_conditional_swap_taker_fee_rate: 0.0,
|
token_conditional_swap_taker_fee_rate: 0.0,
|
||||||
token_conditional_swap_maker_fee_rate: 0.0,
|
token_conditional_swap_maker_fee_rate: 0.0,
|
||||||
flash_loan_swap_fee_rate: 0.0,
|
flash_loan_swap_fee_rate: 0.0,
|
||||||
|
interest_curve_scaling: 1.0,
|
||||||
|
interest_target_utilization: 0.5,
|
||||||
};
|
};
|
||||||
|
|
||||||
let bank = Pubkey::find_program_address(
|
let bank = Pubkey::find_program_address(
|
||||||
|
@ -1241,6 +1243,8 @@ pub fn token_edit_instruction_default() -> mango_v4::instruction::TokenEdit {
|
||||||
token_conditional_swap_taker_fee_rate_opt: None,
|
token_conditional_swap_taker_fee_rate_opt: None,
|
||||||
token_conditional_swap_maker_fee_rate_opt: None,
|
token_conditional_swap_maker_fee_rate_opt: None,
|
||||||
flash_loan_swap_fee_rate_opt: None,
|
flash_loan_swap_fee_rate_opt: None,
|
||||||
|
interest_curve_scaling_opt: None,
|
||||||
|
interest_target_utilization_opt: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -410,6 +410,8 @@ export class MangoClient {
|
||||||
params.tokenConditionalSwapTakerFeeRate,
|
params.tokenConditionalSwapTakerFeeRate,
|
||||||
params.tokenConditionalSwapMakerFeeRate,
|
params.tokenConditionalSwapMakerFeeRate,
|
||||||
params.flashLoanSwapFeeRate,
|
params.flashLoanSwapFeeRate,
|
||||||
|
params.interestCurveScaling,
|
||||||
|
params.interestTargetUtilization,
|
||||||
)
|
)
|
||||||
.accounts({
|
.accounts({
|
||||||
group: group.publicKey,
|
group: group.publicKey,
|
||||||
|
@ -485,6 +487,8 @@ export class MangoClient {
|
||||||
params.tokenConditionalSwapTakerFeeRate,
|
params.tokenConditionalSwapTakerFeeRate,
|
||||||
params.tokenConditionalSwapMakerFeeRate,
|
params.tokenConditionalSwapMakerFeeRate,
|
||||||
params.flashLoanSwapFeeRate,
|
params.flashLoanSwapFeeRate,
|
||||||
|
params.interestCurveScaling,
|
||||||
|
params.interestTargetUtilization,
|
||||||
)
|
)
|
||||||
.accounts({
|
.accounts({
|
||||||
group: group.publicKey,
|
group: group.publicKey,
|
||||||
|
|
|
@ -25,6 +25,8 @@ export interface TokenRegisterParams {
|
||||||
tokenConditionalSwapTakerFeeRate: number;
|
tokenConditionalSwapTakerFeeRate: number;
|
||||||
tokenConditionalSwapMakerFeeRate: number;
|
tokenConditionalSwapMakerFeeRate: number;
|
||||||
flashLoanSwapFeeRate: number;
|
flashLoanSwapFeeRate: number;
|
||||||
|
interestCurveScaling: number;
|
||||||
|
interestTargetUtilization: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DefaultTokenRegisterParams: TokenRegisterParams = {
|
export const DefaultTokenRegisterParams: TokenRegisterParams = {
|
||||||
|
@ -35,10 +37,10 @@ export const DefaultTokenRegisterParams: TokenRegisterParams = {
|
||||||
groupInsuranceFund: false,
|
groupInsuranceFund: false,
|
||||||
interestRateParams: {
|
interestRateParams: {
|
||||||
util0: 0.5,
|
util0: 0.5,
|
||||||
rate0: 0.072,
|
rate0: 0.018,
|
||||||
util1: 0.8,
|
util1: 0.8,
|
||||||
rate1: 0.2,
|
rate1: 0.05,
|
||||||
maxRate: 2,
|
maxRate: 0.5,
|
||||||
adjustmentFactor: 0.004,
|
adjustmentFactor: 0.004,
|
||||||
},
|
},
|
||||||
loanFeeRate: 0.0005,
|
loanFeeRate: 0.0005,
|
||||||
|
@ -60,6 +62,8 @@ export const DefaultTokenRegisterParams: TokenRegisterParams = {
|
||||||
tokenConditionalSwapTakerFeeRate: 0.0005,
|
tokenConditionalSwapTakerFeeRate: 0.0005,
|
||||||
tokenConditionalSwapMakerFeeRate: 0.0005,
|
tokenConditionalSwapMakerFeeRate: 0.0005,
|
||||||
flashLoanSwapFeeRate: 0.0005,
|
flashLoanSwapFeeRate: 0.0005,
|
||||||
|
interestCurveScaling: 4.0,
|
||||||
|
interestTargetUtilization: 0.5,
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface TokenEditParams {
|
export interface TokenEditParams {
|
||||||
|
@ -90,6 +94,8 @@ export interface TokenEditParams {
|
||||||
tokenConditionalSwapTakerFeeRate: number | null;
|
tokenConditionalSwapTakerFeeRate: number | null;
|
||||||
tokenConditionalSwapMakerFeeRate: number | null;
|
tokenConditionalSwapMakerFeeRate: number | null;
|
||||||
flashLoanSwapFeeRate: number | null;
|
flashLoanSwapFeeRate: number | null;
|
||||||
|
interestCurveScaling: number | null;
|
||||||
|
interestTargetUtilization: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NullTokenEditParams: TokenEditParams = {
|
export const NullTokenEditParams: TokenEditParams = {
|
||||||
|
@ -120,6 +126,8 @@ export const NullTokenEditParams: TokenEditParams = {
|
||||||
tokenConditionalSwapTakerFeeRate: null,
|
tokenConditionalSwapTakerFeeRate: null,
|
||||||
tokenConditionalSwapMakerFeeRate: null,
|
tokenConditionalSwapMakerFeeRate: null,
|
||||||
flashLoanSwapFeeRate: null,
|
flashLoanSwapFeeRate: null,
|
||||||
|
interestCurveScaling: null,
|
||||||
|
interestTargetUtilization: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface PerpEditParams {
|
export interface PerpEditParams {
|
||||||
|
|
|
@ -596,6 +596,14 @@ export type MangoV4 = {
|
||||||
{
|
{
|
||||||
"name": "flashLoanSwapFeeRate",
|
"name": "flashLoanSwapFeeRate",
|
||||||
"type": "f32"
|
"type": "f32"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "interestCurveScaling",
|
||||||
|
"type": "f32"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "interestTargetUtilization",
|
||||||
|
"type": "f32"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -930,6 +938,18 @@ export type MangoV4 = {
|
||||||
"type": {
|
"type": {
|
||||||
"option": "f32"
|
"option": "f32"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "interestCurveScalingOpt",
|
||||||
|
"type": {
|
||||||
|
"option": "f32"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "interestTargetUtilizationOpt",
|
||||||
|
"type": {
|
||||||
|
"option": "f32"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -7033,12 +7053,29 @@ export type MangoV4 = {
|
||||||
"name": "flashLoanSwapFeeRate",
|
"name": "flashLoanSwapFeeRate",
|
||||||
"type": "f32"
|
"type": "f32"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "interestTargetUtilization",
|
||||||
|
"docs": [
|
||||||
|
"Target utilization: If actual utilization is higher, scale up interest.",
|
||||||
|
"If it's lower, scale down interest (if possible)"
|
||||||
|
],
|
||||||
|
"type": "f32"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "interestCurveScaling",
|
||||||
|
"docs": [
|
||||||
|
"Current interest curve scaling, always >= 1.0",
|
||||||
|
"",
|
||||||
|
"Except when first migrating to having this field, then 0.0"
|
||||||
|
],
|
||||||
|
"type": "f64"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "reserved",
|
"name": "reserved",
|
||||||
"type": {
|
"type": {
|
||||||
"array": [
|
"array": [
|
||||||
"u8",
|
"u8",
|
||||||
2092
|
2080
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11335,6 +11372,56 @@ export type MangoV4 = {
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "UpdateRateLogV2",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "mangoGroup",
|
||||||
|
"type": "publicKey",
|
||||||
|
"index": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tokenIndex",
|
||||||
|
"type": "u16",
|
||||||
|
"index": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "rate0",
|
||||||
|
"type": "i128",
|
||||||
|
"index": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "util0",
|
||||||
|
"type": "i128",
|
||||||
|
"index": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "rate1",
|
||||||
|
"type": "i128",
|
||||||
|
"index": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "util1",
|
||||||
|
"type": "i128",
|
||||||
|
"index": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "maxRate",
|
||||||
|
"type": "i128",
|
||||||
|
"index": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "curveScaling",
|
||||||
|
"type": "f64",
|
||||||
|
"index": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "targetUtilization",
|
||||||
|
"type": "f32",
|
||||||
|
"index": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "TokenLiqWithTokenLog",
|
"name": "TokenLiqWithTokenLog",
|
||||||
"fields": [
|
"fields": [
|
||||||
|
@ -13699,6 +13786,14 @@ export const IDL: MangoV4 = {
|
||||||
{
|
{
|
||||||
"name": "flashLoanSwapFeeRate",
|
"name": "flashLoanSwapFeeRate",
|
||||||
"type": "f32"
|
"type": "f32"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "interestCurveScaling",
|
||||||
|
"type": "f32"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "interestTargetUtilization",
|
||||||
|
"type": "f32"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -14033,6 +14128,18 @@ export const IDL: MangoV4 = {
|
||||||
"type": {
|
"type": {
|
||||||
"option": "f32"
|
"option": "f32"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "interestCurveScalingOpt",
|
||||||
|
"type": {
|
||||||
|
"option": "f32"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "interestTargetUtilizationOpt",
|
||||||
|
"type": {
|
||||||
|
"option": "f32"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -20136,12 +20243,29 @@ export const IDL: MangoV4 = {
|
||||||
"name": "flashLoanSwapFeeRate",
|
"name": "flashLoanSwapFeeRate",
|
||||||
"type": "f32"
|
"type": "f32"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "interestTargetUtilization",
|
||||||
|
"docs": [
|
||||||
|
"Target utilization: If actual utilization is higher, scale up interest.",
|
||||||
|
"If it's lower, scale down interest (if possible)"
|
||||||
|
],
|
||||||
|
"type": "f32"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "interestCurveScaling",
|
||||||
|
"docs": [
|
||||||
|
"Current interest curve scaling, always >= 1.0",
|
||||||
|
"",
|
||||||
|
"Except when first migrating to having this field, then 0.0"
|
||||||
|
],
|
||||||
|
"type": "f64"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "reserved",
|
"name": "reserved",
|
||||||
"type": {
|
"type": {
|
||||||
"array": [
|
"array": [
|
||||||
"u8",
|
"u8",
|
||||||
2092
|
2080
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24438,6 +24562,56 @@ export const IDL: MangoV4 = {
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "UpdateRateLogV2",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "mangoGroup",
|
||||||
|
"type": "publicKey",
|
||||||
|
"index": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tokenIndex",
|
||||||
|
"type": "u16",
|
||||||
|
"index": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "rate0",
|
||||||
|
"type": "i128",
|
||||||
|
"index": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "util0",
|
||||||
|
"type": "i128",
|
||||||
|
"index": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "rate1",
|
||||||
|
"type": "i128",
|
||||||
|
"index": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "util1",
|
||||||
|
"type": "i128",
|
||||||
|
"index": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "maxRate",
|
||||||
|
"type": "i128",
|
||||||
|
"index": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "curveScaling",
|
||||||
|
"type": "f64",
|
||||||
|
"index": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "targetUtilization",
|
||||||
|
"type": "f32",
|
||||||
|
"index": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "TokenLiqWithTokenLog",
|
"name": "TokenLiqWithTokenLog",
|
||||||
"fields": [
|
"fields": [
|
||||||
|
|
Loading…
Reference in New Issue