multi legged interest rates + keeper ix
Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>
This commit is contained in:
parent
84d2ec6e34
commit
257ccf76ad
|
@ -16,6 +16,7 @@ pub use serum3_place_order::*;
|
|||
pub use serum3_register_market::*;
|
||||
pub use serum3_settle_funds::*;
|
||||
pub use set_stub_oracle::*;
|
||||
pub use update_index::*;
|
||||
pub use withdraw::*;
|
||||
|
||||
mod close_account;
|
||||
|
@ -36,4 +37,5 @@ mod serum3_place_order;
|
|||
mod serum3_register_market;
|
||||
mod serum3_settle_funds;
|
||||
mod set_stub_oracle;
|
||||
mod update_index;
|
||||
mod withdraw;
|
||||
|
|
|
@ -76,9 +76,15 @@ pub struct RegisterToken<'info> {
|
|||
|
||||
// TODO: should this be "configure_mint", we pass an explicit index, and allow
|
||||
// overwriting config as long as the mint account stays the same?
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn register_token(
|
||||
ctx: Context<RegisterToken>,
|
||||
token_index: TokenIndex,
|
||||
util0: f32,
|
||||
rate0: f32,
|
||||
util1: f32,
|
||||
rate1: f32,
|
||||
max_rate: f32,
|
||||
maint_asset_weight: f32,
|
||||
init_asset_weight: f32,
|
||||
maint_liab_weight: f32,
|
||||
|
@ -97,6 +103,12 @@ pub fn register_token(
|
|||
borrow_index: INDEX_START,
|
||||
indexed_total_deposits: I80F48::ZERO,
|
||||
indexed_total_borrows: I80F48::ZERO,
|
||||
last_updated: Clock::get()?.unix_timestamp,
|
||||
util0: I80F48::from_num(util0),
|
||||
rate0: I80F48::from_num(rate0),
|
||||
util1: I80F48::from_num(util1),
|
||||
rate1: I80F48::from_num(rate1),
|
||||
max_rate: I80F48::from_num(max_rate),
|
||||
maint_asset_weight: I80F48::from_num(maint_asset_weight),
|
||||
init_asset_weight: I80F48::from_num(init_asset_weight),
|
||||
maint_liab_weight: I80F48::from_num(maint_liab_weight),
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
use anchor_lang::prelude::*;
|
||||
|
||||
use crate::state::Bank;
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct UpdateIndex<'info> {
|
||||
#[account(mut)]
|
||||
pub bank: AccountLoader<'info, Bank>,
|
||||
}
|
||||
pub fn update_index(ctx: Context<UpdateIndex>) -> Result<()> {
|
||||
let now_ts = Clock::get()?.unix_timestamp;
|
||||
|
||||
let mut bank = ctx.accounts.bank.load_mut()?;
|
||||
bank.update_index(now_ts)?;
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -33,9 +33,15 @@ pub mod mango_v4 {
|
|||
instructions::create_group(ctx)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn register_token(
|
||||
ctx: Context<RegisterToken>,
|
||||
token_index: TokenIndex,
|
||||
util0: f32,
|
||||
rate0: f32,
|
||||
util1: f32,
|
||||
rate1: f32,
|
||||
max_rate: f32,
|
||||
maint_asset_weight: f32,
|
||||
init_asset_weight: f32,
|
||||
maint_liab_weight: f32,
|
||||
|
@ -45,6 +51,11 @@ pub mod mango_v4 {
|
|||
instructions::register_token(
|
||||
ctx,
|
||||
token_index,
|
||||
util0,
|
||||
rate0,
|
||||
util1,
|
||||
rate1,
|
||||
max_rate,
|
||||
maint_asset_weight,
|
||||
init_asset_weight,
|
||||
maint_liab_weight,
|
||||
|
@ -53,6 +64,10 @@ pub mod mango_v4 {
|
|||
)
|
||||
}
|
||||
|
||||
pub fn update_index(ctx: Context<UpdateIndex>) -> Result<()> {
|
||||
instructions::update_index(ctx)
|
||||
}
|
||||
|
||||
pub fn create_account(ctx: Context<CreateAccount>, account_num: u8) -> Result<()> {
|
||||
instructions::create_account(ctx, account_num)
|
||||
}
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
use super::{TokenAccount, TokenIndex};
|
||||
use crate::util::checked_math as cm;
|
||||
use anchor_lang::prelude::*;
|
||||
use fixed::types::I80F48;
|
||||
use fixed_macro::types::I80F48;
|
||||
use static_assertions::const_assert_eq;
|
||||
use std::mem::size_of;
|
||||
|
||||
use super::{TokenAccount, TokenIndex};
|
||||
use crate::util::checked_math as cm;
|
||||
pub const YEAR: I80F48 = I80F48!(31536000);
|
||||
|
||||
#[account(zero_copy)]
|
||||
pub struct Bank {
|
||||
|
@ -21,10 +23,13 @@ pub struct Bank {
|
|||
/// total deposits/borrows, for utilization
|
||||
pub indexed_total_deposits: I80F48,
|
||||
pub indexed_total_borrows: I80F48,
|
||||
// todo: multi-leg interest
|
||||
// pub optimal_util: I80F48,
|
||||
// pub optimal_rate: I80F48,
|
||||
// pub max_rate: I80F48,
|
||||
|
||||
pub last_updated: i64,
|
||||
pub util0: I80F48,
|
||||
pub rate0: I80F48,
|
||||
pub util1: I80F48,
|
||||
pub rate1: I80F48,
|
||||
pub max_rate: I80F48,
|
||||
|
||||
// This is a _lot_ of bytes (64) - seems unnecessary
|
||||
// (could maybe store them in one byte each, as an informal U1F7?
|
||||
|
@ -47,10 +52,14 @@ pub struct Bank {
|
|||
|
||||
pub reserved: [u8; 6],
|
||||
}
|
||||
const_assert_eq!(size_of::<Bank>(), 32 * 4 + 16 * 10 + 2 + 6);
|
||||
const_assert_eq!(size_of::<Bank>(), 32 * 4 + 8 + 16 * 15 + 2 + 6);
|
||||
const_assert_eq!(size_of::<Bank>() % 8, 0);
|
||||
|
||||
impl Bank {
|
||||
pub fn native_total_borrows(&self) -> I80F48 {
|
||||
self.borrow_index * self.indexed_total_borrows
|
||||
}
|
||||
|
||||
pub fn native_total_deposits(&self) -> I80F48 {
|
||||
self.deposit_index * self.indexed_total_deposits
|
||||
}
|
||||
|
@ -154,6 +163,67 @@ impl Bank {
|
|||
self.withdraw(position, -native_amount)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_index(&mut self, now_ts: i64) -> Result<()> {
|
||||
let utilization = cm!(self.native_total_borrows() / self.native_total_deposits());
|
||||
|
||||
let interest_rate = self.compute_interest_rate(utilization);
|
||||
|
||||
let diff_ts = I80F48::from_num(now_ts - self.last_updated);
|
||||
|
||||
let borrow_interest: I80F48 = cm!(interest_rate * diff_ts);
|
||||
let deposit_interest = cm!(borrow_interest * utilization);
|
||||
|
||||
self.last_updated = Clock::get()?.unix_timestamp;
|
||||
|
||||
if borrow_interest <= I80F48::ZERO || deposit_interest <= I80F48::ZERO {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.borrow_index = cm!((self.borrow_index * borrow_interest) / YEAR + self.borrow_index);
|
||||
self.deposit_index =
|
||||
cm!((self.deposit_index * deposit_interest) / YEAR + self.deposit_index);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// returns the current interest rate in APR
|
||||
#[inline(always)]
|
||||
pub fn compute_interest_rate(&self, utilization: I80F48) -> I80F48 {
|
||||
Bank::interest_rate_curve_calculator(
|
||||
utilization,
|
||||
self.util0,
|
||||
self.rate0,
|
||||
self.util1,
|
||||
self.rate1,
|
||||
self.max_rate,
|
||||
)
|
||||
}
|
||||
|
||||
/// calcualtor function that can be used to compute an interest
|
||||
/// rate based on the given parameters
|
||||
#[inline(always)]
|
||||
pub fn interest_rate_curve_calculator(
|
||||
utilization: I80F48,
|
||||
util0: I80F48,
|
||||
rate0: I80F48,
|
||||
util1: I80F48,
|
||||
rate1: I80F48,
|
||||
max_rate: I80F48,
|
||||
) -> I80F48 {
|
||||
if utilization <= util0 {
|
||||
let slope = cm!(rate0 / util0);
|
||||
cm!(slope * utilization)
|
||||
} else if utilization <= util1 {
|
||||
let extra_util = cm!(utilization - util0);
|
||||
let slope = cm!((rate1 - rate0) / (util1 - util0));
|
||||
cm!(rate0 + slope * extra_util)
|
||||
} else {
|
||||
let extra_util = utilization - util1;
|
||||
let slope = (max_rate - rate1) / (I80F48::ONE - util1);
|
||||
rate1 + slope * extra_util
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
@ -444,6 +444,11 @@ impl<'keypair> ClientInstruction for DepositInstruction<'keypair> {
|
|||
pub struct RegisterTokenInstruction<'keypair> {
|
||||
pub token_index: TokenIndex,
|
||||
pub decimals: u8,
|
||||
pub util0: f32,
|
||||
pub rate0: f32,
|
||||
pub util1: f32,
|
||||
pub rate1: f32,
|
||||
pub max_rate: f32,
|
||||
pub maint_asset_weight: f32,
|
||||
pub init_asset_weight: f32,
|
||||
pub maint_liab_weight: f32,
|
||||
|
@ -467,6 +472,11 @@ impl<'keypair> ClientInstruction for RegisterTokenInstruction<'keypair> {
|
|||
let program_id = mango_v4::id();
|
||||
let instruction = Self::Instruction {
|
||||
token_index: self.token_index,
|
||||
util0: self.util0,
|
||||
rate0: self.rate0,
|
||||
util1: self.util1,
|
||||
rate1: self.rate1,
|
||||
max_rate: self.max_rate,
|
||||
maint_asset_weight: self.maint_asset_weight,
|
||||
init_asset_weight: self.init_asset_weight,
|
||||
maint_liab_weight: self.maint_liab_weight,
|
||||
|
@ -1393,3 +1403,26 @@ impl ClientInstruction for BenchmarkInstruction {
|
|||
vec![]
|
||||
}
|
||||
}
|
||||
pub struct UpdateIndexInstruction {
|
||||
pub bank: Pubkey,
|
||||
}
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl ClientInstruction for UpdateIndexInstruction {
|
||||
type Accounts = mango_v4::accounts::UpdateIndex;
|
||||
type Instruction = mango_v4::instruction::UpdateIndex;
|
||||
async fn to_instruction(
|
||||
&self,
|
||||
_loader: impl ClientAccountLoader + 'async_trait,
|
||||
) -> (Self::Accounts, instruction::Instruction) {
|
||||
let program_id = mango_v4::id();
|
||||
let instruction = Self::Instruction {};
|
||||
let accounts = Self::Accounts { bank: self.bank };
|
||||
|
||||
let instruction = make_instruction(program_id, &accounts, instruction);
|
||||
(accounts, instruction)
|
||||
}
|
||||
|
||||
fn signers(&self) -> Vec<&Keypair> {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -72,6 +72,11 @@ impl<'a> GroupWithTokensConfig<'a> {
|
|||
RegisterTokenInstruction {
|
||||
token_index,
|
||||
decimals: mint.decimals,
|
||||
util0: 0.50,
|
||||
rate0: 0.06,
|
||||
util1: 0.70,
|
||||
rate1: 1.3,
|
||||
max_rate: 1.50,
|
||||
maint_asset_weight: 0.8,
|
||||
init_asset_weight: 0.6,
|
||||
maint_liab_weight: 1.2,
|
||||
|
|
|
@ -3,6 +3,7 @@ use std::sync::{Arc, RwLock};
|
|||
|
||||
use anchor_lang::AccountDeserialize;
|
||||
use anchor_spl::token::TokenAccount;
|
||||
use solana_program::clock::UnixTimestamp;
|
||||
use solana_program::{program_pack::Pack, rent::*, system_instruction};
|
||||
use solana_program_test::*;
|
||||
use solana_sdk::{
|
||||
|
@ -15,6 +16,8 @@ use solana_sdk::{
|
|||
};
|
||||
use spl_token::*;
|
||||
|
||||
use super::mango_client::ClientAccountLoader;
|
||||
|
||||
pub struct SolanaCookie {
|
||||
pub context: RefCell<ProgramTestContext>,
|
||||
pub rent: Rent,
|
||||
|
@ -73,6 +76,21 @@ impl SolanaCookie {
|
|||
.unwrap();
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
|
||||
pub async fn advance_clock(&self) {
|
||||
let mut clock = self.get_clock().await;
|
||||
let old_ts = clock.unix_timestamp;
|
||||
|
||||
while clock.unix_timestamp <= old_ts {
|
||||
self.context
|
||||
.borrow_mut()
|
||||
.warp_to_slot(clock.slot + 50)
|
||||
.unwrap();
|
||||
clock = self.get_clock().await;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_newest_slot_from_history(&self) -> u64 {
|
||||
self.context
|
||||
.borrow_mut()
|
||||
|
|
|
@ -0,0 +1,117 @@
|
|||
#![cfg(feature = "test-bpf")]
|
||||
|
||||
use mango_v4::state::Bank;
|
||||
use solana_program_test::*;
|
||||
use solana_sdk::{signature::Keypair, transport::TransportError};
|
||||
|
||||
use program_test::*;
|
||||
|
||||
mod program_test;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_update_index() -> Result<(), TransportError> {
|
||||
let context = TestContext::new().await;
|
||||
let solana = &context.solana.clone();
|
||||
|
||||
let admin = &Keypair::new();
|
||||
let owner = &context.users[0].key;
|
||||
let payer = &context.users[1].key;
|
||||
let mints = &context.mints[0..2];
|
||||
let payer_mint_accounts = &context.users[1].token_accounts[0..2];
|
||||
|
||||
//
|
||||
// SETUP: Create a group and an account to fill the vaults
|
||||
//
|
||||
|
||||
let mango_setup::GroupWithTokens { group, tokens } = mango_setup::GroupWithTokensConfig {
|
||||
admin,
|
||||
payer,
|
||||
mints,
|
||||
}
|
||||
.create(solana)
|
||||
.await;
|
||||
|
||||
// deposit some funds, to the vaults aren't empty
|
||||
let deposit_account = send_tx(
|
||||
solana,
|
||||
CreateAccountInstruction {
|
||||
account_num: 0,
|
||||
group,
|
||||
owner,
|
||||
payer,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.account;
|
||||
for &token_account in payer_mint_accounts {
|
||||
send_tx(
|
||||
solana,
|
||||
DepositInstruction {
|
||||
amount: 10000,
|
||||
account: deposit_account,
|
||||
token_account,
|
||||
token_authority: payer,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let withdraw_account = send_tx(
|
||||
solana,
|
||||
CreateAccountInstruction {
|
||||
account_num: 1,
|
||||
group,
|
||||
owner,
|
||||
payer,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.account;
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
DepositInstruction {
|
||||
amount: 100000,
|
||||
account: withdraw_account,
|
||||
token_account: payer_mint_accounts[1],
|
||||
token_authority: payer,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
WithdrawInstruction {
|
||||
amount: 5000,
|
||||
allow_borrow: true,
|
||||
account: withdraw_account,
|
||||
owner,
|
||||
token_account: context.users[0].token_accounts[0],
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let bank_before_update_index = solana.get_account::<Bank>(tokens[0].bank).await;
|
||||
|
||||
solana.advance_clock().await;
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
UpdateIndexInstruction {
|
||||
bank: tokens[0].bank,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let bank_after_update_index = solana.get_account::<Bank>(tokens[0].bank).await;
|
||||
assert!(bank_before_update_index.deposit_index < bank_after_update_index.deposit_index);
|
||||
assert!(bank_before_update_index.borrow_index < bank_after_update_index.borrow_index);
|
||||
|
||||
Ok(())
|
||||
}
|
Loading…
Reference in New Issue