Merge branch 'dev'
This commit is contained in:
commit
efed3a03ab
|
@ -9,7 +9,7 @@ on:
|
|||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
SOLANA_VERSION: '1.9.14'
|
||||
SOLANA_VERSION: '1.10.29'
|
||||
RUST_TOOLCHAIN: 1.60.0
|
||||
LOG_PROGRAM: 'm43thNJ58XCjL798ZSq6JGAG1BnWskhdq5or6kcnfsD'
|
||||
|
||||
|
@ -65,10 +65,12 @@ jobs:
|
|||
echo "Generating keypair..."
|
||||
solana-keygen new -o "$HOME/.config/solana/id.json" --no-passphrase --silent
|
||||
- name: Build all deps
|
||||
run: cargo build-bpf
|
||||
run: |
|
||||
cargo build-bpf || true
|
||||
cargo +bpf build-bpf
|
||||
# Run bpf tests and output to runner and log
|
||||
- name: Run tests
|
||||
run: cargo test-bpf 2> >(tee raw-test-bpf.log >&2)
|
||||
run: cargo +bpf test-bpf 2> >(tee raw-test-bpf.log >&2)
|
||||
- name: Save raw log
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
|
|
|
@ -10,7 +10,7 @@ on:
|
|||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
SOLANA_VERSION: "1.9.14"
|
||||
SOLANA_VERSION: "1.10.29"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
# Mango v4 Program Change Log
|
||||
|
||||
Update this for each mainnet deployment.
|
||||
|
||||
## not on mainnet
|
||||
- Account data was rearranged to put fields that are often used with gPA first
|
||||
- The `CreateGroup` instruction now requires an `insurance_mint` account, which is
|
||||
used as the mint for the `insurance_vault` token account it creates. Pass the
|
||||
USDC mint address.
|
||||
- The token with `token_index` zero is now required to be the `insurance_mint`.
|
||||
Trying to register a different token for index zero will now fail.
|
||||
- New instruction: `LiqTokenBankruptcy` to resolve insurance fund payouts and
|
||||
socialized loss for bankrupt accounts.
|
||||
- The `PerpCreateMarket` instruction no longer requires a `quote_token_index`
|
||||
argument. The USDC/insurance mint is always used as quote currency for perps.
|
||||
- The `UpdateIndex` instruction now requires the `oracle` account to be passed
|
||||
for logging purposes.
|
||||
- New instructions: `AccountEdit`, `TokenEdit`, `PerpEditMarket` for reconfiguring.
|
||||
- The `delegate` field on `MangoAccount` is now used and many instructions can be
|
||||
called by the account delegate.
|
||||
|
||||
- Renamed instructions:
|
||||
- create/close_group -> group_create/close
|
||||
- create/edit/close_account -> account_create/edit/close
|
||||
- update_index -> token_update_index
|
||||
- create/set_stub_oracle -> stub_oracle_create/set
|
|
@ -78,12 +78,12 @@ impl MangoClient {
|
|||
// Mango Account
|
||||
let mut mango_account_tuples = program.accounts::<MangoAccount>(vec![
|
||||
RpcFilterType::Memcmp(Memcmp {
|
||||
offset: 40,
|
||||
offset: 8,
|
||||
bytes: MemcmpEncodedBytes::Base58(group.to_string()),
|
||||
encoding: None,
|
||||
}),
|
||||
RpcFilterType::Memcmp(Memcmp {
|
||||
offset: 72,
|
||||
offset: 40,
|
||||
bytes: MemcmpEncodedBytes::Base58(payer.pubkey().to_string()),
|
||||
encoding: None,
|
||||
}),
|
||||
|
@ -103,7 +103,7 @@ impl MangoClient {
|
|||
.instruction(Instruction {
|
||||
program_id: mango_v4::id(),
|
||||
accounts: anchor_lang::ToAccountMetas::to_account_metas(
|
||||
&mango_v4::accounts::CreateAccount {
|
||||
&mango_v4::accounts::AccountCreate {
|
||||
group,
|
||||
owner: payer.pubkey(),
|
||||
account: {
|
||||
|
@ -124,7 +124,7 @@ impl MangoClient {
|
|||
None,
|
||||
),
|
||||
data: anchor_lang::InstructionData::data(
|
||||
&mango_v4::instruction::CreateAccount {
|
||||
&mango_v4::instruction::AccountCreate {
|
||||
account_num,
|
||||
name: mango_account_name.to_owned(),
|
||||
},
|
||||
|
@ -135,12 +135,12 @@ impl MangoClient {
|
|||
}
|
||||
let mango_account_tuples = program.accounts::<MangoAccount>(vec![
|
||||
RpcFilterType::Memcmp(Memcmp {
|
||||
offset: 40,
|
||||
offset: 8,
|
||||
bytes: MemcmpEncodedBytes::Base58(group.to_string()),
|
||||
encoding: None,
|
||||
}),
|
||||
RpcFilterType::Memcmp(Memcmp {
|
||||
offset: 72,
|
||||
offset: 40,
|
||||
bytes: MemcmpEncodedBytes::Base58(payer.pubkey().to_string()),
|
||||
encoding: None,
|
||||
}),
|
||||
|
@ -155,7 +155,7 @@ impl MangoClient {
|
|||
let mut banks_cache = HashMap::new();
|
||||
let mut banks_cache_by_token_index = HashMap::new();
|
||||
let bank_tuples = program.accounts::<Bank>(vec![RpcFilterType::Memcmp(Memcmp {
|
||||
offset: 24,
|
||||
offset: 8,
|
||||
bytes: MemcmpEncodedBytes::Base58(group.to_string()),
|
||||
encoding: None,
|
||||
})])?;
|
||||
|
@ -197,7 +197,7 @@ impl MangoClient {
|
|||
let mut serum3_external_markets_cache = HashMap::new();
|
||||
let serum3_market_tuples =
|
||||
program.accounts::<Serum3Market>(vec![RpcFilterType::Memcmp(Memcmp {
|
||||
offset: 24,
|
||||
offset: 8,
|
||||
bytes: MemcmpEncodedBytes::Base58(group.to_string()),
|
||||
encoding: None,
|
||||
})])?;
|
||||
|
@ -221,7 +221,7 @@ impl MangoClient {
|
|||
let mut perp_markets_cache_by_perp_market_index = HashMap::new();
|
||||
let perp_market_tuples =
|
||||
program.accounts::<PerpMarket>(vec![RpcFilterType::Memcmp(Memcmp {
|
||||
offset: 24,
|
||||
offset: 8,
|
||||
bytes: MemcmpEncodedBytes::Base58(group.to_string()),
|
||||
encoding: None,
|
||||
})])?;
|
||||
|
@ -279,12 +279,12 @@ impl MangoClient {
|
|||
pub fn get_account(&self) -> Result<(Pubkey, MangoAccount), anchor_client::ClientError> {
|
||||
let mango_accounts = self.program().accounts::<MangoAccount>(vec![
|
||||
RpcFilterType::Memcmp(Memcmp {
|
||||
offset: 40,
|
||||
offset: 8,
|
||||
bytes: MemcmpEncodedBytes::Base58(self.group().to_string()),
|
||||
encoding: None,
|
||||
}),
|
||||
RpcFilterType::Memcmp(Memcmp {
|
||||
offset: 72,
|
||||
offset: 40,
|
||||
bytes: MemcmpEncodedBytes::Base58(self.payer().to_string()),
|
||||
encoding: None,
|
||||
}),
|
||||
|
|
|
@ -2,7 +2,7 @@ use std::{sync::Arc, time::Duration};
|
|||
|
||||
use crate::MangoClient;
|
||||
|
||||
use anchor_lang::__private::bytemuck::cast_ref;
|
||||
use anchor_lang::{__private::bytemuck::cast_ref, solana_program};
|
||||
use futures::Future;
|
||||
use mango_v4::state::{EventQueue, EventType, FillEvent, OutEvent, PerpMarket, TokenIndex};
|
||||
use solana_sdk::{
|
||||
|
@ -19,7 +19,7 @@ pub async fn runner(
|
|||
.banks_cache
|
||||
.values()
|
||||
.map(|banks_for_a_token| {
|
||||
loop_update_index(
|
||||
loop_update_index_and_rate(
|
||||
mango_client.clone(),
|
||||
banks_for_a_token.get(0).unwrap().1.token_index,
|
||||
)
|
||||
|
@ -48,7 +48,7 @@ pub async fn runner(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn loop_update_index(mango_client: Arc<MangoClient>, token_index: TokenIndex) {
|
||||
pub async fn loop_update_index_and_rate(mango_client: Arc<MangoClient>, token_index: TokenIndex) {
|
||||
let mut interval = time::interval(Duration::from_secs(5));
|
||||
loop {
|
||||
interval.tick().await;
|
||||
|
@ -58,7 +58,9 @@ pub async fn loop_update_index(mango_client: Arc<MangoClient>, token_index: Toke
|
|||
let res = tokio::task::spawn_blocking(move || -> anyhow::Result<()> {
|
||||
let mint_info = client.get_mint_info(&token_index);
|
||||
let banks_for_a_token = client.banks_cache_by_token_index.get(&token_index).unwrap();
|
||||
let token_name = banks_for_a_token.get(0).unwrap().1.name();
|
||||
let some_bank = banks_for_a_token.get(0).unwrap().1;
|
||||
let token_name = some_bank.name();
|
||||
let oracle = some_bank.oracle;
|
||||
|
||||
let bank_pubkeys_for_a_token = banks_for_a_token
|
||||
.into_iter()
|
||||
|
@ -72,11 +74,15 @@ pub async fn loop_update_index(mango_client: Arc<MangoClient>, token_index: Toke
|
|||
let mut ix = Instruction {
|
||||
program_id: mango_v4::id(),
|
||||
accounts: anchor_lang::ToAccountMetas::to_account_metas(
|
||||
&mango_v4::accounts::UpdateIndex { mint_info },
|
||||
&mango_v4::accounts::TokenUpdateIndexAndRate {
|
||||
mint_info,
|
||||
oracle,
|
||||
instructions: solana_program::sysvar::instructions::id(),
|
||||
},
|
||||
None,
|
||||
),
|
||||
data: anchor_lang::InstructionData::data(
|
||||
&mango_v4::instruction::UpdateIndex {},
|
||||
&mango_v4::instruction::TokenUpdateIndexAndRate {},
|
||||
),
|
||||
};
|
||||
let mut banks = bank_pubkeys_for_a_token
|
||||
|
@ -95,7 +101,11 @@ pub async fn loop_update_index(mango_client: Arc<MangoClient>, token_index: Toke
|
|||
if let Err(e) = sig_result {
|
||||
log::error!("{:?}", e)
|
||||
} else {
|
||||
log::info!("update_index {} {:?}", token_name, sig_result.unwrap())
|
||||
log::info!(
|
||||
"update_index_and_rate {} {:?}",
|
||||
token_name,
|
||||
sig_result.unwrap()
|
||||
)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -9,14 +9,16 @@
|
|||
"url": "https://blockworks.foundation"
|
||||
},
|
||||
"main": "dist/cjs/src/index.js",
|
||||
"module": "dist/esm/src/index.js",
|
||||
"types": "dist/types/src/index.d.ts",
|
||||
"sideEffects": false,
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "npm run build:cjs",
|
||||
"build": "npm run build:esm; npm run build:cjs",
|
||||
"build:cjs": "tsc -p tsconfig.cjs.json",
|
||||
"build:esm": "tsc -p tsconfig.esm.json",
|
||||
"clean": "rm -rf dist",
|
||||
"example1-user": "ts-node ts/client/src/scripts/example1-user.ts",
|
||||
"example1-admin": "ts-node ts/client/src/scripts/example1-admin.ts",
|
||||
|
@ -30,6 +32,7 @@
|
|||
"validate": "npm run typecheck && npm run test && npm run lint && npm run format-check"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jup-ag/core": "^1.0.0-beta.28",
|
||||
"@tsconfig/recommended": "^1.0.1",
|
||||
"@types/bs58": "^4.0.1",
|
||||
"@types/chai": "^4.3.0",
|
||||
|
@ -55,7 +58,6 @@
|
|||
"trailingComma": "all"
|
||||
},
|
||||
"dependencies": {
|
||||
"@jup-ag/core": "^1.0.0-beta.27",
|
||||
"@project-serum/anchor": "^0.24.2",
|
||||
"@project-serum/serum": "^0.13.65",
|
||||
"@pythnetwork/client": "^2.7.0",
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use anchor_lang::prelude::*;
|
||||
use core::fmt::Display;
|
||||
|
||||
// todo: group error blocks by kind
|
||||
// todo: add comments which indicate decimal code for an error
|
||||
|
@ -6,16 +7,111 @@ use anchor_lang::prelude::*;
|
|||
pub enum MangoError {
|
||||
#[msg("")]
|
||||
SomeError,
|
||||
#[msg("")]
|
||||
#[msg("checked math error")]
|
||||
MathError,
|
||||
#[msg("")]
|
||||
UnexpectedOracle,
|
||||
#[msg("")]
|
||||
#[msg("oracle type cannot be determined")]
|
||||
UnknownOracleType,
|
||||
#[msg("")]
|
||||
InvalidFlashLoanTargetCpiProgram,
|
||||
#[msg("")]
|
||||
#[msg("health must be positive")]
|
||||
HealthMustBePositive,
|
||||
#[msg("The account is bankrupt")]
|
||||
#[msg("the account is bankrupt")]
|
||||
IsBankrupt,
|
||||
#[msg("the account is not bankrupt")]
|
||||
IsNotBankrupt,
|
||||
#[msg("no free token position index")]
|
||||
NoFreeTokenPositionIndex,
|
||||
#[msg("no free serum3 open orders index")]
|
||||
NoFreeSerum3OpenOrdersIndex,
|
||||
#[msg("no free perp position index")]
|
||||
NoFreePerpPositionIndex,
|
||||
#[msg("serum3 open orders exist already")]
|
||||
Serum3OpenOrdersExistAlready,
|
||||
}
|
||||
|
||||
pub trait Contextable {
|
||||
/// Add a context string `c` to a Result or Error
|
||||
///
|
||||
/// Example: foo().context("calling foo")?;
|
||||
fn context(self, c: impl Display) -> Self;
|
||||
|
||||
/// Like `context()`, but evaluate the context string lazily
|
||||
///
|
||||
/// Use this if it's expensive to generate, like a format!() call.
|
||||
fn with_context<C, F>(self, c: F) -> Self
|
||||
where
|
||||
C: Display,
|
||||
F: FnOnce() -> C;
|
||||
}
|
||||
|
||||
impl Contextable for Error {
|
||||
fn context(self, c: impl Display) -> Self {
|
||||
match self {
|
||||
Error::AnchorError(err) => Error::AnchorError(AnchorError {
|
||||
error_msg: if err.error_msg.is_empty() {
|
||||
format!("{}", c)
|
||||
} else {
|
||||
format!("{}; {}", err.error_msg, c)
|
||||
},
|
||||
..err
|
||||
}),
|
||||
// Maybe wrap somehow?
|
||||
Error::ProgramError(err) => Error::ProgramError(err),
|
||||
}
|
||||
}
|
||||
fn with_context<C, F>(self, c: F) -> Self
|
||||
where
|
||||
C: Display,
|
||||
F: FnOnce() -> C,
|
||||
{
|
||||
self.context(c())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Contextable for Result<T> {
|
||||
fn context(self, c: impl Display) -> Self {
|
||||
if let Err(err) = self {
|
||||
Err(err.context(c))
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
fn with_context<C, F>(self, c: F) -> Self
|
||||
where
|
||||
C: Display,
|
||||
F: FnOnce() -> C,
|
||||
{
|
||||
if let Err(err) = self {
|
||||
Err(err.context(c()))
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an Error with a particular message, using format!() style arguments
|
||||
///
|
||||
/// Example: error_msg!("index {} not found", index)
|
||||
#[macro_export]
|
||||
macro_rules! error_msg {
|
||||
($($arg:tt)*) => {
|
||||
error!(MangoError::SomeError).context(format!($($arg)*))
|
||||
};
|
||||
}
|
||||
|
||||
/// Like anchor's require!(), but with a customizable message
|
||||
///
|
||||
/// Example: require!(condition, "the condition on account {} was violated", account_key);
|
||||
#[macro_export]
|
||||
macro_rules! require_msg {
|
||||
($invariant:expr, $($arg:tt)*) => {
|
||||
if !($invariant) {
|
||||
Err(error_msg!($($arg)*))?;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub use error_msg;
|
||||
pub use require_msg;
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
use anchor_lang::prelude::*;
|
||||
use fixed::types::I80F48;
|
||||
|
||||
use crate::state::{PerpMarketIndex, TokenIndex};
|
||||
use crate::state::{HealthCache, PerpMarketIndex, TokenIndex};
|
||||
|
||||
#[event]
|
||||
#[derive(Debug)]
|
||||
pub struct MangoAccountData {
|
||||
pub health_cache: HealthCache,
|
||||
pub init_health: I80F48,
|
||||
pub maint_health: I80F48,
|
||||
pub equity: Equity,
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
use anchor_lang::prelude::*;
|
||||
use anchor_spl::token::Token;
|
||||
|
||||
use crate::error::*;
|
||||
use crate::state::*;
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct CloseAccount<'info> {
|
||||
pub struct AccountClose<'info> {
|
||||
pub group: AccountLoader<'info, Group>,
|
||||
|
||||
#[account(
|
||||
mut,
|
||||
// note: should never be the delegate
|
||||
has_one = owner,
|
||||
has_one = group,
|
||||
close = sol_destination
|
||||
|
@ -23,7 +25,7 @@ pub struct CloseAccount<'info> {
|
|||
pub token_program: Program<'info, Token>,
|
||||
}
|
||||
|
||||
pub fn close_account(ctx: Context<CloseAccount>) -> Result<()> {
|
||||
pub fn account_close(ctx: Context<AccountClose>) -> Result<()> {
|
||||
let group = ctx.accounts.group.load()?;
|
||||
|
||||
// don't perform checks if group is just testing
|
||||
|
@ -32,9 +34,9 @@ pub fn close_account(ctx: Context<CloseAccount>) -> Result<()> {
|
|||
}
|
||||
|
||||
let account = ctx.accounts.account.load()?;
|
||||
require_eq!(account.being_liquidated, 0);
|
||||
require!(!account.being_liquidated(), MangoError::SomeError);
|
||||
require!(!account.is_bankrupt(), MangoError::SomeError);
|
||||
require_eq!(account.delegate, Pubkey::default());
|
||||
require_eq!(account.is_bankrupt, 0);
|
||||
for ele in account.tokens.values {
|
||||
require_eq!(ele.is_active(), false);
|
||||
}
|
|
@ -6,7 +6,7 @@ use crate::util::fill32_from_str;
|
|||
|
||||
#[derive(Accounts)]
|
||||
#[instruction(account_num: u8)]
|
||||
pub struct CreateAccount<'info> {
|
||||
pub struct AccountCreate<'info> {
|
||||
pub group: AccountLoader<'info, Group>,
|
||||
|
||||
#[account(
|
||||
|
@ -17,7 +17,6 @@ pub struct CreateAccount<'info> {
|
|||
space = 8 + std::mem::size_of::<MangoAccount>(),
|
||||
)]
|
||||
pub account: AccountLoader<'info, MangoAccount>,
|
||||
|
||||
pub owner: Signer<'info>,
|
||||
|
||||
#[account(mut)]
|
||||
|
@ -26,7 +25,7 @@ pub struct CreateAccount<'info> {
|
|||
pub system_program: Program<'info, System>,
|
||||
}
|
||||
|
||||
pub fn create_account(ctx: Context<CreateAccount>, account_num: u8, name: String) -> Result<()> {
|
||||
pub fn account_create(ctx: Context<AccountCreate>, account_num: u8, name: String) -> Result<()> {
|
||||
let mut account = ctx.accounts.account.load_init()?;
|
||||
|
||||
account.name = fill32_from_str(name)?;
|
||||
|
@ -38,8 +37,8 @@ pub fn create_account(ctx: Context<CreateAccount>, account_num: u8, name: String
|
|||
account.tokens = MangoAccountTokenPositions::default();
|
||||
account.serum3 = MangoAccountSerum3Orders::default();
|
||||
account.perps = MangoAccountPerpPositions::default();
|
||||
account.being_liquidated = 0;
|
||||
account.is_bankrupt = 0;
|
||||
account.set_being_liquidated(false);
|
||||
account.set_bankrupt(false);
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
use crate::error::MangoError;
|
||||
use anchor_lang::prelude::*;
|
||||
|
||||
use crate::state::*;
|
||||
use crate::util::fill32_from_str;
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct AccountEdit<'info> {
|
||||
pub group: AccountLoader<'info, Group>,
|
||||
|
||||
#[account(
|
||||
mut,
|
||||
// Note: should never be the delegate
|
||||
has_one = owner,
|
||||
has_one = group,
|
||||
)]
|
||||
pub account: AccountLoader<'info, MangoAccount>,
|
||||
pub owner: Signer<'info>,
|
||||
}
|
||||
|
||||
pub fn account_edit(
|
||||
ctx: Context<AccountEdit>,
|
||||
name_opt: Option<String>,
|
||||
// note: can also be used to unset by using the default pubkey here as a param
|
||||
delegate_opt: Option<Pubkey>,
|
||||
) -> Result<()> {
|
||||
require!(
|
||||
name_opt.is_some() || delegate_opt.is_some(),
|
||||
MangoError::SomeError
|
||||
);
|
||||
|
||||
let mut account = ctx.accounts.account.load_mut()?;
|
||||
|
||||
// msg!("old account {:#?}", account);
|
||||
|
||||
// note: unchanged fields are inline, and match exact definition in create_account
|
||||
// please maintain, and don't remove, makes it easy to reason about which support modification by owner
|
||||
|
||||
if let Some(name) = name_opt {
|
||||
account.name = fill32_from_str(name)?;
|
||||
}
|
||||
|
||||
// unchanged -
|
||||
// owner
|
||||
// account_num
|
||||
// bump
|
||||
|
||||
if let Some(delegate) = delegate_opt {
|
||||
account.delegate = delegate;
|
||||
}
|
||||
|
||||
// unchanged -
|
||||
// tokens
|
||||
// serum3
|
||||
// perps
|
||||
// being_liquidated
|
||||
// is_bankrupt
|
||||
|
||||
// msg!("new account {:#?}", account);
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -17,15 +17,17 @@ pub fn compute_account_data(ctx: Context<ComputeAccountData>) -> Result<()> {
|
|||
|
||||
let account_retriever = ScanningAccountRetriever::new(ctx.remaining_accounts, &group_pk)?;
|
||||
|
||||
let init_health = compute_health(&account, HealthType::Init, &account_retriever)?;
|
||||
let maint_health = compute_health(&account, HealthType::Maint, &account_retriever)?;
|
||||
let health_cache = new_health_cache(&account, &account_retriever)?;
|
||||
let init_health = health_cache.health(HealthType::Init);
|
||||
let maint_health = health_cache.health(HealthType::Maint);
|
||||
|
||||
let equity = compute_equity(&account, &account_retriever)?;
|
||||
|
||||
emit!(MangoAccountData {
|
||||
health_cache,
|
||||
init_health,
|
||||
maint_health,
|
||||
equity
|
||||
equity,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -32,7 +32,7 @@ pub struct FlashLoan<'info> {
|
|||
#[account(
|
||||
mut,
|
||||
has_one = group,
|
||||
has_one = owner,
|
||||
constraint = account.load()?.is_owner_or_delegate(owner.key()),
|
||||
)]
|
||||
pub account: AccountLoader<'info, MangoAccount>,
|
||||
|
||||
|
@ -85,7 +85,7 @@ pub fn flash_loan<'key, 'accounts, 'remaining, 'info>(
|
|||
|
||||
let group = ctx.accounts.group.load()?;
|
||||
let mut account = ctx.accounts.account.load_mut()?;
|
||||
require!(account.is_bankrupt == 0, MangoError::IsBankrupt);
|
||||
require!(!account.is_bankrupt(), MangoError::IsBankrupt);
|
||||
|
||||
// Go over the banks passed as health accounts and:
|
||||
// - Ensure that all banks that are passed in have activated positions.
|
||||
|
|
|
@ -37,11 +37,12 @@ pub struct FlashLoan2End<'info> {
|
|||
pub group: AccountLoader<'info, Group>,
|
||||
#[account(
|
||||
mut,
|
||||
has_one = owner,
|
||||
has_one = group,
|
||||
constraint = account.load()?.is_owner_or_delegate(owner.key()),
|
||||
)]
|
||||
pub account: AccountLoader<'info, MangoAccount>,
|
||||
pub owner: Signer<'info>,
|
||||
|
||||
pub token_program: Program<'info, Token>,
|
||||
}
|
||||
|
||||
|
@ -161,7 +162,7 @@ pub fn flash_loan2_end<'key, 'accounts, 'remaining, 'info>(
|
|||
let group_seeds = group_seeds!(group);
|
||||
|
||||
let mut account = ctx.accounts.account.load_mut()?;
|
||||
require!(account.is_bankrupt == 0, MangoError::IsBankrupt);
|
||||
require!(!account.is_bankrupt(), MangoError::IsBankrupt);
|
||||
|
||||
// Find index at which vaults start
|
||||
let vaults_index = ctx
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use crate::accounts_zerocopy::*;
|
||||
use crate::error::MangoError;
|
||||
use crate::error::*;
|
||||
use crate::group_seeds;
|
||||
use crate::logs::{FlashLoanLog, FlashLoanTokenDetail, TokenBalanceLog};
|
||||
use crate::state::{
|
||||
|
@ -40,10 +40,11 @@ pub struct FlashLoan3Begin<'info> {
|
|||
pub struct FlashLoan3End<'info> {
|
||||
#[account(
|
||||
mut,
|
||||
has_one = owner,
|
||||
constraint = account.load()?.is_owner_or_delegate(owner.key()),
|
||||
)]
|
||||
pub account: AccountLoader<'info, MangoAccount>,
|
||||
pub owner: Signer<'info>,
|
||||
|
||||
pub token_program: Program<'info, Token>,
|
||||
}
|
||||
|
||||
|
@ -54,11 +55,7 @@ pub fn flash_loan3_begin<'key, 'accounts, 'remaining, 'info>(
|
|||
loan_amounts: Vec<u64>,
|
||||
) -> Result<()> {
|
||||
let num_loans = loan_amounts.len();
|
||||
require_eq!(
|
||||
ctx.remaining_accounts.len(),
|
||||
3 * num_loans,
|
||||
MangoError::SomeError
|
||||
);
|
||||
require_eq!(ctx.remaining_accounts.len(), 3 * num_loans);
|
||||
let banks = &ctx.remaining_accounts[..num_loans];
|
||||
let vaults = &ctx.remaining_accounts[num_loans..2 * num_loans];
|
||||
let token_accounts = &ctx.remaining_accounts[2 * num_loans..];
|
||||
|
@ -105,10 +102,9 @@ pub fn flash_loan3_begin<'key, 'accounts, 'remaining, 'info>(
|
|||
|
||||
// Forbid FlashLoan3Begin to be called from CPI (it does not have to be the first instruction)
|
||||
let current_ix = tx_instructions::load_instruction_at_checked(current_index, ixs)?;
|
||||
require_keys_eq!(
|
||||
current_ix.program_id,
|
||||
*ctx.program_id,
|
||||
MangoError::SomeError
|
||||
require_msg!(
|
||||
current_ix.program_id == *ctx.program_id,
|
||||
"FlashLoan3Begin must be a top-level instruction"
|
||||
);
|
||||
|
||||
// The only other mango instruction that must appear before the end of the tx is
|
||||
|
@ -119,19 +115,22 @@ pub fn flash_loan3_begin<'key, 'accounts, 'remaining, 'info>(
|
|||
let ix = match tx_instructions::load_instruction_at_checked(index, ixs) {
|
||||
Ok(ix) => ix,
|
||||
Err(ProgramError::InvalidArgument) => break, // past the last instruction
|
||||
Err(e) => Err(e)?,
|
||||
Err(e) => return Err(e.into()),
|
||||
};
|
||||
|
||||
// Check that the mango program key is not used
|
||||
if ix.program_id == crate::id() {
|
||||
// must be the last mango ix -- this could possibly be relaxed, but right now
|
||||
// we need to guard against multiple FlashLoanEnds
|
||||
require!(!found_end, MangoError::SomeError);
|
||||
require_msg!(
|
||||
!found_end,
|
||||
"the transaction must not contain a Mango instruction after FlashLoan3End"
|
||||
);
|
||||
found_end = true;
|
||||
|
||||
// must be the FlashLoan3End instruction
|
||||
require!(
|
||||
&ix.data[0..8] == &[163, 231, 155, 56, 201, 68, 84, 148],
|
||||
ix.data[0..8] == [163, 231, 155, 56, 201, 68, 84, 148],
|
||||
MangoError::SomeError
|
||||
);
|
||||
|
||||
|
@ -139,18 +138,21 @@ pub fn flash_loan3_begin<'key, 'accounts, 'remaining, 'info>(
|
|||
let begin_accounts = &ctx.remaining_accounts[num_loans..];
|
||||
let end_accounts = &ix.accounts[ix.accounts.len() - 2 * num_loans..];
|
||||
for (begin_account, end_account) in begin_accounts.iter().zip(end_accounts.iter()) {
|
||||
require_keys_eq!(*begin_account.key, end_account.pubkey);
|
||||
require_msg!(*begin_account.key == end_account.pubkey, "the trailing accounts passed to FlashLoan3Begin and End must match, found {} on begin and {} on end", begin_account.key, end_account.pubkey);
|
||||
}
|
||||
} else {
|
||||
// ensure no one can cpi into mango either
|
||||
for meta in ix.accounts.iter() {
|
||||
require_keys_neq!(meta.pubkey, crate::id());
|
||||
require_msg!(meta.pubkey != crate::id(), "instructions between FlashLoan3Begin and End may not use the Mango program account");
|
||||
}
|
||||
}
|
||||
|
||||
index += 1;
|
||||
}
|
||||
require!(found_end, MangoError::SomeError);
|
||||
require_msg!(
|
||||
found_end,
|
||||
"found no FlashLoan3End instruction in transaction"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
@ -167,7 +169,7 @@ pub fn flash_loan3_end<'key, 'accounts, 'remaining, 'info>(
|
|||
ctx: Context<'key, 'accounts, 'remaining, 'info, FlashLoan3End<'info>>,
|
||||
) -> Result<()> {
|
||||
let mut account = ctx.accounts.account.load_mut()?;
|
||||
require!(account.is_bankrupt == 0, MangoError::IsBankrupt);
|
||||
require!(!account.is_bankrupt(), MangoError::IsBankrupt);
|
||||
|
||||
// Find index at which vaults start
|
||||
let vaults_index = ctx
|
||||
|
@ -181,7 +183,7 @@ pub fn flash_loan3_end<'key, 'accounts, 'remaining, 'info>(
|
|||
|
||||
maybe_token_account.unwrap().owner == account.group
|
||||
})
|
||||
.ok_or_else(|| error!(MangoError::SomeError))?;
|
||||
.ok_or_else(|| error_msg!("expected at least one vault token account to be passed"))?;
|
||||
let vaults_len = (ctx.remaining_accounts.len() - vaults_index) / 2;
|
||||
require_eq!(ctx.remaining_accounts.len(), vaults_index + 2 * vaults_len);
|
||||
|
||||
|
@ -252,7 +254,14 @@ pub fn flash_loan3_end<'key, 'accounts, 'remaining, 'info>(
|
|||
}
|
||||
|
||||
// all vaults must have had matching banks
|
||||
require!(vaults_with_banks.iter().all(|&b| b), MangoError::SomeError);
|
||||
for (i, has_bank) in vaults_with_banks.iter().enumerate() {
|
||||
require_msg!(
|
||||
has_bank,
|
||||
"missing bank for vault index {}, address {}",
|
||||
i,
|
||||
vaults[i].key
|
||||
);
|
||||
}
|
||||
|
||||
// Check pre-cpi health
|
||||
// NOTE: This health check isn't strictly necessary. It will be, later, when
|
||||
|
|
|
@ -3,7 +3,7 @@ use anchor_lang::prelude::*;
|
|||
use anchor_spl::token::Token;
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct CloseGroup<'info> {
|
||||
pub struct GroupClose<'info> {
|
||||
#[account(
|
||||
mut,
|
||||
constraint = group.load()?.testing == 1,
|
||||
|
@ -21,7 +21,7 @@ pub struct CloseGroup<'info> {
|
|||
pub token_program: Program<'info, Token>,
|
||||
}
|
||||
|
||||
pub fn close_group(_ctx: Context<CloseGroup>) -> Result<()> {
|
||||
pub fn group_close(_ctx: Context<GroupClose>) -> Result<()> {
|
||||
// TODO: checks
|
||||
Ok(())
|
||||
}
|
|
@ -1,11 +1,12 @@
|
|||
use anchor_lang::prelude::*;
|
||||
use anchor_spl::token::{Mint, Token, TokenAccount};
|
||||
|
||||
use crate::error::*;
|
||||
use crate::state::*;
|
||||
|
||||
#[derive(Accounts)]
|
||||
#[instruction(group_num: u32)]
|
||||
pub struct CreateGroup<'info> {
|
||||
pub struct GroupCreate<'info> {
|
||||
#[account(
|
||||
init,
|
||||
seeds = [b"Group".as_ref(), admin.key().as_ref(), &group_num.to_le_bytes()],
|
||||
|
@ -17,15 +18,31 @@ pub struct CreateGroup<'info> {
|
|||
|
||||
pub admin: Signer<'info>,
|
||||
|
||||
pub insurance_mint: Account<'info, Mint>,
|
||||
|
||||
#[account(
|
||||
init,
|
||||
seeds = [group.key().as_ref(), b"InsuranceVault".as_ref()],
|
||||
bump,
|
||||
token::authority = group,
|
||||
token::mint = insurance_mint,
|
||||
payer = payer
|
||||
)]
|
||||
pub insurance_vault: Account<'info, TokenAccount>,
|
||||
|
||||
#[account(mut)]
|
||||
pub payer: Signer<'info>,
|
||||
|
||||
pub token_program: Program<'info, Token>,
|
||||
pub system_program: Program<'info, System>,
|
||||
pub rent: Sysvar<'info, Rent>,
|
||||
}
|
||||
|
||||
pub fn create_group(ctx: Context<CreateGroup>, group_num: u32, testing: u8) -> Result<()> {
|
||||
pub fn group_create(ctx: Context<GroupCreate>, group_num: u32, testing: u8) -> Result<()> {
|
||||
let mut group = ctx.accounts.group.load_init()?;
|
||||
group.admin = ctx.accounts.admin.key();
|
||||
group.insurance_vault = ctx.accounts.insurance_vault.key();
|
||||
group.insurance_mint = ctx.accounts.insurance_mint.key();
|
||||
group.bump = *ctx.bumps.get("group").ok_or(MangoError::SomeError)?;
|
||||
group.group_num = group_num;
|
||||
group.testing = testing;
|
|
@ -0,0 +1,217 @@
|
|||
use anchor_lang::prelude::*;
|
||||
use anchor_spl::token;
|
||||
use anchor_spl::token::Token;
|
||||
use anchor_spl::token::TokenAccount;
|
||||
use fixed::types::I80F48;
|
||||
|
||||
use crate::accounts_zerocopy::*;
|
||||
use crate::error::*;
|
||||
use crate::state::ScanningAccountRetriever;
|
||||
use crate::state::*;
|
||||
use crate::util::checked_math as cm;
|
||||
|
||||
// Remaining accounts:
|
||||
// - all banks for liab_token_index (writable)
|
||||
// - merged health accounts for liqor+liqee
|
||||
#[derive(Accounts)]
|
||||
#[instruction(liab_token_index: TokenIndex)]
|
||||
pub struct LiqTokenBankruptcy<'info> {
|
||||
#[account(
|
||||
has_one = insurance_vault,
|
||||
)]
|
||||
pub group: AccountLoader<'info, Group>,
|
||||
|
||||
#[account(
|
||||
mut,
|
||||
has_one = group,
|
||||
constraint = liqor.load()?.is_owner_or_delegate(liqor_owner.key()),
|
||||
)]
|
||||
pub liqor: AccountLoader<'info, MangoAccount>,
|
||||
pub liqor_owner: Signer<'info>,
|
||||
|
||||
#[account(
|
||||
mut,
|
||||
has_one = group,
|
||||
)]
|
||||
pub liqee: AccountLoader<'info, MangoAccount>,
|
||||
|
||||
#[account(
|
||||
has_one = group,
|
||||
constraint = liab_mint_info.load()?.token_index == liab_token_index,
|
||||
)]
|
||||
pub liab_mint_info: AccountLoader<'info, MintInfo>,
|
||||
|
||||
#[account(mut)]
|
||||
pub quote_vault: Account<'info, TokenAccount>,
|
||||
|
||||
#[account(mut)]
|
||||
pub insurance_vault: Account<'info, TokenAccount>,
|
||||
|
||||
pub token_program: Program<'info, Token>,
|
||||
}
|
||||
|
||||
impl<'info> LiqTokenBankruptcy<'info> {
|
||||
pub fn transfer_ctx(&self) -> CpiContext<'_, '_, '_, 'info, token::Transfer<'info>> {
|
||||
let program = self.token_program.to_account_info();
|
||||
let accounts = token::Transfer {
|
||||
from: self.insurance_vault.to_account_info(),
|
||||
to: self.quote_vault.to_account_info(),
|
||||
authority: self.group.to_account_info(),
|
||||
};
|
||||
CpiContext::new(program, accounts)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn liq_token_bankruptcy(
|
||||
ctx: Context<LiqTokenBankruptcy>,
|
||||
liab_token_index: TokenIndex,
|
||||
max_liab_transfer: I80F48,
|
||||
) -> Result<()> {
|
||||
let group = ctx.accounts.group.load()?;
|
||||
let group_pk = &ctx.accounts.group.key();
|
||||
|
||||
// split remaining accounts into banks and health
|
||||
let liab_mint_info = ctx.accounts.liab_mint_info.load()?;
|
||||
let bank_pks = liab_mint_info.banks();
|
||||
let (bank_ais, health_ais) = &ctx.remaining_accounts.split_at(bank_pks.len());
|
||||
require!(
|
||||
bank_ais.iter().map(|ai| ai.key).eq(bank_pks.iter()),
|
||||
MangoError::SomeError
|
||||
);
|
||||
|
||||
let mut liqor = ctx.accounts.liqor.load_mut()?;
|
||||
require!(!liqor.is_bankrupt(), MangoError::IsBankrupt);
|
||||
|
||||
let mut liqee = ctx.accounts.liqee.load_mut()?;
|
||||
require!(liqee.is_bankrupt(), MangoError::IsNotBankrupt);
|
||||
|
||||
let liab_bank = bank_ais[0].load::<Bank>()?;
|
||||
let liab_deposit_index = liab_bank.deposit_index;
|
||||
let (liqee_liab, liqee_raw_token_index) = liqee.tokens.get_mut(liab_token_index)?;
|
||||
let mut remaining_liab_loss = -liqee_liab.native(&liab_bank);
|
||||
require_gt!(remaining_liab_loss, I80F48::ZERO);
|
||||
drop(liab_bank);
|
||||
|
||||
let mut account_retriever = ScanningAccountRetriever::new(health_ais, group_pk)?;
|
||||
|
||||
// find insurance transfer amount
|
||||
let (liab_bank, liab_price, opt_quote_bank_and_price) =
|
||||
account_retriever.banks_mut_and_oracles(liab_token_index, QUOTE_TOKEN_INDEX)?;
|
||||
let liab_fee_factor = if liab_token_index == QUOTE_TOKEN_INDEX {
|
||||
I80F48::ONE
|
||||
} else {
|
||||
cm!(I80F48::ONE + liab_bank.liquidation_fee)
|
||||
};
|
||||
let liab_price_adjusted = cm!(liab_price * liab_fee_factor);
|
||||
|
||||
let liab_transfer_unrounded = remaining_liab_loss.min(max_liab_transfer);
|
||||
let insurance_transfer = cm!(liab_transfer_unrounded * liab_price_adjusted)
|
||||
.checked_ceil()
|
||||
.unwrap()
|
||||
.checked_to_num::<u64>()
|
||||
.unwrap()
|
||||
.min(ctx.accounts.insurance_vault.amount);
|
||||
let insurance_fund_exhausted = insurance_transfer == ctx.accounts.insurance_vault.amount;
|
||||
|
||||
let insurance_transfer_i80f48 = I80F48::from(insurance_transfer);
|
||||
|
||||
// AUDIT: v3 does this, but it seems bad, because it can make liab_transfer
|
||||
// exceed max_liab_transfer due to the ceil() above! Otoh, not doing it would allow
|
||||
// liquidators to exploit the insurance fund for 1 native token each call.
|
||||
let liab_transfer = cm!(insurance_transfer_i80f48 / liab_price_adjusted);
|
||||
|
||||
let mut liqee_liab_active = true;
|
||||
if insurance_transfer > 0 {
|
||||
// in the end, the liqee gets liab assets
|
||||
liqee_liab_active = liab_bank.deposit(liqee_liab, liab_transfer)?;
|
||||
remaining_liab_loss = -liqee_liab.native(&liab_bank);
|
||||
|
||||
// move insurance assets into quote bank
|
||||
let group_seeds = group_seeds!(group);
|
||||
token::transfer(
|
||||
ctx.accounts.transfer_ctx().with_signer(&[group_seeds]),
|
||||
insurance_transfer,
|
||||
)?;
|
||||
|
||||
// move quote assets into liqor and withdraw liab assets
|
||||
if let Some((quote_bank, _)) = opt_quote_bank_and_price {
|
||||
require_keys_eq!(quote_bank.vault, ctx.accounts.quote_vault.key());
|
||||
require_keys_eq!(quote_bank.mint, ctx.accounts.insurance_vault.mint);
|
||||
|
||||
// credit the liqor
|
||||
let (liqor_quote, liqor_quote_raw_token_index, _) =
|
||||
liqor.tokens.get_mut_or_create(QUOTE_TOKEN_INDEX)?;
|
||||
let liqor_quote_active = quote_bank.deposit(liqor_quote, insurance_transfer_i80f48)?;
|
||||
|
||||
// transfer liab from liqee to liqor
|
||||
let (liqor_liab, liqor_liab_raw_token_index, _) =
|
||||
liqor.tokens.get_mut_or_create(liab_token_index)?;
|
||||
let liqor_liab_active = liab_bank.withdraw_with_fee(liqor_liab, liab_transfer)?;
|
||||
|
||||
// Check liqor's health
|
||||
let liqor_health = compute_health(&liqor, HealthType::Init, &account_retriever)?;
|
||||
require!(liqor_health >= 0, MangoError::HealthMustBePositive);
|
||||
|
||||
if !liqor_quote_active {
|
||||
liqor.tokens.deactivate(liqor_quote_raw_token_index);
|
||||
}
|
||||
if !liqor_liab_active {
|
||||
liqor.tokens.deactivate(liqor_liab_raw_token_index);
|
||||
}
|
||||
} else {
|
||||
// For liab_token_index == QUOTE_TOKEN_INDEX: the insurance fund deposits directly into liqee,
|
||||
// without a fee or the liqor being involved
|
||||
require_eq!(liab_token_index, QUOTE_TOKEN_INDEX);
|
||||
require_eq!(liab_price_adjusted, I80F48::ONE);
|
||||
require_eq!(insurance_transfer_i80f48, liab_transfer);
|
||||
}
|
||||
}
|
||||
drop(account_retriever);
|
||||
|
||||
// Socialize loss
|
||||
if insurance_fund_exhausted && remaining_liab_loss.is_positive() {
|
||||
// find the total deposits
|
||||
let mut indexed_total_deposits = I80F48::ZERO;
|
||||
for bank_ai in bank_ais.iter() {
|
||||
let bank = bank_ai.load::<Bank>()?;
|
||||
indexed_total_deposits = cm!(indexed_total_deposits + bank.indexed_deposits);
|
||||
}
|
||||
|
||||
// This is the solution to:
|
||||
// total_indexed_deposits * (deposit_index - new_deposit_index) = remaining_liab_loss
|
||||
// AUDIT: Could it happen that remaining_liab_loss > total_indexed_deposits * deposit_index?
|
||||
// Probably not.
|
||||
let new_deposit_index =
|
||||
cm!(liab_deposit_index - remaining_liab_loss / indexed_total_deposits);
|
||||
|
||||
let mut amount_to_credit = remaining_liab_loss;
|
||||
let mut position_active = true;
|
||||
for bank_ai in bank_ais.iter() {
|
||||
let mut bank = bank_ai.load_mut::<Bank>()?;
|
||||
bank.deposit_index = new_deposit_index;
|
||||
|
||||
// credit liqee on each bank where we can offset borrows
|
||||
let amount_for_bank = amount_to_credit.min(bank.native_borrows());
|
||||
if amount_for_bank.is_positive() {
|
||||
position_active = bank.deposit(liqee_liab, amount_for_bank)?;
|
||||
amount_to_credit = cm!(amount_to_credit - amount_for_bank);
|
||||
if amount_to_credit.is_zero() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
require!(!position_active, MangoError::SomeError);
|
||||
liqee_liab_active = false;
|
||||
}
|
||||
|
||||
// If the account has no more borrows then it's no longer bankrupt
|
||||
let account_retriever = ScanningAccountRetriever::new(health_ais, group_pk)?;
|
||||
let liqee_health_cache = new_health_cache(&liqee, &account_retriever)?;
|
||||
liqee.set_bankrupt(liqee_health_cache.has_borrows());
|
||||
|
||||
if !liqee_liab_active {
|
||||
liqee.tokens.deactivate(liqee_raw_token_index);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -15,9 +15,9 @@ pub struct LiqTokenWithToken<'info> {
|
|||
#[account(
|
||||
mut,
|
||||
has_one = group,
|
||||
constraint = liqor.load()?.is_owner_or_delegate(liqor_owner.key()),
|
||||
)]
|
||||
pub liqor: AccountLoader<'info, MangoAccount>,
|
||||
#[account(address = liqor.load()?.owner)]
|
||||
pub liqor_owner: Signer<'info>,
|
||||
|
||||
#[account(
|
||||
|
@ -39,24 +39,24 @@ pub fn liq_token_with_token(
|
|||
let mut account_retriever = ScanningAccountRetriever::new(ctx.remaining_accounts, group_pk)?;
|
||||
|
||||
let mut liqor = ctx.accounts.liqor.load_mut()?;
|
||||
require!(liqor.is_bankrupt == 0, MangoError::IsBankrupt);
|
||||
require!(!liqor.is_bankrupt(), MangoError::IsBankrupt);
|
||||
|
||||
let mut liqee = ctx.accounts.liqee.load_mut()?;
|
||||
require!(liqee.is_bankrupt == 0, MangoError::IsBankrupt);
|
||||
require!(!liqee.is_bankrupt(), MangoError::IsBankrupt);
|
||||
|
||||
// Initial liqee health check
|
||||
let mut liqee_health_cache = new_health_cache(&liqee, &account_retriever)?;
|
||||
let init_health = liqee_health_cache.health(HealthType::Init)?;
|
||||
if liqee.being_liquidated != 0 {
|
||||
let init_health = liqee_health_cache.health(HealthType::Init);
|
||||
if liqee.being_liquidated() {
|
||||
if init_health > I80F48::ZERO {
|
||||
liqee.being_liquidated = 0;
|
||||
liqee.set_being_liquidated(false);
|
||||
msg!("Liqee init_health above zero");
|
||||
return Ok(());
|
||||
}
|
||||
} else {
|
||||
let maint_health = liqee_health_cache.health(HealthType::Maint)?;
|
||||
let maint_health = liqee_health_cache.health(HealthType::Maint);
|
||||
require!(maint_health < I80F48::ZERO, MangoError::SomeError);
|
||||
liqee.being_liquidated = 1;
|
||||
liqee.set_being_liquidated(true);
|
||||
}
|
||||
|
||||
//
|
||||
|
@ -68,16 +68,18 @@ pub fn liq_token_with_token(
|
|||
//
|
||||
// This must happen _after_ the health computation, since immutable borrows of
|
||||
// the bank are not allowed at the same time.
|
||||
let (asset_bank, liab_bank, asset_price, liab_price) =
|
||||
let (asset_bank, asset_price, opt_liab_bank_and_price) =
|
||||
account_retriever.banks_mut_and_oracles(asset_token_index, liab_token_index)?;
|
||||
let (liab_bank, liab_price) = opt_liab_bank_and_price.unwrap();
|
||||
|
||||
let liqee_assets_native = liqee
|
||||
.tokens
|
||||
.get(asset_bank.token_index)?
|
||||
.native(&asset_bank);
|
||||
// The main complication here is that we can't keep the liqee_asset_position and liqee_liab_position
|
||||
// borrows alive at the same time. Possibly adding get_mut_pair() would be helpful.
|
||||
let (liqee_asset_position, liqee_asset_raw_index) = liqee.tokens.get(asset_token_index)?;
|
||||
let liqee_assets_native = liqee_asset_position.native(&asset_bank);
|
||||
require!(liqee_assets_native.is_positive(), MangoError::SomeError);
|
||||
|
||||
let liqee_liab_native = liqee.tokens.get(liab_bank.token_index)?.native(&liab_bank);
|
||||
let (liqee_liab_position, liqee_liab_raw_index) = liqee.tokens.get(liab_token_index)?;
|
||||
let liqee_liab_native = liqee_liab_position.native(&liab_bank);
|
||||
require!(liqee_liab_native.is_negative(), MangoError::SomeError);
|
||||
|
||||
// TODO why sum of both tokens liquidation fees? Add comment
|
||||
|
@ -115,18 +117,24 @@ pub fn liq_token_with_token(
|
|||
let asset_transfer = cm!(liab_transfer * liab_price_adjusted / asset_price);
|
||||
|
||||
// Apply the balance changes to the liqor and liqee accounts
|
||||
liab_bank.deposit(liqee.tokens.get_mut(liab_token_index)?, liab_transfer)?;
|
||||
liab_bank.withdraw_with_fee(
|
||||
liqor.tokens.get_mut_or_create(liab_token_index)?.0,
|
||||
liab_transfer,
|
||||
)?;
|
||||
let liqee_liab_position = liqee.tokens.get_mut_raw(liqee_liab_raw_index);
|
||||
let liqee_liab_active = liab_bank.deposit(liqee_liab_position, liab_transfer)?;
|
||||
let liqee_liab_position_indexed = liqee_liab_position.indexed_position;
|
||||
|
||||
asset_bank.deposit(
|
||||
liqor.tokens.get_mut_or_create(asset_token_index)?.0,
|
||||
asset_transfer,
|
||||
)?;
|
||||
asset_bank
|
||||
.withdraw_without_fee(liqee.tokens.get_mut(asset_token_index)?, asset_transfer)?;
|
||||
let (liqor_liab_position, liqor_liab_raw_index, _) =
|
||||
liqor.tokens.get_mut_or_create(liab_token_index)?;
|
||||
let liqor_liab_active = liab_bank.withdraw_with_fee(liqor_liab_position, liab_transfer)?;
|
||||
let liqor_liab_position_indexed = liqor_liab_position.indexed_position;
|
||||
|
||||
let (liqor_asset_position, liqor_asset_raw_index, _) =
|
||||
liqor.tokens.get_mut_or_create(asset_token_index)?;
|
||||
let liqor_asset_active = asset_bank.deposit(liqor_asset_position, asset_transfer)?;
|
||||
let liqor_asset_position_indexed = liqor_asset_position.indexed_position;
|
||||
|
||||
let liqee_asset_position = liqee.tokens.get_mut_raw(liqee_asset_raw_index);
|
||||
let liqee_asset_active =
|
||||
asset_bank.withdraw_without_fee(liqee_asset_position, asset_transfer)?;
|
||||
let liqee_asset_position_indexed = liqee_asset_position.indexed_position;
|
||||
|
||||
// Update the health cache
|
||||
liqee_health_cache.adjust_token_balance(liab_token_index, liab_transfer)?;
|
||||
|
@ -154,11 +162,7 @@ pub fn liq_token_with_token(
|
|||
emit!(TokenBalanceLog {
|
||||
mango_account: ctx.accounts.liqee.key(),
|
||||
token_index: asset_token_index,
|
||||
indexed_position: liqee
|
||||
.tokens
|
||||
.get_mut(asset_token_index)?
|
||||
.indexed_position
|
||||
.to_bits(),
|
||||
indexed_position: liqee_asset_position_indexed.to_bits(),
|
||||
deposit_index: asset_bank.deposit_index.to_bits(),
|
||||
borrow_index: asset_bank.borrow_index.to_bits(),
|
||||
price: asset_price.to_bits(),
|
||||
|
@ -167,11 +171,7 @@ pub fn liq_token_with_token(
|
|||
emit!(TokenBalanceLog {
|
||||
mango_account: ctx.accounts.liqee.key(),
|
||||
token_index: liab_token_index,
|
||||
indexed_position: liqee
|
||||
.tokens
|
||||
.get_mut(liab_token_index)?
|
||||
.indexed_position
|
||||
.to_bits(),
|
||||
indexed_position: liqee_liab_position_indexed.to_bits(),
|
||||
deposit_index: liab_bank.deposit_index.to_bits(),
|
||||
borrow_index: liab_bank.borrow_index.to_bits(),
|
||||
price: liab_price.to_bits(),
|
||||
|
@ -180,11 +180,7 @@ pub fn liq_token_with_token(
|
|||
emit!(TokenBalanceLog {
|
||||
mango_account: ctx.accounts.liqor.key(),
|
||||
token_index: asset_token_index,
|
||||
indexed_position: liqor
|
||||
.tokens
|
||||
.get_mut(asset_token_index)?
|
||||
.indexed_position
|
||||
.to_bits(),
|
||||
indexed_position: liqor_asset_position_indexed.to_bits(),
|
||||
deposit_index: asset_bank.deposit_index.to_bits(),
|
||||
borrow_index: asset_bank.borrow_index.to_bits(),
|
||||
price: asset_price.to_bits(),
|
||||
|
@ -193,34 +189,42 @@ pub fn liq_token_with_token(
|
|||
emit!(TokenBalanceLog {
|
||||
mango_account: ctx.accounts.liqor.key(),
|
||||
token_index: liab_token_index,
|
||||
indexed_position: liqor
|
||||
.tokens
|
||||
.get_mut(liab_token_index)?
|
||||
.indexed_position
|
||||
.to_bits(),
|
||||
indexed_position: liqor_liab_position_indexed.to_bits(),
|
||||
deposit_index: liab_bank.deposit_index.to_bits(),
|
||||
borrow_index: liab_bank.borrow_index.to_bits(),
|
||||
price: liab_price.to_bits(),
|
||||
});
|
||||
|
||||
// Since we use a scanning account retriever, it's safe to deactivate inactive token positions
|
||||
if !liqee_asset_active {
|
||||
liqee.tokens.deactivate(liqee_asset_raw_index);
|
||||
}
|
||||
if !liqee_liab_active {
|
||||
liqee.tokens.deactivate(liqee_liab_raw_index);
|
||||
}
|
||||
if !liqor_asset_active {
|
||||
liqor.tokens.deactivate(liqor_asset_raw_index);
|
||||
}
|
||||
if !liqor_liab_active {
|
||||
liqor.tokens.deactivate(liqor_liab_raw_index)
|
||||
}
|
||||
}
|
||||
|
||||
// Check liqee health again
|
||||
let maint_health = liqee_health_cache.health(HealthType::Maint)?;
|
||||
let maint_health = liqee_health_cache.health(HealthType::Maint);
|
||||
if maint_health < I80F48::ZERO {
|
||||
// TODO: bankruptcy check?
|
||||
liqee.set_bankrupt(!liqee_health_cache.has_liquidatable_assets());
|
||||
} else {
|
||||
let init_health = liqee_health_cache.health(HealthType::Init)?;
|
||||
let init_health = liqee_health_cache.health(HealthType::Init);
|
||||
|
||||
// this is equivalent to one native USDC or 1e-6 USDC
|
||||
// This is used as threshold to flip flag instead of 0 because of dust issues
|
||||
liqee.being_liquidated = if init_health < -I80F48::ONE { 1 } else { 0 };
|
||||
liqee.set_being_liquidated(init_health < -I80F48::ONE);
|
||||
}
|
||||
|
||||
// Check liqor's health
|
||||
let liqor_health = compute_health(&liqor, HealthType::Init, &account_retriever)?;
|
||||
require!(liqor_health >= 0, MangoError::HealthMustBePositive);
|
||||
|
||||
// TOOD: this must deactivate token accounts if the deposit/withdraw calls above call for it
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
pub use account_close::*;
|
||||
pub use account_create::*;
|
||||
pub use account_edit::*;
|
||||
pub use benchmark::*;
|
||||
pub use close_account::*;
|
||||
pub use close_group::*;
|
||||
pub use close_stub_oracle::*;
|
||||
pub use compute_account_data::*;
|
||||
pub use create_account::*;
|
||||
pub use create_group::*;
|
||||
pub use create_stub_oracle::*;
|
||||
pub use flash_loan::*;
|
||||
pub use flash_loan2::*;
|
||||
pub use flash_loan3::*;
|
||||
pub use group_close::*;
|
||||
pub use group_create::*;
|
||||
pub use liq_token_bankruptcy::*;
|
||||
pub use liq_token_with_token::*;
|
||||
pub use perp_cancel_all_orders::*;
|
||||
pub use perp_cancel_all_orders_by_side::*;
|
||||
|
@ -17,6 +17,7 @@ pub use perp_cancel_order_by_client_order_id::*;
|
|||
pub use perp_close_market::*;
|
||||
pub use perp_consume_events::*;
|
||||
pub use perp_create_market::*;
|
||||
pub use perp_edit_market::*;
|
||||
pub use perp_place_order::*;
|
||||
pub use perp_update_funding::*;
|
||||
pub use serum3_cancel_all_orders::*;
|
||||
|
@ -28,25 +29,28 @@ pub use serum3_liq_force_cancel_orders::*;
|
|||
pub use serum3_place_order::*;
|
||||
pub use serum3_register_market::*;
|
||||
pub use serum3_settle_funds::*;
|
||||
pub use set_stub_oracle::*;
|
||||
pub use stub_oracle_close::*;
|
||||
pub use stub_oracle_create::*;
|
||||
pub use stub_oracle_set::*;
|
||||
pub use token_add_bank::*;
|
||||
pub use token_deposit::*;
|
||||
pub use token_deregister::*;
|
||||
pub use token_edit::*;
|
||||
pub use token_register::*;
|
||||
pub use token_update_index_and_rate::*;
|
||||
pub use token_withdraw::*;
|
||||
pub use update_index::*;
|
||||
|
||||
mod account_close;
|
||||
mod account_create;
|
||||
mod account_edit;
|
||||
mod benchmark;
|
||||
mod close_account;
|
||||
mod close_group;
|
||||
mod close_stub_oracle;
|
||||
mod compute_account_data;
|
||||
mod create_account;
|
||||
mod create_group;
|
||||
mod create_stub_oracle;
|
||||
mod flash_loan;
|
||||
mod flash_loan2;
|
||||
mod flash_loan3;
|
||||
mod group_close;
|
||||
mod group_create;
|
||||
mod liq_token_bankruptcy;
|
||||
mod liq_token_with_token;
|
||||
mod perp_cancel_all_orders;
|
||||
mod perp_cancel_all_orders_by_side;
|
||||
|
@ -55,6 +59,7 @@ mod perp_cancel_order_by_client_order_id;
|
|||
mod perp_close_market;
|
||||
mod perp_consume_events;
|
||||
mod perp_create_market;
|
||||
mod perp_edit_market;
|
||||
mod perp_place_order;
|
||||
mod perp_update_funding;
|
||||
mod serum3_cancel_all_orders;
|
||||
|
@ -66,10 +71,13 @@ mod serum3_liq_force_cancel_orders;
|
|||
mod serum3_place_order;
|
||||
mod serum3_register_market;
|
||||
mod serum3_settle_funds;
|
||||
mod set_stub_oracle;
|
||||
mod stub_oracle_close;
|
||||
mod stub_oracle_create;
|
||||
mod stub_oracle_set;
|
||||
mod token_add_bank;
|
||||
mod token_deposit;
|
||||
mod token_deregister;
|
||||
mod token_edit;
|
||||
mod token_register;
|
||||
mod token_update_index_and_rate;
|
||||
mod token_withdraw;
|
||||
mod update_index;
|
||||
|
|
|
@ -10,9 +10,10 @@ pub struct PerpCancelAllOrders<'info> {
|
|||
#[account(
|
||||
mut,
|
||||
has_one = group,
|
||||
has_one = owner,
|
||||
constraint = account.load()?.is_owner_or_delegate(owner.key()),
|
||||
)]
|
||||
pub account: AccountLoader<'info, MangoAccount>,
|
||||
pub owner: Signer<'info>,
|
||||
|
||||
#[account(
|
||||
mut,
|
||||
|
@ -25,13 +26,11 @@ pub struct PerpCancelAllOrders<'info> {
|
|||
pub asks: AccountLoader<'info, BookSide>,
|
||||
#[account(mut)]
|
||||
pub bids: AccountLoader<'info, BookSide>,
|
||||
|
||||
pub owner: Signer<'info>,
|
||||
}
|
||||
|
||||
pub fn perp_cancel_all_orders(ctx: Context<PerpCancelAllOrders>, limit: u8) -> Result<()> {
|
||||
let mut mango_account = ctx.accounts.account.load_mut()?;
|
||||
require!(mango_account.is_bankrupt == 0, MangoError::IsBankrupt);
|
||||
require!(!mango_account.is_bankrupt(), MangoError::IsBankrupt);
|
||||
|
||||
let mut perp_market = ctx.accounts.perp_market.load_mut()?;
|
||||
let bids = ctx.accounts.bids.load_mut()?;
|
||||
|
|
|
@ -10,9 +10,10 @@ pub struct PerpCancelAllOrdersBySide<'info> {
|
|||
#[account(
|
||||
mut,
|
||||
has_one = group,
|
||||
has_one = owner,
|
||||
constraint = account.load()?.is_owner_or_delegate(owner.key()),
|
||||
)]
|
||||
pub account: AccountLoader<'info, MangoAccount>,
|
||||
pub owner: Signer<'info>,
|
||||
|
||||
#[account(
|
||||
mut,
|
||||
|
@ -25,8 +26,6 @@ pub struct PerpCancelAllOrdersBySide<'info> {
|
|||
pub asks: AccountLoader<'info, BookSide>,
|
||||
#[account(mut)]
|
||||
pub bids: AccountLoader<'info, BookSide>,
|
||||
|
||||
pub owner: Signer<'info>,
|
||||
}
|
||||
|
||||
pub fn perp_cancel_all_orders_by_side(
|
||||
|
@ -35,7 +34,7 @@ pub fn perp_cancel_all_orders_by_side(
|
|||
limit: u8,
|
||||
) -> Result<()> {
|
||||
let mut mango_account = ctx.accounts.account.load_mut()?;
|
||||
require!(mango_account.is_bankrupt == 0, MangoError::IsBankrupt);
|
||||
require!(!mango_account.is_bankrupt(), MangoError::IsBankrupt);
|
||||
|
||||
let mut perp_market = ctx.accounts.perp_market.load_mut()?;
|
||||
let bids = ctx.accounts.bids.load_mut()?;
|
||||
|
|
|
@ -10,9 +10,10 @@ pub struct PerpCancelOrder<'info> {
|
|||
#[account(
|
||||
mut,
|
||||
has_one = group,
|
||||
has_one = owner,
|
||||
constraint = account.load()?.is_owner_or_delegate(owner.key()),
|
||||
)]
|
||||
pub account: AccountLoader<'info, MangoAccount>,
|
||||
pub owner: Signer<'info>,
|
||||
|
||||
#[account(
|
||||
mut,
|
||||
|
@ -25,13 +26,11 @@ pub struct PerpCancelOrder<'info> {
|
|||
pub asks: AccountLoader<'info, BookSide>,
|
||||
#[account(mut)]
|
||||
pub bids: AccountLoader<'info, BookSide>,
|
||||
|
||||
pub owner: Signer<'info>,
|
||||
}
|
||||
|
||||
pub fn perp_cancel_order(ctx: Context<PerpCancelOrder>, order_id: i128) -> Result<()> {
|
||||
let mut mango_account = ctx.accounts.account.load_mut()?;
|
||||
require!(mango_account.is_bankrupt == 0, MangoError::IsBankrupt);
|
||||
require!(!mango_account.is_bankrupt(), MangoError::IsBankrupt);
|
||||
|
||||
let perp_market = ctx.accounts.perp_market.load_mut()?;
|
||||
let bids = ctx.accounts.bids.load_mut()?;
|
||||
|
|
|
@ -10,9 +10,10 @@ pub struct PerpCancelOrderByClientOrderId<'info> {
|
|||
#[account(
|
||||
mut,
|
||||
has_one = group,
|
||||
has_one = owner,
|
||||
constraint = account.load()?.is_owner_or_delegate(owner.key()),
|
||||
)]
|
||||
pub account: AccountLoader<'info, MangoAccount>,
|
||||
pub owner: Signer<'info>,
|
||||
|
||||
#[account(
|
||||
mut,
|
||||
|
@ -25,8 +26,6 @@ pub struct PerpCancelOrderByClientOrderId<'info> {
|
|||
pub asks: AccountLoader<'info, BookSide>,
|
||||
#[account(mut)]
|
||||
pub bids: AccountLoader<'info, BookSide>,
|
||||
|
||||
pub owner: Signer<'info>,
|
||||
}
|
||||
|
||||
pub fn perp_cancel_order_by_client_order_id(
|
||||
|
@ -34,7 +33,7 @@ pub fn perp_cancel_order_by_client_order_id(
|
|||
client_order_id: u64,
|
||||
) -> Result<()> {
|
||||
let mut mango_account = ctx.accounts.account.load_mut()?;
|
||||
require!(mango_account.is_bankrupt == 0, MangoError::IsBankrupt);
|
||||
require!(!mango_account.is_bankrupt(), MangoError::IsBankrupt);
|
||||
|
||||
let perp_market = ctx.accounts.perp_market.load_mut()?;
|
||||
let bids = ctx.accounts.bids.load_mut()?;
|
||||
|
|
|
@ -50,7 +50,6 @@ pub fn perp_create_market(
|
|||
oracle_config: OracleConfig,
|
||||
base_token_index_opt: Option<TokenIndex>,
|
||||
base_token_decimals: u8,
|
||||
quote_token_index: TokenIndex,
|
||||
quote_lot_size: i64,
|
||||
base_lot_size: i64,
|
||||
maint_asset_weight: f32,
|
||||
|
@ -96,7 +95,8 @@ pub fn perp_create_market(
|
|||
base_token_decimals,
|
||||
perp_market_index,
|
||||
base_token_index: base_token_index_opt.ok_or(TokenIndex::MAX).unwrap(),
|
||||
quote_token_index,
|
||||
padding: Default::default(),
|
||||
reserved: Default::default(),
|
||||
};
|
||||
|
||||
let mut bids = ctx.accounts.bids.load_init()?;
|
||||
|
|
|
@ -0,0 +1,118 @@
|
|||
use crate::state::*;
|
||||
use anchor_lang::prelude::*;
|
||||
use fixed::types::I80F48;
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct PerpEditMarket<'info> {
|
||||
#[account(
|
||||
has_one = admin,
|
||||
)]
|
||||
pub group: AccountLoader<'info, Group>,
|
||||
pub admin: Signer<'info>,
|
||||
|
||||
#[account(
|
||||
mut,
|
||||
has_one = group
|
||||
)]
|
||||
pub perp_market: AccountLoader<'info, PerpMarket>,
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn perp_edit_market(
|
||||
ctx: Context<PerpEditMarket>,
|
||||
oracle_opt: Option<Pubkey>,
|
||||
oracle_config_opt: Option<OracleConfig>,
|
||||
base_token_index_opt: Option<TokenIndex>,
|
||||
base_token_decimals_opt: Option<u8>,
|
||||
maint_asset_weight_opt: Option<f32>,
|
||||
init_asset_weight_opt: Option<f32>,
|
||||
maint_liab_weight_opt: Option<f32>,
|
||||
init_liab_weight_opt: Option<f32>,
|
||||
liquidation_fee_opt: Option<f32>,
|
||||
maker_fee_opt: Option<f32>,
|
||||
taker_fee_opt: Option<f32>,
|
||||
min_funding_opt: Option<f32>,
|
||||
max_funding_opt: Option<f32>,
|
||||
impact_quantity_opt: Option<i64>,
|
||||
) -> Result<()> {
|
||||
let mut perp_market = ctx.accounts.perp_market.load_mut()?;
|
||||
|
||||
// note: unchanged fields are inline, and match exact definition in perp_register_market
|
||||
// please maintain, and don't remove, makes it easy to reason about which support admin modification
|
||||
|
||||
// unchanged -
|
||||
// name
|
||||
// group
|
||||
|
||||
if let Some(oracle) = oracle_opt {
|
||||
perp_market.oracle = oracle;
|
||||
}
|
||||
if let Some(oracle_config) = oracle_config_opt {
|
||||
perp_market.oracle_config = oracle_config;
|
||||
};
|
||||
|
||||
// unchanged -
|
||||
// bids
|
||||
// asks
|
||||
// event_queue
|
||||
// quote_lot_size
|
||||
// base_lot_size
|
||||
|
||||
if let Some(maint_asset_weight) = maint_asset_weight_opt {
|
||||
perp_market.maint_asset_weight = I80F48::from_num(maint_asset_weight);
|
||||
}
|
||||
if let Some(init_asset_weight) = init_asset_weight_opt {
|
||||
perp_market.init_asset_weight = I80F48::from_num(init_asset_weight);
|
||||
}
|
||||
if let Some(maint_liab_weight) = maint_liab_weight_opt {
|
||||
perp_market.maint_liab_weight = I80F48::from_num(maint_liab_weight);
|
||||
}
|
||||
if let Some(init_liab_weight) = init_liab_weight_opt {
|
||||
perp_market.init_liab_weight = I80F48::from_num(init_liab_weight);
|
||||
}
|
||||
if let Some(liquidation_fee) = liquidation_fee_opt {
|
||||
perp_market.liquidation_fee = I80F48::from_num(liquidation_fee);
|
||||
}
|
||||
|
||||
if let Some(maker_fee) = maker_fee_opt {
|
||||
perp_market.maker_fee = I80F48::from_num(maker_fee);
|
||||
}
|
||||
if let Some(taker_fee) = taker_fee_opt {
|
||||
perp_market.taker_fee = I80F48::from_num(taker_fee);
|
||||
}
|
||||
|
||||
if let Some(min_funding) = min_funding_opt {
|
||||
perp_market.min_funding = I80F48::from_num(min_funding);
|
||||
}
|
||||
if let Some(max_funding) = max_funding_opt {
|
||||
perp_market.max_funding = I80F48::from_num(max_funding);
|
||||
}
|
||||
if let Some(impact_quantity) = impact_quantity_opt {
|
||||
perp_market.impact_quantity = impact_quantity;
|
||||
}
|
||||
|
||||
// unchanged -
|
||||
// long_funding
|
||||
// short_funding
|
||||
// funding_last_updated
|
||||
// open_interest
|
||||
// seq_num
|
||||
// fees_accrued
|
||||
// bump
|
||||
|
||||
if let Some(base_token_decimals) = base_token_decimals_opt {
|
||||
perp_market.base_token_decimals = base_token_decimals;
|
||||
}
|
||||
|
||||
// unchanged -
|
||||
// perp_market_index
|
||||
|
||||
if let Some(base_token_index) = base_token_index_opt {
|
||||
perp_market.base_token_index = base_token_index;
|
||||
}
|
||||
|
||||
// unchanged -
|
||||
// quote_token_index
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -14,9 +14,10 @@ pub struct PerpPlaceOrder<'info> {
|
|||
#[account(
|
||||
mut,
|
||||
has_one = group,
|
||||
has_one = owner,
|
||||
constraint = account.load()?.is_owner_or_delegate(owner.key()),
|
||||
)]
|
||||
pub account: AccountLoader<'info, MangoAccount>,
|
||||
pub owner: Signer<'info>,
|
||||
|
||||
#[account(
|
||||
mut,
|
||||
|
@ -36,8 +37,6 @@ pub struct PerpPlaceOrder<'info> {
|
|||
|
||||
/// CHECK: The oracle can be one of several different account types and the pubkey is checked above
|
||||
pub oracle: UncheckedAccount<'info>,
|
||||
|
||||
pub owner: Signer<'info>,
|
||||
}
|
||||
|
||||
// TODO
|
||||
|
@ -80,7 +79,7 @@ pub fn perp_place_order(
|
|||
limit: u8,
|
||||
) -> Result<()> {
|
||||
let mut mango_account = ctx.accounts.account.load_mut()?;
|
||||
require!(mango_account.is_bankrupt == 0, MangoError::IsBankrupt);
|
||||
require!(!mango_account.is_bankrupt(), MangoError::IsBankrupt);
|
||||
let mango_account_pk = ctx.accounts.account.key();
|
||||
|
||||
{
|
||||
|
|
|
@ -10,7 +10,7 @@ pub struct Serum3CancelAllOrders<'info> {
|
|||
#[account(
|
||||
mut,
|
||||
has_one = group,
|
||||
has_one = owner,
|
||||
constraint = account.load()?.is_owner_or_delegate(owner.key()),
|
||||
)]
|
||||
pub account: AccountLoader<'info, MangoAccount>,
|
||||
pub owner: Signer<'info>,
|
||||
|
@ -50,7 +50,7 @@ pub fn serum3_cancel_all_orders(ctx: Context<Serum3CancelAllOrders>, limit: u8)
|
|||
//
|
||||
{
|
||||
let account = ctx.accounts.account.load()?;
|
||||
require!(account.is_bankrupt == 0, MangoError::IsBankrupt);
|
||||
require!(!account.is_bankrupt(), MangoError::IsBankrupt);
|
||||
|
||||
let serum_market = ctx.accounts.serum_market.load()?;
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ pub struct Serum3CancelOrder<'info> {
|
|||
#[account(
|
||||
mut,
|
||||
has_one = group,
|
||||
has_one = owner,
|
||||
constraint = account.load()?.is_owner_or_delegate(owner.key()),
|
||||
)]
|
||||
pub account: AccountLoader<'info, MangoAccount>,
|
||||
pub owner: Signer<'info>,
|
||||
|
@ -63,7 +63,7 @@ pub fn serum3_cancel_order(
|
|||
//
|
||||
{
|
||||
let account = ctx.accounts.account.load()?;
|
||||
require!(account.is_bankrupt == 0, MangoError::IsBankrupt);
|
||||
require!(!account.is_bankrupt(), MangoError::IsBankrupt);
|
||||
|
||||
// Validate open_orders
|
||||
require!(
|
||||
|
|
|
@ -10,7 +10,7 @@ pub struct Serum3CloseOpenOrders<'info> {
|
|||
#[account(
|
||||
mut,
|
||||
has_one = group,
|
||||
has_one = owner,
|
||||
constraint = account.load()?.is_owner_or_delegate(owner.key()),
|
||||
)]
|
||||
pub account: AccountLoader<'info, MangoAccount>,
|
||||
pub owner: Signer<'info>,
|
||||
|
@ -41,7 +41,7 @@ pub fn serum3_close_open_orders(ctx: Context<Serum3CloseOpenOrders>) -> Result<(
|
|||
//
|
||||
let mut account = ctx.accounts.account.load_mut()?;
|
||||
let serum_market = ctx.accounts.serum_market.load()?;
|
||||
require!(account.is_bankrupt == 0, MangoError::IsBankrupt);
|
||||
require!(!account.is_bankrupt(), MangoError::IsBankrupt);
|
||||
// Validate open_orders
|
||||
require!(
|
||||
account
|
||||
|
|
|
@ -10,9 +10,10 @@ pub struct Serum3CreateOpenOrders<'info> {
|
|||
#[account(
|
||||
mut,
|
||||
has_one = group,
|
||||
has_one = owner,
|
||||
constraint = account.load()?.is_owner_or_delegate(owner.key()),
|
||||
)]
|
||||
pub account: AccountLoader<'info, MangoAccount>,
|
||||
pub owner: Signer<'info>,
|
||||
|
||||
#[account(
|
||||
has_one = group,
|
||||
|
@ -38,8 +39,6 @@ pub struct Serum3CreateOpenOrders<'info> {
|
|||
/// CHECK: Newly created by serum cpi call
|
||||
pub open_orders: UncheckedAccount<'info>,
|
||||
|
||||
pub owner: Signer<'info>,
|
||||
|
||||
#[account(mut)]
|
||||
pub payer: Signer<'info>,
|
||||
|
||||
|
@ -52,7 +51,7 @@ pub fn serum3_create_open_orders(ctx: Context<Serum3CreateOpenOrders>) -> Result
|
|||
|
||||
let serum_market = ctx.accounts.serum_market.load()?;
|
||||
let mut account = ctx.accounts.account.load_mut()?;
|
||||
require!(account.is_bankrupt == 0, MangoError::IsBankrupt);
|
||||
require!(!account.is_bankrupt(), MangoError::IsBankrupt);
|
||||
let serum_account = account.serum3.create(serum_market.market_index)?;
|
||||
serum_account.open_orders = ctx.accounts.open_orders.key();
|
||||
serum_account.base_token_index = serum_market.base_token_index;
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
use anchor_lang::prelude::*;
|
||||
use anchor_spl::token::{Token, TokenAccount};
|
||||
use fixed::types::I80F48;
|
||||
|
||||
use crate::error::*;
|
||||
use crate::instructions::apply_vault_difference;
|
||||
use crate::state::*;
|
||||
|
||||
#[derive(Accounts)]
|
||||
|
@ -73,7 +73,7 @@ pub fn serum3_liq_force_cancel_orders(
|
|||
//
|
||||
{
|
||||
let account = ctx.accounts.account.load()?;
|
||||
require!(account.is_bankrupt == 0, MangoError::IsBankrupt);
|
||||
require!(!account.is_bankrupt(), MangoError::IsBankrupt);
|
||||
let serum_market = ctx.accounts.serum_market.load()?;
|
||||
|
||||
// Validate open_orders
|
||||
|
@ -139,23 +139,19 @@ pub fn serum3_liq_force_cancel_orders(
|
|||
let after_quote_vault = ctx.accounts.quote_vault.amount;
|
||||
|
||||
// Charge the difference in vault balances to the user's account
|
||||
{
|
||||
let mut account = ctx.accounts.account.load_mut()?;
|
||||
|
||||
let mut base_bank = ctx.accounts.base_bank.load_mut()?;
|
||||
let base_position = account.tokens.get_mut(base_bank.token_index)?;
|
||||
base_bank.deposit(
|
||||
base_position,
|
||||
I80F48::from(after_base_vault) - I80F48::from(before_base_vault),
|
||||
)?;
|
||||
|
||||
let mut quote_bank = ctx.accounts.quote_bank.load_mut()?;
|
||||
let quote_position = account.tokens.get_mut(quote_bank.token_index)?;
|
||||
quote_bank.deposit(
|
||||
quote_position,
|
||||
I80F48::from(after_quote_vault) - I80F48::from(before_quote_vault),
|
||||
)?;
|
||||
}
|
||||
let mut account = ctx.accounts.account.load_mut()?;
|
||||
let mut base_bank = ctx.accounts.base_bank.load_mut()?;
|
||||
let mut quote_bank = ctx.accounts.quote_bank.load_mut()?;
|
||||
apply_vault_difference(
|
||||
&mut account,
|
||||
&mut base_bank,
|
||||
after_base_vault,
|
||||
before_base_vault,
|
||||
&mut quote_bank,
|
||||
after_quote_vault,
|
||||
before_quote_vault,
|
||||
)?
|
||||
.deactivate_inactive_token_accounts(&mut account);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -86,7 +86,7 @@ pub struct Serum3PlaceOrder<'info> {
|
|||
#[account(
|
||||
mut,
|
||||
has_one = group,
|
||||
has_one = owner,
|
||||
constraint = account.load()?.is_owner_or_delegate(owner.key()),
|
||||
)]
|
||||
pub account: AccountLoader<'info, MangoAccount>,
|
||||
pub owner: Signer<'info>,
|
||||
|
@ -168,7 +168,7 @@ pub fn serum3_place_order(
|
|||
//
|
||||
{
|
||||
let account = ctx.accounts.account.load()?;
|
||||
require!(account.is_bankrupt == 0, MangoError::IsBankrupt);
|
||||
require!(!account.is_bankrupt(), MangoError::IsBankrupt);
|
||||
|
||||
// Validate open_orders
|
||||
require!(
|
||||
|
@ -260,25 +260,31 @@ pub fn serum3_place_order(
|
|||
let after_quote_vault = ctx.accounts.quote_vault.amount;
|
||||
|
||||
// Charge the difference in vault balances to the user's account
|
||||
apply_vault_difference(
|
||||
ctx.accounts.account.load_mut()?,
|
||||
ctx.accounts.base_bank.load_mut()?,
|
||||
after_base_vault,
|
||||
before_base_vault,
|
||||
ctx.accounts.quote_bank.load_mut()?,
|
||||
after_quote_vault,
|
||||
before_quote_vault,
|
||||
)?;
|
||||
let mut account = ctx.accounts.account.load_mut()?;
|
||||
let vault_difference_result = {
|
||||
let mut base_bank = ctx.accounts.base_bank.load_mut()?;
|
||||
let mut quote_bank = ctx.accounts.quote_bank.load_mut()?;
|
||||
apply_vault_difference(
|
||||
&mut account,
|
||||
&mut base_bank,
|
||||
after_base_vault,
|
||||
before_base_vault,
|
||||
&mut quote_bank,
|
||||
after_quote_vault,
|
||||
before_quote_vault,
|
||||
)?
|
||||
};
|
||||
|
||||
//
|
||||
// Health check
|
||||
//
|
||||
let account = ctx.accounts.account.load()?;
|
||||
let retriever = new_fixed_order_account_retriever(ctx.remaining_accounts, &account)?;
|
||||
let health = compute_health(&account, HealthType::Init, &retriever)?;
|
||||
msg!("health: {}", health);
|
||||
require!(health >= 0, MangoError::HealthMustBePositive);
|
||||
|
||||
vault_difference_result.deactivate_inactive_token_accounts(&mut account);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -305,28 +311,51 @@ pub fn inc_maybe_loan(
|
|||
}
|
||||
}
|
||||
|
||||
pub struct VaultDifferenceResult {
|
||||
base_raw_index: usize,
|
||||
base_active: bool,
|
||||
quote_raw_index: usize,
|
||||
quote_active: bool,
|
||||
}
|
||||
|
||||
impl VaultDifferenceResult {
|
||||
pub fn deactivate_inactive_token_accounts(&self, account: &mut MangoAccount) {
|
||||
if !self.base_active {
|
||||
account.tokens.deactivate(self.base_raw_index);
|
||||
}
|
||||
if !self.quote_active {
|
||||
account.tokens.deactivate(self.quote_raw_index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_vault_difference(
|
||||
mut account: std::cell::RefMut<MangoAccount>,
|
||||
mut base_bank: std::cell::RefMut<Bank>,
|
||||
account: &mut MangoAccount,
|
||||
base_bank: &mut Bank,
|
||||
after_base_vault: u64,
|
||||
before_base_vault: u64,
|
||||
mut quote_bank: std::cell::RefMut<Bank>,
|
||||
quote_bank: &mut Bank,
|
||||
after_quote_vault: u64,
|
||||
before_quote_vault: u64,
|
||||
) -> Result<()> {
|
||||
) -> Result<VaultDifferenceResult> {
|
||||
// TODO: Applying the loan origination fee here may be too early: it should only be
|
||||
// charged if an order executes and the loan materializes? Otherwise MMs that place
|
||||
// an order without having the funds will be charged for each place_order!
|
||||
|
||||
let base_position = account.tokens.get_mut(base_bank.token_index)?;
|
||||
let (base_position, base_raw_index) = account.tokens.get_mut(base_bank.token_index)?;
|
||||
let base_change = I80F48::from(after_base_vault) - I80F48::from(before_base_vault);
|
||||
base_bank.change_with_fee(base_position, base_change)?;
|
||||
let base_active = base_bank.change_with_fee(base_position, base_change)?;
|
||||
|
||||
let quote_position = account.tokens.get_mut(quote_bank.token_index)?;
|
||||
let (quote_position, quote_raw_index) = account.tokens.get_mut(quote_bank.token_index)?;
|
||||
let quote_change = I80F48::from(after_quote_vault) - I80F48::from(before_quote_vault);
|
||||
quote_bank.change_with_fee(quote_position, quote_change)?;
|
||||
let quote_active = quote_bank.change_with_fee(quote_position, quote_change)?;
|
||||
|
||||
Ok(())
|
||||
Ok(VaultDifferenceResult {
|
||||
base_raw_index,
|
||||
base_active,
|
||||
quote_raw_index,
|
||||
quote_active,
|
||||
})
|
||||
}
|
||||
|
||||
fn cpi_place_order(ctx: &Serum3PlaceOrder, order: NewOrderInstructionV3) -> Result<()> {
|
||||
|
|
|
@ -74,6 +74,7 @@ pub fn serum3_register_market(
|
|||
base_token_index: base_bank.token_index,
|
||||
quote_token_index: quote_bank.token_index,
|
||||
bump: *ctx.bumps.get("serum_market").ok_or(MangoError::SomeError)?,
|
||||
padding: Default::default(),
|
||||
reserved: Default::default(),
|
||||
};
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ pub struct Serum3SettleFunds<'info> {
|
|||
#[account(
|
||||
mut,
|
||||
has_one = group,
|
||||
has_one = owner,
|
||||
constraint = account.load()?.is_owner_or_delegate(owner.key()),
|
||||
)]
|
||||
pub account: AccountLoader<'info, MangoAccount>,
|
||||
pub owner: Signer<'info>,
|
||||
|
@ -77,7 +77,7 @@ pub fn serum3_settle_funds(ctx: Context<Serum3SettleFunds>) -> Result<()> {
|
|||
//
|
||||
{
|
||||
let account = ctx.accounts.account.load()?;
|
||||
require!(account.is_bankrupt == 0, MangoError::IsBankrupt);
|
||||
require!(!account.is_bankrupt(), MangoError::IsBankrupt);
|
||||
|
||||
// Validate open_orders
|
||||
require!(
|
||||
|
@ -149,17 +149,19 @@ pub fn serum3_settle_funds(ctx: Context<Serum3SettleFunds>) -> Result<()> {
|
|||
let after_quote_vault = ctx.accounts.quote_vault.amount;
|
||||
|
||||
// Charge the difference in vault balances to the user's account
|
||||
let base_bank = ctx.accounts.base_bank.load_mut()?;
|
||||
let quote_bank = ctx.accounts.quote_bank.load_mut()?;
|
||||
let mut account = ctx.accounts.account.load_mut()?;
|
||||
let mut base_bank = ctx.accounts.base_bank.load_mut()?;
|
||||
let mut quote_bank = ctx.accounts.quote_bank.load_mut()?;
|
||||
apply_vault_difference(
|
||||
ctx.accounts.account.load_mut()?,
|
||||
base_bank,
|
||||
&mut account,
|
||||
&mut base_bank,
|
||||
after_base_vault,
|
||||
before_base_vault,
|
||||
quote_bank,
|
||||
&mut quote_bank,
|
||||
after_quote_vault,
|
||||
before_quote_vault,
|
||||
)?;
|
||||
)?
|
||||
.deactivate_inactive_token_accounts(&mut account);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
@ -185,7 +187,7 @@ pub fn charge_maybe_fees(
|
|||
serum3_account.previous_native_coin_reserved = after_oo.native_coin_reserved();
|
||||
|
||||
// loan origination fees
|
||||
let coin_token_account = account.tokens.get_mut(coin_bank.token_index)?;
|
||||
let coin_token_account = account.tokens.get_mut(coin_bank.token_index)?.0;
|
||||
let coin_token_native = coin_token_account.native(&coin_bank);
|
||||
|
||||
if coin_token_native.is_negative() {
|
||||
|
@ -195,7 +197,7 @@ pub fn charge_maybe_fees(
|
|||
// charge the loan origination fee
|
||||
coin_bank
|
||||
.borrow_mut()
|
||||
.charge_loan_origination_fee(coin_token_account, actualized_loan)?;
|
||||
.withdraw_loan_origination_fee(coin_token_account, actualized_loan)?;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -209,7 +211,7 @@ pub fn charge_maybe_fees(
|
|||
serum3_account.previous_native_pc_reserved = after_oo.native_pc_reserved();
|
||||
|
||||
// loan origination fees
|
||||
let pc_token_account = account.tokens.get_mut(pc_bank.token_index)?;
|
||||
let pc_token_account = account.tokens.get_mut(pc_bank.token_index)?.0;
|
||||
let pc_token_native = pc_token_account.native(&pc_bank);
|
||||
|
||||
if pc_token_native.is_negative() {
|
||||
|
@ -219,7 +221,7 @@ pub fn charge_maybe_fees(
|
|||
// charge the loan origination fee
|
||||
pc_bank
|
||||
.borrow_mut()
|
||||
.charge_loan_origination_fee(pc_token_account, actualized_loan)?;
|
||||
.withdraw_loan_origination_fee(pc_token_account, actualized_loan)?;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ use anchor_spl::token::Token;
|
|||
use crate::state::*;
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct CloseStubOracle<'info> {
|
||||
pub struct StubOracleClose<'info> {
|
||||
#[account(
|
||||
constraint = group.load()?.testing == 1,
|
||||
has_one = admin,
|
||||
|
@ -27,6 +27,6 @@ pub struct CloseStubOracle<'info> {
|
|||
pub token_program: Program<'info, Token>,
|
||||
}
|
||||
|
||||
pub fn close_stub_oracle(_ctx: Context<CloseStubOracle>) -> Result<()> {
|
||||
pub fn stub_oracle_close(_ctx: Context<StubOracleClose>) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
|
@ -5,7 +5,7 @@ use fixed::types::I80F48;
|
|||
use crate::state::*;
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct CreateStubOracle<'info> {
|
||||
pub struct StubOracleCreate<'info> {
|
||||
#[account(
|
||||
has_one = admin,
|
||||
)]
|
||||
|
@ -30,7 +30,7 @@ pub struct CreateStubOracle<'info> {
|
|||
pub system_program: Program<'info, System>,
|
||||
}
|
||||
|
||||
pub fn create_stub_oracle(ctx: Context<CreateStubOracle>, price: I80F48) -> Result<()> {
|
||||
pub fn stub_oracle_create(ctx: Context<StubOracleCreate>, price: I80F48) -> Result<()> {
|
||||
let mut oracle = ctx.accounts.oracle.load_init()?;
|
||||
oracle.group = ctx.accounts.group.key();
|
||||
oracle.mint = ctx.accounts.token_mint.key();
|
|
@ -4,7 +4,7 @@ use fixed::types::I80F48;
|
|||
use crate::state::*;
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct SetStubOracle<'info> {
|
||||
pub struct StubOracleSet<'info> {
|
||||
#[account(
|
||||
has_one = admin,
|
||||
)]
|
||||
|
@ -22,8 +22,7 @@ pub struct SetStubOracle<'info> {
|
|||
pub payer: Signer<'info>,
|
||||
}
|
||||
|
||||
// TODO: add admin requirement for changing price
|
||||
pub fn set_stub_oracle(ctx: Context<SetStubOracle>, price: I80F48) -> Result<()> {
|
||||
pub fn stub_oracle_set(ctx: Context<StubOracleSet>, price: I80F48) -> Result<()> {
|
||||
let mut oracle = ctx.accounts.oracle.load_mut()?;
|
||||
oracle.price = price;
|
||||
oracle.last_updated = Clock::get()?.unix_timestamp;
|
|
@ -6,6 +6,7 @@ use fixed::types::I80F48;
|
|||
|
||||
use crate::error::*;
|
||||
use crate::state::*;
|
||||
use crate::util::checked_math as cm;
|
||||
|
||||
use crate::logs::{DepositLog, TokenBalanceLog};
|
||||
|
||||
|
@ -54,20 +55,21 @@ impl<'info> TokenDeposit<'info> {
|
|||
// That would save a lot of computation that needs to go into finding the
|
||||
// right index for the mint.
|
||||
pub fn token_deposit(ctx: Context<TokenDeposit>, amount: u64) -> Result<()> {
|
||||
require!(amount > 0, MangoError::SomeError);
|
||||
require_msg!(amount > 0, "deposit amount must be positive");
|
||||
|
||||
let token_index = ctx.accounts.bank.load()?.token_index;
|
||||
|
||||
// Get the account's position for that token index
|
||||
let mut account = ctx.accounts.account.load_mut()?;
|
||||
require!(account.is_bankrupt == 0, MangoError::IsBankrupt);
|
||||
require!(!account.is_bankrupt(), MangoError::IsBankrupt);
|
||||
|
||||
let (position, raw_token_index, active_token_index) =
|
||||
account.tokens.get_mut_or_create(token_index)?;
|
||||
|
||||
let amount_i80f48 = I80F48::from(amount);
|
||||
let position_is_active = {
|
||||
let mut bank = ctx.accounts.bank.load_mut()?;
|
||||
bank.deposit(position, I80F48::from(amount))?
|
||||
bank.deposit(position, amount_i80f48)?
|
||||
};
|
||||
|
||||
// Transfer the actual tokens
|
||||
|
@ -78,6 +80,10 @@ pub fn token_deposit(ctx: Context<TokenDeposit>, amount: u64) -> Result<()> {
|
|||
let retriever = new_fixed_order_account_retriever(ctx.remaining_accounts, &account)?;
|
||||
let (bank, oracle_price) =
|
||||
retriever.bank_and_oracle(&ctx.accounts.group.key(), active_token_index, token_index)?;
|
||||
|
||||
// Update the net deposits - adjust by price so different tokens are on the same basis (in USD terms)
|
||||
account.net_deposits += cm!(amount_i80f48 * oracle_price * QUOTE_NATIVE_TO_UI).to_num::<f32>();
|
||||
|
||||
emit!(TokenBalanceLog {
|
||||
mango_account: ctx.accounts.account.key(),
|
||||
token_index: token_index,
|
||||
|
@ -92,7 +98,8 @@ pub fn token_deposit(ctx: Context<TokenDeposit>, amount: u64) -> Result<()> {
|
|||
// TODO: This will be used to disable is_bankrupt or being_liquidated
|
||||
// when health recovers sufficiently
|
||||
//
|
||||
let health = compute_health(&account, HealthType::Init, &retriever)?;
|
||||
let health = compute_health(&account, HealthType::Init, &retriever)
|
||||
.context("post-deposit init health")?;
|
||||
msg!("health: {}", health);
|
||||
|
||||
//
|
||||
|
|
|
@ -40,11 +40,7 @@ pub fn token_deregister<'key, 'accounts, 'remaining, 'info>(
|
|||
) -> Result<()> {
|
||||
let mint_info = ctx.accounts.mint_info.load()?;
|
||||
{
|
||||
let total_banks = mint_info
|
||||
.banks
|
||||
.iter()
|
||||
.filter(|bank| *bank != &Pubkey::default())
|
||||
.count();
|
||||
let total_banks = mint_info.num_banks();
|
||||
require_eq!(total_banks * 2, ctx.remaining_accounts.len());
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,122 @@
|
|||
use anchor_lang::prelude::*;
|
||||
|
||||
use fixed::types::I80F48;
|
||||
|
||||
use super::InterestRateParams;
|
||||
use crate::accounts_zerocopy::LoadMutZeroCopyRef;
|
||||
|
||||
use crate::state::*;
|
||||
|
||||
#[derive(Accounts)]
|
||||
#[instruction(token_index: TokenIndex, bank_num: u64)]
|
||||
pub struct TokenEdit<'info> {
|
||||
#[account(
|
||||
has_one = admin,
|
||||
)]
|
||||
pub group: AccountLoader<'info, Group>,
|
||||
pub admin: Signer<'info>,
|
||||
|
||||
#[account(
|
||||
has_one = group
|
||||
)]
|
||||
pub mint_info: AccountLoader<'info, MintInfo>,
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn token_edit(
|
||||
ctx: Context<TokenEdit>,
|
||||
bank_num: u64,
|
||||
oracle_opt: Option<Pubkey>,
|
||||
oracle_config_opt: Option<OracleConfig>,
|
||||
interest_rate_params_opt: Option<InterestRateParams>,
|
||||
loan_fee_rate_opt: Option<f32>,
|
||||
loan_origination_fee_rate_opt: Option<f32>,
|
||||
maint_asset_weight_opt: Option<f32>,
|
||||
init_asset_weight_opt: Option<f32>,
|
||||
maint_liab_weight_opt: Option<f32>,
|
||||
init_liab_weight_opt: Option<f32>,
|
||||
liquidation_fee_opt: Option<f32>,
|
||||
) -> Result<()> {
|
||||
ctx.accounts
|
||||
.mint_info
|
||||
.load()?
|
||||
.verify_banks_ais(ctx.remaining_accounts)?;
|
||||
|
||||
for ai in ctx.remaining_accounts.iter() {
|
||||
let mut bank = ai.load_mut::<Bank>()?;
|
||||
|
||||
// note: unchanged fields are inline, and match exact definition in register_token
|
||||
// please maintain, and don't remove, makes it easy to reason about which support admin modification
|
||||
|
||||
// unchanged -
|
||||
// name
|
||||
// group
|
||||
// mint
|
||||
// vault
|
||||
|
||||
if let Some(oracle) = oracle_opt {
|
||||
bank.oracle = oracle;
|
||||
}
|
||||
if let Some(oracle_config) = oracle_config_opt {
|
||||
bank.oracle_config = oracle_config;
|
||||
};
|
||||
|
||||
// unchanged -
|
||||
// deposit_index
|
||||
// borrow_index
|
||||
// cached_indexed_total_deposits
|
||||
// cached_indexed_total_borrows
|
||||
// indexed_deposits
|
||||
// indexed_borrows
|
||||
// last_updated
|
||||
|
||||
if let Some(ref interest_rate_params) = interest_rate_params_opt {
|
||||
// TODO: add a require! verifying relation between the parameters
|
||||
bank.adjustment_factor = I80F48::from_num(interest_rate_params.adjustment_factor);
|
||||
bank.util0 = I80F48::from_num(interest_rate_params.util0);
|
||||
bank.rate0 = I80F48::from_num(interest_rate_params.rate0);
|
||||
bank.util1 = I80F48::from_num(interest_rate_params.util1);
|
||||
bank.rate1 = I80F48::from_num(interest_rate_params.rate1);
|
||||
bank.max_rate = I80F48::from_num(interest_rate_params.max_rate);
|
||||
}
|
||||
|
||||
// unchanged -
|
||||
// collected_fees_native
|
||||
|
||||
if let Some(loan_origination_fee_rate) = loan_origination_fee_rate_opt {
|
||||
bank.loan_origination_fee_rate = I80F48::from_num(loan_origination_fee_rate);
|
||||
}
|
||||
if let Some(loan_fee_rate) = loan_fee_rate_opt {
|
||||
bank.loan_fee_rate = I80F48::from_num(loan_fee_rate);
|
||||
}
|
||||
|
||||
if let Some(maint_asset_weight) = maint_asset_weight_opt {
|
||||
bank.maint_asset_weight = I80F48::from_num(maint_asset_weight);
|
||||
}
|
||||
if let Some(init_asset_weight) = init_asset_weight_opt {
|
||||
bank.init_asset_weight = I80F48::from_num(init_asset_weight);
|
||||
}
|
||||
if let Some(maint_liab_weight) = maint_liab_weight_opt {
|
||||
bank.maint_liab_weight = I80F48::from_num(maint_liab_weight);
|
||||
}
|
||||
if let Some(init_liab_weight) = init_liab_weight_opt {
|
||||
bank.init_liab_weight = I80F48::from_num(init_liab_weight);
|
||||
}
|
||||
if let Some(liquidation_fee) = liquidation_fee_opt {
|
||||
bank.liquidation_fee = I80F48::from_num(liquidation_fee);
|
||||
}
|
||||
|
||||
// unchanged -
|
||||
// dust
|
||||
// flash_loan_vault_initial
|
||||
// flash_loan_approved_amount
|
||||
// token_index
|
||||
// bump
|
||||
// mint_decimals
|
||||
// bank_num
|
||||
// reserved
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -82,6 +82,7 @@ pub struct InterestRateParams {
|
|||
pub util1: f32,
|
||||
pub rate1: f32,
|
||||
pub max_rate: f32,
|
||||
pub adjustment_factor: f32,
|
||||
}
|
||||
|
||||
// TODO: should this be "configure_mint", we pass an explicit index, and allow
|
||||
|
@ -106,6 +107,14 @@ pub fn token_register(
|
|||
|
||||
require_eq!(bank_num, 0);
|
||||
|
||||
// Require token 0 to be in the insurance token
|
||||
if token_index == QUOTE_TOKEN_INDEX {
|
||||
require_keys_eq!(
|
||||
ctx.accounts.group.load()?.insurance_mint,
|
||||
ctx.accounts.mint.key()
|
||||
);
|
||||
}
|
||||
|
||||
let mut bank = ctx.accounts.bank.load_init()?;
|
||||
*bank = Bank {
|
||||
name: fill16_from_str(name)?,
|
||||
|
@ -120,8 +129,11 @@ pub fn token_register(
|
|||
cached_indexed_total_borrows: I80F48::ZERO,
|
||||
indexed_deposits: I80F48::ZERO,
|
||||
indexed_borrows: I80F48::ZERO,
|
||||
last_updated: Clock::get()?.unix_timestamp,
|
||||
index_last_updated: Clock::get()?.unix_timestamp,
|
||||
bank_rate_last_updated: Clock::get()?.unix_timestamp,
|
||||
// TODO: add a require! verifying relation between the parameters
|
||||
avg_utilization: I80F48::ZERO,
|
||||
adjustment_factor: I80F48::from_num(interest_rate_params.adjustment_factor),
|
||||
util0: I80F48::from_num(interest_rate_params.util0),
|
||||
rate0: I80F48::from_num(interest_rate_params.rate0),
|
||||
util1: I80F48::from_num(interest_rate_params.util1),
|
||||
|
@ -162,6 +174,7 @@ pub fn token_register(
|
|||
token_index,
|
||||
address_lookup_table_bank_index: alt_previous_size as u8,
|
||||
address_lookup_table_oracle_index: alt_previous_size as u8 + 1,
|
||||
padding: Default::default(),
|
||||
reserved: Default::default(),
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,163 @@
|
|||
use anchor_lang::prelude::*;
|
||||
|
||||
use crate::error::MangoError;
|
||||
use crate::logs::{UpdateIndexLog, UpdateRateLog};
|
||||
use crate::state::HOUR;
|
||||
use crate::{
|
||||
accounts_zerocopy::{AccountInfoRef, LoadMutZeroCopyRef, LoadZeroCopyRef},
|
||||
state::{oracle_price, Bank, MintInfo},
|
||||
};
|
||||
use anchor_lang::solana_program::sysvar::instructions as tx_instructions;
|
||||
use checked_math as cm;
|
||||
use fixed::types::I80F48;
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct TokenUpdateIndexAndRate<'info> {
|
||||
#[account(
|
||||
has_one = oracle
|
||||
)]
|
||||
pub mint_info: AccountLoader<'info, MintInfo>,
|
||||
|
||||
pub oracle: UncheckedAccount<'info>,
|
||||
|
||||
#[account(address = tx_instructions::ID)]
|
||||
pub instructions: UncheckedAccount<'info>,
|
||||
}
|
||||
|
||||
pub fn token_update_index_and_rate(ctx: Context<TokenUpdateIndexAndRate>) -> Result<()> {
|
||||
{
|
||||
let ixs = ctx.accounts.instructions.as_ref();
|
||||
|
||||
let mut index = 0;
|
||||
loop {
|
||||
let ix = match tx_instructions::load_instruction_at_checked(index, ixs) {
|
||||
Ok(ix) => ix,
|
||||
Err(ProgramError::InvalidArgument) => break,
|
||||
Err(e) => return Err(e.into()),
|
||||
};
|
||||
|
||||
// 1. we want to forbid token deposit and token withdraw and similar
|
||||
// (serum3 place order could be used as a withdraw and a serum3 cancel order as a deposit)
|
||||
// to be called in same tx as this ix to prevent index or rate manipulation,
|
||||
// for now we just whitelist to other token_update_index_and_rate ix
|
||||
// 2. we want to forbid cpi, since ix we would like to blacklist could just be called from cpi
|
||||
require!(
|
||||
ix.program_id == crate::id()
|
||||
&& ix.data[0..8] == [131, 136, 194, 39, 11, 50, 10, 198], // token_update_index_and_rate
|
||||
MangoError::SomeError
|
||||
);
|
||||
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
let mint_info = ctx.accounts.mint_info.load()?;
|
||||
|
||||
ctx.accounts
|
||||
.mint_info
|
||||
.load()?
|
||||
.verify_banks_ais(ctx.remaining_accounts)?;
|
||||
|
||||
let now_ts = Clock::get()?.unix_timestamp;
|
||||
|
||||
// compute indexed_total
|
||||
let mut indexed_total_deposits = I80F48::ZERO;
|
||||
let mut indexed_total_borrows = I80F48::ZERO;
|
||||
for ai in ctx.remaining_accounts.iter() {
|
||||
let bank = ai.load::<Bank>()?;
|
||||
indexed_total_deposits = cm!(indexed_total_deposits + bank.indexed_deposits);
|
||||
indexed_total_borrows = cm!(indexed_total_borrows + bank.indexed_borrows);
|
||||
}
|
||||
|
||||
// compute and set latest index and average utilization on each bank
|
||||
{
|
||||
let some_bank = ctx.remaining_accounts[0].load::<Bank>()?;
|
||||
|
||||
let now_ts_i80f48 = I80F48::from_num(now_ts);
|
||||
let diff_ts = I80F48::from_num(now_ts - some_bank.index_last_updated);
|
||||
|
||||
let (deposit_index, borrow_index) =
|
||||
some_bank.compute_index(indexed_total_deposits, indexed_total_borrows, diff_ts)?;
|
||||
|
||||
let new_avg_utilization = some_bank.compute_new_avg_utilization(
|
||||
indexed_total_deposits,
|
||||
indexed_total_borrows,
|
||||
now_ts_i80f48,
|
||||
);
|
||||
|
||||
let price = oracle_price(
|
||||
&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?,
|
||||
some_bank.oracle_config.conf_filter,
|
||||
some_bank.mint_decimals,
|
||||
)?;
|
||||
emit!(UpdateIndexLog {
|
||||
mango_group: mint_info.group.key(),
|
||||
token_index: mint_info.token_index,
|
||||
deposit_index: deposit_index.to_bits(),
|
||||
borrow_index: borrow_index.to_bits(),
|
||||
avg_utilization: new_avg_utilization.to_bits(),
|
||||
price: price.to_bits()
|
||||
});
|
||||
|
||||
drop(some_bank);
|
||||
|
||||
msg!("indexed_total_deposits {}", indexed_total_deposits);
|
||||
msg!("indexed_total_borrows {}", indexed_total_borrows);
|
||||
msg!("diff_ts {}", diff_ts);
|
||||
msg!("deposit_index {}", deposit_index);
|
||||
msg!("borrow_index {}", borrow_index);
|
||||
msg!("avg_utilization {}", new_avg_utilization);
|
||||
|
||||
for ai in ctx.remaining_accounts.iter() {
|
||||
let mut bank = ai.load_mut::<Bank>()?;
|
||||
|
||||
bank.cached_indexed_total_deposits = indexed_total_deposits;
|
||||
bank.cached_indexed_total_borrows = indexed_total_borrows;
|
||||
|
||||
bank.index_last_updated = now_ts;
|
||||
bank.charge_loan_fee(diff_ts);
|
||||
|
||||
bank.deposit_index = deposit_index;
|
||||
bank.borrow_index = borrow_index;
|
||||
|
||||
bank.avg_utilization = new_avg_utilization;
|
||||
}
|
||||
}
|
||||
|
||||
// compute optimal rates, and max rate and set them on the bank
|
||||
{
|
||||
let some_bank = ctx.remaining_accounts[0].load::<Bank>()?;
|
||||
|
||||
let diff_ts = I80F48::from_num(now_ts - some_bank.bank_rate_last_updated);
|
||||
|
||||
// update each hour
|
||||
if diff_ts > HOUR {
|
||||
let (rate0, rate1, max_rate) = some_bank.compute_rates();
|
||||
|
||||
emit!(UpdateRateLog {
|
||||
mango_group: mint_info.group.key(),
|
||||
token_index: mint_info.token_index,
|
||||
rate0: rate0.to_bits(),
|
||||
rate1: rate1.to_bits(),
|
||||
max_rate: max_rate.to_bits(),
|
||||
});
|
||||
|
||||
drop(some_bank);
|
||||
|
||||
msg!("rate0 {}", rate0);
|
||||
msg!("rate1 {}", rate1);
|
||||
msg!("max_rate {}", max_rate);
|
||||
|
||||
for ai in ctx.remaining_accounts.iter() {
|
||||
let mut bank = ai.load_mut::<Bank>()?;
|
||||
|
||||
bank.bank_rate_last_updated = now_ts;
|
||||
bank.rate0 = rate0;
|
||||
bank.rate1 = rate1;
|
||||
bank.max_rate = max_rate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -8,6 +8,7 @@ use fixed::types::I80F48;
|
|||
|
||||
use crate::logs::{TokenBalanceLog, WithdrawLog};
|
||||
use crate::state::new_fixed_order_account_retriever;
|
||||
use crate::util::checked_math as cm;
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct TokenWithdraw<'info> {
|
||||
|
@ -16,6 +17,7 @@ pub struct TokenWithdraw<'info> {
|
|||
#[account(
|
||||
mut,
|
||||
has_one = group,
|
||||
// note: should never be the delegate
|
||||
has_one = owner,
|
||||
)]
|
||||
pub account: AccountLoader<'info, MangoAccount>,
|
||||
|
@ -56,21 +58,21 @@ impl<'info> TokenWithdraw<'info> {
|
|||
// right index for the mint.
|
||||
// TODO: https://github.com/blockworks-foundation/mango-v4/commit/15961ec81c7e9324b37d79d0e2a1650ce6bd981d comments
|
||||
pub fn token_withdraw(ctx: Context<TokenWithdraw>, amount: u64, allow_borrow: bool) -> Result<()> {
|
||||
require!(amount > 0, MangoError::SomeError);
|
||||
require_msg!(amount > 0, "withdraw amount must be positive");
|
||||
|
||||
let group = ctx.accounts.group.load()?;
|
||||
let token_index = ctx.accounts.bank.load()?.token_index;
|
||||
|
||||
// Get the account's position for that token index
|
||||
let mut account = ctx.accounts.account.load_mut()?;
|
||||
require!(account.is_bankrupt == 0, MangoError::IsBankrupt);
|
||||
require!(!account.is_bankrupt(), MangoError::IsBankrupt);
|
||||
|
||||
let (position, raw_token_index, active_token_index) =
|
||||
account.tokens.get_mut_or_create(token_index)?;
|
||||
|
||||
// The bank will also be passed in remainingAccounts. Use an explicit scope
|
||||
// to drop the &mut before we borrow it immutably again later.
|
||||
let position_is_active = {
|
||||
let (position_is_active, amount_i80f48) = {
|
||||
let mut bank = ctx.accounts.bank.load_mut()?;
|
||||
let native_position = position.native(&bank);
|
||||
|
||||
|
@ -104,7 +106,7 @@ pub fn token_withdraw(ctx: Context<TokenWithdraw>, amount: u64, allow_borrow: bo
|
|||
amount,
|
||||
)?;
|
||||
|
||||
position_is_active
|
||||
(position_is_active, amount_i80f48)
|
||||
};
|
||||
|
||||
let indexed_position = position.indexed_position;
|
||||
|
@ -112,6 +114,10 @@ pub fn token_withdraw(ctx: Context<TokenWithdraw>, amount: u64, allow_borrow: bo
|
|||
let retriever = new_fixed_order_account_retriever(ctx.remaining_accounts, &account)?;
|
||||
let (bank, oracle_price) =
|
||||
retriever.bank_and_oracle(&ctx.accounts.group.key(), active_token_index, token_index)?;
|
||||
|
||||
// Update the net deposits - adjust by price so different tokens are on the same basis (in USD terms)
|
||||
account.net_deposits -= cm!(amount_i80f48 * oracle_price * QUOTE_NATIVE_TO_UI).to_num::<f32>();
|
||||
|
||||
emit!(TokenBalanceLog {
|
||||
mango_account: ctx.accounts.account.key(),
|
||||
token_index: token_index,
|
||||
|
@ -124,7 +130,8 @@ pub fn token_withdraw(ctx: Context<TokenWithdraw>, amount: u64, allow_borrow: bo
|
|||
//
|
||||
// Health check
|
||||
//
|
||||
let health = compute_health(&account, HealthType::Init, &retriever)?;
|
||||
let health = compute_health(&account, HealthType::Init, &retriever)
|
||||
.context("post-withdraw init health")?;
|
||||
msg!("health: {}", health);
|
||||
require!(health >= 0, MangoError::HealthMustBePositive);
|
||||
|
||||
|
|
|
@ -1,98 +0,0 @@
|
|||
use anchor_lang::prelude::*;
|
||||
|
||||
use crate::logs::UpdateIndexLog;
|
||||
use crate::{
|
||||
accounts_zerocopy::{LoadMutZeroCopyRef, LoadZeroCopyRef},
|
||||
error::MangoError,
|
||||
state::{Bank, MintInfo},
|
||||
};
|
||||
use checked_math as cm;
|
||||
use fixed::types::I80F48;
|
||||
#[derive(Accounts)]
|
||||
pub struct UpdateIndex<'info> {
|
||||
pub mint_info: AccountLoader<'info, MintInfo>,
|
||||
}
|
||||
|
||||
pub fn update_index(ctx: Context<UpdateIndex>) -> Result<()> {
|
||||
let mint_info = ctx.accounts.mint_info.load()?;
|
||||
|
||||
let total_banks = mint_info
|
||||
.banks
|
||||
.iter()
|
||||
.filter(|bank| *bank != &Pubkey::default())
|
||||
.count();
|
||||
|
||||
require_eq!(total_banks, ctx.remaining_accounts.len());
|
||||
let all_banks = ctx.remaining_accounts;
|
||||
check_banks(all_banks, &mint_info)?;
|
||||
|
||||
let mut indexed_total_deposits = I80F48::ZERO;
|
||||
let mut indexed_total_borrows = I80F48::ZERO;
|
||||
for ai in all_banks.iter() {
|
||||
let bank = ai.load::<Bank>()?;
|
||||
indexed_total_deposits = cm!(indexed_total_deposits + bank.indexed_deposits);
|
||||
indexed_total_borrows = cm!(indexed_total_borrows + bank.indexed_borrows);
|
||||
}
|
||||
|
||||
let now_ts = Clock::get()?.unix_timestamp;
|
||||
let (diff_ts, deposit_index, borrow_index) = {
|
||||
let mut some_bank = all_banks[0].load_mut::<Bank>()?;
|
||||
|
||||
// TODO: should we enforce a minimum window between 2 update_index ix calls?
|
||||
let diff_ts = I80F48::from_num(now_ts - some_bank.last_updated);
|
||||
|
||||
let (deposit_index, borrow_index) =
|
||||
some_bank.compute_index(indexed_total_deposits, indexed_total_borrows, diff_ts)?;
|
||||
|
||||
(diff_ts, deposit_index, borrow_index)
|
||||
};
|
||||
|
||||
msg!("indexed_total_deposits {}", indexed_total_deposits);
|
||||
msg!("indexed_total_borrows {}", indexed_total_borrows);
|
||||
msg!("diff_ts {}", diff_ts);
|
||||
msg!("deposit_index {}", deposit_index);
|
||||
msg!("borrow_index {}", borrow_index);
|
||||
|
||||
for ai in all_banks.iter() {
|
||||
let mut bank = ai.load_mut::<Bank>()?;
|
||||
|
||||
bank.cached_indexed_total_deposits = indexed_total_deposits;
|
||||
bank.cached_indexed_total_borrows = indexed_total_borrows;
|
||||
|
||||
bank.last_updated = now_ts;
|
||||
bank.charge_loan_fee(diff_ts);
|
||||
|
||||
bank.deposit_index = deposit_index;
|
||||
bank.borrow_index = borrow_index;
|
||||
|
||||
// clarkeni TODO: add prices
|
||||
emit!(UpdateIndexLog {
|
||||
mango_group: bank.group.key(),
|
||||
token_index: bank.token_index,
|
||||
deposit_index: bank.deposit_index.to_bits(),
|
||||
borrow_index: bank.borrow_index.to_bits(),
|
||||
// price: oracle_price.to_bits(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_banks(all_banks: &[AccountInfo], mint_info: &MintInfo) -> Result<()> {
|
||||
for (idx, ai) in all_banks.iter().enumerate() {
|
||||
match ai.load::<Bank>() {
|
||||
Ok(bank) => {
|
||||
if mint_info.token_index != bank.token_index
|
||||
|| mint_info.group != bank.group
|
||||
// todo: just below check should be enough, above 2 checks are superfluous and defensive
|
||||
|| mint_info.banks[idx] != ai.key()
|
||||
{
|
||||
return Err(error!(MangoError::SomeError));
|
||||
}
|
||||
}
|
||||
Err(error) => return Err(error),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -30,12 +30,12 @@ pub mod mango_v4 {
|
|||
|
||||
use super::*;
|
||||
|
||||
pub fn create_group(ctx: Context<CreateGroup>, group_num: u32, testing: u8) -> Result<()> {
|
||||
instructions::create_group(ctx, group_num, testing)
|
||||
pub fn group_create(ctx: Context<GroupCreate>, group_num: u32, testing: u8) -> Result<()> {
|
||||
instructions::group_create(ctx, group_num, testing)
|
||||
}
|
||||
|
||||
pub fn close_group(ctx: Context<CloseGroup>) -> Result<()> {
|
||||
instructions::close_group(ctx)
|
||||
pub fn group_close(ctx: Context<GroupClose>) -> Result<()> {
|
||||
instructions::group_close(ctx)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
|
@ -71,6 +71,37 @@ pub mod mango_v4 {
|
|||
)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn token_edit(
|
||||
ctx: Context<TokenEdit>,
|
||||
bank_num: u64,
|
||||
oracle_opt: Option<Pubkey>,
|
||||
oracle_config_opt: Option<OracleConfig>,
|
||||
interest_rate_params_opt: Option<InterestRateParams>,
|
||||
loan_fee_rate_opt: Option<f32>,
|
||||
loan_origination_fee_rate_opt: Option<f32>,
|
||||
maint_asset_weight_opt: Option<f32>,
|
||||
init_asset_weight_opt: Option<f32>,
|
||||
maint_liab_weight_opt: Option<f32>,
|
||||
init_liab_weight_opt: Option<f32>,
|
||||
liquidation_fee_opt: Option<f32>,
|
||||
) -> Result<()> {
|
||||
instructions::token_edit(
|
||||
ctx,
|
||||
bank_num,
|
||||
oracle_opt,
|
||||
oracle_config_opt,
|
||||
interest_rate_params_opt,
|
||||
loan_fee_rate_opt,
|
||||
loan_origination_fee_rate_opt,
|
||||
maint_asset_weight_opt,
|
||||
init_asset_weight_opt,
|
||||
maint_liab_weight_opt,
|
||||
init_liab_weight_opt,
|
||||
liquidation_fee_opt,
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn token_add_bank(
|
||||
ctx: Context<TokenAddBank>,
|
||||
|
@ -87,22 +118,28 @@ pub mod mango_v4 {
|
|||
instructions::token_deregister(ctx, token_index)
|
||||
}
|
||||
|
||||
pub fn update_index(ctx: Context<UpdateIndex>) -> Result<()> {
|
||||
instructions::update_index(ctx)
|
||||
pub fn token_update_index_and_rate(ctx: Context<TokenUpdateIndexAndRate>) -> Result<()> {
|
||||
instructions::token_update_index_and_rate(ctx)
|
||||
}
|
||||
|
||||
pub fn create_account(
|
||||
ctx: Context<CreateAccount>,
|
||||
pub fn account_create(
|
||||
ctx: Context<AccountCreate>,
|
||||
account_num: u8,
|
||||
name: String,
|
||||
) -> Result<()> {
|
||||
instructions::create_account(ctx, account_num, name)
|
||||
instructions::account_create(ctx, account_num, name)
|
||||
}
|
||||
|
||||
// TODO set delegate
|
||||
pub fn account_edit(
|
||||
ctx: Context<AccountEdit>,
|
||||
name_opt: Option<String>,
|
||||
delegate_opt: Option<Pubkey>,
|
||||
) -> Result<()> {
|
||||
instructions::account_edit(ctx, name_opt, delegate_opt)
|
||||
}
|
||||
|
||||
pub fn close_account(ctx: Context<CloseAccount>) -> Result<()> {
|
||||
instructions::close_account(ctx)
|
||||
pub fn account_close(ctx: Context<AccountClose>) -> Result<()> {
|
||||
instructions::account_close(ctx)
|
||||
}
|
||||
|
||||
// todo:
|
||||
|
@ -110,22 +147,24 @@ pub mod mango_v4 {
|
|||
// because generic anchor clients won't know how to deal with it
|
||||
// and it's tricky to use in typescript generally
|
||||
// lets do an interface pass later
|
||||
pub fn create_stub_oracle(ctx: Context<CreateStubOracle>, price: I80F48) -> Result<()> {
|
||||
instructions::create_stub_oracle(ctx, price)
|
||||
pub fn stub_oracle_create(ctx: Context<StubOracleCreate>, price: I80F48) -> Result<()> {
|
||||
instructions::stub_oracle_create(ctx, price)
|
||||
}
|
||||
|
||||
pub fn close_stub_oracle(ctx: Context<CloseStubOracle>) -> Result<()> {
|
||||
instructions::close_stub_oracle(ctx)
|
||||
pub fn stub_oracle_close(ctx: Context<StubOracleClose>) -> Result<()> {
|
||||
instructions::stub_oracle_close(ctx)
|
||||
}
|
||||
|
||||
pub fn set_stub_oracle(ctx: Context<SetStubOracle>, price: I80F48) -> Result<()> {
|
||||
instructions::set_stub_oracle(ctx, price)
|
||||
pub fn stub_oracle_set(ctx: Context<StubOracleSet>, price: I80F48) -> Result<()> {
|
||||
instructions::stub_oracle_set(ctx, price)
|
||||
}
|
||||
|
||||
// NOTE: keep disc synced in token_update_index_and_rate ix
|
||||
pub fn token_deposit(ctx: Context<TokenDeposit>, amount: u64) -> Result<()> {
|
||||
instructions::token_deposit(ctx, amount)
|
||||
}
|
||||
|
||||
// NOTE: keep disc synced in token_update_index_and_rate ix
|
||||
pub fn token_withdraw(
|
||||
ctx: Context<TokenWithdraw>,
|
||||
amount: u64,
|
||||
|
@ -162,6 +201,7 @@ pub mod mango_v4 {
|
|||
instructions::flash_loan3_begin(ctx, loan_amounts)
|
||||
}
|
||||
|
||||
// NOTE: keep disc synced in flash_loan3.rs
|
||||
pub fn flash_loan3_end<'key, 'accounts, 'remaining, 'info>(
|
||||
ctx: Context<'key, 'accounts, 'remaining, 'info, FlashLoan3End<'info>>,
|
||||
) -> Result<()> {
|
||||
|
@ -182,6 +222,11 @@ pub mod mango_v4 {
|
|||
instructions::serum3_register_market(ctx, market_index, name)
|
||||
}
|
||||
|
||||
// note:
|
||||
// pub fn serum3_edit_market - doesn't exist since a mango serum3 market only contains the properties
|
||||
// registered base and quote token pairs, and serum3 external market its pointing to, and none of them
|
||||
// should be edited once set on creation
|
||||
|
||||
pub fn serum3_deregister_market(ctx: Context<Serum3DeregisterMarket>) -> Result<()> {
|
||||
instructions::serum3_deregister_market(ctx)
|
||||
}
|
||||
|
@ -260,6 +305,14 @@ pub mod mango_v4 {
|
|||
)
|
||||
}
|
||||
|
||||
pub fn liq_token_bankruptcy(
|
||||
ctx: Context<LiqTokenBankruptcy>,
|
||||
liab_token_index: TokenIndex,
|
||||
max_liab_transfer: I80F48,
|
||||
) -> Result<()> {
|
||||
instructions::liq_token_bankruptcy(ctx, liab_token_index, max_liab_transfer)
|
||||
}
|
||||
|
||||
///
|
||||
/// Perps
|
||||
///
|
||||
|
@ -272,7 +325,6 @@ pub mod mango_v4 {
|
|||
oracle_config: OracleConfig,
|
||||
base_token_index_opt: Option<TokenIndex>,
|
||||
base_token_decimals: u8,
|
||||
quote_token_index: TokenIndex,
|
||||
quote_lot_size: i64,
|
||||
base_lot_size: i64,
|
||||
maint_asset_weight: f32,
|
||||
|
@ -293,7 +345,6 @@ pub mod mango_v4 {
|
|||
oracle_config,
|
||||
base_token_index_opt,
|
||||
base_token_decimals,
|
||||
quote_token_index,
|
||||
quote_lot_size,
|
||||
base_lot_size,
|
||||
maint_asset_weight,
|
||||
|
@ -309,6 +360,43 @@ pub mod mango_v4 {
|
|||
)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn perp_edit_market(
|
||||
ctx: Context<PerpEditMarket>,
|
||||
oracle_opt: Option<Pubkey>,
|
||||
oracle_config_opt: Option<OracleConfig>,
|
||||
base_token_index_opt: Option<TokenIndex>,
|
||||
base_token_decimals_opt: Option<u8>,
|
||||
maint_asset_weight_opt: Option<f32>,
|
||||
init_asset_weight_opt: Option<f32>,
|
||||
maint_liab_weight_opt: Option<f32>,
|
||||
init_liab_weight_opt: Option<f32>,
|
||||
liquidation_fee_opt: Option<f32>,
|
||||
maker_fee_opt: Option<f32>,
|
||||
taker_fee_opt: Option<f32>,
|
||||
min_funding_opt: Option<f32>,
|
||||
max_funding_opt: Option<f32>,
|
||||
impact_quantity_opt: Option<i64>,
|
||||
) -> Result<()> {
|
||||
instructions::perp_edit_market(
|
||||
ctx,
|
||||
oracle_opt,
|
||||
oracle_config_opt,
|
||||
base_token_index_opt,
|
||||
base_token_decimals_opt,
|
||||
maint_asset_weight_opt,
|
||||
init_asset_weight_opt,
|
||||
maint_liab_weight_opt,
|
||||
init_liab_weight_opt,
|
||||
liquidation_fee_opt,
|
||||
maker_fee_opt,
|
||||
taker_fee_opt,
|
||||
min_funding_opt,
|
||||
max_funding_opt,
|
||||
impact_quantity_opt,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn perp_close_market(ctx: Context<PerpCloseMarket>) -> Result<()> {
|
||||
instructions::perp_close_market(ctx)
|
||||
}
|
||||
|
|
|
@ -130,9 +130,19 @@ pub struct UpdateFundingLog {
|
|||
pub struct UpdateIndexLog {
|
||||
pub mango_group: Pubkey,
|
||||
pub token_index: u16,
|
||||
pub deposit_index: i128, // I80F48
|
||||
pub borrow_index: i128, // I80F48
|
||||
// pub price: i128, // I80F48
|
||||
pub deposit_index: i128, // I80F48
|
||||
pub borrow_index: i128, // I80F48
|
||||
pub avg_utilization: i128, // I80F48
|
||||
pub price: i128, // I80F48
|
||||
}
|
||||
|
||||
#[event]
|
||||
pub struct UpdateRateLog {
|
||||
pub mango_group: Pubkey,
|
||||
pub token_index: u16,
|
||||
pub rate0: i128, // I80F48
|
||||
pub rate1: i128, // I80F48
|
||||
pub max_rate: i128, // I80F48
|
||||
}
|
||||
|
||||
#[event]
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
use super::{OracleConfig, TokenIndex, TokenPosition};
|
||||
use crate::error::MangoError;
|
||||
use crate::util::checked_math as cm;
|
||||
use anchor_lang::prelude::*;
|
||||
use fixed::types::I80F48;
|
||||
|
@ -8,14 +7,18 @@ use static_assertions::const_assert_eq;
|
|||
|
||||
use std::mem::size_of;
|
||||
|
||||
pub const DAY: I80F48 = I80F48!(86400);
|
||||
pub const YEAR: I80F48 = I80F48!(31536000);
|
||||
pub const HOUR: i64 = 3600;
|
||||
pub const DAY: i64 = 86400;
|
||||
pub const DAY_I80F48: I80F48 = I80F48!(86400);
|
||||
pub const YEAR_I80F48: I80F48 = I80F48!(31536000);
|
||||
|
||||
#[account(zero_copy)]
|
||||
pub struct Bank {
|
||||
// ABI: Clients rely on this being at offset 8
|
||||
pub group: Pubkey,
|
||||
|
||||
pub name: [u8; 16],
|
||||
|
||||
pub group: Pubkey,
|
||||
pub mint: Pubkey,
|
||||
pub vault: Pubkey,
|
||||
pub oracle: Pubkey,
|
||||
|
@ -27,17 +30,30 @@ pub struct Bank {
|
|||
pub deposit_index: I80F48,
|
||||
pub borrow_index: I80F48,
|
||||
|
||||
/// total deposits/borrows, only updated during UpdateIndex
|
||||
/// TODO: These values could be dropped from the bank, they're written in UpdateIndex
|
||||
/// total deposits/borrows, only updated during UpdateIndexAndRate
|
||||
/// TODO: These values could be dropped from the bank, they're written in UpdateIndexAndRate
|
||||
/// and never read.
|
||||
pub cached_indexed_total_deposits: I80F48,
|
||||
pub cached_indexed_total_borrows: I80F48,
|
||||
|
||||
/// deposits/borrows for this bank
|
||||
///
|
||||
/// Note that these may become negative. It's perfectly fine for users to borrow one one bank
|
||||
/// (increasing indexed_borrows there) and paying back on another (possibly decreasing indexed_borrows
|
||||
/// below zero).
|
||||
///
|
||||
/// The vault amount is not deducable from these values.
|
||||
///
|
||||
/// These become meaningful when summed over all banks (like in update_index_and_rate).
|
||||
pub indexed_deposits: I80F48,
|
||||
pub indexed_borrows: I80F48,
|
||||
|
||||
pub last_updated: i64,
|
||||
pub index_last_updated: i64,
|
||||
pub bank_rate_last_updated: i64,
|
||||
|
||||
pub avg_utilization: I80F48,
|
||||
|
||||
pub adjustment_factor: I80F48,
|
||||
pub util0: I80F48,
|
||||
pub rate0: I80F48,
|
||||
pub util1: I80F48,
|
||||
|
@ -83,7 +99,7 @@ pub struct Bank {
|
|||
}
|
||||
const_assert_eq!(
|
||||
size_of::<Bank>(),
|
||||
16 + 32 * 4 + 8 + 16 * 21 + 2 * 8 + 2 + 1 + 1 + 4 + 8
|
||||
16 + 32 * 4 + 8 * 2 + 16 * 23 + 2 * 8 + 2 + 1 + 1 + 4 + 8
|
||||
);
|
||||
const_assert_eq!(size_of::<Bank>() % 8, 0);
|
||||
|
||||
|
@ -108,7 +124,9 @@ impl std::fmt::Debug for Bank {
|
|||
)
|
||||
.field("indexed_deposits", &self.indexed_deposits)
|
||||
.field("indexed_borrows", &self.indexed_borrows)
|
||||
.field("last_updated", &self.last_updated)
|
||||
.field("index_last_updated", &self.index_last_updated)
|
||||
.field("bank_rate_last_updated", &self.bank_rate_last_updated)
|
||||
.field("avg_utilization", &self.avg_utilization)
|
||||
.field("util0", &self.util0)
|
||||
.field("rate0", &self.rate0)
|
||||
.field("util1", &self.util1)
|
||||
|
@ -149,7 +167,10 @@ impl Bank {
|
|||
cached_indexed_total_borrows: existing_bank.cached_indexed_total_borrows,
|
||||
indexed_deposits: I80F48::ZERO,
|
||||
indexed_borrows: I80F48::ZERO,
|
||||
last_updated: existing_bank.last_updated,
|
||||
index_last_updated: existing_bank.index_last_updated,
|
||||
bank_rate_last_updated: existing_bank.bank_rate_last_updated,
|
||||
avg_utilization: existing_bank.avg_utilization,
|
||||
adjustment_factor: existing_bank.adjustment_factor,
|
||||
util0: existing_bank.util0,
|
||||
rate0: existing_bank.rate0,
|
||||
util1: existing_bank.util1,
|
||||
|
@ -197,17 +218,31 @@ impl Bank {
|
|||
position: &mut TokenPosition,
|
||||
mut native_amount: I80F48,
|
||||
) -> Result<bool> {
|
||||
require!(native_amount >= 0, MangoError::SomeError);
|
||||
require_gte!(native_amount, 0);
|
||||
let native_position = position.native(self);
|
||||
|
||||
// Adding DELTA to amount/index helps because (amount/index)*index <= amount, but
|
||||
// we want to ensure that users can withdraw the same amount they have deposited, so
|
||||
// (amount/index + delta)*index >= amount is a better guarantee.
|
||||
// Additionally, we require that we don't adjust values if
|
||||
// (native / index) * index == native, because we sometimes call this function with
|
||||
// values that are products of index.
|
||||
let div_rounding_up = |native: I80F48, index: I80F48| {
|
||||
let indexed = cm!(native / index);
|
||||
if cm!(indexed * index) < native {
|
||||
cm!(indexed + I80F48::DELTA)
|
||||
} else {
|
||||
indexed
|
||||
}
|
||||
};
|
||||
|
||||
if native_position.is_negative() {
|
||||
let new_native_position = cm!(native_position + native_amount);
|
||||
let indexed_change = cm!(native_amount / self.borrow_index + I80F48::DELTA);
|
||||
let indexed_change = div_rounding_up(native_amount, self.borrow_index);
|
||||
// this is only correct if it's not positive, because it scales the whole amount by borrow_index
|
||||
let new_indexed_value = cm!(position.indexed_position + indexed_change);
|
||||
if new_indexed_value.is_negative() {
|
||||
// pay back borrows only, leaving a negative position
|
||||
let indexed_change = cm!(native_amount / self.borrow_index + I80F48::DELTA);
|
||||
self.indexed_borrows = cm!(self.indexed_borrows - indexed_change);
|
||||
position.indexed_position = cm!(position.indexed_position + indexed_change);
|
||||
return Ok(true);
|
||||
|
@ -227,10 +262,7 @@ impl Bank {
|
|||
}
|
||||
|
||||
// add to deposits
|
||||
// Adding DELTA to amount/index helps because (amount/index)*index <= amount, but
|
||||
// we want to ensure that users can withdraw the same amount they have deposited, so
|
||||
// (amount/index + delta)*index >= amount is a better guarantee.
|
||||
let indexed_change = cm!(native_amount / self.deposit_index + I80F48::DELTA);
|
||||
let indexed_change = div_rounding_up(native_amount, self.deposit_index);
|
||||
self.indexed_deposits = cm!(self.indexed_deposits + indexed_change);
|
||||
position.indexed_position = cm!(position.indexed_position + indexed_change);
|
||||
|
||||
|
@ -269,7 +301,7 @@ impl Bank {
|
|||
mut native_amount: I80F48,
|
||||
with_loan_origination_fee: bool,
|
||||
) -> Result<bool> {
|
||||
require!(native_amount >= 0, MangoError::SomeError);
|
||||
require_gte!(native_amount, 0);
|
||||
let native_position = position.native(self);
|
||||
|
||||
if native_position.is_positive() {
|
||||
|
@ -299,7 +331,9 @@ impl Bank {
|
|||
}
|
||||
|
||||
if with_loan_origination_fee {
|
||||
self.charge_loan_origination_fee(position, native_amount)?;
|
||||
let loan_origination_fee = cm!(self.loan_origination_fee_rate * native_amount);
|
||||
self.collected_fees_native = cm!(self.collected_fees_native + loan_origination_fee);
|
||||
native_amount = cm!(native_amount + loan_origination_fee);
|
||||
}
|
||||
|
||||
// add to borrows
|
||||
|
@ -310,21 +344,17 @@ impl Bank {
|
|||
Ok(true)
|
||||
}
|
||||
|
||||
// charge only loan origination fee, assuming borrow has already happened
|
||||
pub fn charge_loan_origination_fee(
|
||||
// withdraw the loan origination fee for a borrow that happenend earlier
|
||||
pub fn withdraw_loan_origination_fee(
|
||||
&mut self,
|
||||
position: &mut TokenPosition,
|
||||
already_borrowed_native_amount: I80F48,
|
||||
) -> Result<()> {
|
||||
) -> Result<bool> {
|
||||
let loan_origination_fee =
|
||||
cm!(self.loan_origination_fee_rate * already_borrowed_native_amount);
|
||||
self.collected_fees_native = cm!(self.collected_fees_native + loan_origination_fee);
|
||||
|
||||
let indexed_change = cm!(loan_origination_fee / self.borrow_index);
|
||||
self.indexed_borrows = cm!(self.indexed_borrows + indexed_change);
|
||||
position.indexed_position = cm!(position.indexed_position - indexed_change);
|
||||
|
||||
Ok(())
|
||||
self.withdraw_internal(position, loan_origination_fee, false)
|
||||
}
|
||||
|
||||
/// Change a position without applying the loan origination fee
|
||||
|
@ -357,31 +387,32 @@ impl Bank {
|
|||
pub fn charge_loan_fee(&mut self, diff_ts: I80F48) {
|
||||
let native_borrows_old = self.native_borrows();
|
||||
self.indexed_borrows =
|
||||
cm!((self.indexed_borrows * (I80F48::ONE + self.loan_fee_rate * (diff_ts / YEAR))));
|
||||
cm!((self.indexed_borrows
|
||||
* (I80F48::ONE + self.loan_fee_rate * (diff_ts / YEAR_I80F48))));
|
||||
self.collected_fees_native =
|
||||
cm!(self.collected_fees_native + self.native_borrows() - native_borrows_old);
|
||||
}
|
||||
|
||||
pub fn compute_index(
|
||||
&mut self,
|
||||
&self,
|
||||
indexed_total_deposits: I80F48,
|
||||
indexed_total_borrows: I80F48,
|
||||
diff_ts: I80F48,
|
||||
) -> Result<(I80F48, I80F48)> {
|
||||
// compute index based on utilization
|
||||
let native_total_deposits = self.deposit_index * indexed_total_deposits;
|
||||
let native_total_borrows = self.borrow_index * indexed_total_borrows;
|
||||
let native_total_deposits = cm!(self.deposit_index * indexed_total_deposits);
|
||||
let native_total_borrows = cm!(self.borrow_index * indexed_total_borrows);
|
||||
|
||||
let utilization = if native_total_deposits == I80F48::ZERO {
|
||||
let instantaneous_utilization = if native_total_deposits == I80F48::ZERO {
|
||||
I80F48::ZERO
|
||||
} else {
|
||||
cm!(native_total_borrows / native_total_deposits)
|
||||
};
|
||||
|
||||
let interest_rate = self.compute_interest_rate(utilization);
|
||||
let borrow_interest_rate = self.compute_interest_rate(instantaneous_utilization);
|
||||
|
||||
let borrow_interest: I80F48 = cm!(interest_rate * diff_ts);
|
||||
let deposit_interest = cm!(borrow_interest * utilization);
|
||||
let borrow_interest: I80F48 = cm!(borrow_interest_rate * diff_ts);
|
||||
let deposit_interest = cm!(borrow_interest * instantaneous_utilization);
|
||||
|
||||
// msg!("utilization {}", utilization);
|
||||
// msg!("interest_rate {}", interest_rate);
|
||||
|
@ -392,9 +423,10 @@ impl Bank {
|
|||
return Ok((self.deposit_index, self.borrow_index));
|
||||
}
|
||||
|
||||
let borrow_index = cm!((self.borrow_index * borrow_interest) / YEAR + self.borrow_index);
|
||||
let borrow_index =
|
||||
cm!((self.borrow_index * borrow_interest) / YEAR_I80F48 + self.borrow_index);
|
||||
let deposit_index =
|
||||
cm!((self.deposit_index * deposit_interest) / YEAR + self.deposit_index);
|
||||
cm!((self.deposit_index * deposit_interest) / YEAR_I80F48 + self.deposit_index);
|
||||
|
||||
Ok((deposit_index, borrow_index))
|
||||
}
|
||||
|
@ -423,7 +455,6 @@ impl Bank {
|
|||
rate1: I80F48,
|
||||
max_rate: I80F48,
|
||||
) -> I80F48 {
|
||||
// TODO: daffy: use optimal interest from oracle
|
||||
if utilization <= util0 {
|
||||
let slope = cm!(rate0 / util0);
|
||||
cm!(slope * utilization)
|
||||
|
@ -437,6 +468,50 @@ impl Bank {
|
|||
cm!(rate1 + slope * extra_util)
|
||||
}
|
||||
}
|
||||
|
||||
// compute new avg utilization
|
||||
pub fn compute_new_avg_utilization(
|
||||
&self,
|
||||
indexed_total_deposits: I80F48,
|
||||
indexed_total_borrows: I80F48,
|
||||
now_ts: I80F48,
|
||||
) -> I80F48 {
|
||||
if now_ts == I80F48::ZERO {
|
||||
return I80F48::ZERO;
|
||||
}
|
||||
|
||||
let native_total_deposits = self.deposit_index * indexed_total_deposits;
|
||||
let native_total_borrows = self.borrow_index * indexed_total_borrows;
|
||||
let instantaneous_utilization = if native_total_deposits == I80F48::ZERO {
|
||||
I80F48::ZERO
|
||||
} else {
|
||||
cm!(native_total_borrows / native_total_deposits)
|
||||
};
|
||||
|
||||
// combine old and new with relevant factors to form new avg_utilization
|
||||
// scaling factor for previous avg_utilization is old_ts/new_ts
|
||||
// scaling factor for instantaneous utilization is (new_ts - old_ts) / new_ts
|
||||
let bank_rate_last_updated_i80f48 = I80F48::from_num(self.bank_rate_last_updated);
|
||||
(self.avg_utilization * bank_rate_last_updated_i80f48
|
||||
+ instantaneous_utilization * (now_ts - bank_rate_last_updated_i80f48))
|
||||
/ now_ts
|
||||
}
|
||||
|
||||
// computes new optimal rates and max rate
|
||||
pub fn compute_rates(&self) -> (I80F48, I80F48, I80F48) {
|
||||
// since we have 3 interest rate legs, consider the middle point of the middle leg as the optimal util
|
||||
let optimal_util = (self.util0 + self.util1) / 2;
|
||||
// use avg_utilization and not instantaneous_utilization so that rates cannot be manupulated easily
|
||||
let util_diff = self.avg_utilization - optimal_util;
|
||||
// move rates up when utilization is above optimal utilization, and vice versa
|
||||
let adjustment = I80F48::ONE + self.adjustment_factor * util_diff;
|
||||
// irrespective of which leg current utilization is in, update all rates
|
||||
(
|
||||
cm!(self.rate0 * adjustment),
|
||||
cm!(self.rate1 * adjustment),
|
||||
cm!(self.max_rate * adjustment),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
|
@ -565,4 +640,34 @@ mod tests {
|
|||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compute_new_avg_utilization() {
|
||||
let mut bank = Bank::zeroed();
|
||||
bank.deposit_index = I80F48::from_num(1.0);
|
||||
bank.borrow_index = I80F48::from_num(1.0);
|
||||
bank.bank_rate_last_updated = 0;
|
||||
|
||||
let compute_new_avg_utilization_runner =
|
||||
|bank: &mut Bank, utilization: I80F48, now_ts: i64| {
|
||||
bank.avg_utilization = bank.compute_new_avg_utilization(
|
||||
I80F48::ONE,
|
||||
utilization,
|
||||
I80F48::from_num(now_ts),
|
||||
);
|
||||
bank.bank_rate_last_updated = now_ts;
|
||||
};
|
||||
|
||||
compute_new_avg_utilization_runner(&mut bank, I80F48::ZERO, 0);
|
||||
assert_eq!(bank.avg_utilization, I80F48::ZERO);
|
||||
|
||||
compute_new_avg_utilization_runner(&mut bank, I80F48::from_num(0.5), 10);
|
||||
assert!((bank.avg_utilization - I80F48::from_num(0.5)).abs() < 0.0001);
|
||||
|
||||
compute_new_avg_utilization_runner(&mut bank, I80F48::from_num(0.8), 15);
|
||||
assert!((bank.avg_utilization - I80F48::from_num(0.6)).abs() < 0.0001);
|
||||
|
||||
compute_new_avg_utilization_runner(&mut bank, I80F48::ONE, 20);
|
||||
assert!((bank.avg_utilization - I80F48::from_num(0.7)).abs() < 0.0001);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,22 +4,30 @@ use std::mem::size_of;
|
|||
|
||||
// TODO: Assuming we allow up to 65536 different tokens
|
||||
pub type TokenIndex = u16;
|
||||
pub const QUOTE_TOKEN_INDEX: TokenIndex = 0;
|
||||
|
||||
#[account(zero_copy)]
|
||||
#[derive(Debug)]
|
||||
pub struct Group {
|
||||
// Relying on Anchor's discriminator be sufficient for our versioning needs?
|
||||
// pub meta_data: MetaData,
|
||||
// ABI: Clients rely on this being at offset 8
|
||||
pub admin: Pubkey,
|
||||
|
||||
// ABI: Clients rely on this being at offset 40
|
||||
pub group_num: u32,
|
||||
|
||||
pub padding: [u8; 4],
|
||||
|
||||
pub insurance_vault: Pubkey,
|
||||
pub insurance_mint: Pubkey,
|
||||
|
||||
pub bump: u8,
|
||||
// Only support closing/deregistering groups, stub oracles, tokens, and markets
|
||||
// if testing == 1
|
||||
pub testing: u8,
|
||||
pub padding: [u8; 2],
|
||||
pub group_num: u32,
|
||||
pub padding2: [u8; 6],
|
||||
pub reserved: [u8; 8],
|
||||
}
|
||||
const_assert_eq!(size_of::<Group>(), 48);
|
||||
const_assert_eq!(size_of::<Group>(), 32 * 3 + 4 + 4 + 1 * 2 + 6 + 8);
|
||||
const_assert_eq!(size_of::<Group>() % 8, 0);
|
||||
|
||||
#[macro_export]
|
||||
|
|
|
@ -1,16 +1,21 @@
|
|||
use anchor_lang::prelude::*;
|
||||
use anchor_lang::ZeroCopy;
|
||||
|
||||
use fixed::types::I80F48;
|
||||
use fixed_macro::types::I80F48;
|
||||
use serum_dex::state::OpenOrders;
|
||||
|
||||
use std::cell::Ref;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::accounts_zerocopy::*;
|
||||
use crate::error::MangoError;
|
||||
use crate::error::*;
|
||||
use crate::serum3_cpi;
|
||||
use crate::state::{oracle_price, Bank, MangoAccount, PerpMarket, PerpMarketIndex, TokenIndex};
|
||||
use crate::util::checked_math as cm;
|
||||
|
||||
const BANKRUPTCY_DUST_THRESHOLD: I80F48 = I80F48!(0.000001);
|
||||
|
||||
/// This trait abstracts how to find accounts needed for the health computation.
|
||||
///
|
||||
/// There are different ways they are retrieved from remainingAccounts, based
|
||||
|
@ -62,7 +67,7 @@ pub fn new_fixed_order_account_retriever<'a, 'info>(
|
|||
let expected_ais = cm!(active_token_len * 2 // banks + oracles
|
||||
+ active_perp_len // PerpMarkets
|
||||
+ active_serum3_len); // open_orders
|
||||
require_eq!(ais.len(), expected_ais, MangoError::SomeError);
|
||||
require_eq!(ais.len(), expected_ais);
|
||||
|
||||
Ok(FixedOrderAccountRetriever {
|
||||
ais: ais
|
||||
|
@ -76,11 +81,22 @@ pub fn new_fixed_order_account_retriever<'a, 'info>(
|
|||
}
|
||||
|
||||
impl<T: KeyedAccountReader> FixedOrderAccountRetriever<T> {
|
||||
fn bank(&self, group: &Pubkey, account_index: usize) -> Result<&Bank> {
|
||||
fn bank(&self, group: &Pubkey, account_index: usize, token_index: TokenIndex) -> Result<&Bank> {
|
||||
let bank = self.ais[account_index].load::<Bank>()?;
|
||||
require!(&bank.group == group, MangoError::SomeError);
|
||||
require_keys_eq!(bank.group, *group);
|
||||
require_eq!(bank.token_index, token_index);
|
||||
Ok(bank)
|
||||
}
|
||||
|
||||
fn oracle_price(&self, account_index: usize, bank: &Bank) -> Result<I80F48> {
|
||||
let oracle = &self.ais[cm!(self.n_banks + account_index)];
|
||||
require_keys_eq!(bank.oracle, *oracle.key());
|
||||
Ok(oracle_price(
|
||||
oracle,
|
||||
bank.oracle_config.conf_filter,
|
||||
bank.mint_decimals,
|
||||
)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: KeyedAccountReader> AccountRetriever for FixedOrderAccountRetriever<T> {
|
||||
|
@ -90,14 +106,27 @@ impl<T: KeyedAccountReader> AccountRetriever for FixedOrderAccountRetriever<T> {
|
|||
account_index: usize,
|
||||
token_index: TokenIndex,
|
||||
) -> Result<(&Bank, I80F48)> {
|
||||
let bank = self.bank(group, account_index)?;
|
||||
require_eq!(bank.token_index, token_index, MangoError::SomeError);
|
||||
let oracle = &self.ais[cm!(self.n_banks + account_index)];
|
||||
require_keys_eq!(bank.oracle, *oracle.key(), MangoError::SomeError);
|
||||
Ok((
|
||||
bank,
|
||||
oracle_price(oracle, bank.oracle_config.conf_filter, bank.mint_decimals)?,
|
||||
))
|
||||
let bank = self
|
||||
.bank(group, account_index, token_index)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"loading bank with health account index {}, token index {}, passed account {}",
|
||||
account_index,
|
||||
token_index,
|
||||
self.ais[account_index].key(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let oracle_price = self.oracle_price(account_index, bank).with_context(|| {
|
||||
format!(
|
||||
"getting oracle for bank with health account index {} and token index {}, passed account {}",
|
||||
account_index,
|
||||
token_index,
|
||||
self.ais[self.n_banks + account_index].key(),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok((bank, oracle_price))
|
||||
}
|
||||
|
||||
fn perp_market(
|
||||
|
@ -107,19 +136,35 @@ impl<T: KeyedAccountReader> AccountRetriever for FixedOrderAccountRetriever<T> {
|
|||
perp_market_index: PerpMarketIndex,
|
||||
) -> Result<&PerpMarket> {
|
||||
let ai = &self.ais[cm!(self.begin_perp + account_index)];
|
||||
let market = ai.load::<PerpMarket>()?;
|
||||
require!(&market.group == group, MangoError::SomeError);
|
||||
require!(
|
||||
market.perp_market_index == perp_market_index,
|
||||
MangoError::SomeError
|
||||
);
|
||||
Ok(market)
|
||||
(|| {
|
||||
let market = ai.load::<PerpMarket>()?;
|
||||
require_keys_eq!(market.group, *group);
|
||||
require_eq!(market.perp_market_index, perp_market_index);
|
||||
Ok(market)
|
||||
})()
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"loading perp market with health account index {} and perp market index {}, passed account {}",
|
||||
account_index,
|
||||
perp_market_index,
|
||||
ai.key(),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn serum_oo(&self, account_index: usize, key: &Pubkey) -> Result<&OpenOrders> {
|
||||
let ai = &self.ais[cm!(self.begin_serum3 + account_index)];
|
||||
require!(key == ai.key(), MangoError::SomeError);
|
||||
serum3_cpi::load_open_orders(ai)
|
||||
(|| {
|
||||
require_keys_eq!(*key, *ai.key());
|
||||
serum3_cpi::load_open_orders(ai)
|
||||
})()
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"loading serum open orders with health account index {}, passed account {}",
|
||||
account_index,
|
||||
ai.key(),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -136,47 +181,58 @@ pub struct ScanningAccountRetriever<'a, 'info> {
|
|||
perp_index_map: HashMap<PerpMarketIndex, usize>,
|
||||
}
|
||||
|
||||
// Returns None if `ai` doesn't have the owner or discriminator for T
|
||||
fn can_load_as<'a, T: ZeroCopy + Owner>(
|
||||
(i, ai): (usize, &'a AccountInfo),
|
||||
) -> Option<(usize, Result<Ref<'a, T>>)> {
|
||||
let load_result = ai.load::<T>();
|
||||
match load_result {
|
||||
Err(Error::AnchorError(error))
|
||||
if error.error_code_number == ErrorCode::AccountDiscriminatorMismatch as u32
|
||||
|| error.error_code_number == ErrorCode::AccountOwnedByWrongProgram as u32 =>
|
||||
{
|
||||
return None;
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
Some((i, load_result))
|
||||
}
|
||||
|
||||
impl<'a, 'info> ScanningAccountRetriever<'a, 'info> {
|
||||
pub fn new(ais: &'a [AccountInfo<'info>], group: &Pubkey) -> Result<Self> {
|
||||
// find all Bank accounts
|
||||
let mut token_index_map = HashMap::with_capacity(ais.len() / 2);
|
||||
for (i, ai) in ais.iter().enumerate() {
|
||||
match ai.load::<Bank>() {
|
||||
Ok(bank) => {
|
||||
require!(&bank.group == group, MangoError::SomeError);
|
||||
ais.iter()
|
||||
.enumerate()
|
||||
.map_while(can_load_as::<Bank>)
|
||||
.try_for_each(|(i, loaded)| {
|
||||
(|| {
|
||||
let bank = loaded?;
|
||||
require_keys_eq!(bank.group, *group);
|
||||
token_index_map.insert(bank.token_index, i);
|
||||
}
|
||||
Err(Error::AnchorError(error))
|
||||
if error.error_code_number
|
||||
== ErrorCode::AccountDiscriminatorMismatch as u32
|
||||
|| error.error_code_number
|
||||
== ErrorCode::AccountOwnedByWrongProgram as u32 =>
|
||||
{
|
||||
break;
|
||||
}
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
}
|
||||
Ok(())
|
||||
})()
|
||||
.with_context(|| format!("scanning banks, health account index {}", i))
|
||||
})?;
|
||||
|
||||
// skip all banks and oracles, then find number of PerpMarket accounts
|
||||
let skip = token_index_map.len() * 2;
|
||||
let mut perp_index_map = HashMap::with_capacity(ais.len() - skip);
|
||||
for (i, ai) in ais[skip..].iter().enumerate() {
|
||||
match ai.load::<PerpMarket>() {
|
||||
Ok(perp_market) => {
|
||||
require!(&perp_market.group == group, MangoError::SomeError);
|
||||
ais[skip..]
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map_while(can_load_as::<PerpMarket>)
|
||||
.try_for_each(|(i, loaded)| {
|
||||
(|| {
|
||||
let perp_market = loaded?;
|
||||
require_keys_eq!(perp_market.group, *group);
|
||||
perp_index_map.insert(perp_market.perp_market_index, cm!(skip + i));
|
||||
}
|
||||
Err(Error::AnchorError(error))
|
||||
if error.error_code_number
|
||||
== ErrorCode::AccountDiscriminatorMismatch as u32
|
||||
|| error.error_code_number
|
||||
== ErrorCode::AccountOwnedByWrongProgram as u32 =>
|
||||
{
|
||||
break;
|
||||
}
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
}
|
||||
Ok(())
|
||||
})()
|
||||
.with_context(|| {
|
||||
format!("scanning perp markets, health account index {}", i + skip)
|
||||
})
|
||||
})?;
|
||||
|
||||
Ok(Self {
|
||||
ais: ais
|
||||
|
@ -201,7 +257,7 @@ impl<'a, 'info> ScanningAccountRetriever<'a, 'info> {
|
|||
Ok(*self
|
||||
.token_index_map
|
||||
.get(&token_index)
|
||||
.ok_or_else(|| error!(MangoError::SomeError))?)
|
||||
.ok_or_else(|| error_msg!("token index {} not found", token_index))?)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
@ -209,14 +265,24 @@ impl<'a, 'info> ScanningAccountRetriever<'a, 'info> {
|
|||
Ok(*self
|
||||
.perp_index_map
|
||||
.get(&perp_market_index)
|
||||
.ok_or_else(|| error!(MangoError::SomeError))?)
|
||||
.ok_or_else(|| error_msg!("perp market index {} not found", perp_market_index))?)
|
||||
}
|
||||
|
||||
pub fn banks_mut_and_oracles(
|
||||
&mut self,
|
||||
token_index1: TokenIndex,
|
||||
token_index2: TokenIndex,
|
||||
) -> Result<(&mut Bank, &mut Bank, I80F48, I80F48)> {
|
||||
) -> Result<(&mut Bank, I80F48, Option<(&mut Bank, I80F48)>)> {
|
||||
let n_banks = self.n_banks();
|
||||
if token_index1 == token_index2 {
|
||||
let index = self.bank_index(token_index1)?;
|
||||
let (bank_part, oracle_part) = self.ais.split_at_mut(index + 1);
|
||||
let bank = bank_part[index].load_mut_fully_unchecked::<Bank>()?;
|
||||
let oracle = &oracle_part[n_banks - 1];
|
||||
require_keys_eq!(bank.oracle, *oracle.key);
|
||||
let price = oracle_price(oracle, bank.oracle_config.conf_filter, bank.mint_decimals)?;
|
||||
return Ok((bank, price, None));
|
||||
}
|
||||
let index1 = self.bank_index(token_index1)?;
|
||||
let index2 = self.bank_index(token_index2)?;
|
||||
let (first, second, swap) = if index1 < index2 {
|
||||
|
@ -224,7 +290,6 @@ impl<'a, 'info> ScanningAccountRetriever<'a, 'info> {
|
|||
} else {
|
||||
(index2, index1, true)
|
||||
};
|
||||
let n_banks = self.n_banks();
|
||||
|
||||
// split_at_mut after the first bank and after the second bank
|
||||
let (first_bank_part, second_part) = self.ais.split_at_mut(first + 1);
|
||||
|
@ -234,16 +299,16 @@ impl<'a, 'info> ScanningAccountRetriever<'a, 'info> {
|
|||
let bank2 = second_bank_part[second - (first + 1)].load_mut_fully_unchecked::<Bank>()?;
|
||||
let oracle1 = &oracles_part[cm!(n_banks + first - (second + 1))];
|
||||
let oracle2 = &oracles_part[cm!(n_banks + second - (second + 1))];
|
||||
require!(&bank1.oracle == oracle1.key, MangoError::SomeError);
|
||||
require!(&bank2.oracle == oracle2.key, MangoError::SomeError);
|
||||
require_keys_eq!(bank1.oracle, *oracle1.key);
|
||||
require_keys_eq!(bank2.oracle, *oracle2.key);
|
||||
let mint_decimals1 = bank1.mint_decimals;
|
||||
let mint_decimals2 = bank2.mint_decimals;
|
||||
let price1 = oracle_price(oracle1, bank1.oracle_config.conf_filter, mint_decimals1)?;
|
||||
let price2 = oracle_price(oracle2, bank2.oracle_config.conf_filter, mint_decimals2)?;
|
||||
if swap {
|
||||
Ok((bank2, bank1, price2, price1))
|
||||
Ok((bank2, price2, Some((bank1, price1))))
|
||||
} else {
|
||||
Ok((bank1, bank2, price1, price2))
|
||||
Ok((bank1, price1, Some((bank2, price2))))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -251,7 +316,7 @@ impl<'a, 'info> ScanningAccountRetriever<'a, 'info> {
|
|||
let index = self.bank_index(token_index)?;
|
||||
let bank = self.ais[index].load_fully_unchecked::<Bank>()?;
|
||||
let oracle = &self.ais[cm!(self.n_banks() + index)];
|
||||
require!(&bank.oracle == oracle.key, MangoError::SomeError);
|
||||
require_keys_eq!(bank.oracle, *oracle.key);
|
||||
Ok((
|
||||
bank,
|
||||
oracle_price(oracle, bank.oracle_config.conf_filter, bank.mint_decimals)?,
|
||||
|
@ -267,7 +332,7 @@ impl<'a, 'info> ScanningAccountRetriever<'a, 'info> {
|
|||
let oo = self.ais[self.begin_serum3()..]
|
||||
.iter()
|
||||
.find(|ai| ai.key == key)
|
||||
.ok_or_else(|| error!(MangoError::SomeError))?;
|
||||
.ok_or_else(|| error_msg!("no serum3 open orders for key {}", key))?;
|
||||
serum3_cpi::load_open_orders(oo)
|
||||
}
|
||||
}
|
||||
|
@ -322,7 +387,7 @@ pub fn compute_health_from_fixed_accounts(
|
|||
let expected_ais = cm!(active_token_len * 2 // banks + oracles
|
||||
+ active_perp_len // PerpMarkets
|
||||
+ active_serum3_len); // open_orders
|
||||
require!(ais.len() == expected_ais, MangoError::SomeError);
|
||||
require_eq!(ais.len(), expected_ais);
|
||||
|
||||
let retriever = FixedOrderAccountRetriever {
|
||||
ais: ais
|
||||
|
@ -333,7 +398,7 @@ pub fn compute_health_from_fixed_accounts(
|
|||
begin_perp: cm!(active_token_len * 2),
|
||||
begin_serum3: cm!(active_token_len * 2 + active_perp_len),
|
||||
};
|
||||
new_health_cache(account, &retriever)?.health(health_type)
|
||||
Ok(new_health_cache(account, &retriever)?.health(health_type))
|
||||
}
|
||||
|
||||
/// Compute health with an arbitrary AccountRetriever
|
||||
|
@ -342,10 +407,11 @@ pub fn compute_health(
|
|||
health_type: HealthType,
|
||||
retriever: &impl AccountRetriever,
|
||||
) -> Result<I80F48> {
|
||||
new_health_cache(account, retriever)?.health(health_type)
|
||||
Ok(new_health_cache(account, retriever)?.health(health_type))
|
||||
}
|
||||
|
||||
struct TokenInfo {
|
||||
#[derive(AnchorDeserialize, AnchorSerialize)]
|
||||
pub struct TokenInfo {
|
||||
token_index: TokenIndex,
|
||||
maint_asset_weight: I80F48,
|
||||
init_asset_weight: I80F48,
|
||||
|
@ -374,9 +440,20 @@ impl TokenInfo {
|
|||
HealthType::Maint => self.maint_liab_weight,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn health_contribution(&self, health_type: HealthType) -> I80F48 {
|
||||
let weight = if self.balance.is_negative() {
|
||||
self.liab_weight(health_type)
|
||||
} else {
|
||||
self.asset_weight(health_type)
|
||||
};
|
||||
cm!(self.balance * weight)
|
||||
}
|
||||
}
|
||||
|
||||
struct Serum3Info {
|
||||
#[derive(AnchorSerialize, AnchorDeserialize)]
|
||||
pub struct Serum3Info {
|
||||
reserved: I80F48,
|
||||
base_index: usize,
|
||||
quote_index: usize,
|
||||
|
@ -421,7 +498,8 @@ impl Serum3Info {
|
|||
}
|
||||
}
|
||||
|
||||
struct PerpInfo {
|
||||
#[derive(AnchorSerialize, AnchorDeserialize)]
|
||||
pub struct PerpInfo {
|
||||
maint_asset_weight: I80F48,
|
||||
init_asset_weight: I80F48,
|
||||
maint_liab_weight: I80F48,
|
||||
|
@ -460,6 +538,7 @@ impl PerpInfo {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(AnchorSerialize, AnchorDeserialize)]
|
||||
pub struct HealthCache {
|
||||
token_infos: Vec<TokenInfo>,
|
||||
serum3_infos: Vec<Serum3Info>,
|
||||
|
@ -467,21 +546,13 @@ pub struct HealthCache {
|
|||
}
|
||||
|
||||
impl HealthCache {
|
||||
pub fn health(&self, health_type: HealthType) -> Result<I80F48> {
|
||||
pub fn health(&self, health_type: HealthType) -> I80F48 {
|
||||
let mut health = I80F48::ZERO;
|
||||
for token_info in self.token_infos.iter() {
|
||||
let contrib = health_contribution(health_type, token_info, token_info.balance)?;
|
||||
let sum = |contrib| {
|
||||
health = cm!(health + contrib);
|
||||
}
|
||||
for serum3_info in self.serum3_infos.iter() {
|
||||
let contrib = serum3_info.health_contribution(health_type, &self.token_infos);
|
||||
health = cm!(health + contrib);
|
||||
}
|
||||
for perp_info in self.perp_infos.iter() {
|
||||
let contrib = perp_info.health_contribution(health_type);
|
||||
health = cm!(health + contrib);
|
||||
}
|
||||
Ok(health)
|
||||
};
|
||||
self.health_sum(health_type, sum);
|
||||
health
|
||||
}
|
||||
|
||||
pub fn adjust_token_balance(&mut self, token_index: TokenIndex, change: I80F48) -> Result<()> {
|
||||
|
@ -489,27 +560,80 @@ impl HealthCache {
|
|||
.token_infos
|
||||
.iter_mut()
|
||||
.find(|t| t.token_index == token_index)
|
||||
.ok_or_else(|| error!(MangoError::SomeError))?;
|
||||
.ok_or_else(|| error_msg!("token index {} not found", token_index))?;
|
||||
entry.balance = cm!(entry.balance + change * entry.oracle_price);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute health contribution for a given balance
|
||||
/// wart: independent of the balance stored in TokenInfo
|
||||
/// balance is in health-reference-token native units
|
||||
#[inline(always)]
|
||||
fn health_contribution(
|
||||
health_type: HealthType,
|
||||
info: &TokenInfo,
|
||||
balance: I80F48,
|
||||
) -> Result<I80F48> {
|
||||
let weight = if balance.is_negative() {
|
||||
info.liab_weight(health_type)
|
||||
} else {
|
||||
info.asset_weight(health_type)
|
||||
};
|
||||
Ok(cm!(balance * weight))
|
||||
pub fn has_liquidatable_assets(&self) -> bool {
|
||||
let spot_liquidatable = self.token_infos.iter().any(|ti| {
|
||||
ti.balance > BANKRUPTCY_DUST_THRESHOLD || ti.serum3_max_reserved.is_positive()
|
||||
});
|
||||
let perp_liquidatable = self
|
||||
.perp_infos
|
||||
.iter()
|
||||
.any(|p| p.base != 0 || p.quote > BANKRUPTCY_DUST_THRESHOLD);
|
||||
spot_liquidatable || perp_liquidatable
|
||||
}
|
||||
|
||||
pub fn has_borrows(&self) -> bool {
|
||||
// AUDIT: Can we really guarantee that liquidation/bankruptcy resolution always leaves
|
||||
// non-negative balances?
|
||||
let spot_borrows = self.token_infos.iter().any(|ti| ti.balance.is_negative());
|
||||
let perp_borrows = self
|
||||
.perp_infos
|
||||
.iter()
|
||||
.any(|p| p.quote.is_negative() || p.base != 0);
|
||||
spot_borrows || perp_borrows
|
||||
}
|
||||
|
||||
fn health_sum(&self, health_type: HealthType, mut action: impl FnMut(I80F48)) {
|
||||
for token_info in self.token_infos.iter() {
|
||||
let contrib = token_info.health_contribution(health_type);
|
||||
action(contrib);
|
||||
}
|
||||
for serum3_info in self.serum3_infos.iter() {
|
||||
let contrib = serum3_info.health_contribution(health_type, &self.token_infos);
|
||||
action(contrib);
|
||||
}
|
||||
for perp_info in self.perp_infos.iter() {
|
||||
let contrib = perp_info.health_contribution(health_type);
|
||||
action(contrib);
|
||||
}
|
||||
}
|
||||
|
||||
/// Sum of only the positive health components (assets) and
|
||||
/// sum of absolute values of all negative health components (liabs, always >= 0)
|
||||
pub fn health_assets_and_liabs(&self, health_type: HealthType) -> (I80F48, I80F48) {
|
||||
let mut assets = I80F48::ZERO;
|
||||
let mut liabs = I80F48::ZERO;
|
||||
let sum = |contrib| {
|
||||
if contrib > 0 {
|
||||
assets = cm!(assets + contrib);
|
||||
} else {
|
||||
liabs = cm!(liabs - contrib);
|
||||
}
|
||||
};
|
||||
self.health_sum(health_type, sum);
|
||||
(assets, liabs)
|
||||
}
|
||||
|
||||
/// The health ratio is
|
||||
/// - 0 if health is 0 - meaning assets = liabs
|
||||
/// - 100 if there's 2x as many assets as liabs
|
||||
/// - 200 if there's 3x as many assets as liabs
|
||||
/// - MAX if liabs = 0
|
||||
///
|
||||
/// Maybe talking about the collateralization ratio assets/liabs is more intuitive?
|
||||
pub fn health_ratio(&self, health_type: HealthType) -> I80F48 {
|
||||
let (assets, liabs) = self.health_assets_and_liabs(health_type);
|
||||
let hundred = I80F48::from(100);
|
||||
if liabs > 0 {
|
||||
cm!(hundred * (assets - liabs) / liabs)
|
||||
} else {
|
||||
I80F48::MAX
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a HealthCache for an account and its health accounts.
|
||||
|
@ -523,7 +647,7 @@ pub fn new_health_cache(
|
|||
infos
|
||||
.iter()
|
||||
.position(|ti| ti.token_index == token_index)
|
||||
.ok_or_else(|| error!(MangoError::SomeError))
|
||||
.ok_or_else(|| error_msg!("token index {} not found", token_index))
|
||||
}
|
||||
|
||||
for (i, position) in account.tokens.iter_active().enumerate() {
|
||||
|
@ -835,7 +959,6 @@ mod tests {
|
|||
perp1.data().group = group;
|
||||
perp1.data().perp_market_index = 9;
|
||||
perp1.data().base_token_index = 4;
|
||||
perp1.data().quote_token_index = 1;
|
||||
perp1.data().init_asset_weight = I80F48::from_num(1.0 - 0.2f64);
|
||||
perp1.data().init_liab_weight = I80F48::from_num(1.0 + 0.2f64);
|
||||
perp1.data().maint_asset_weight = I80F48::from_num(1.0 - 0.1f64);
|
||||
|
@ -914,7 +1037,8 @@ mod tests {
|
|||
assert_eq!(retriever.perp_index_map.len(), 1);
|
||||
|
||||
{
|
||||
let (b1, b2, o1, o2) = retriever.banks_mut_and_oracles(1, 4).unwrap();
|
||||
let (b1, o1, opt_b2o2) = retriever.banks_mut_and_oracles(1, 4).unwrap();
|
||||
let (b2, o2) = opt_b2o2.unwrap();
|
||||
assert_eq!(b1.token_index, 1);
|
||||
assert_eq!(o1, I80F48::ONE);
|
||||
assert_eq!(b2.token_index, 4);
|
||||
|
@ -922,13 +1046,21 @@ mod tests {
|
|||
}
|
||||
|
||||
{
|
||||
let (b1, b2, o1, o2) = retriever.banks_mut_and_oracles(4, 1).unwrap();
|
||||
let (b1, o1, opt_b2o2) = retriever.banks_mut_and_oracles(4, 1).unwrap();
|
||||
let (b2, o2) = opt_b2o2.unwrap();
|
||||
assert_eq!(b1.token_index, 4);
|
||||
assert_eq!(o1, 5 * I80F48::ONE);
|
||||
assert_eq!(b2.token_index, 1);
|
||||
assert_eq!(o2, I80F48::ONE);
|
||||
}
|
||||
|
||||
{
|
||||
let (b1, o1, opt_b2o2) = retriever.banks_mut_and_oracles(4, 4).unwrap();
|
||||
assert!(opt_b2o2.is_none());
|
||||
assert_eq!(b1.token_index, 4);
|
||||
assert_eq!(o1, 5 * I80F48::ONE);
|
||||
}
|
||||
|
||||
retriever.banks_mut_and_oracles(4, 2).unwrap_err();
|
||||
|
||||
let oo = retriever.serum_oo(0, &oo1key).unwrap();
|
||||
|
@ -1001,7 +1133,6 @@ mod tests {
|
|||
perp1.data().group = group;
|
||||
perp1.data().perp_market_index = 9;
|
||||
perp1.data().base_token_index = 4;
|
||||
perp1.data().quote_token_index = 1;
|
||||
perp1.data().init_asset_weight = I80F48::from_num(1.0 - 0.2f64);
|
||||
perp1.data().init_liab_weight = I80F48::from_num(1.0 + 0.2f64);
|
||||
perp1.data().maint_asset_weight = I80F48::from_num(1.0 - 0.1f64);
|
||||
|
|
|
@ -121,18 +121,26 @@ impl MangoAccountTokenPositions {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn get(&self, token_index: TokenIndex) -> Result<&TokenPosition> {
|
||||
/// Returns
|
||||
/// - the position
|
||||
/// - the raw index into the token positions list (for use with get_raw/deactivate)
|
||||
pub fn get(&self, token_index: TokenIndex) -> Result<(&TokenPosition, usize)> {
|
||||
self.values
|
||||
.iter()
|
||||
.find(|p| p.is_active_for_token(token_index))
|
||||
.ok_or_else(|| error!(MangoError::SomeError)) // TODO: not found error
|
||||
.enumerate()
|
||||
.find_map(|(raw_index, p)| p.is_active_for_token(token_index).then(|| (p, raw_index)))
|
||||
.ok_or_else(|| error_msg!("position for token index {} not found", token_index))
|
||||
}
|
||||
|
||||
pub fn get_mut(&mut self, token_index: TokenIndex) -> Result<&mut TokenPosition> {
|
||||
/// Returns
|
||||
/// - the position
|
||||
/// - the raw index into the token positions list (for use with get_raw/deactivate)
|
||||
pub fn get_mut(&mut self, token_index: TokenIndex) -> Result<(&mut TokenPosition, usize)> {
|
||||
self.values
|
||||
.iter_mut()
|
||||
.find(|p| p.is_active_for_token(token_index))
|
||||
.ok_or_else(|| error!(MangoError::SomeError)) // TODO: not found error
|
||||
.enumerate()
|
||||
.find_map(|(raw_index, p)| p.is_active_for_token(token_index).then(|| (p, raw_index)))
|
||||
.ok_or_else(|| error_msg!("position for token index {} not found", token_index))
|
||||
}
|
||||
|
||||
pub fn get_mut_raw(&mut self, raw_token_index: usize) -> &mut TokenPosition {
|
||||
|
@ -178,7 +186,8 @@ impl MangoAccountTokenPositions {
|
|||
}
|
||||
Ok((v, raw_index, bank_index))
|
||||
} else {
|
||||
err!(MangoError::SomeError) // TODO: No free space
|
||||
err!(MangoError::NoFreeTokenPositionIndex)
|
||||
.context(format!("when looking for token index {}", token_index))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -287,7 +296,7 @@ impl MangoAccountSerum3Orders {
|
|||
|
||||
pub fn create(&mut self, market_index: Serum3MarketIndex) -> Result<&mut Serum3Orders> {
|
||||
if self.find(market_index).is_some() {
|
||||
return err!(MangoError::SomeError); // exists already
|
||||
return err!(MangoError::Serum3OpenOrdersExistAlready);
|
||||
}
|
||||
if let Some(v) = self.values.iter_mut().find(|p| !p.is_active()) {
|
||||
*v = Serum3Orders {
|
||||
|
@ -296,7 +305,7 @@ impl MangoAccountSerum3Orders {
|
|||
};
|
||||
Ok(v)
|
||||
} else {
|
||||
err!(MangoError::SomeError) // no space
|
||||
err!(MangoError::NoFreeSerum3OpenOrdersIndex)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -305,7 +314,7 @@ impl MangoAccountSerum3Orders {
|
|||
.values
|
||||
.iter()
|
||||
.position(|p| p.is_active_for_market(market_index))
|
||||
.ok_or(MangoError::SomeError)?;
|
||||
.ok_or_else(|| error_msg!("serum3 open orders index {} not found", market_index))?;
|
||||
|
||||
self.values[index].market_index = Serum3MarketIndex::MAX;
|
||||
|
||||
|
@ -541,7 +550,7 @@ impl MangoAccountPerpPositions {
|
|||
if let Some(i) = pos {
|
||||
Ok((&mut self.accounts[i], i))
|
||||
} else {
|
||||
err!(MangoError::SomeError) // TODO: No free space
|
||||
err!(MangoError::NoFreePerpPositionIndex)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -587,10 +596,7 @@ impl MangoAccountPerpPositions {
|
|||
}
|
||||
|
||||
pub fn remove_order(&mut self, slot: usize, quantity: i64) -> Result<()> {
|
||||
require!(
|
||||
self.order_market[slot] != FREE_ORDER_SLOT,
|
||||
MangoError::SomeError
|
||||
);
|
||||
require_neq!(self.order_market[slot], FREE_ORDER_SLOT);
|
||||
let order_side = self.order_side[slot];
|
||||
let perp_market_index = self.order_market[slot];
|
||||
let perp_account = self.get_account_mut_or_create(perp_market_index).unwrap().0;
|
||||
|
@ -704,11 +710,14 @@ impl Default for MangoAccountPerpPositions {
|
|||
|
||||
#[account(zero_copy)]
|
||||
pub struct MangoAccount {
|
||||
pub name: [u8; 32],
|
||||
|
||||
// ABI: Clients rely on this being at offset 8
|
||||
pub group: Pubkey,
|
||||
|
||||
// ABI: Clients rely on this being at offset 40
|
||||
pub owner: Pubkey,
|
||||
|
||||
pub name: [u8; 32],
|
||||
|
||||
// Alternative authority/signer of transactions for a mango account
|
||||
pub delegate: Pubkey,
|
||||
|
||||
|
@ -723,16 +732,24 @@ pub struct MangoAccount {
|
|||
pub perps: MangoAccountPerpPositions,
|
||||
|
||||
/// This account cannot open new positions or borrow until `init_health >= 0`
|
||||
pub being_liquidated: u8,
|
||||
being_liquidated: u8,
|
||||
|
||||
/// This account cannot do anything except go through `resolve_bankruptcy`
|
||||
pub is_bankrupt: u8,
|
||||
is_bankrupt: u8,
|
||||
|
||||
pub account_num: u8,
|
||||
pub bump: u8,
|
||||
|
||||
// pub info: [u8; INFO_LEN], // TODO: Info could be in a separate PDA?
|
||||
pub reserved: [u8; 4],
|
||||
|
||||
// Cumulative (deposits - withdraws)
|
||||
// using USD prices at the time of the deposit/withdraw
|
||||
// in UI USD units
|
||||
pub net_deposits: f32,
|
||||
// Cumulative settles on perp positions
|
||||
// TODO: unimplemented
|
||||
pub net_settled: f32,
|
||||
}
|
||||
const_assert_eq!(
|
||||
size_of::<MangoAccount>(),
|
||||
|
@ -742,6 +759,7 @@ const_assert_eq!(
|
|||
+ size_of::<MangoAccountPerpPositions>()
|
||||
+ 4
|
||||
+ 4
|
||||
+ 2 * 4 // net_deposits and net_settled
|
||||
);
|
||||
const_assert_eq!(size_of::<MangoAccount>() % 8, 0);
|
||||
|
||||
|
@ -770,6 +788,26 @@ impl MangoAccount {
|
|||
.unwrap()
|
||||
.trim_matches(char::from(0))
|
||||
}
|
||||
|
||||
pub fn is_owner_or_delegate(&self, ix_signer: Pubkey) -> bool {
|
||||
self.owner == ix_signer || self.delegate == ix_signer
|
||||
}
|
||||
|
||||
pub fn is_bankrupt(&self) -> bool {
|
||||
self.is_bankrupt != 0
|
||||
}
|
||||
|
||||
pub fn set_bankrupt(&mut self, b: bool) {
|
||||
self.is_bankrupt = if b { 1 } else { 0 };
|
||||
}
|
||||
|
||||
pub fn being_liquidated(&self) -> bool {
|
||||
self.being_liquidated != 0
|
||||
}
|
||||
|
||||
pub fn set_being_liquidated(&mut self, b: bool) {
|
||||
self.being_liquidated = if b { 1 } else { 0 };
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MangoAccount {
|
||||
|
@ -787,6 +825,8 @@ impl Default for MangoAccount {
|
|||
account_num: 0,
|
||||
bump: 0,
|
||||
reserved: Default::default(),
|
||||
net_deposits: 0.0,
|
||||
net_settled: 0.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,8 @@ use anchor_lang::prelude::*;
|
|||
use static_assertions::const_assert_eq;
|
||||
use std::mem::size_of;
|
||||
|
||||
use crate::error::*;
|
||||
|
||||
use super::TokenIndex;
|
||||
|
||||
pub const MAX_BANKS: usize = 6;
|
||||
|
@ -13,25 +15,28 @@ pub const MAX_BANKS: usize = 6;
|
|||
#[account(zero_copy)]
|
||||
#[derive(Debug)]
|
||||
pub struct MintInfo {
|
||||
// TODO: none of these pubkeys are needed, remove?
|
||||
// ABI: Clients rely on this being at offset 8
|
||||
pub group: Pubkey,
|
||||
|
||||
// ABI: Clients rely on this being at offset 40
|
||||
pub token_index: TokenIndex,
|
||||
|
||||
pub padding: [u8; 6],
|
||||
pub mint: Pubkey,
|
||||
pub banks: [Pubkey; MAX_BANKS],
|
||||
pub vaults: [Pubkey; MAX_BANKS],
|
||||
pub oracle: Pubkey,
|
||||
pub address_lookup_table: Pubkey,
|
||||
|
||||
pub token_index: TokenIndex,
|
||||
|
||||
// describe what address map relevant accounts are found on
|
||||
pub address_lookup_table_bank_index: u8,
|
||||
pub address_lookup_table_oracle_index: u8,
|
||||
|
||||
pub reserved: [u8; 4],
|
||||
pub reserved: [u8; 6],
|
||||
}
|
||||
const_assert_eq!(
|
||||
size_of::<MintInfo>(),
|
||||
MAX_BANKS * 2 * 32 + 4 * 32 + 2 + 2 + 4
|
||||
MAX_BANKS * 2 * 32 + 4 * 32 + 2 + 6 + 2 + 6
|
||||
);
|
||||
const_assert_eq!(size_of::<MintInfo>() % 8, 0);
|
||||
|
||||
|
@ -44,4 +49,25 @@ impl MintInfo {
|
|||
pub fn first_vault(&self) -> Pubkey {
|
||||
self.vaults[0]
|
||||
}
|
||||
|
||||
pub fn num_banks(&self) -> usize {
|
||||
self.banks
|
||||
.iter()
|
||||
.position(|&b| b == Pubkey::default())
|
||||
.unwrap_or(MAX_BANKS)
|
||||
}
|
||||
|
||||
pub fn banks(&self) -> &[Pubkey] {
|
||||
&self.banks[..self.num_banks()]
|
||||
}
|
||||
|
||||
pub fn verify_banks_ais(&self, all_bank_ais: &[AccountInfo]) -> Result<()> {
|
||||
require_msg!(
|
||||
all_bank_ais.iter().map(|ai| ai.key).eq(self.banks().iter()),
|
||||
"the passed banks {:?} don't match banks in mint_info {:?}",
|
||||
all_bank_ais.iter().map(|ai| ai.key).collect::<Vec<_>>(),
|
||||
self.banks()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,8 +12,6 @@ use crate::accounts_zerocopy::*;
|
|||
use crate::checked_math as cm;
|
||||
use crate::error::MangoError;
|
||||
|
||||
pub const QUOTE_DECIMALS: i8 = 6;
|
||||
|
||||
const LOOKUP_START: i8 = -12;
|
||||
const LOOKUP: [I80F48; 25] = [
|
||||
I80F48::from_bits((1 << 48) / 10i128.pow(12u32)),
|
||||
|
@ -44,6 +42,9 @@ const LOOKUP: [I80F48; 25] = [
|
|||
];
|
||||
const LOOKUP_FN: fn(i8) -> usize = |decimals: i8| (decimals - LOOKUP_START) as usize;
|
||||
|
||||
pub const QUOTE_DECIMALS: i8 = 6;
|
||||
pub const QUOTE_NATIVE_TO_UI: I80F48 = LOOKUP[(-QUOTE_DECIMALS - LOOKUP_START) as usize];
|
||||
|
||||
pub mod switchboard_v1_devnet_oracle {
|
||||
use solana_program::declare_id;
|
||||
declare_id!("7azgmy1pFXHikv36q1zZASvFq5vFa39TT9NweVugKKTU");
|
||||
|
@ -71,7 +72,9 @@ pub enum OracleType {
|
|||
|
||||
#[account(zero_copy)]
|
||||
pub struct StubOracle {
|
||||
// ABI: Clients rely on this being at offset 8
|
||||
pub group: Pubkey,
|
||||
// ABI: Clients rely on this being at offset 40
|
||||
pub mint: Pubkey,
|
||||
pub price: I80F48,
|
||||
pub last_updated: i64,
|
||||
|
|
|
@ -6,19 +6,30 @@ use fixed::types::I80F48;
|
|||
use static_assertions::const_assert_eq;
|
||||
|
||||
use crate::state::orderbook::order_type::Side;
|
||||
use crate::state::{TokenIndex, DAY};
|
||||
use crate::state::TokenIndex;
|
||||
use crate::util::checked_math as cm;
|
||||
|
||||
use super::{Book, OracleConfig};
|
||||
use super::{Book, OracleConfig, DAY_I80F48};
|
||||
|
||||
pub type PerpMarketIndex = u16;
|
||||
|
||||
#[account(zero_copy)]
|
||||
#[derive(Debug)]
|
||||
pub struct PerpMarket {
|
||||
pub name: [u8; 16],
|
||||
|
||||
// ABI: Clients rely on this being at offset 8
|
||||
pub group: Pubkey,
|
||||
|
||||
// TODO: Remove!
|
||||
// ABI: Clients rely on this being at offset 40
|
||||
pub base_token_index: TokenIndex,
|
||||
|
||||
/// Lookup indices
|
||||
pub perp_market_index: PerpMarketIndex,
|
||||
|
||||
pub padding: [u8; 4],
|
||||
|
||||
pub name: [u8; 16],
|
||||
|
||||
pub oracle: Pubkey,
|
||||
|
||||
pub oracle_config: OracleConfig,
|
||||
|
@ -75,18 +86,12 @@ pub struct PerpMarket {
|
|||
|
||||
pub base_token_decimals: u8,
|
||||
|
||||
/// Lookup indices
|
||||
pub perp_market_index: PerpMarketIndex,
|
||||
|
||||
pub base_token_index: TokenIndex,
|
||||
|
||||
/// Cannot be chosen freely, must be the health-reference token, same for all PerpMarkets
|
||||
pub quote_token_index: TokenIndex,
|
||||
pub reserved: [u8; 6],
|
||||
}
|
||||
|
||||
const_assert_eq!(
|
||||
size_of::<PerpMarket>(),
|
||||
16 + 32 * 2 + 16 + 32 * 3 + 8 * 2 + 16 * 11 + 8 * 2 + 8 * 2 + 16 + 8
|
||||
32 + 2 + 2 + 4 + 16 + 32 + 16 + 32 * 3 + 8 * 2 + 16 * 11 + 8 * 2 + 8 * 2 + 16 + 2 + 6
|
||||
);
|
||||
const_assert_eq!(size_of::<PerpMarket>() % 8, 0);
|
||||
|
||||
|
@ -129,7 +134,7 @@ impl PerpMarket {
|
|||
};
|
||||
|
||||
let diff_ts = I80F48::from_num(now_ts - self.funding_last_updated as u64);
|
||||
let time_factor = cm!(diff_ts / DAY);
|
||||
let time_factor = cm!(diff_ts / DAY_I80F48);
|
||||
let base_lot_size = I80F48::from_num(self.base_lot_size);
|
||||
let funding_delta = cm!(index_price * diff_price * base_lot_size * time_factor);
|
||||
|
||||
|
|
|
@ -9,19 +9,26 @@ pub type Serum3MarketIndex = u16;
|
|||
#[account(zero_copy)]
|
||||
#[derive(Debug)]
|
||||
pub struct Serum3Market {
|
||||
pub name: [u8; 16],
|
||||
// ABI: Clients rely on this being at offset 8
|
||||
pub group: Pubkey,
|
||||
// ABI: Clients rely on this being at offset 40
|
||||
pub base_token_index: TokenIndex,
|
||||
// ABI: Clients rely on this being at offset 42
|
||||
pub quote_token_index: TokenIndex,
|
||||
pub padding: [u8; 4],
|
||||
pub name: [u8; 16],
|
||||
pub serum_program: Pubkey,
|
||||
pub serum_market_external: Pubkey,
|
||||
|
||||
pub market_index: Serum3MarketIndex,
|
||||
pub base_token_index: TokenIndex,
|
||||
pub quote_token_index: TokenIndex,
|
||||
|
||||
pub bump: u8,
|
||||
pub reserved: [u8; 1],
|
||||
pub reserved: [u8; 5],
|
||||
}
|
||||
const_assert_eq!(size_of::<Serum3Market>(), 16 + 32 * 3 + 3 * 2 + 1 + 1);
|
||||
const_assert_eq!(
|
||||
size_of::<Serum3Market>(),
|
||||
32 + 2 + 2 + 4 + 16 + 2 * 32 + 2 + 1 + 5
|
||||
);
|
||||
const_assert_eq!(size_of::<Serum3Market>() % 8, 0);
|
||||
|
||||
impl Serum3Market {
|
||||
|
|
|
@ -13,8 +13,6 @@ use solana_program::instruction::Instruction;
|
|||
use solana_sdk::instruction;
|
||||
use solana_sdk::signature::{Keypair, Signer};
|
||||
use solana_sdk::transport::TransportError;
|
||||
use spl_associated_token_account::get_associated_token_address;
|
||||
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
|
||||
|
@ -233,7 +231,9 @@ async fn derive_liquidation_remaining_account_metas(
|
|||
liqee: &MangoAccount,
|
||||
liqor: &MangoAccount,
|
||||
asset_token_index: TokenIndex,
|
||||
asset_bank_index: usize,
|
||||
liab_token_index: TokenIndex,
|
||||
liab_bank_index: usize,
|
||||
) -> Vec<AccountMeta> {
|
||||
let mut banks = vec![];
|
||||
let mut oracles = vec![];
|
||||
|
@ -245,7 +245,13 @@ async fn derive_liquidation_remaining_account_metas(
|
|||
.unique();
|
||||
for token_index in token_indexes {
|
||||
let mint_info = get_mint_info_by_token_index(account_loader, liqee, token_index).await;
|
||||
let writable_bank = token_index == asset_token_index || token_index == liab_token_index;
|
||||
let (bank_index, writable_bank) = if token_index == asset_token_index {
|
||||
(asset_bank_index, true)
|
||||
} else if token_index == liab_token_index {
|
||||
(liab_bank_index, true)
|
||||
} else {
|
||||
(0, false)
|
||||
};
|
||||
// TODO: ALTs are unavailable
|
||||
// let lookup_table = account_loader
|
||||
// .load_bytes(&mint_info.address_lookup_table)
|
||||
|
@ -257,7 +263,7 @@ async fn derive_liquidation_remaining_account_metas(
|
|||
// writable_bank,
|
||||
// ));
|
||||
// oracles.push(addresses[mint_info.address_lookup_table_oracle_index as usize]);
|
||||
banks.push((mint_info.first_bank(), writable_bank));
|
||||
banks.push((mint_info.banks[bank_index], writable_bank));
|
||||
oracles.push(mint_info.oracle);
|
||||
}
|
||||
|
||||
|
@ -308,6 +314,12 @@ pub async fn account_position(solana: &SolanaCookie, account: Pubkey, bank: Pubk
|
|||
native.round().to_num::<i64>()
|
||||
}
|
||||
|
||||
pub async fn account_position_closed(solana: &SolanaCookie, account: Pubkey, bank: Pubkey) -> bool {
|
||||
let account_data: MangoAccount = solana.get_account(account).await;
|
||||
let bank_data: Bank = solana.get_account(bank).await;
|
||||
account_data.tokens.find(bank_data.token_index).is_none()
|
||||
}
|
||||
|
||||
pub async fn account_position_f64(solana: &SolanaCookie, account: Pubkey, bank: Pubkey) -> f64 {
|
||||
let account_data: MangoAccount = solana.get_account(account).await;
|
||||
let bank_data: Bank = solana.get_account(bank).await;
|
||||
|
@ -629,6 +641,7 @@ pub struct TokenWithdrawInstruction<'keypair> {
|
|||
pub account: Pubkey,
|
||||
pub owner: &'keypair Keypair,
|
||||
pub token_account: Pubkey,
|
||||
pub bank_index: usize,
|
||||
}
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl<'keypair> ClientInstruction for TokenWithdrawInstruction<'keypair> {
|
||||
|
@ -661,7 +674,7 @@ impl<'keypair> ClientInstruction for TokenWithdrawInstruction<'keypair> {
|
|||
let health_check_metas = derive_health_check_remaining_account_metas(
|
||||
&account_loader,
|
||||
&account,
|
||||
Some(mint_info.first_bank()),
|
||||
Some(mint_info.banks[self.bank_index]),
|
||||
false,
|
||||
None,
|
||||
)
|
||||
|
@ -671,8 +684,8 @@ impl<'keypair> ClientInstruction for TokenWithdrawInstruction<'keypair> {
|
|||
group: account.group,
|
||||
account: self.account,
|
||||
owner: self.owner.pubkey(),
|
||||
bank: mint_info.first_bank(),
|
||||
vault: mint_info.first_vault(),
|
||||
bank: mint_info.banks[self.bank_index],
|
||||
vault: mint_info.vaults[self.bank_index],
|
||||
token_account: self.token_account,
|
||||
token_program: Token::id(),
|
||||
};
|
||||
|
@ -688,15 +701,16 @@ impl<'keypair> ClientInstruction for TokenWithdrawInstruction<'keypair> {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct TokenDepositInstruction<'keypair> {
|
||||
pub struct TokenDepositInstruction {
|
||||
pub amount: u64,
|
||||
|
||||
pub account: Pubkey,
|
||||
pub token_account: Pubkey,
|
||||
pub token_authority: &'keypair Keypair,
|
||||
pub token_authority: Keypair,
|
||||
pub bank_index: usize,
|
||||
}
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl<'keypair> ClientInstruction for TokenDepositInstruction<'keypair> {
|
||||
impl ClientInstruction for TokenDepositInstruction {
|
||||
type Accounts = mango_v4::accounts::TokenDeposit;
|
||||
type Instruction = mango_v4::instruction::TokenDeposit;
|
||||
async fn to_instruction(
|
||||
|
@ -725,7 +739,7 @@ impl<'keypair> ClientInstruction for TokenDepositInstruction<'keypair> {
|
|||
let health_check_metas = derive_health_check_remaining_account_metas(
|
||||
&account_loader,
|
||||
&account,
|
||||
Some(mint_info.first_bank()),
|
||||
Some(mint_info.banks[self.bank_index]),
|
||||
false,
|
||||
None,
|
||||
)
|
||||
|
@ -734,8 +748,8 @@ impl<'keypair> ClientInstruction for TokenDepositInstruction<'keypair> {
|
|||
let accounts = Self::Accounts {
|
||||
group: account.group,
|
||||
account: self.account,
|
||||
bank: mint_info.first_bank(),
|
||||
vault: mint_info.first_vault(),
|
||||
bank: mint_info.banks[self.bank_index],
|
||||
vault: mint_info.vaults[self.bank_index],
|
||||
token_account: self.token_account,
|
||||
token_authority: self.token_authority.pubkey(),
|
||||
token_program: Token::id(),
|
||||
|
@ -748,13 +762,14 @@ impl<'keypair> ClientInstruction for TokenDepositInstruction<'keypair> {
|
|||
}
|
||||
|
||||
fn signers(&self) -> Vec<&Keypair> {
|
||||
vec![self.token_authority]
|
||||
vec![&self.token_authority]
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TokenRegisterInstruction<'keypair> {
|
||||
pub token_index: TokenIndex,
|
||||
pub decimals: u8,
|
||||
pub adjustment_factor: f32,
|
||||
pub util0: f32,
|
||||
pub rate0: f32,
|
||||
pub util1: f32,
|
||||
|
@ -791,6 +806,7 @@ impl<'keypair> ClientInstruction for TokenRegisterInstruction<'keypair> {
|
|||
conf_filter: I80F48::from_num::<f32>(0.10),
|
||||
},
|
||||
interest_rate_params: InterestRateParams {
|
||||
adjustment_factor: self.adjustment_factor,
|
||||
util0: self.util0,
|
||||
rate0: self.rate0,
|
||||
util1: self.util1,
|
||||
|
@ -879,7 +895,6 @@ pub struct TokenAddBankInstruction<'keypair> {
|
|||
|
||||
pub group: Pubkey,
|
||||
pub admin: &'keypair Keypair,
|
||||
pub mint: Pubkey,
|
||||
pub address_lookup_table: Pubkey,
|
||||
pub payer: &'keypair Keypair,
|
||||
}
|
||||
|
@ -889,7 +904,7 @@ impl<'keypair> ClientInstruction for TokenAddBankInstruction<'keypair> {
|
|||
type Instruction = mango_v4::instruction::TokenAddBank;
|
||||
async fn to_instruction(
|
||||
&self,
|
||||
_account_loader: impl ClientAccountLoader + 'async_trait,
|
||||
account_loader: impl ClientAccountLoader + 'async_trait,
|
||||
) -> (Self::Accounts, instruction::Instruction) {
|
||||
let program_id = mango_v4::id();
|
||||
let instruction = Self::Instruction {
|
||||
|
@ -927,12 +942,12 @@ impl<'keypair> ClientInstruction for TokenAddBankInstruction<'keypair> {
|
|||
&program_id,
|
||||
)
|
||||
.0;
|
||||
|
||||
let existing_bank_data: Bank = account_loader.load(&existing_bank).await.unwrap();
|
||||
let mint = existing_bank_data.mint;
|
||||
|
||||
let mint_info = Pubkey::find_program_address(
|
||||
&[
|
||||
self.group.as_ref(),
|
||||
b"MintInfo".as_ref(),
|
||||
self.mint.as_ref(),
|
||||
],
|
||||
&[self.group.as_ref(), b"MintInfo".as_ref(), mint.as_ref()],
|
||||
&program_id,
|
||||
)
|
||||
.0;
|
||||
|
@ -940,7 +955,7 @@ impl<'keypair> ClientInstruction for TokenAddBankInstruction<'keypair> {
|
|||
let accounts = Self::Accounts {
|
||||
group: self.group,
|
||||
admin: self.admin.pubkey(),
|
||||
mint: self.mint,
|
||||
mint: mint,
|
||||
existing_bank,
|
||||
bank,
|
||||
vault,
|
||||
|
@ -1031,7 +1046,7 @@ impl<'keypair> ClientInstruction for TokenDeregisterInstruction<'keypair> {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct SetStubOracleInstruction<'keypair> {
|
||||
pub struct StubOracleSetInstruction<'keypair> {
|
||||
pub mint: Pubkey,
|
||||
pub group: Pubkey,
|
||||
pub admin: &'keypair Keypair,
|
||||
|
@ -1039,9 +1054,9 @@ pub struct SetStubOracleInstruction<'keypair> {
|
|||
pub price: &'static str,
|
||||
}
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl<'keypair> ClientInstruction for SetStubOracleInstruction<'keypair> {
|
||||
type Accounts = mango_v4::accounts::SetStubOracle;
|
||||
type Instruction = mango_v4::instruction::SetStubOracle;
|
||||
impl<'keypair> ClientInstruction for StubOracleSetInstruction<'keypair> {
|
||||
type Accounts = mango_v4::accounts::StubOracleSet;
|
||||
type Instruction = mango_v4::instruction::StubOracleSet;
|
||||
|
||||
async fn to_instruction(
|
||||
&self,
|
||||
|
@ -1078,16 +1093,16 @@ impl<'keypair> ClientInstruction for SetStubOracleInstruction<'keypair> {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct CreateStubOracle<'keypair> {
|
||||
pub struct StubOracleCreate<'keypair> {
|
||||
pub group: Pubkey,
|
||||
pub mint: Pubkey,
|
||||
pub admin: &'keypair Keypair,
|
||||
pub payer: &'keypair Keypair,
|
||||
}
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl<'keypair> ClientInstruction for CreateStubOracle<'keypair> {
|
||||
type Accounts = mango_v4::accounts::CreateStubOracle;
|
||||
type Instruction = mango_v4::instruction::CreateStubOracle;
|
||||
impl<'keypair> ClientInstruction for StubOracleCreate<'keypair> {
|
||||
type Accounts = mango_v4::accounts::StubOracleCreate;
|
||||
type Instruction = mango_v4::instruction::StubOracleCreate;
|
||||
|
||||
async fn to_instruction(
|
||||
&self,
|
||||
|
@ -1126,16 +1141,16 @@ impl<'keypair> ClientInstruction for CreateStubOracle<'keypair> {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct CloseStubOracleInstruction<'keypair> {
|
||||
pub struct StubOracleCloseInstruction<'keypair> {
|
||||
pub group: Pubkey,
|
||||
pub mint: Pubkey,
|
||||
pub admin: &'keypair Keypair,
|
||||
pub sol_destination: Pubkey,
|
||||
}
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl<'keypair> ClientInstruction for CloseStubOracleInstruction<'keypair> {
|
||||
type Accounts = mango_v4::accounts::CloseStubOracle;
|
||||
type Instruction = mango_v4::instruction::CloseStubOracle;
|
||||
impl<'keypair> ClientInstruction for StubOracleCloseInstruction<'keypair> {
|
||||
type Accounts = mango_v4::accounts::StubOracleClose;
|
||||
type Instruction = mango_v4::instruction::StubOracleClose;
|
||||
|
||||
async fn to_instruction(
|
||||
&self,
|
||||
|
@ -1171,14 +1186,15 @@ impl<'keypair> ClientInstruction for CloseStubOracleInstruction<'keypair> {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct CreateGroupInstruction<'keypair> {
|
||||
pub struct GroupCreateInstruction<'keypair> {
|
||||
pub admin: &'keypair Keypair,
|
||||
pub payer: &'keypair Keypair,
|
||||
pub insurance_mint: Pubkey,
|
||||
}
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl<'keypair> ClientInstruction for CreateGroupInstruction<'keypair> {
|
||||
type Accounts = mango_v4::accounts::CreateGroup;
|
||||
type Instruction = mango_v4::instruction::CreateGroup;
|
||||
impl<'keypair> ClientInstruction for GroupCreateInstruction<'keypair> {
|
||||
type Accounts = mango_v4::accounts::GroupCreate;
|
||||
type Instruction = mango_v4::instruction::GroupCreate;
|
||||
async fn to_instruction(
|
||||
&self,
|
||||
_account_loader: impl ClientAccountLoader + 'async_trait,
|
||||
|
@ -1199,11 +1215,21 @@ impl<'keypair> ClientInstruction for CreateGroupInstruction<'keypair> {
|
|||
)
|
||||
.0;
|
||||
|
||||
let insurance_vault = Pubkey::find_program_address(
|
||||
&[group.as_ref(), b"InsuranceVault".as_ref()],
|
||||
&program_id,
|
||||
)
|
||||
.0;
|
||||
|
||||
let accounts = Self::Accounts {
|
||||
group,
|
||||
admin: self.admin.pubkey(),
|
||||
insurance_mint: self.insurance_mint,
|
||||
insurance_vault,
|
||||
payer: self.payer.pubkey(),
|
||||
token_program: Token::id(),
|
||||
system_program: System::id(),
|
||||
rent: sysvar::rent::Rent::id(),
|
||||
};
|
||||
|
||||
let instruction = make_instruction(program_id, &accounts, instruction);
|
||||
|
@ -1215,15 +1241,15 @@ impl<'keypair> ClientInstruction for CreateGroupInstruction<'keypair> {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct CloseGroupInstruction<'keypair> {
|
||||
pub struct GroupCloseInstruction<'keypair> {
|
||||
pub admin: &'keypair Keypair,
|
||||
pub group: Pubkey,
|
||||
pub sol_destination: Pubkey,
|
||||
}
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl<'keypair> ClientInstruction for CloseGroupInstruction<'keypair> {
|
||||
type Accounts = mango_v4::accounts::CloseGroup;
|
||||
type Instruction = mango_v4::instruction::CloseGroup;
|
||||
impl<'keypair> ClientInstruction for GroupCloseInstruction<'keypair> {
|
||||
type Accounts = mango_v4::accounts::GroupClose;
|
||||
type Instruction = mango_v4::instruction::GroupClose;
|
||||
async fn to_instruction(
|
||||
&self,
|
||||
_account_loader: impl ClientAccountLoader + 'async_trait,
|
||||
|
@ -1247,7 +1273,7 @@ impl<'keypair> ClientInstruction for CloseGroupInstruction<'keypair> {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct CreateAccountInstruction<'keypair> {
|
||||
pub struct AccountCreateInstruction<'keypair> {
|
||||
pub account_num: u8,
|
||||
|
||||
pub group: Pubkey,
|
||||
|
@ -1255,15 +1281,15 @@ pub struct CreateAccountInstruction<'keypair> {
|
|||
pub payer: &'keypair Keypair,
|
||||
}
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl<'keypair> ClientInstruction for CreateAccountInstruction<'keypair> {
|
||||
type Accounts = mango_v4::accounts::CreateAccount;
|
||||
type Instruction = mango_v4::instruction::CreateAccount;
|
||||
impl<'keypair> ClientInstruction for AccountCreateInstruction<'keypair> {
|
||||
type Accounts = mango_v4::accounts::AccountCreate;
|
||||
type Instruction = mango_v4::instruction::AccountCreate;
|
||||
async fn to_instruction(
|
||||
&self,
|
||||
_account_loader: impl ClientAccountLoader + 'async_trait,
|
||||
) -> (Self::Accounts, instruction::Instruction) {
|
||||
let program_id = mango_v4::id();
|
||||
let instruction = mango_v4::instruction::CreateAccount {
|
||||
let instruction = mango_v4::instruction::AccountCreate {
|
||||
account_num: self.account_num,
|
||||
name: "my_mango_account".to_string(),
|
||||
};
|
||||
|
@ -1279,7 +1305,7 @@ impl<'keypair> ClientInstruction for CreateAccountInstruction<'keypair> {
|
|||
)
|
||||
.0;
|
||||
|
||||
let accounts = mango_v4::accounts::CreateAccount {
|
||||
let accounts = mango_v4::accounts::AccountCreate {
|
||||
group: self.group,
|
||||
owner: self.owner.pubkey(),
|
||||
account,
|
||||
|
@ -1296,16 +1322,63 @@ impl<'keypair> ClientInstruction for CreateAccountInstruction<'keypair> {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct CloseAccountInstruction<'keypair> {
|
||||
pub struct AccountEditInstruction<'keypair> {
|
||||
pub account_num: u8,
|
||||
pub group: Pubkey,
|
||||
pub owner: &'keypair Keypair,
|
||||
pub name: String,
|
||||
pub delegate: Pubkey,
|
||||
}
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl<'keypair> ClientInstruction for AccountEditInstruction<'keypair> {
|
||||
type Accounts = mango_v4::accounts::AccountEdit;
|
||||
type Instruction = mango_v4::instruction::AccountEdit;
|
||||
async fn to_instruction(
|
||||
&self,
|
||||
_account_loader: impl ClientAccountLoader + 'async_trait,
|
||||
) -> (Self::Accounts, instruction::Instruction) {
|
||||
let program_id = mango_v4::id();
|
||||
let instruction = mango_v4::instruction::AccountEdit {
|
||||
name_opt: Option::from(self.name.to_string()),
|
||||
delegate_opt: Option::from(self.delegate),
|
||||
};
|
||||
|
||||
let account = Pubkey::find_program_address(
|
||||
&[
|
||||
self.group.as_ref(),
|
||||
b"MangoAccount".as_ref(),
|
||||
self.owner.pubkey().as_ref(),
|
||||
&self.account_num.to_le_bytes(),
|
||||
],
|
||||
&program_id,
|
||||
)
|
||||
.0;
|
||||
|
||||
let accounts = mango_v4::accounts::AccountEdit {
|
||||
group: self.group,
|
||||
account,
|
||||
owner: self.owner.pubkey(),
|
||||
};
|
||||
|
||||
let instruction = make_instruction(program_id, &accounts, instruction);
|
||||
(accounts, instruction)
|
||||
}
|
||||
|
||||
fn signers(&self) -> Vec<&Keypair> {
|
||||
vec![self.owner]
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AccountCloseInstruction<'keypair> {
|
||||
pub group: Pubkey,
|
||||
pub account: Pubkey,
|
||||
pub owner: &'keypair Keypair,
|
||||
pub sol_destination: Pubkey,
|
||||
}
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl<'keypair> ClientInstruction for CloseAccountInstruction<'keypair> {
|
||||
type Accounts = mango_v4::accounts::CloseAccount;
|
||||
type Instruction = mango_v4::instruction::CloseAccount;
|
||||
impl<'keypair> ClientInstruction for AccountCloseInstruction<'keypair> {
|
||||
type Accounts = mango_v4::accounts::AccountClose;
|
||||
type Instruction = mango_v4::instruction::AccountClose;
|
||||
async fn to_instruction(
|
||||
&self,
|
||||
_account_loader: impl ClientAccountLoader + 'async_trait,
|
||||
|
@ -1939,7 +2012,9 @@ pub struct LiqTokenWithTokenInstruction<'keypair> {
|
|||
pub liqor_owner: &'keypair Keypair,
|
||||
|
||||
pub asset_token_index: TokenIndex,
|
||||
pub asset_bank_index: usize,
|
||||
pub liab_token_index: TokenIndex,
|
||||
pub liab_bank_index: usize,
|
||||
pub max_liab_transfer: I80F48,
|
||||
}
|
||||
#[async_trait::async_trait(?Send)]
|
||||
|
@ -1964,7 +2039,9 @@ impl<'keypair> ClientInstruction for LiqTokenWithTokenInstruction<'keypair> {
|
|||
&liqee,
|
||||
&liqor,
|
||||
self.asset_token_index,
|
||||
self.asset_bank_index,
|
||||
self.liab_token_index,
|
||||
self.liab_bank_index,
|
||||
)
|
||||
.await;
|
||||
|
||||
|
@ -1986,6 +2063,94 @@ impl<'keypair> ClientInstruction for LiqTokenWithTokenInstruction<'keypair> {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct LiqTokenBankruptcyInstruction<'keypair> {
|
||||
pub liqee: Pubkey,
|
||||
pub liqor: Pubkey,
|
||||
pub liqor_owner: &'keypair Keypair,
|
||||
|
||||
pub liab_token_index: TokenIndex,
|
||||
pub max_liab_transfer: I80F48,
|
||||
pub liab_mint_info: Pubkey,
|
||||
}
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl<'keypair> ClientInstruction for LiqTokenBankruptcyInstruction<'keypair> {
|
||||
type Accounts = mango_v4::accounts::LiqTokenBankruptcy;
|
||||
type Instruction = mango_v4::instruction::LiqTokenBankruptcy;
|
||||
async fn to_instruction(
|
||||
&self,
|
||||
account_loader: impl ClientAccountLoader + 'async_trait,
|
||||
) -> (Self::Accounts, instruction::Instruction) {
|
||||
let program_id = mango_v4::id();
|
||||
let instruction = Self::Instruction {
|
||||
liab_token_index: self.liab_token_index,
|
||||
max_liab_transfer: self.max_liab_transfer,
|
||||
};
|
||||
|
||||
let liab_mint_info: MintInfo = account_loader.load(&self.liab_mint_info).await.unwrap();
|
||||
let liqee: MangoAccount = account_loader.load(&self.liqee).await.unwrap();
|
||||
let liqor: MangoAccount = account_loader.load(&self.liqor).await.unwrap();
|
||||
let health_check_metas = derive_liquidation_remaining_account_metas(
|
||||
&account_loader,
|
||||
&liqee,
|
||||
&liqor,
|
||||
QUOTE_TOKEN_INDEX,
|
||||
0,
|
||||
self.liab_token_index,
|
||||
0,
|
||||
)
|
||||
.await;
|
||||
|
||||
let group: Group = account_loader.load(&liqee.group).await.unwrap();
|
||||
|
||||
let quote_mint_info = Pubkey::find_program_address(
|
||||
&[
|
||||
liqee.group.as_ref(),
|
||||
b"MintInfo".as_ref(),
|
||||
group.insurance_mint.as_ref(),
|
||||
],
|
||||
&program_id,
|
||||
)
|
||||
.0;
|
||||
let quote_mint_info: MintInfo = account_loader.load("e_mint_info).await.unwrap();
|
||||
|
||||
let insurance_vault = Pubkey::find_program_address(
|
||||
&[liqee.group.as_ref(), b"InsuranceVault".as_ref()],
|
||||
&program_id,
|
||||
)
|
||||
.0;
|
||||
|
||||
let accounts = Self::Accounts {
|
||||
group: liqee.group,
|
||||
liqee: self.liqee,
|
||||
liqor: self.liqor,
|
||||
liqor_owner: self.liqor_owner.pubkey(),
|
||||
liab_mint_info: self.liab_mint_info,
|
||||
quote_vault: quote_mint_info.first_vault(),
|
||||
insurance_vault,
|
||||
token_program: Token::id(),
|
||||
};
|
||||
|
||||
let mut instruction = make_instruction(program_id, &accounts, instruction);
|
||||
let mut bank_ams = liab_mint_info
|
||||
.banks()
|
||||
.iter()
|
||||
.map(|bank| AccountMeta {
|
||||
pubkey: *bank,
|
||||
is_signer: false,
|
||||
is_writable: true,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
instruction.accounts.append(&mut bank_ams);
|
||||
instruction.accounts.extend(health_check_metas.into_iter());
|
||||
|
||||
(accounts, instruction)
|
||||
}
|
||||
|
||||
fn signers(&self) -> Vec<&Keypair> {
|
||||
vec![self.liqor_owner]
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PerpCreateMarketInstruction<'keypair> {
|
||||
pub group: Pubkey,
|
||||
pub admin: &'keypair Keypair,
|
||||
|
@ -1997,7 +2162,6 @@ pub struct PerpCreateMarketInstruction<'keypair> {
|
|||
pub perp_market_index: PerpMarketIndex,
|
||||
pub base_token_index: TokenIndex,
|
||||
pub base_token_decimals: u8,
|
||||
pub quote_token_index: TokenIndex,
|
||||
pub quote_lot_size: i64,
|
||||
pub base_lot_size: i64,
|
||||
pub maint_asset_weight: f32,
|
||||
|
@ -2024,7 +2188,6 @@ impl<'keypair> ClientInstruction for PerpCreateMarketInstruction<'keypair> {
|
|||
},
|
||||
perp_market_index: self.perp_market_index,
|
||||
base_token_index_opt: Option::from(self.base_token_index),
|
||||
quote_token_index: self.quote_token_index,
|
||||
quote_lot_size: self.quote_lot_size,
|
||||
base_lot_size: self.base_lot_size,
|
||||
maint_asset_weight: self.maint_asset_weight,
|
||||
|
@ -2384,29 +2547,32 @@ impl ClientInstruction for BenchmarkInstruction {
|
|||
vec![]
|
||||
}
|
||||
}
|
||||
pub struct UpdateIndexInstruction {
|
||||
pub struct TokenUpdateIndexAndRateInstruction {
|
||||
pub mint_info: Pubkey,
|
||||
pub banks: Vec<Pubkey>,
|
||||
}
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl ClientInstruction for UpdateIndexInstruction {
|
||||
type Accounts = mango_v4::accounts::UpdateIndex;
|
||||
type Instruction = mango_v4::instruction::UpdateIndex;
|
||||
impl ClientInstruction for TokenUpdateIndexAndRateInstruction {
|
||||
type Accounts = mango_v4::accounts::TokenUpdateIndexAndRate;
|
||||
type Instruction = mango_v4::instruction::TokenUpdateIndexAndRate;
|
||||
async fn to_instruction(
|
||||
&self,
|
||||
_loader: impl ClientAccountLoader + 'async_trait,
|
||||
loader: impl ClientAccountLoader + 'async_trait,
|
||||
) -> (Self::Accounts, instruction::Instruction) {
|
||||
let program_id = mango_v4::id();
|
||||
let instruction = Self::Instruction {};
|
||||
|
||||
let mint_info: MintInfo = loader.load(&self.mint_info).await.unwrap();
|
||||
|
||||
let accounts = Self::Accounts {
|
||||
mint_info: self.mint_info,
|
||||
oracle: mint_info.oracle,
|
||||
instructions: solana_program::sysvar::instructions::id(),
|
||||
};
|
||||
|
||||
let mut instruction = make_instruction(program_id, &accounts, instruction);
|
||||
let mut bank_ams = self
|
||||
.banks
|
||||
let mut bank_ams = mint_info
|
||||
.banks()
|
||||
.iter()
|
||||
.filter(|bank| **bank != Pubkey::default())
|
||||
.map(|bank| AccountMeta {
|
||||
pubkey: *bank,
|
||||
is_signer: false,
|
||||
|
|
|
@ -18,12 +18,14 @@ pub struct Token {
|
|||
pub mint: MintCookie,
|
||||
pub oracle: Pubkey,
|
||||
pub bank: Pubkey,
|
||||
pub bank1: Pubkey,
|
||||
pub vault: Pubkey,
|
||||
pub mint_info: Pubkey,
|
||||
}
|
||||
|
||||
pub struct GroupWithTokens {
|
||||
pub group: Pubkey,
|
||||
pub insurance_vault: Pubkey,
|
||||
pub tokens: Vec<Token>,
|
||||
}
|
||||
|
||||
|
@ -34,10 +36,18 @@ impl<'a> GroupWithTokensConfig<'a> {
|
|||
payer,
|
||||
mints,
|
||||
} = self;
|
||||
let group = send_tx(solana, CreateGroupInstruction { admin, payer })
|
||||
.await
|
||||
.unwrap()
|
||||
.group;
|
||||
let create_group_accounts = send_tx(
|
||||
solana,
|
||||
GroupCreateInstruction {
|
||||
admin,
|
||||
payer,
|
||||
insurance_mint: mints[0].pubkey,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let group = create_group_accounts.group;
|
||||
let insurance_vault = create_group_accounts.insurance_vault;
|
||||
|
||||
let address_lookup_table = solana.create_address_lookup_table(admin, payer).await;
|
||||
|
||||
|
@ -45,7 +55,7 @@ impl<'a> GroupWithTokensConfig<'a> {
|
|||
for (index, mint) in mints.iter().enumerate() {
|
||||
let create_stub_oracle_accounts = send_tx(
|
||||
solana,
|
||||
CreateStubOracle {
|
||||
StubOracleCreate {
|
||||
group,
|
||||
mint: mint.pubkey,
|
||||
admin,
|
||||
|
@ -57,7 +67,7 @@ impl<'a> GroupWithTokensConfig<'a> {
|
|||
let oracle = create_stub_oracle_accounts.oracle;
|
||||
send_tx(
|
||||
solana,
|
||||
SetStubOracleInstruction {
|
||||
StubOracleSetInstruction {
|
||||
group,
|
||||
admin,
|
||||
mint: mint.pubkey,
|
||||
|
@ -73,6 +83,7 @@ impl<'a> GroupWithTokensConfig<'a> {
|
|||
TokenRegisterInstruction {
|
||||
token_index,
|
||||
decimals: mint.decimals,
|
||||
adjustment_factor: 0.01,
|
||||
util0: 0.40,
|
||||
rate0: 0.07,
|
||||
util1: 0.80,
|
||||
|
@ -94,14 +105,13 @@ impl<'a> GroupWithTokensConfig<'a> {
|
|||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let _ = send_tx(
|
||||
let add_bank_accounts = send_tx(
|
||||
solana,
|
||||
TokenAddBankInstruction {
|
||||
token_index,
|
||||
bank_num: 1,
|
||||
group,
|
||||
admin,
|
||||
mint: mint.pubkey,
|
||||
address_lookup_table,
|
||||
payer,
|
||||
},
|
||||
|
@ -117,11 +127,16 @@ impl<'a> GroupWithTokensConfig<'a> {
|
|||
mint: mint.clone(),
|
||||
oracle,
|
||||
bank,
|
||||
bank1: add_bank_accounts.bank,
|
||||
vault,
|
||||
mint_info,
|
||||
});
|
||||
}
|
||||
|
||||
GroupWithTokens { group, tokens }
|
||||
GroupWithTokens {
|
||||
group,
|
||||
insurance_vault,
|
||||
tokens,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ use bytemuck::{bytes_of, Contiguous};
|
|||
use solana_program::program_error::ProgramError;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use solana_sdk::signature::Keypair;
|
||||
use std::ops::Deref;
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn gen_signer_seeds<'a>(nonce: &'a u64, acc_pk: &'a Pubkey) -> [&'a [u8]; 2] {
|
||||
|
@ -32,3 +33,47 @@ pub fn create_signer_key_and_nonce(program_id: &Pubkey, acc_pk: &Pubkey) -> (Pub
|
|||
pub fn clone_keypair(keypair: &Keypair) -> Keypair {
|
||||
Keypair::from_base58_string(&keypair.to_base58_string())
|
||||
}
|
||||
|
||||
// Add clone() to Keypair, totally safe in tests
|
||||
pub trait ClonableKeypair {
|
||||
fn clone(&self) -> Self;
|
||||
}
|
||||
impl ClonableKeypair for Keypair {
|
||||
fn clone(&self) -> Self {
|
||||
clone_keypair(self)
|
||||
}
|
||||
}
|
||||
|
||||
// Make a clonable and defaultable Keypair newtype
|
||||
pub struct TestKeypair(pub Keypair);
|
||||
impl Clone for TestKeypair {
|
||||
fn clone(&self) -> Self {
|
||||
TestKeypair(self.0.clone())
|
||||
}
|
||||
}
|
||||
impl Default for TestKeypair {
|
||||
fn default() -> Self {
|
||||
TestKeypair(Keypair::from_bytes(&[0u8; 64]).unwrap())
|
||||
}
|
||||
}
|
||||
impl AsRef<Keypair> for TestKeypair {
|
||||
fn as_ref(&self) -> &Keypair {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
impl Deref for TestKeypair {
|
||||
type Target = Keypair;
|
||||
fn deref(&self) -> &Keypair {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
impl From<&Keypair> for TestKeypair {
|
||||
fn from(k: &Keypair) -> Self {
|
||||
Self(k.clone())
|
||||
}
|
||||
}
|
||||
impl From<Keypair> for TestKeypair {
|
||||
fn from(k: Keypair) -> Self {
|
||||
Self(k)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,642 @@
|
|||
#![cfg(feature = "test-bpf")]
|
||||
|
||||
use fixed::types::I80F48;
|
||||
use solana_program_test::*;
|
||||
use solana_sdk::{
|
||||
signature::{Keypair, Signer},
|
||||
transport::TransportError,
|
||||
};
|
||||
|
||||
use mango_v4::state::*;
|
||||
use program_test::*;
|
||||
|
||||
mod program_test;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_bankrupt_tokens_socialize_loss() -> 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..4];
|
||||
let payer_mint_accounts = &context.users[1].token_accounts[0..4];
|
||||
|
||||
//
|
||||
// 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;
|
||||
let borrow_token1 = &tokens[0];
|
||||
let borrow_token2 = &tokens[1];
|
||||
let collateral_token1 = &tokens[2];
|
||||
let collateral_token2 = &tokens[3];
|
||||
|
||||
// deposit some funds, to the vaults aren't empty
|
||||
let vault_account = send_tx(
|
||||
solana,
|
||||
AccountCreateInstruction {
|
||||
account_num: 2,
|
||||
group,
|
||||
owner,
|
||||
payer,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.account;
|
||||
let vault_amount = 100000;
|
||||
for &token_account in payer_mint_accounts {
|
||||
send_tx(
|
||||
solana,
|
||||
TokenDepositInstruction {
|
||||
amount: vault_amount,
|
||||
account: vault_account,
|
||||
token_account,
|
||||
token_authority: payer.clone(),
|
||||
bank_index: 1,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// also add a tiny amount to bank0 for borrow_token1, so we can test multi-bank socialized loss
|
||||
send_tx(
|
||||
solana,
|
||||
TokenDepositInstruction {
|
||||
amount: 10,
|
||||
account: vault_account,
|
||||
token_account: payer_mint_accounts[0],
|
||||
token_authority: payer.clone(),
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
//
|
||||
// SETUP: Make an account with some collateral and some borrows
|
||||
//
|
||||
let account = send_tx(
|
||||
solana,
|
||||
AccountCreateInstruction {
|
||||
account_num: 0,
|
||||
group,
|
||||
owner,
|
||||
payer,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.account;
|
||||
|
||||
let deposit1_amount = 1000;
|
||||
let deposit2_amount = 20;
|
||||
send_tx(
|
||||
solana,
|
||||
TokenDepositInstruction {
|
||||
amount: deposit1_amount,
|
||||
account,
|
||||
token_account: payer_mint_accounts[2],
|
||||
token_authority: payer.clone(),
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
send_tx(
|
||||
solana,
|
||||
TokenDepositInstruction {
|
||||
amount: deposit2_amount,
|
||||
account,
|
||||
token_account: payer_mint_accounts[3],
|
||||
token_authority: payer.clone(),
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let borrow1_amount = 350;
|
||||
let borrow1_amount_bank0 = 10;
|
||||
let borrow1_amount_bank1 = borrow1_amount - borrow1_amount_bank0;
|
||||
let borrow2_amount = 50;
|
||||
send_tx(
|
||||
solana,
|
||||
TokenWithdrawInstruction {
|
||||
amount: borrow1_amount_bank1,
|
||||
allow_borrow: true,
|
||||
account,
|
||||
owner,
|
||||
token_account: payer_mint_accounts[0],
|
||||
bank_index: 1,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
send_tx(
|
||||
solana,
|
||||
TokenWithdrawInstruction {
|
||||
amount: borrow1_amount_bank0,
|
||||
allow_borrow: true,
|
||||
account,
|
||||
owner,
|
||||
token_account: payer_mint_accounts[0],
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
send_tx(
|
||||
solana,
|
||||
TokenWithdrawInstruction {
|
||||
amount: borrow2_amount,
|
||||
allow_borrow: true,
|
||||
account,
|
||||
owner,
|
||||
token_account: payer_mint_accounts[1],
|
||||
bank_index: 1,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
//
|
||||
// SETUP: Change the oracle to make health go very negative
|
||||
//
|
||||
send_tx(
|
||||
solana,
|
||||
StubOracleSetInstruction {
|
||||
group,
|
||||
admin,
|
||||
mint: borrow_token1.mint.pubkey,
|
||||
payer,
|
||||
price: "20.0",
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
//
|
||||
// SETUP: liquidate all the collateral against borrow1
|
||||
//
|
||||
|
||||
// eat collateral1
|
||||
send_tx(
|
||||
solana,
|
||||
LiqTokenWithTokenInstruction {
|
||||
liqee: account,
|
||||
liqor: vault_account,
|
||||
liqor_owner: owner,
|
||||
asset_token_index: collateral_token1.index,
|
||||
asset_bank_index: 1,
|
||||
liab_token_index: borrow_token1.index,
|
||||
liab_bank_index: 1,
|
||||
max_liab_transfer: I80F48::from_num(100000.0),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(account_position_closed(solana, account, collateral_token1.bank).await);
|
||||
assert_eq!(
|
||||
account_position(solana, account, borrow_token1.bank).await,
|
||||
(-350.0f64 + (1000.0 / 20.0 / 1.04)).round() as i64
|
||||
);
|
||||
let liqee: MangoAccount = solana.get_account(account).await;
|
||||
assert!(liqee.being_liquidated());
|
||||
assert!(!liqee.is_bankrupt());
|
||||
|
||||
// eat collateral2, leaving the account bankrupt
|
||||
send_tx(
|
||||
solana,
|
||||
LiqTokenWithTokenInstruction {
|
||||
liqee: account,
|
||||
liqor: vault_account,
|
||||
liqor_owner: owner,
|
||||
asset_token_index: collateral_token2.index,
|
||||
asset_bank_index: 1,
|
||||
liab_token_index: borrow_token1.index,
|
||||
liab_bank_index: 1,
|
||||
max_liab_transfer: I80F48::from_num(100000.0),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(account_position_closed(solana, account, collateral_token2.bank).await,);
|
||||
let borrow1_after_liq = -350.0f64 + (1000.0 / 20.0 / 1.04) + (20.0 / 20.0 / 1.04);
|
||||
assert_eq!(
|
||||
account_position(solana, account, borrow_token1.bank).await,
|
||||
borrow1_after_liq.round() as i64
|
||||
);
|
||||
let liqee: MangoAccount = solana.get_account(account).await;
|
||||
assert!(liqee.being_liquidated());
|
||||
assert!(liqee.is_bankrupt());
|
||||
|
||||
//
|
||||
// TEST: socialize loss on borrow1 and 2
|
||||
//
|
||||
|
||||
let vault_before = account_position(solana, vault_account, borrow_token1.bank).await;
|
||||
send_tx(
|
||||
solana,
|
||||
LiqTokenBankruptcyInstruction {
|
||||
liqee: account,
|
||||
liqor: vault_account,
|
||||
liqor_owner: owner,
|
||||
liab_token_index: borrow_token1.index,
|
||||
liab_mint_info: borrow_token1.mint_info,
|
||||
max_liab_transfer: I80F48::from_num(100000.0),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
account_position(solana, vault_account, borrow_token1.bank).await,
|
||||
vault_before + (borrow1_after_liq.round() as i64)
|
||||
);
|
||||
let liqee: MangoAccount = solana.get_account(account).await;
|
||||
assert!(liqee.being_liquidated());
|
||||
assert!(liqee.is_bankrupt());
|
||||
assert!(account_position_closed(solana, account, borrow_token1.bank).await);
|
||||
// both bank's borrows were completely wiped: no one else borrowed
|
||||
let borrow1_bank0: Bank = solana.get_account(borrow_token1.bank).await;
|
||||
let borrow1_bank1: Bank = solana.get_account(borrow_token1.bank).await;
|
||||
assert_eq!(borrow1_bank0.native_borrows(), 0);
|
||||
assert_eq!(borrow1_bank1.native_borrows(), 0);
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
LiqTokenBankruptcyInstruction {
|
||||
liqee: account,
|
||||
liqor: vault_account,
|
||||
liqor_owner: owner,
|
||||
liab_token_index: borrow_token2.index,
|
||||
liab_mint_info: borrow_token2.mint_info,
|
||||
max_liab_transfer: I80F48::from_num(100000.0),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
account_position(solana, vault_account, borrow_token2.bank).await,
|
||||
(vault_amount - borrow2_amount) as i64
|
||||
);
|
||||
let liqee: MangoAccount = solana.get_account(account).await;
|
||||
assert!(liqee.being_liquidated()); // TODO: no longer being liquidated?
|
||||
assert!(!liqee.is_bankrupt());
|
||||
assert!(account_position_closed(solana, account, borrow_token2.bank).await);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_bankrupt_tokens_insurance_fund() -> 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..4];
|
||||
let payer_mint_accounts = &context.users[1].token_accounts[0..4];
|
||||
|
||||
//
|
||||
// SETUP: Create a group and an account to fill the vaults
|
||||
//
|
||||
|
||||
let mango_setup::GroupWithTokens {
|
||||
group,
|
||||
tokens,
|
||||
insurance_vault,
|
||||
} = mango_setup::GroupWithTokensConfig {
|
||||
admin,
|
||||
payer,
|
||||
mints,
|
||||
}
|
||||
.create(solana)
|
||||
.await;
|
||||
let borrow_token1 = &tokens[0]; // USDC
|
||||
let borrow_token2 = &tokens[1];
|
||||
let collateral_token1 = &tokens[2];
|
||||
let collateral_token2 = &tokens[3];
|
||||
|
||||
// fund the insurance vault
|
||||
{
|
||||
let mut tx = ClientTransaction::new(solana);
|
||||
tx.add_instruction_direct(
|
||||
spl_token::instruction::transfer(
|
||||
&spl_token::ID,
|
||||
&payer_mint_accounts[0],
|
||||
&insurance_vault,
|
||||
&payer.pubkey(),
|
||||
&[&payer.pubkey()],
|
||||
1051,
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
tx.add_signer(payer);
|
||||
tx.send().await.unwrap();
|
||||
}
|
||||
|
||||
// deposit some funds, to the vaults aren't empty
|
||||
let vault_account = send_tx(
|
||||
solana,
|
||||
AccountCreateInstruction {
|
||||
account_num: 2,
|
||||
group,
|
||||
owner,
|
||||
payer,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.account;
|
||||
let vault_amount = 100000;
|
||||
for &token_account in payer_mint_accounts {
|
||||
send_tx(
|
||||
solana,
|
||||
TokenDepositInstruction {
|
||||
amount: vault_amount,
|
||||
account: vault_account,
|
||||
token_account,
|
||||
token_authority: payer.clone(),
|
||||
bank_index: 1,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// also add a tiny amount to bank0 for borrow_token1, so we can test multi-bank socialized loss
|
||||
send_tx(
|
||||
solana,
|
||||
TokenDepositInstruction {
|
||||
amount: 10,
|
||||
account: vault_account,
|
||||
token_account: payer_mint_accounts[0],
|
||||
token_authority: payer.clone(),
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
//
|
||||
// SETUP: Make an account with some collateral and some borrows
|
||||
//
|
||||
let account = send_tx(
|
||||
solana,
|
||||
AccountCreateInstruction {
|
||||
account_num: 0,
|
||||
group,
|
||||
owner,
|
||||
payer,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.account;
|
||||
|
||||
let deposit1_amount = 20;
|
||||
let deposit2_amount = 1000;
|
||||
send_tx(
|
||||
solana,
|
||||
TokenDepositInstruction {
|
||||
amount: deposit1_amount,
|
||||
account,
|
||||
token_account: payer_mint_accounts[2],
|
||||
token_authority: payer.clone(),
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
send_tx(
|
||||
solana,
|
||||
TokenDepositInstruction {
|
||||
amount: deposit2_amount,
|
||||
account,
|
||||
token_account: payer_mint_accounts[3],
|
||||
token_authority: payer.clone(),
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let borrow1_amount = 50;
|
||||
let borrow1_amount_bank0 = 10;
|
||||
let borrow1_amount_bank1 = borrow1_amount - borrow1_amount_bank0;
|
||||
let borrow2_amount = 350;
|
||||
send_tx(
|
||||
solana,
|
||||
TokenWithdrawInstruction {
|
||||
amount: borrow1_amount_bank1,
|
||||
allow_borrow: true,
|
||||
account,
|
||||
owner,
|
||||
token_account: payer_mint_accounts[0],
|
||||
bank_index: 1,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
send_tx(
|
||||
solana,
|
||||
TokenWithdrawInstruction {
|
||||
amount: borrow1_amount_bank0,
|
||||
allow_borrow: true,
|
||||
account,
|
||||
owner,
|
||||
token_account: payer_mint_accounts[0],
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
send_tx(
|
||||
solana,
|
||||
TokenWithdrawInstruction {
|
||||
amount: borrow2_amount,
|
||||
allow_borrow: true,
|
||||
account,
|
||||
owner,
|
||||
token_account: payer_mint_accounts[1],
|
||||
bank_index: 1,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
//
|
||||
// SETUP: Change the oracle to make health go very negative
|
||||
//
|
||||
send_tx(
|
||||
solana,
|
||||
StubOracleSetInstruction {
|
||||
group,
|
||||
admin,
|
||||
mint: borrow_token2.mint.pubkey,
|
||||
payer,
|
||||
price: "20.0",
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
//
|
||||
// SETUP: liquidate all the collateral against borrow2
|
||||
//
|
||||
|
||||
// eat collateral1
|
||||
send_tx(
|
||||
solana,
|
||||
LiqTokenWithTokenInstruction {
|
||||
liqee: account,
|
||||
liqor: vault_account,
|
||||
liqor_owner: owner,
|
||||
asset_token_index: collateral_token1.index,
|
||||
asset_bank_index: 1,
|
||||
liab_token_index: borrow_token2.index,
|
||||
liab_bank_index: 1,
|
||||
max_liab_transfer: I80F48::from_num(100000.0),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(account_position_closed(solana, account, collateral_token1.bank).await);
|
||||
let liqee: MangoAccount = solana.get_account(account).await;
|
||||
assert!(liqee.being_liquidated());
|
||||
assert!(!liqee.is_bankrupt());
|
||||
|
||||
// eat collateral2, leaving the account bankrupt
|
||||
send_tx(
|
||||
solana,
|
||||
LiqTokenWithTokenInstruction {
|
||||
liqee: account,
|
||||
liqor: vault_account,
|
||||
liqor_owner: owner,
|
||||
asset_token_index: collateral_token2.index,
|
||||
asset_bank_index: 1,
|
||||
liab_token_index: borrow_token2.index,
|
||||
liab_bank_index: 1,
|
||||
max_liab_transfer: I80F48::from_num(100000.0),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(account_position_closed(solana, account, collateral_token2.bank).await,);
|
||||
let liqee: MangoAccount = solana.get_account(account).await;
|
||||
assert!(liqee.being_liquidated());
|
||||
assert!(liqee.is_bankrupt());
|
||||
|
||||
//
|
||||
// TEST: use the insurance fund to liquidate borrow1 and borrow2
|
||||
//
|
||||
|
||||
// bankruptcy of an USDC liability: just transfers funds from insurance vault to liqee,
|
||||
// the liqor is uninvolved
|
||||
let insurance_vault_before = solana.token_account_balance(insurance_vault).await;
|
||||
let liqor_before = account_position(solana, vault_account, borrow_token1.bank).await;
|
||||
send_tx(
|
||||
solana,
|
||||
LiqTokenBankruptcyInstruction {
|
||||
liqee: account,
|
||||
liqor: vault_account,
|
||||
liqor_owner: owner,
|
||||
liab_token_index: borrow_token1.index,
|
||||
liab_mint_info: borrow_token1.mint_info,
|
||||
max_liab_transfer: I80F48::from_num(100000.0),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let liqee: MangoAccount = solana.get_account(account).await;
|
||||
assert!(liqee.being_liquidated());
|
||||
assert!(liqee.is_bankrupt());
|
||||
assert!(account_position_closed(solana, account, borrow_token1.bank).await);
|
||||
assert_eq!(
|
||||
solana.token_account_balance(insurance_vault).await,
|
||||
// the loan origination fees push the borrow above 50.0 and cause this rounding
|
||||
insurance_vault_before - borrow1_amount - 1
|
||||
);
|
||||
assert_eq!(
|
||||
account_position(solana, vault_account, borrow_token1.bank).await,
|
||||
liqor_before
|
||||
);
|
||||
|
||||
// bankruptcy of a non-USDC liability: USDC to liqor, liability to liqee
|
||||
// liquidating only a partial amount
|
||||
let liab_before = account_position_f64(solana, account, borrow_token2.bank).await;
|
||||
let insurance_vault_before = solana.token_account_balance(insurance_vault).await;
|
||||
let liqor_before = account_position(solana, vault_account, borrow_token1.bank).await;
|
||||
let liab_transfer: f64 = 500.0 / 20.0;
|
||||
send_tx(
|
||||
solana,
|
||||
LiqTokenBankruptcyInstruction {
|
||||
liqee: account,
|
||||
liqor: vault_account,
|
||||
liqor_owner: owner,
|
||||
liab_token_index: borrow_token2.index,
|
||||
liab_mint_info: borrow_token2.mint_info,
|
||||
max_liab_transfer: I80F48::from_num(liab_transfer),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let liqee: MangoAccount = solana.get_account(account).await;
|
||||
assert!(liqee.being_liquidated());
|
||||
assert!(liqee.is_bankrupt());
|
||||
assert!(account_position_closed(solana, account, borrow_token1.bank).await);
|
||||
assert_eq!(
|
||||
account_position(solana, account, borrow_token2.bank).await,
|
||||
(liab_before + liab_transfer) as i64
|
||||
);
|
||||
let usdc_amount = (liab_transfer * 20.0 * 1.02).ceil() as u64;
|
||||
assert_eq!(
|
||||
solana.token_account_balance(insurance_vault).await,
|
||||
insurance_vault_before - usdc_amount
|
||||
);
|
||||
assert_eq!(
|
||||
account_position(solana, vault_account, borrow_token1.bank).await,
|
||||
liqor_before + usdc_amount as i64
|
||||
);
|
||||
|
||||
// bankruptcy of a non-USDC liability: USDC to liqor, liability to liqee
|
||||
// liquidating fully and then doing socialized loss because the insurance fund is exhausted
|
||||
let insurance_vault_before = solana.token_account_balance(insurance_vault).await;
|
||||
let liqor_before = account_position(solana, vault_account, borrow_token1.bank).await;
|
||||
send_tx(
|
||||
solana,
|
||||
LiqTokenBankruptcyInstruction {
|
||||
liqee: account,
|
||||
liqor: vault_account,
|
||||
liqor_owner: owner,
|
||||
liab_token_index: borrow_token2.index,
|
||||
liab_mint_info: borrow_token2.mint_info,
|
||||
max_liab_transfer: I80F48::from_num(1000000.0),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let liqee: MangoAccount = solana.get_account(account).await;
|
||||
assert!(liqee.being_liquidated());
|
||||
assert!(!liqee.is_bankrupt());
|
||||
assert!(account_position_closed(solana, account, borrow_token1.bank).await);
|
||||
assert!(account_position_closed(solana, account, borrow_token2.bank).await);
|
||||
assert_eq!(solana.token_account_balance(insurance_vault).await, 0);
|
||||
assert_eq!(
|
||||
account_position(solana, vault_account, borrow_token1.bank).await,
|
||||
liqor_before + insurance_vault_before as i64
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -27,7 +27,7 @@ async fn test_basic() -> Result<(), TransportError> {
|
|||
// SETUP: Create a group, account, register a token (mint0)
|
||||
//
|
||||
|
||||
let mango_setup::GroupWithTokens { group, tokens } = mango_setup::GroupWithTokensConfig {
|
||||
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
|
||||
admin,
|
||||
payer,
|
||||
mints,
|
||||
|
@ -39,7 +39,7 @@ async fn test_basic() -> Result<(), TransportError> {
|
|||
|
||||
let account = send_tx(
|
||||
solana,
|
||||
CreateAccountInstruction {
|
||||
AccountCreateInstruction {
|
||||
account_num: 0,
|
||||
group,
|
||||
owner,
|
||||
|
@ -63,7 +63,8 @@ async fn test_basic() -> Result<(), TransportError> {
|
|||
amount: deposit_amount,
|
||||
account,
|
||||
token_account: payer_mint0_account,
|
||||
token_authority: payer,
|
||||
token_authority: payer.clone(),
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
|
@ -80,6 +81,13 @@ async fn test_basic() -> Result<(), TransportError> {
|
|||
);
|
||||
let bank_data: Bank = solana.get_account(bank).await;
|
||||
assert!(bank_data.native_deposits() - I80F48::from_num(deposit_amount) < dust_threshold);
|
||||
|
||||
let account_data: MangoAccount = solana.get_account(account).await;
|
||||
// Assumes oracle price of 1
|
||||
assert_eq!(
|
||||
account_data.net_deposits,
|
||||
(I80F48::from_num(deposit_amount) * QUOTE_NATIVE_TO_UI).to_num::<f32>()
|
||||
);
|
||||
}
|
||||
|
||||
//
|
||||
|
@ -111,6 +119,7 @@ async fn test_basic() -> Result<(), TransportError> {
|
|||
account,
|
||||
owner,
|
||||
token_account: payer_mint0_account,
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
|
@ -130,6 +139,13 @@ async fn test_basic() -> Result<(), TransportError> {
|
|||
bank_data.native_deposits() - I80F48::from_num(start_amount - withdraw_amount)
|
||||
< dust_threshold
|
||||
);
|
||||
|
||||
let account_data: MangoAccount = solana.get_account(account).await;
|
||||
// Assumes oracle price of 1
|
||||
assert_eq!(
|
||||
account_data.net_deposits,
|
||||
(I80F48::from_num(start_amount - withdraw_amount) * QUOTE_NATIVE_TO_UI).to_num::<f32>()
|
||||
);
|
||||
}
|
||||
|
||||
//
|
||||
|
@ -139,12 +155,8 @@ async fn test_basic() -> Result<(), TransportError> {
|
|||
// withdraw whatever is remaining, can't close bank vault without this
|
||||
send_tx(
|
||||
solana,
|
||||
UpdateIndexInstruction {
|
||||
TokenUpdateIndexAndRateInstruction {
|
||||
mint_info: tokens[0].mint_info,
|
||||
banks: {
|
||||
let mint_info: MintInfo = solana.get_account(tokens[0].mint_info).await;
|
||||
mint_info.banks.to_vec()
|
||||
},
|
||||
},
|
||||
)
|
||||
.await
|
||||
|
@ -158,6 +170,7 @@ async fn test_basic() -> Result<(), TransportError> {
|
|||
account,
|
||||
owner,
|
||||
token_account: payer_mint0_account,
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
|
@ -166,7 +179,7 @@ async fn test_basic() -> Result<(), TransportError> {
|
|||
// close account
|
||||
send_tx(
|
||||
solana,
|
||||
CloseAccountInstruction {
|
||||
AccountCloseInstruction {
|
||||
group,
|
||||
account,
|
||||
owner,
|
||||
|
@ -204,7 +217,7 @@ async fn test_basic() -> Result<(), TransportError> {
|
|||
// close stub oracle
|
||||
send_tx(
|
||||
solana,
|
||||
CloseStubOracleInstruction {
|
||||
StubOracleCloseInstruction {
|
||||
group,
|
||||
mint: bank_data.mint,
|
||||
admin,
|
||||
|
@ -217,7 +230,7 @@ async fn test_basic() -> Result<(), TransportError> {
|
|||
// close group
|
||||
send_tx(
|
||||
solana,
|
||||
CloseGroupInstruction {
|
||||
GroupCloseInstruction {
|
||||
group,
|
||||
admin,
|
||||
sol_destination: payer.pubkey(),
|
||||
|
|
|
@ -0,0 +1,151 @@
|
|||
#![cfg(feature = "test-bpf")]
|
||||
|
||||
use solana_program_test::*;
|
||||
use solana_sdk::{signature::Keypair, signature::Signer, transport::TransportError};
|
||||
|
||||
use mango_v4::state::*;
|
||||
use program_test::*;
|
||||
|
||||
mod program_test;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_delegate() -> 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 delegate = &context.users[1].key;
|
||||
let mints = &context.mints[0..1];
|
||||
let payer_mint0_account = context.users[1].token_accounts[0];
|
||||
|
||||
//
|
||||
// SETUP: Create a group, register a token (mint0), create an account
|
||||
//
|
||||
|
||||
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
|
||||
admin,
|
||||
payer,
|
||||
mints,
|
||||
}
|
||||
.create(solana)
|
||||
.await;
|
||||
let bank = tokens[0].bank;
|
||||
|
||||
let account = send_tx(
|
||||
solana,
|
||||
AccountCreateInstruction {
|
||||
account_num: 0,
|
||||
group,
|
||||
owner,
|
||||
payer,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.account;
|
||||
|
||||
// deposit
|
||||
send_tx(
|
||||
solana,
|
||||
TokenDepositInstruction {
|
||||
amount: 100,
|
||||
account,
|
||||
token_account: payer_mint0_account,
|
||||
token_authority: payer.clone(),
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
//
|
||||
// TEST: Edit account - Set delegate
|
||||
//
|
||||
{
|
||||
send_tx(
|
||||
solana,
|
||||
AccountEditInstruction {
|
||||
delegate: delegate.pubkey(),
|
||||
account_num: 0,
|
||||
group,
|
||||
owner,
|
||||
name: "new_name".to_owned(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
//
|
||||
// TEST: Edit account as delegate - should fail
|
||||
//
|
||||
{
|
||||
let res = send_tx(
|
||||
solana,
|
||||
AccountEditInstruction {
|
||||
delegate: delegate.pubkey(),
|
||||
account_num: 0,
|
||||
group,
|
||||
owner: delegate,
|
||||
name: "new_name".to_owned(),
|
||||
},
|
||||
)
|
||||
.await;
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
//
|
||||
// TEST: Withdraw funds as delegate should fail
|
||||
//
|
||||
{
|
||||
let withdraw_amount = 50;
|
||||
let res = send_tx(
|
||||
solana,
|
||||
TokenWithdrawInstruction {
|
||||
amount: withdraw_amount,
|
||||
allow_borrow: true,
|
||||
account,
|
||||
owner: delegate,
|
||||
token_account: payer_mint0_account,
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
//
|
||||
// TEST: Close account as delegate should fail
|
||||
//
|
||||
{
|
||||
let bank_data: Bank = solana.get_account(bank).await;
|
||||
send_tx(
|
||||
solana,
|
||||
TokenWithdrawInstruction {
|
||||
amount: bank_data.native_deposits().to_num(),
|
||||
allow_borrow: false,
|
||||
account,
|
||||
owner,
|
||||
token_account: payer_mint0_account,
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let res = send_tx(
|
||||
solana,
|
||||
AccountCloseInstruction {
|
||||
group,
|
||||
account,
|
||||
owner: delegate,
|
||||
sol_destination: payer.pubkey(),
|
||||
},
|
||||
)
|
||||
.await;
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -31,14 +31,14 @@ async fn test_group_address_lookup_tables() -> Result<()> {
|
|||
// SETUP: Create a group
|
||||
//
|
||||
|
||||
let group = send_tx(solana, CreateGroupInstruction { admin, payer })
|
||||
let group = send_tx(solana, GroupCreateInstruction { admin, payer })
|
||||
.await
|
||||
.unwrap()
|
||||
.group;
|
||||
|
||||
let account = send_tx(
|
||||
solana,
|
||||
CreateAccountInstruction {
|
||||
AccountCreateInstruction {
|
||||
account_num: 0,
|
||||
group,
|
||||
owner,
|
||||
|
@ -56,7 +56,7 @@ async fn test_group_address_lookup_tables() -> Result<()> {
|
|||
let register_mint = |index: TokenIndex, mint: MintCookie, address_lookup_table: Pubkey| async move {
|
||||
let create_stub_oracle_accounts = send_tx(
|
||||
solana,
|
||||
CreateStubOracle {
|
||||
StubOracleCreate {
|
||||
mint: mint.pubkey,
|
||||
payer,
|
||||
},
|
||||
|
@ -66,7 +66,7 @@ async fn test_group_address_lookup_tables() -> Result<()> {
|
|||
let oracle = create_stub_oracle_accounts.oracle;
|
||||
send_tx(
|
||||
solana,
|
||||
SetStubOracleInstruction {
|
||||
StubOracleSetInstruction {
|
||||
group,
|
||||
admin,
|
||||
mint: mint.pubkey,
|
||||
|
|
|
@ -35,7 +35,7 @@ async fn test_health_compute_tokens() -> Result<(), TransportError> {
|
|||
|
||||
let account = send_tx(
|
||||
solana,
|
||||
CreateAccountInstruction {
|
||||
AccountCreateInstruction {
|
||||
account_num: 0,
|
||||
group,
|
||||
owner,
|
||||
|
@ -59,7 +59,8 @@ async fn test_health_compute_tokens() -> Result<(), TransportError> {
|
|||
amount: deposit_amount,
|
||||
account,
|
||||
token_account,
|
||||
token_authority: payer,
|
||||
token_authority: payer.clone(),
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
|
@ -88,7 +89,7 @@ async fn test_health_compute_serum() -> Result<(), TransportError> {
|
|||
// SETUP: Create a group and an account
|
||||
//
|
||||
|
||||
let mango_setup::GroupWithTokens { group, tokens } = mango_setup::GroupWithTokensConfig {
|
||||
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
|
||||
admin,
|
||||
payer,
|
||||
mints,
|
||||
|
@ -98,7 +99,7 @@ async fn test_health_compute_serum() -> Result<(), TransportError> {
|
|||
|
||||
let account = send_tx(
|
||||
solana,
|
||||
CreateAccountInstruction {
|
||||
AccountCreateInstruction {
|
||||
account_num: 0,
|
||||
group,
|
||||
owner,
|
||||
|
@ -169,7 +170,8 @@ async fn test_health_compute_serum() -> Result<(), TransportError> {
|
|||
amount: 10,
|
||||
account,
|
||||
token_account: payer_mint_accounts[0],
|
||||
token_authority: payer,
|
||||
token_authority: payer.clone(),
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
|
@ -198,7 +200,7 @@ async fn test_health_compute_perp() -> Result<(), TransportError> {
|
|||
// SETUP: Create a group and an account
|
||||
//
|
||||
|
||||
let mango_setup::GroupWithTokens { group, tokens } = mango_setup::GroupWithTokensConfig {
|
||||
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
|
||||
admin,
|
||||
payer,
|
||||
mints,
|
||||
|
@ -208,7 +210,7 @@ async fn test_health_compute_perp() -> Result<(), TransportError> {
|
|||
|
||||
let account = send_tx(
|
||||
solana,
|
||||
CreateAccountInstruction {
|
||||
AccountCreateInstruction {
|
||||
account_num: 0,
|
||||
group,
|
||||
owner,
|
||||
|
@ -226,7 +228,8 @@ async fn test_health_compute_perp() -> Result<(), TransportError> {
|
|||
amount: 1000,
|
||||
account,
|
||||
token_account: payer_mint_accounts[0],
|
||||
token_authority: payer,
|
||||
token_authority: payer.clone(),
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
|
@ -268,7 +271,6 @@ async fn test_health_compute_perp() -> Result<(), TransportError> {
|
|||
perp_market_index: perp_market_index as PerpMarketIndex,
|
||||
base_token_index: quote_token.index,
|
||||
base_token_decimals: quote_token.mint.decimals,
|
||||
quote_token_index: token.index,
|
||||
quote_lot_size: 10,
|
||||
base_lot_size: 100,
|
||||
maint_asset_weight: 0.975,
|
||||
|
@ -323,7 +325,8 @@ async fn test_health_compute_perp() -> Result<(), TransportError> {
|
|||
amount: 10,
|
||||
account,
|
||||
token_account: payer_mint_accounts[0],
|
||||
token_authority: payer,
|
||||
token_authority: payer.clone(),
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
|
|
|
@ -27,7 +27,7 @@ async fn test_liq_tokens_force_cancel() -> Result<(), TransportError> {
|
|||
// SETUP: Create a group and an account to fill the vaults
|
||||
//
|
||||
|
||||
let mango_setup::GroupWithTokens { group, tokens } = mango_setup::GroupWithTokensConfig {
|
||||
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
|
||||
admin,
|
||||
payer,
|
||||
mints,
|
||||
|
@ -40,7 +40,7 @@ async fn test_liq_tokens_force_cancel() -> Result<(), TransportError> {
|
|||
// deposit some funds, to the vaults aren't empty
|
||||
let vault_account = send_tx(
|
||||
solana,
|
||||
CreateAccountInstruction {
|
||||
AccountCreateInstruction {
|
||||
account_num: 2,
|
||||
group,
|
||||
owner,
|
||||
|
@ -57,7 +57,8 @@ async fn test_liq_tokens_force_cancel() -> Result<(), TransportError> {
|
|||
amount: 10000,
|
||||
account: vault_account,
|
||||
token_account,
|
||||
token_authority: payer,
|
||||
token_authority: payer.clone(),
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
|
@ -94,7 +95,7 @@ async fn test_liq_tokens_force_cancel() -> Result<(), TransportError> {
|
|||
//
|
||||
let account = send_tx(
|
||||
solana,
|
||||
CreateAccountInstruction {
|
||||
AccountCreateInstruction {
|
||||
account_num: 0,
|
||||
group,
|
||||
owner,
|
||||
|
@ -112,7 +113,8 @@ async fn test_liq_tokens_force_cancel() -> Result<(), TransportError> {
|
|||
amount: deposit_amount,
|
||||
account,
|
||||
token_account: payer_mint_accounts[1],
|
||||
token_authority: payer,
|
||||
token_authority: payer.clone(),
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
|
@ -159,7 +161,7 @@ async fn test_liq_tokens_force_cancel() -> Result<(), TransportError> {
|
|||
//
|
||||
send_tx(
|
||||
solana,
|
||||
SetStubOracleInstruction {
|
||||
StubOracleSetInstruction {
|
||||
group,
|
||||
admin,
|
||||
mint: base_token.mint.pubkey,
|
||||
|
@ -179,6 +181,7 @@ async fn test_liq_tokens_force_cancel() -> Result<(), TransportError> {
|
|||
account,
|
||||
owner,
|
||||
token_account: payer_mint_accounts[1],
|
||||
bank_index: 0,
|
||||
}
|
||||
)
|
||||
.await
|
||||
|
@ -207,6 +210,7 @@ async fn test_liq_tokens_force_cancel() -> Result<(), TransportError> {
|
|||
account,
|
||||
owner,
|
||||
token_account: payer_mint_accounts[1],
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
|
@ -230,7 +234,7 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> {
|
|||
// SETUP: Create a group and an account to fill the vaults
|
||||
//
|
||||
|
||||
let mango_setup::GroupWithTokens { group, tokens } = mango_setup::GroupWithTokensConfig {
|
||||
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
|
||||
admin,
|
||||
payer,
|
||||
mints,
|
||||
|
@ -245,7 +249,7 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> {
|
|||
// deposit some funds, to the vaults aren't empty
|
||||
let vault_account = send_tx(
|
||||
solana,
|
||||
CreateAccountInstruction {
|
||||
AccountCreateInstruction {
|
||||
account_num: 2,
|
||||
group,
|
||||
owner,
|
||||
|
@ -262,7 +266,8 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> {
|
|||
amount: 100000,
|
||||
account: vault_account,
|
||||
token_account,
|
||||
token_authority: payer,
|
||||
token_authority: payer.clone(),
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
|
@ -274,7 +279,7 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> {
|
|||
//
|
||||
let account = send_tx(
|
||||
solana,
|
||||
CreateAccountInstruction {
|
||||
AccountCreateInstruction {
|
||||
account_num: 0,
|
||||
group,
|
||||
owner,
|
||||
|
@ -293,7 +298,8 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> {
|
|||
amount: deposit1_amount,
|
||||
account,
|
||||
token_account: payer_mint_accounts[2],
|
||||
token_authority: payer,
|
||||
token_authority: payer.clone(),
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
|
@ -304,7 +310,8 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> {
|
|||
amount: deposit2_amount,
|
||||
account,
|
||||
token_account: payer_mint_accounts[3],
|
||||
token_authority: payer,
|
||||
token_authority: payer.clone(),
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
|
@ -320,6 +327,7 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> {
|
|||
account,
|
||||
owner,
|
||||
token_account: payer_mint_accounts[0],
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
|
@ -332,6 +340,7 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> {
|
|||
account,
|
||||
owner,
|
||||
token_account: payer_mint_accounts[1],
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
|
@ -342,7 +351,7 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> {
|
|||
//
|
||||
send_tx(
|
||||
solana,
|
||||
SetStubOracleInstruction {
|
||||
StubOracleSetInstruction {
|
||||
group,
|
||||
admin,
|
||||
mint: borrow_token1.mint.pubkey,
|
||||
|
@ -365,6 +374,8 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> {
|
|||
liqor_owner: owner,
|
||||
asset_token_index: collateral_token2.index,
|
||||
liab_token_index: borrow_token2.index,
|
||||
asset_bank_index: 0,
|
||||
liab_bank_index: 0,
|
||||
max_liab_transfer: I80F48::from_num(10000.0),
|
||||
},
|
||||
)
|
||||
|
@ -376,12 +387,10 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> {
|
|||
account_position(solana, account, borrow_token2.bank).await,
|
||||
-50 + 19
|
||||
);
|
||||
assert_eq!(
|
||||
account_position(solana, account, collateral_token2.bank).await,
|
||||
0
|
||||
);
|
||||
assert!(account_position_closed(solana, account, collateral_token2.bank).await,);
|
||||
let liqee: MangoAccount = solana.get_account(account).await;
|
||||
assert_eq!(liqee.being_liquidated, 1);
|
||||
assert!(liqee.being_liquidated());
|
||||
assert!(!liqee.is_bankrupt());
|
||||
|
||||
//
|
||||
// TEST: liquidate the remaining borrow2 against collateral1,
|
||||
|
@ -396,22 +405,22 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> {
|
|||
asset_token_index: collateral_token1.index,
|
||||
liab_token_index: borrow_token2.index,
|
||||
max_liab_transfer: I80F48::from_num(10000.0),
|
||||
asset_bank_index: 0,
|
||||
liab_bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// the asset cost for 50-19=31 borrow2 is 31 * 1.04 = 32.24
|
||||
assert_eq!(
|
||||
account_position(solana, account, borrow_token2.bank).await,
|
||||
0
|
||||
);
|
||||
assert!(account_position_closed(solana, account, borrow_token2.bank).await);
|
||||
assert_eq!(
|
||||
account_position(solana, account, collateral_token1.bank).await,
|
||||
1000 - 32
|
||||
);
|
||||
let liqee: MangoAccount = solana.get_account(account).await;
|
||||
assert_eq!(liqee.being_liquidated, 1);
|
||||
assert!(liqee.being_liquidated());
|
||||
assert!(!liqee.is_bankrupt());
|
||||
|
||||
//
|
||||
// TEST: liquidate borrow1 with collateral1, but place a limit
|
||||
|
@ -425,6 +434,8 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> {
|
|||
asset_token_index: collateral_token1.index,
|
||||
liab_token_index: borrow_token1.index,
|
||||
max_liab_transfer: I80F48::from_num(10.0),
|
||||
asset_bank_index: 0,
|
||||
liab_bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
|
@ -440,7 +451,8 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> {
|
|||
1000 - 32 - 21
|
||||
);
|
||||
let liqee: MangoAccount = solana.get_account(account).await;
|
||||
assert_eq!(liqee.being_liquidated, 1);
|
||||
assert!(liqee.being_liquidated());
|
||||
assert!(!liqee.is_bankrupt());
|
||||
|
||||
//
|
||||
// TEST: liquidate borrow1 with collateral1, making the account healthy again
|
||||
|
@ -454,6 +466,8 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> {
|
|||
asset_token_index: collateral_token1.index,
|
||||
liab_token_index: borrow_token1.index,
|
||||
max_liab_transfer: I80F48::from_num(10000.0),
|
||||
asset_bank_index: 0,
|
||||
liab_bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
|
@ -472,7 +486,8 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> {
|
|||
1000 - 32 - 535 - 1
|
||||
);
|
||||
let liqee: MangoAccount = solana.get_account(account).await;
|
||||
assert_eq!(liqee.being_liquidated, 0);
|
||||
assert!(!liqee.being_liquidated());
|
||||
assert!(!liqee.is_bankrupt());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ async fn test_margin_trade1() -> Result<(), BanksClientError> {
|
|||
// SETUP: Create a group, account, register a token (mint0)
|
||||
//
|
||||
|
||||
let mango_setup::GroupWithTokens { group, tokens } = mango_setup::GroupWithTokensConfig {
|
||||
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
|
||||
admin,
|
||||
payer,
|
||||
mints,
|
||||
|
@ -51,7 +51,7 @@ async fn test_margin_trade1() -> Result<(), BanksClientError> {
|
|||
|
||||
let provider_account = send_tx(
|
||||
solana,
|
||||
CreateAccountInstruction {
|
||||
AccountCreateInstruction {
|
||||
account_num: 1,
|
||||
group,
|
||||
owner,
|
||||
|
@ -68,7 +68,8 @@ async fn test_margin_trade1() -> Result<(), BanksClientError> {
|
|||
amount: provided_amount,
|
||||
account: provider_account,
|
||||
token_account: payer_mint0_account,
|
||||
token_authority: payer,
|
||||
token_authority: payer.clone(),
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
|
@ -79,7 +80,8 @@ async fn test_margin_trade1() -> Result<(), BanksClientError> {
|
|||
amount: provided_amount,
|
||||
account: provider_account,
|
||||
token_account: payer_mint1_account,
|
||||
token_authority: payer,
|
||||
token_authority: payer.clone(),
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
|
@ -91,7 +93,7 @@ async fn test_margin_trade1() -> Result<(), BanksClientError> {
|
|||
|
||||
let account = send_tx(
|
||||
solana,
|
||||
CreateAccountInstruction {
|
||||
AccountCreateInstruction {
|
||||
account_num: 0,
|
||||
group,
|
||||
owner,
|
||||
|
@ -115,7 +117,8 @@ async fn test_margin_trade1() -> Result<(), BanksClientError> {
|
|||
amount: deposit_amount_initial,
|
||||
account,
|
||||
token_account: payer_mint0_account,
|
||||
token_authority: payer,
|
||||
token_authority: payer.clone(),
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
|
@ -347,7 +350,7 @@ async fn test_margin_trade2() -> Result<(), BanksClientError> {
|
|||
// SETUP: Create a group, account, register a token (mint0)
|
||||
//
|
||||
|
||||
let mango_setup::GroupWithTokens { group, tokens } = mango_setup::GroupWithTokensConfig {
|
||||
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
|
||||
admin,
|
||||
payer,
|
||||
mints,
|
||||
|
@ -364,7 +367,7 @@ async fn test_margin_trade2() -> Result<(), BanksClientError> {
|
|||
|
||||
let provider_account = send_tx(
|
||||
solana,
|
||||
CreateAccountInstruction {
|
||||
AccountCreateInstruction {
|
||||
account_num: 1,
|
||||
group,
|
||||
owner,
|
||||
|
@ -381,7 +384,8 @@ async fn test_margin_trade2() -> Result<(), BanksClientError> {
|
|||
amount: provided_amount,
|
||||
account: provider_account,
|
||||
token_account: payer_mint0_account,
|
||||
token_authority: payer,
|
||||
token_authority: payer.clone(),
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
|
@ -392,7 +396,8 @@ async fn test_margin_trade2() -> Result<(), BanksClientError> {
|
|||
amount: provided_amount,
|
||||
account: provider_account,
|
||||
token_account: payer_mint1_account,
|
||||
token_authority: payer,
|
||||
token_authority: payer.clone(),
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
|
@ -404,7 +409,7 @@ async fn test_margin_trade2() -> Result<(), BanksClientError> {
|
|||
|
||||
let account = send_tx(
|
||||
solana,
|
||||
CreateAccountInstruction {
|
||||
AccountCreateInstruction {
|
||||
account_num: 0,
|
||||
group,
|
||||
owner,
|
||||
|
@ -428,7 +433,8 @@ async fn test_margin_trade2() -> Result<(), BanksClientError> {
|
|||
amount: deposit_amount_initial,
|
||||
account,
|
||||
token_account: payer_mint0_account,
|
||||
token_authority: payer,
|
||||
token_authority: payer.clone(),
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
|
@ -605,7 +611,7 @@ async fn test_margin_trade3() -> Result<(), BanksClientError> {
|
|||
// SETUP: Create a group, account, register a token (mint0)
|
||||
//
|
||||
|
||||
let mango_setup::GroupWithTokens { group, tokens } = mango_setup::GroupWithTokensConfig {
|
||||
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
|
||||
admin,
|
||||
payer,
|
||||
mints,
|
||||
|
@ -622,7 +628,7 @@ async fn test_margin_trade3() -> Result<(), BanksClientError> {
|
|||
|
||||
let provider_account = send_tx(
|
||||
solana,
|
||||
CreateAccountInstruction {
|
||||
AccountCreateInstruction {
|
||||
account_num: 1,
|
||||
group,
|
||||
owner,
|
||||
|
@ -639,7 +645,8 @@ async fn test_margin_trade3() -> Result<(), BanksClientError> {
|
|||
amount: provided_amount,
|
||||
account: provider_account,
|
||||
token_account: payer_mint0_account,
|
||||
token_authority: payer,
|
||||
token_authority: payer.clone(),
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
|
@ -650,7 +657,8 @@ async fn test_margin_trade3() -> Result<(), BanksClientError> {
|
|||
amount: provided_amount,
|
||||
account: provider_account,
|
||||
token_account: payer_mint1_account,
|
||||
token_authority: payer,
|
||||
token_authority: payer.clone(),
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
|
@ -662,7 +670,7 @@ async fn test_margin_trade3() -> Result<(), BanksClientError> {
|
|||
|
||||
let account = send_tx(
|
||||
solana,
|
||||
CreateAccountInstruction {
|
||||
AccountCreateInstruction {
|
||||
account_num: 0,
|
||||
group,
|
||||
owner,
|
||||
|
@ -686,7 +694,8 @@ async fn test_margin_trade3() -> Result<(), BanksClientError> {
|
|||
amount: deposit_amount_initial,
|
||||
account,
|
||||
token_account: payer_mint0_account,
|
||||
token_authority: payer,
|
||||
token_authority: payer.clone(),
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
|
|
|
@ -24,7 +24,7 @@ async fn test_perp() -> Result<(), TransportError> {
|
|||
// SETUP: Create a group and an account
|
||||
//
|
||||
|
||||
let mango_setup::GroupWithTokens { group, tokens } = mango_setup::GroupWithTokensConfig {
|
||||
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
|
||||
admin,
|
||||
payer,
|
||||
mints,
|
||||
|
@ -34,7 +34,7 @@ async fn test_perp() -> Result<(), TransportError> {
|
|||
|
||||
let account_0 = send_tx(
|
||||
solana,
|
||||
CreateAccountInstruction {
|
||||
AccountCreateInstruction {
|
||||
account_num: 0,
|
||||
group,
|
||||
owner,
|
||||
|
@ -47,7 +47,7 @@ async fn test_perp() -> Result<(), TransportError> {
|
|||
|
||||
let account_1 = send_tx(
|
||||
solana,
|
||||
CreateAccountInstruction {
|
||||
AccountCreateInstruction {
|
||||
account_num: 1,
|
||||
group,
|
||||
owner,
|
||||
|
@ -70,7 +70,8 @@ async fn test_perp() -> Result<(), TransportError> {
|
|||
amount: deposit_amount,
|
||||
account: account_0,
|
||||
token_account: payer_mint_accounts[0],
|
||||
token_authority: payer,
|
||||
token_authority: payer.clone(),
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
|
@ -82,7 +83,8 @@ async fn test_perp() -> Result<(), TransportError> {
|
|||
amount: deposit_amount,
|
||||
account: account_0,
|
||||
token_account: payer_mint_accounts[1],
|
||||
token_authority: payer,
|
||||
token_authority: payer.clone(),
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
|
@ -98,7 +100,8 @@ async fn test_perp() -> Result<(), TransportError> {
|
|||
amount: deposit_amount,
|
||||
account: account_1,
|
||||
token_account: payer_mint_accounts[0],
|
||||
token_authority: payer,
|
||||
token_authority: payer.clone(),
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
|
@ -110,7 +113,8 @@ async fn test_perp() -> Result<(), TransportError> {
|
|||
amount: deposit_amount,
|
||||
account: account_1,
|
||||
token_account: payer_mint_accounts[1],
|
||||
token_authority: payer,
|
||||
token_authority: payer.clone(),
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
|
@ -150,7 +154,6 @@ async fn test_perp() -> Result<(), TransportError> {
|
|||
perp_market_index: 0,
|
||||
base_token_index: tokens[0].index,
|
||||
base_token_decimals: tokens[0].mint.decimals,
|
||||
quote_token_index: tokens[1].index,
|
||||
quote_lot_size: 10,
|
||||
base_lot_size: 100,
|
||||
maint_asset_weight: 0.975,
|
||||
|
|
|
@ -26,7 +26,7 @@ async fn test_position_lifetime() -> Result<()> {
|
|||
// SETUP: Create a group and accounts
|
||||
//
|
||||
|
||||
let mango_setup::GroupWithTokens { group, tokens } = mango_setup::GroupWithTokensConfig {
|
||||
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
|
||||
admin,
|
||||
payer,
|
||||
mints,
|
||||
|
@ -36,7 +36,7 @@ async fn test_position_lifetime() -> Result<()> {
|
|||
|
||||
let account = send_tx(
|
||||
solana,
|
||||
CreateAccountInstruction {
|
||||
AccountCreateInstruction {
|
||||
account_num: 0,
|
||||
group,
|
||||
owner,
|
||||
|
@ -49,7 +49,7 @@ async fn test_position_lifetime() -> Result<()> {
|
|||
|
||||
let funding_account = send_tx(
|
||||
solana,
|
||||
CreateAccountInstruction {
|
||||
AccountCreateInstruction {
|
||||
account_num: 1,
|
||||
group,
|
||||
owner,
|
||||
|
@ -72,7 +72,8 @@ async fn test_position_lifetime() -> Result<()> {
|
|||
amount: funding_amount,
|
||||
account: funding_account,
|
||||
token_account: payer_token,
|
||||
token_authority: payer,
|
||||
token_authority: payer.clone(),
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
|
@ -95,7 +96,8 @@ async fn test_position_lifetime() -> Result<()> {
|
|||
amount: deposit_amount,
|
||||
account,
|
||||
token_account: payer_token,
|
||||
token_authority: payer,
|
||||
token_authority: payer.clone(),
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
|
@ -112,6 +114,7 @@ async fn test_position_lifetime() -> Result<()> {
|
|||
account,
|
||||
owner,
|
||||
token_account: payer_token,
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
|
@ -145,7 +148,8 @@ async fn test_position_lifetime() -> Result<()> {
|
|||
amount: collateral_amount,
|
||||
account,
|
||||
token_account: payer_mint_accounts[0],
|
||||
token_authority: payer,
|
||||
token_authority: payer.clone(),
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
|
@ -161,6 +165,7 @@ async fn test_position_lifetime() -> Result<()> {
|
|||
account,
|
||||
owner,
|
||||
token_account: payer_mint_accounts[1],
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
|
@ -179,7 +184,8 @@ async fn test_position_lifetime() -> Result<()> {
|
|||
amount: borrow_amount + 2,
|
||||
account,
|
||||
token_account: payer_mint_accounts[1],
|
||||
token_authority: payer,
|
||||
token_authority: payer.clone(),
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
|
@ -193,6 +199,7 @@ async fn test_position_lifetime() -> Result<()> {
|
|||
account,
|
||||
owner,
|
||||
token_account: payer_mint_accounts[1],
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
|
@ -208,6 +215,7 @@ async fn test_position_lifetime() -> Result<()> {
|
|||
account,
|
||||
owner,
|
||||
token_account: payer_mint_accounts[0],
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
|
|
|
@ -26,7 +26,7 @@ async fn test_serum() -> Result<(), TransportError> {
|
|||
// SETUP: Create a group and an account
|
||||
//
|
||||
|
||||
let mango_setup::GroupWithTokens { group, tokens } = mango_setup::GroupWithTokensConfig {
|
||||
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
|
||||
admin,
|
||||
payer,
|
||||
mints,
|
||||
|
@ -38,7 +38,7 @@ async fn test_serum() -> Result<(), TransportError> {
|
|||
|
||||
let account = send_tx(
|
||||
solana,
|
||||
CreateAccountInstruction {
|
||||
AccountCreateInstruction {
|
||||
account_num: 0,
|
||||
group,
|
||||
owner,
|
||||
|
@ -69,7 +69,8 @@ async fn test_serum() -> Result<(), TransportError> {
|
|||
amount: deposit_amount,
|
||||
account,
|
||||
token_account: payer_mint_accounts[0],
|
||||
token_authority: payer,
|
||||
token_authority: payer.clone(),
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
|
@ -81,7 +82,8 @@ async fn test_serum() -> Result<(), TransportError> {
|
|||
amount: deposit_amount,
|
||||
account,
|
||||
token_account: payer_mint_accounts[1],
|
||||
token_authority: payer,
|
||||
token_authority: payer.clone(),
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
#![cfg(feature = "test-bpf")]
|
||||
|
||||
use mango_v4::state::{Bank, MintInfo};
|
||||
use mango_v4::state::*;
|
||||
use solana_program_test::*;
|
||||
use solana_sdk::{signature::Keypair, transport::TransportError};
|
||||
|
||||
|
@ -9,7 +9,7 @@ use program_test::*;
|
|||
mod program_test;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_update_index() -> Result<(), TransportError> {
|
||||
async fn test_token_update_index_and_rate() -> Result<(), TransportError> {
|
||||
let context = TestContext::new().await;
|
||||
let solana = &context.solana.clone();
|
||||
|
||||
|
@ -23,7 +23,7 @@ async fn test_update_index() -> Result<(), TransportError> {
|
|||
// SETUP: Create a group and an account to fill the vaults
|
||||
//
|
||||
|
||||
let mango_setup::GroupWithTokens { group, tokens } = mango_setup::GroupWithTokensConfig {
|
||||
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
|
||||
admin,
|
||||
payer,
|
||||
mints,
|
||||
|
@ -34,7 +34,7 @@ async fn test_update_index() -> Result<(), TransportError> {
|
|||
// deposit some funds, to the vaults aren't empty
|
||||
let deposit_account = send_tx(
|
||||
solana,
|
||||
CreateAccountInstruction {
|
||||
AccountCreateInstruction {
|
||||
account_num: 0,
|
||||
group,
|
||||
owner,
|
||||
|
@ -51,7 +51,8 @@ async fn test_update_index() -> Result<(), TransportError> {
|
|||
amount: 10000,
|
||||
account: deposit_account,
|
||||
token_account,
|
||||
token_authority: payer,
|
||||
token_authority: payer.clone(),
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
|
@ -60,7 +61,7 @@ async fn test_update_index() -> Result<(), TransportError> {
|
|||
|
||||
let withdraw_account = send_tx(
|
||||
solana,
|
||||
CreateAccountInstruction {
|
||||
AccountCreateInstruction {
|
||||
account_num: 1,
|
||||
group,
|
||||
owner,
|
||||
|
@ -77,7 +78,8 @@ async fn test_update_index() -> Result<(), TransportError> {
|
|||
amount: 100000,
|
||||
account: withdraw_account,
|
||||
token_account: payer_mint_accounts[1],
|
||||
token_authority: payer,
|
||||
token_authority: payer.clone(),
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
|
@ -91,33 +93,36 @@ async fn test_update_index() -> Result<(), TransportError> {
|
|||
account: withdraw_account,
|
||||
owner,
|
||||
token_account: context.users[0].token_accounts[0],
|
||||
bank_index: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let bank_before_update_index = solana.get_account::<Bank>(tokens[0].bank).await;
|
||||
let bank_before_update_index_and_rate = solana.get_account::<Bank>(tokens[0].bank).await;
|
||||
|
||||
solana.advance_clock().await;
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
UpdateIndexInstruction {
|
||||
TokenUpdateIndexAndRateInstruction {
|
||||
mint_info: tokens[0].mint_info,
|
||||
banks: {
|
||||
let mint_info: MintInfo = solana.get_account(tokens[0].mint_info).await;
|
||||
mint_info.banks.to_vec()
|
||||
},
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let bank_after_update_index = solana.get_account::<Bank>(tokens[0].bank).await;
|
||||
dbg!(bank_after_update_index);
|
||||
dbg!(bank_after_update_index);
|
||||
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);
|
||||
let bank_after_update_index_and_rate = solana.get_account::<Bank>(tokens[0].bank).await;
|
||||
dbg!(bank_after_update_index_and_rate);
|
||||
dbg!(bank_after_update_index_and_rate);
|
||||
assert!(
|
||||
bank_before_update_index_and_rate.deposit_index
|
||||
< bank_after_update_index_and_rate.deposit_index
|
||||
);
|
||||
assert!(
|
||||
bank_before_update_index_and_rate.borrow_index
|
||||
< bank_after_update_index_and_rate.borrow_index
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -15,12 +15,12 @@ anchor build --skip-lint
|
|||
# update types in ts client package
|
||||
cp -v ./target/types/mango_v4.ts ./ts/client/src/mango_v4.ts
|
||||
|
||||
(cd ./ts/client && tsc)
|
||||
(cd ./ts/client && yarn tsc)
|
||||
|
||||
if [[ -z "${NO_DEPLOY}" ]]; then
|
||||
# publish program
|
||||
solana --url https://mango.devnet.rpcpool.com program deploy --program-id $PROGRAM_ID \
|
||||
-k $WALLET_WITH_FUNDS target/deploy/mango_v4.so
|
||||
-k $WALLET_WITH_FUNDS target/deploy/mango_v4.so --skip-fee-check
|
||||
|
||||
# # publish idl
|
||||
# anchor idl upgrade --provider.cluster https://mango.devnet.rpcpool.com --provider.wallet $WALLET_WITH_FUNDS \
|
||||
|
|
|
@ -15,7 +15,7 @@ anchor build --skip-lint
|
|||
# update types in ts client package
|
||||
cp -v ./target/types/mango_v4.ts ./ts/client/src/mango_v4.ts
|
||||
|
||||
(cd ./ts/client && tsc)
|
||||
(cd ./ts/client && yarn tsc)
|
||||
|
||||
if [[ -z "${NO_DEPLOY}" ]]; then
|
||||
# publish program
|
||||
|
@ -31,4 +31,4 @@ fi
|
|||
|
||||
|
||||
# build npm package
|
||||
(cd ./ts/client && tsc)
|
||||
(cd ./ts/client && yarn tsc)
|
||||
|
|
|
@ -141,9 +141,39 @@ export class Bank {
|
|||
}
|
||||
|
||||
toString(): string {
|
||||
return `Bank ${
|
||||
this.tokenIndex
|
||||
} deposit index - ${this.depositIndex.toNumber()}, borrow index - ${this.borrowIndex.toNumber()}`;
|
||||
return (
|
||||
'Bank ' +
|
||||
'\n token index -' +
|
||||
this.tokenIndex +
|
||||
'\n deposit index -' +
|
||||
this.depositIndex.toNumber() +
|
||||
'\n borrow index -' +
|
||||
this.borrowIndex.toNumber() +
|
||||
'\n cachedIndexedTotalDeposits -' +
|
||||
this.cachedIndexedTotalDeposits.toNumber() +
|
||||
'\n cachedIndexedTotalBorrows -' +
|
||||
this.cachedIndexedTotalBorrows.toNumber() +
|
||||
'\n maxRate -' +
|
||||
this.maxRate.toNumber() +
|
||||
'\n util0 -' +
|
||||
this.util0.toNumber() +
|
||||
'\n rate0 -' +
|
||||
this.rate0.toNumber() +
|
||||
'\n util1 -' +
|
||||
this.util1.toNumber() +
|
||||
'\n rate1 -' +
|
||||
this.rate1.toNumber() +
|
||||
'\n maintAssetWeight -' +
|
||||
this.maintAssetWeight.toNumber() +
|
||||
'\n initAssetWeight -' +
|
||||
this.initAssetWeight.toNumber() +
|
||||
'\n maintLiabWeight -' +
|
||||
this.maintLiabWeight.toNumber() +
|
||||
'\n initLiabWeight -' +
|
||||
this.initLiabWeight.toNumber() +
|
||||
'\n liquidationFee -' +
|
||||
this.liquidationFee.toNumber()
|
||||
);
|
||||
}
|
||||
|
||||
nativeDeposits(): I80F48 {
|
||||
|
|
|
@ -171,7 +171,6 @@ export class Group {
|
|||
public async reloadBankPrices(client: MangoClient, ids?: Id): Promise<void> {
|
||||
const banks = Array.from(this?.banksMap, ([, value]) => value);
|
||||
const oracles = banks.map((b) => b.oracle);
|
||||
console.log(oracles.toString());
|
||||
const prices =
|
||||
await client.program.provider.connection.getMultipleAccountsInfo(oracles);
|
||||
|
||||
|
|
|
@ -0,0 +1,224 @@
|
|||
import { I80F48, I80F48Dto, ZERO_I80F48 } from './I80F48';
|
||||
import { HealthType } from './mangoAccount';
|
||||
|
||||
// ░░░░
|
||||
//
|
||||
// ██
|
||||
// ██░░██
|
||||
// ░░ ░░ ██░░░░░░██ ░░░░
|
||||
// ██░░░░░░░░░░██
|
||||
// ██░░░░░░░░░░██
|
||||
// ██░░░░░░░░░░░░░░██
|
||||
// ██░░░░░░██████░░░░░░██
|
||||
// ██░░░░░░██████░░░░░░██
|
||||
// ██░░░░░░░░██████░░░░░░░░██
|
||||
// ██░░░░░░░░██████░░░░░░░░██
|
||||
// ██░░░░░░░░░░██████░░░░░░░░░░██
|
||||
// ██░░░░░░░░░░░░██████░░░░░░░░░░░░██
|
||||
// ██░░░░░░░░░░░░██████░░░░░░░░░░░░██
|
||||
// ██░░░░░░░░░░░░░░██████░░░░░░░░░░░░░░██
|
||||
// ██░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░██
|
||||
// ██░░░░░░░░░░░░░░░░██████░░░░░░░░░░░░░░░░██
|
||||
// ██░░░░░░░░░░░░░░░░██████░░░░░░░░░░░░░░░░██
|
||||
// ██░░░░░░░░░░░░░░░░░░██████░░░░░░░░░░░░░░░░░░██
|
||||
// ░░ ██░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░██
|
||||
// ██████████████████████████████████████████
|
||||
// warning: this code is copy pasta from rust, keep in sync with health.rs
|
||||
|
||||
export class HealthCache {
|
||||
tokenInfos: TokenInfo[];
|
||||
serum3Infos: Serum3Info[];
|
||||
perpInfos: PerpInfo[];
|
||||
|
||||
constructor(dto: HealthCacheDto) {
|
||||
this.tokenInfos = dto.tokenInfos.map((dto) => new TokenInfo(dto));
|
||||
this.serum3Infos = dto.serum3Infos.map((dto) => new Serum3Info(dto));
|
||||
this.perpInfos = dto.perpInfos.map((dto) => new PerpInfo(dto));
|
||||
}
|
||||
|
||||
public health(healthType: HealthType): I80F48 {
|
||||
let health = ZERO_I80F48;
|
||||
for (const tokenInfo of this.tokenInfos) {
|
||||
let contrib = tokenInfo.healthContribution(healthType);
|
||||
health = health.add(contrib);
|
||||
}
|
||||
for (const serum3Info of this.serum3Infos) {
|
||||
let contrib = serum3Info.healthContribution(healthType, this.tokenInfos);
|
||||
health = health.add(contrib);
|
||||
}
|
||||
for (const perpInfo of this.perpInfos) {
|
||||
let contrib = perpInfo.healthContribution(healthType);
|
||||
health = health.add(contrib);
|
||||
}
|
||||
return health;
|
||||
}
|
||||
}
|
||||
|
||||
export class TokenInfo {
|
||||
constructor(dto: TokenInfoDto) {
|
||||
this.tokenIndex = dto.tokenIndex;
|
||||
this.maintAssetWeight = I80F48.from(dto.maintAssetWeight);
|
||||
this.initAssetWeight = I80F48.from(dto.initAssetWeight);
|
||||
this.maintLiabWeight = I80F48.from(dto.maintLiabWeight);
|
||||
this.initLiabWeight = I80F48.from(dto.initLiabWeight);
|
||||
this.oraclePrice = I80F48.from(dto.oraclePrice);
|
||||
this.balance = I80F48.from(dto.balance);
|
||||
this.serum3MaxReserved = I80F48.from(dto.serum3MaxReserved);
|
||||
}
|
||||
|
||||
tokenIndex: number;
|
||||
maintAssetWeight: I80F48;
|
||||
initAssetWeight: I80F48;
|
||||
maintLiabWeight: I80F48;
|
||||
initLiabWeight: I80F48;
|
||||
oraclePrice: I80F48; // native/native
|
||||
// in health-reference-token native units
|
||||
balance: I80F48;
|
||||
// in health-reference-token native units
|
||||
serum3MaxReserved: I80F48;
|
||||
|
||||
assetWeight(healthType: HealthType): I80F48 {
|
||||
return healthType == HealthType.init
|
||||
? this.initAssetWeight
|
||||
: this.maintAssetWeight;
|
||||
}
|
||||
|
||||
liabWeight(healthType: HealthType): I80F48 {
|
||||
return healthType == HealthType.init
|
||||
? this.initLiabWeight
|
||||
: this.maintLiabWeight;
|
||||
}
|
||||
|
||||
healthContribution(healthType: HealthType): I80F48 {
|
||||
return (
|
||||
this.balance.isNeg()
|
||||
? this.liabWeight(healthType)
|
||||
: this.assetWeight(healthType)
|
||||
).mul(this.balance);
|
||||
}
|
||||
}
|
||||
|
||||
export class Serum3Info {
|
||||
constructor(dto: Serum3InfoDto) {
|
||||
this.reserved = I80F48.from(dto.reserved);
|
||||
this.baseIndex = dto.baseIndex;
|
||||
this.quoteIndex = dto.quoteIndex;
|
||||
}
|
||||
|
||||
reserved: I80F48;
|
||||
baseIndex: number;
|
||||
quoteIndex: number;
|
||||
|
||||
healthContribution(healthType: HealthType, tokenInfos: TokenInfo[]): I80F48 {
|
||||
let baseInfo = tokenInfos[this.baseIndex];
|
||||
let quoteInfo = tokenInfos[this.quoteIndex];
|
||||
let reserved = this.reserved;
|
||||
|
||||
if (reserved.isZero()) {
|
||||
return ZERO_I80F48;
|
||||
}
|
||||
|
||||
// How much the health would increase if the reserved balance were applied to the passed
|
||||
// token info?
|
||||
let computeHealthEffect = function (tokenInfo: TokenInfo) {
|
||||
// This balance includes all possible reserved funds from markets that relate to the
|
||||
// token, including this market itself: `reserved` is already included in `max_balance`.
|
||||
let maxBalance = tokenInfo.balance.add(tokenInfo.serum3MaxReserved);
|
||||
|
||||
// Assuming `reserved` was added to `max_balance` last (because that gives the smallest
|
||||
// health effects): how much did health change because of it?
|
||||
let assetPart, liabPart;
|
||||
if (maxBalance.gte(reserved)) {
|
||||
assetPart = reserved;
|
||||
liabPart = ZERO_I80F48;
|
||||
} else if (maxBalance.isNeg()) {
|
||||
assetPart = ZERO_I80F48;
|
||||
liabPart = reserved;
|
||||
} else {
|
||||
assetPart = maxBalance;
|
||||
liabPart = reserved.sub(maxBalance);
|
||||
}
|
||||
|
||||
let assetWeight = tokenInfo.assetWeight(healthType);
|
||||
let liabWeight = tokenInfo.liabWeight(healthType);
|
||||
return assetWeight.mul(assetPart).add(liabWeight.mul(liabPart));
|
||||
};
|
||||
|
||||
let reservedAsBase = computeHealthEffect(baseInfo);
|
||||
let reservedAsQuote = computeHealthEffect(quoteInfo);
|
||||
return reservedAsBase.min(reservedAsQuote);
|
||||
}
|
||||
}
|
||||
|
||||
export class PerpInfo {
|
||||
constructor(dto: PerpInfoDto) {
|
||||
this.maintAssetWeight = I80F48.from(dto.maintAssetWeight);
|
||||
this.initAssetWeight = I80F48.from(dto.initAssetWeight);
|
||||
this.maintLiabWeight = I80F48.from(dto.maintLiabWeight);
|
||||
this.initLiabWeight = I80F48.from(dto.initLiabWeight);
|
||||
this.base = I80F48.from(dto.base);
|
||||
this.quote = I80F48.from(dto.quote);
|
||||
}
|
||||
maintAssetWeight: I80F48;
|
||||
initAssetWeight: I80F48;
|
||||
maintLiabWeight: I80F48;
|
||||
initLiabWeight: I80F48;
|
||||
// in health-reference-token native units, needs scaling by asset/liab
|
||||
base: I80F48;
|
||||
// in health-reference-token native units, no asset/liab factor needed
|
||||
quote: I80F48;
|
||||
|
||||
healthContribution(healthType: HealthType): I80F48 {
|
||||
let weight;
|
||||
if (healthType == HealthType.init && this.base.isNeg()) {
|
||||
weight = this.initLiabWeight;
|
||||
} else if (healthType == HealthType.init && !this.base.isNeg()) {
|
||||
weight = this.initAssetWeight;
|
||||
}
|
||||
if (healthType == HealthType.maint && this.base.isNeg()) {
|
||||
weight = this.maintLiabWeight;
|
||||
}
|
||||
if (healthType == HealthType.maint && !this.base.isNeg()) {
|
||||
weight = this.maintAssetWeight;
|
||||
}
|
||||
|
||||
// FUTURE: Allow v3-style "reliable" markets where we can return
|
||||
// `self.quote + weight * self.base` here
|
||||
return this.quote.add(weight.mul(this.base)).min(ZERO_I80F48);
|
||||
}
|
||||
}
|
||||
|
||||
export class HealthCacheDto {
|
||||
tokenInfos: TokenInfoDto[];
|
||||
serum3Infos: Serum3InfoDto[];
|
||||
perpInfos: PerpInfoDto[];
|
||||
}
|
||||
export class TokenInfoDto {
|
||||
tokenIndex: number;
|
||||
maintAssetWeight: I80F48Dto;
|
||||
initAssetWeight: I80F48Dto;
|
||||
maintLiabWeight: I80F48Dto;
|
||||
initLiabWeight: I80F48Dto;
|
||||
oraclePrice: I80F48Dto; // native/native
|
||||
// in health-reference-token native units
|
||||
balance: I80F48Dto;
|
||||
// in health-reference-token native units
|
||||
serum3MaxReserved: I80F48Dto;
|
||||
}
|
||||
|
||||
export class Serum3InfoDto {
|
||||
reserved: I80F48Dto;
|
||||
baseIndex: number;
|
||||
quoteIndex: number;
|
||||
}
|
||||
|
||||
export class PerpInfoDto {
|
||||
maintAssetWeight: I80F48Dto;
|
||||
initAssetWeight: I80F48Dto;
|
||||
maintLiabWeight: I80F48Dto;
|
||||
initLiabWeight: I80F48Dto;
|
||||
// in health-reference-token native units, needs scaling by asset/liab
|
||||
base: I80F48Dto;
|
||||
// in health-reference-token native units, no asset/liab factor needed
|
||||
quote: I80F48Dto;
|
||||
}
|
|
@ -5,6 +5,7 @@ import { MangoClient } from '../client';
|
|||
import { nativeI80F48ToUi } from '../utils';
|
||||
import { Bank, QUOTE_DECIMALS } from './bank';
|
||||
import { Group } from './group';
|
||||
import { HealthCache, HealthCacheDto } from './healthCache';
|
||||
import { I80F48, I80F48Dto, ONE_I80F48, ZERO_I80F48 } from './I80F48';
|
||||
export class MangoAccount {
|
||||
public tokens: TokenPosition[];
|
||||
|
@ -318,6 +319,7 @@ export class MangoAccount {
|
|||
let res = 'MangoAccount';
|
||||
res = res + '\n pk: ' + this.publicKey.toString();
|
||||
res = res + '\n name: ' + this.name;
|
||||
res = res + '\n delegate: ' + this.delegate;
|
||||
|
||||
res =
|
||||
this.tokensActive().length > 0
|
||||
|
@ -505,12 +507,14 @@ export class HealthType {
|
|||
|
||||
export class MangoAccountData {
|
||||
constructor(
|
||||
public healthCache: HealthCache,
|
||||
public initHealth: I80F48,
|
||||
public maintHealth: I80F48,
|
||||
public equity: Equity,
|
||||
) {}
|
||||
|
||||
static from(event: {
|
||||
healthCache: HealthCacheDto;
|
||||
initHealth: I80F48Dto;
|
||||
maintHealth: I80F48Dto;
|
||||
equity: {
|
||||
|
@ -521,6 +525,7 @@ export class MangoAccountData {
|
|||
tokenAssets: any;
|
||||
}) {
|
||||
return new MangoAccountData(
|
||||
new HealthCache(event.healthCache),
|
||||
I80F48.from(event.initHealth),
|
||||
I80F48.from(event.maintHealth),
|
||||
Equity.from(event.equity),
|
||||
|
|
|
@ -126,6 +126,28 @@ export class PerpMarket {
|
|||
const quoteUnit = Math.pow(10, QUOTE_DECIMALS);
|
||||
return new BN(uiQuote * quoteUnit).div(this.quoteLotSize);
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return (
|
||||
'PerpMarket ' +
|
||||
'\n perpMarketIndex -' +
|
||||
this.perpMarketIndex +
|
||||
'\n maintAssetWeight -' +
|
||||
this.maintAssetWeight.toNumber() +
|
||||
'\n initAssetWeight -' +
|
||||
this.initAssetWeight.toNumber() +
|
||||
'\n maintLiabWeight -' +
|
||||
this.maintLiabWeight.toNumber() +
|
||||
'\n initLiabWeight -' +
|
||||
this.initLiabWeight.toNumber() +
|
||||
'\n liquidationFee -' +
|
||||
this.liquidationFee.toNumber() +
|
||||
'\n makerFee -' +
|
||||
this.makerFee.toNumber() +
|
||||
'\n takerFee -' +
|
||||
this.takerFee.toNumber()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class Side {
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
import { Jupiter } from '@jup-ag/core';
|
||||
import { AnchorProvider, BN, Program, Provider } from '@project-serum/anchor';
|
||||
import { simulateTransaction } from '@project-serum/anchor/dist/cjs/utils/rpc';
|
||||
import { getFeeRates, getFeeTier } from '@project-serum/serum';
|
||||
import { Order } from '@project-serum/serum/lib/market';
|
||||
import {
|
||||
|
@ -67,24 +65,26 @@ export class MangoClient {
|
|||
|
||||
// Group
|
||||
|
||||
public async createGroup(
|
||||
public async groupCreate(
|
||||
groupNum: number,
|
||||
testing: boolean,
|
||||
insuranceMintPk: PublicKey,
|
||||
): Promise<TransactionSignature> {
|
||||
const adminPk = (this.program.provider as AnchorProvider).wallet.publicKey;
|
||||
return await this.program.methods
|
||||
.createGroup(groupNum, testing ? 1 : 0)
|
||||
.groupCreate(groupNum, testing ? 1 : 0)
|
||||
.accounts({
|
||||
admin: adminPk,
|
||||
payer: adminPk,
|
||||
insuranceMint: insuranceMintPk,
|
||||
})
|
||||
.rpc();
|
||||
}
|
||||
|
||||
public async closeGroup(group: Group): Promise<TransactionSignature> {
|
||||
public async groupClose(group: Group): Promise<TransactionSignature> {
|
||||
const adminPk = (this.program.provider as AnchorProvider).wallet.publicKey;
|
||||
return await this.program.methods
|
||||
.closeGroup()
|
||||
.groupClose()
|
||||
.accounts({
|
||||
group: group.publicKey,
|
||||
admin: adminPk,
|
||||
|
@ -120,7 +120,7 @@ export class MangoClient {
|
|||
filters.push({
|
||||
memcmp: {
|
||||
bytes: bs58.encode(bbuf),
|
||||
offset: 44,
|
||||
offset: 40,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -141,6 +141,7 @@ export class MangoClient {
|
|||
oracleConfFilter: number,
|
||||
tokenIndex: number,
|
||||
name: string,
|
||||
adjustmentFactor: number,
|
||||
util0: number,
|
||||
rate0: number,
|
||||
util1: number,
|
||||
|
@ -154,7 +155,6 @@ export class MangoClient {
|
|||
initLiabWeight: number,
|
||||
liquidationFee: number,
|
||||
): Promise<TransactionSignature> {
|
||||
const bn = I80F48.fromNumber(oracleConfFilter).getData();
|
||||
return await this.program.methods
|
||||
.tokenRegister(
|
||||
tokenIndex,
|
||||
|
@ -165,7 +165,7 @@ export class MangoClient {
|
|||
val: I80F48.fromNumber(oracleConfFilter).getData(),
|
||||
},
|
||||
} as any, // future: nested custom types dont typecheck, fix if possible?
|
||||
{ util0, rate0, util1, rate1, maxRate },
|
||||
{ adjustmentFactor, util0, rate0, util1, rate1, maxRate },
|
||||
loanFeeRate,
|
||||
loanOriginationFeeRate,
|
||||
maintAssetWeight,
|
||||
|
@ -185,6 +185,61 @@ export class MangoClient {
|
|||
.rpc();
|
||||
}
|
||||
|
||||
public async tokenEdit(
|
||||
group: Group,
|
||||
tokenName: string,
|
||||
oracle: PublicKey,
|
||||
oracleConfFilter: number,
|
||||
adjustmentFactor: number,
|
||||
util0: number,
|
||||
rate0: number,
|
||||
util1: number,
|
||||
rate1: number,
|
||||
maxRate: number,
|
||||
loanFeeRate: number,
|
||||
loanOriginationFeeRate: number,
|
||||
maintAssetWeight: number,
|
||||
initAssetWeight: number,
|
||||
maintLiabWeight: number,
|
||||
initLiabWeight: number,
|
||||
liquidationFee: number,
|
||||
): Promise<TransactionSignature> {
|
||||
const bank = group.banksMap.get(tokenName)!;
|
||||
const mintInfo = group.mintInfosMap.get(bank.tokenIndex)!;
|
||||
|
||||
return await this.program.methods
|
||||
.tokenEdit(
|
||||
new BN(0),
|
||||
oracle,
|
||||
{
|
||||
confFilter: {
|
||||
val: I80F48.fromNumber(oracleConfFilter).getData(),
|
||||
},
|
||||
} as any, // future: nested custom types dont typecheck, fix if possible?
|
||||
{ adjustmentFactor, util0, rate0, util1, rate1, maxRate },
|
||||
loanFeeRate,
|
||||
loanOriginationFeeRate,
|
||||
maintAssetWeight,
|
||||
initAssetWeight,
|
||||
maintLiabWeight,
|
||||
initLiabWeight,
|
||||
liquidationFee,
|
||||
)
|
||||
.accounts({
|
||||
group: group.publicKey,
|
||||
admin: (this.program.provider as AnchorProvider).wallet.publicKey,
|
||||
mintInfo: mintInfo.publicKey,
|
||||
})
|
||||
.remainingAccounts([
|
||||
{
|
||||
pubkey: bank.publicKey,
|
||||
isWritable: true,
|
||||
isSigner: false,
|
||||
} as AccountMeta,
|
||||
])
|
||||
.rpc({ skipPreflight: true });
|
||||
}
|
||||
|
||||
public async tokenDeregister(
|
||||
group: Group,
|
||||
tokenName: string,
|
||||
|
@ -236,7 +291,7 @@ export class MangoClient {
|
|||
{
|
||||
memcmp: {
|
||||
bytes: group.publicKey.toBase58(),
|
||||
offset: 24,
|
||||
offset: 8,
|
||||
},
|
||||
},
|
||||
])
|
||||
|
@ -275,7 +330,7 @@ export class MangoClient {
|
|||
{
|
||||
memcmp: {
|
||||
bytes: bs58.encode(tokenIndexBuf),
|
||||
offset: 200,
|
||||
offset: 40,
|
||||
},
|
||||
},
|
||||
])
|
||||
|
@ -286,13 +341,13 @@ export class MangoClient {
|
|||
|
||||
// Stub Oracle
|
||||
|
||||
public async createStubOracle(
|
||||
public async stubOracleCreate(
|
||||
group: Group,
|
||||
mintPk: PublicKey,
|
||||
price: number,
|
||||
): Promise<TransactionSignature> {
|
||||
return await this.program.methods
|
||||
.createStubOracle({ val: I80F48.fromNumber(price).getData() })
|
||||
.stubOracleCreate({ val: I80F48.fromNumber(price).getData() })
|
||||
.accounts({
|
||||
group: group.publicKey,
|
||||
admin: (this.program.provider as AnchorProvider).wallet.publicKey,
|
||||
|
@ -302,12 +357,12 @@ export class MangoClient {
|
|||
.rpc();
|
||||
}
|
||||
|
||||
public async closeStubOracle(
|
||||
public async stubOracleClose(
|
||||
group: Group,
|
||||
oracle: PublicKey,
|
||||
): Promise<TransactionSignature> {
|
||||
return await this.program.methods
|
||||
.closeStubOracle()
|
||||
.stubOracleClose()
|
||||
.accounts({
|
||||
group: group.publicKey,
|
||||
oracle: oracle,
|
||||
|
@ -317,13 +372,13 @@ export class MangoClient {
|
|||
.rpc();
|
||||
}
|
||||
|
||||
public async setStubOracle(
|
||||
public async stubOracleSet(
|
||||
group: Group,
|
||||
oraclePk: PublicKey,
|
||||
price: number,
|
||||
): Promise<TransactionSignature> {
|
||||
return await this.program.methods
|
||||
.setStubOracle({ val: I80F48.fromNumber(price).getData() })
|
||||
.stubOracleSet({ val: I80F48.fromNumber(price).getData() })
|
||||
.accounts({
|
||||
group: group.publicKey,
|
||||
admin: (this.program.provider as AnchorProvider).wallet.publicKey,
|
||||
|
@ -382,7 +437,7 @@ export class MangoClient {
|
|||
name?: string,
|
||||
): Promise<TransactionSignature> {
|
||||
return await this.program.methods
|
||||
.createAccount(accountNumber, name ?? '')
|
||||
.accountCreate(accountNumber, name ?? '')
|
||||
.accounts({
|
||||
group: group.publicKey,
|
||||
owner: (this.program.provider as AnchorProvider).wallet.publicKey,
|
||||
|
@ -391,6 +446,22 @@ export class MangoClient {
|
|||
.rpc();
|
||||
}
|
||||
|
||||
public async editMangoAccount(
|
||||
group: Group,
|
||||
mangoAccount: MangoAccount,
|
||||
name?: string,
|
||||
delegate?: PublicKey,
|
||||
): Promise<TransactionSignature> {
|
||||
return await this.program.methods
|
||||
.accountEdit(name, delegate)
|
||||
.accounts({
|
||||
group: group.publicKey,
|
||||
account: mangoAccount.publicKey,
|
||||
owner: (this.program.provider as AnchorProvider).wallet.publicKey,
|
||||
})
|
||||
.rpc();
|
||||
}
|
||||
|
||||
public async getMangoAccount(mangoAccount: MangoAccount) {
|
||||
return MangoAccount.from(
|
||||
mangoAccount.publicKey,
|
||||
|
@ -407,13 +478,13 @@ export class MangoClient {
|
|||
{
|
||||
memcmp: {
|
||||
bytes: group.publicKey.toBase58(),
|
||||
offset: 40,
|
||||
offset: 8,
|
||||
},
|
||||
},
|
||||
{
|
||||
memcmp: {
|
||||
bytes: ownerPk.toBase58(),
|
||||
offset: 72,
|
||||
offset: 40,
|
||||
},
|
||||
},
|
||||
])
|
||||
|
@ -427,7 +498,7 @@ export class MangoClient {
|
|||
mangoAccount: MangoAccount,
|
||||
): Promise<TransactionSignature> {
|
||||
return await this.program.methods
|
||||
.closeAccount()
|
||||
.accountClose()
|
||||
.accounts({
|
||||
group: group.publicKey,
|
||||
account: mangoAccount.publicKey,
|
||||
|
@ -663,7 +734,7 @@ export class MangoClient {
|
|||
{
|
||||
memcmp: {
|
||||
bytes: group.publicKey.toBase58(),
|
||||
offset: 24,
|
||||
offset: 8,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
@ -674,7 +745,7 @@ export class MangoClient {
|
|||
filters.push({
|
||||
memcmp: {
|
||||
bytes: bs58.encode(bbuf),
|
||||
offset: 122,
|
||||
offset: 40,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -685,7 +756,7 @@ export class MangoClient {
|
|||
filters.push({
|
||||
memcmp: {
|
||||
bytes: bs58.encode(qbuf),
|
||||
offset: 124,
|
||||
offset: 42,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -991,7 +1062,6 @@ export class MangoClient {
|
|||
} as any, // future: nested custom types dont typecheck, fix if possible?
|
||||
baseTokenIndex,
|
||||
baseTokenDecimals,
|
||||
quoteTokenIndex,
|
||||
new BN(quoteLotSize),
|
||||
new BN(baseLotSize),
|
||||
maintAssetWeight,
|
||||
|
@ -1054,6 +1124,55 @@ export class MangoClient {
|
|||
.rpc();
|
||||
}
|
||||
|
||||
async perpEditMarket(
|
||||
group: Group,
|
||||
perpMarketName: string,
|
||||
oracle: PublicKey,
|
||||
oracleConfFilter: number,
|
||||
baseTokenIndex: number,
|
||||
baseTokenDecimals: number,
|
||||
maintAssetWeight: number,
|
||||
initAssetWeight: number,
|
||||
maintLiabWeight: number,
|
||||
initLiabWeight: number,
|
||||
liquidationFee: number,
|
||||
makerFee: number,
|
||||
takerFee: number,
|
||||
minFunding: number,
|
||||
maxFunding: number,
|
||||
impactQuantity: number,
|
||||
): Promise<TransactionSignature> {
|
||||
const perpMarket = group.perpMarketsMap.get(perpMarketName)!;
|
||||
|
||||
return await this.program.methods
|
||||
.perpEditMarket(
|
||||
oracle,
|
||||
{
|
||||
confFilter: {
|
||||
val: I80F48.fromNumber(oracleConfFilter).getData(),
|
||||
},
|
||||
} as any, // future: nested custom types dont typecheck, fix if possible?
|
||||
baseTokenIndex,
|
||||
baseTokenDecimals,
|
||||
maintAssetWeight,
|
||||
initAssetWeight,
|
||||
maintLiabWeight,
|
||||
initLiabWeight,
|
||||
liquidationFee,
|
||||
makerFee,
|
||||
takerFee,
|
||||
minFunding,
|
||||
maxFunding,
|
||||
new BN(impactQuantity),
|
||||
)
|
||||
.accounts({
|
||||
group: group.publicKey,
|
||||
admin: (this.program.provider as AnchorProvider).wallet.publicKey,
|
||||
perpMarket: perpMarket.publicKey,
|
||||
})
|
||||
.rpc();
|
||||
}
|
||||
|
||||
async perpCloseMarket(
|
||||
group: Group,
|
||||
perpMarketName: string,
|
||||
|
@ -1078,7 +1197,6 @@ export class MangoClient {
|
|||
public async perpGetMarkets(
|
||||
group: Group,
|
||||
baseTokenIndex?: number,
|
||||
quoteTokenIndex?: number,
|
||||
): Promise<PerpMarket[]> {
|
||||
const bumpfbuf = Buffer.alloc(1);
|
||||
bumpfbuf.writeUInt8(255);
|
||||
|
@ -1087,7 +1205,7 @@ export class MangoClient {
|
|||
{
|
||||
memcmp: {
|
||||
bytes: group.publicKey.toBase58(),
|
||||
offset: 24,
|
||||
offset: 8,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
@ -1098,18 +1216,7 @@ export class MangoClient {
|
|||
filters.push({
|
||||
memcmp: {
|
||||
bytes: bs58.encode(bbuf),
|
||||
offset: 444,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (quoteTokenIndex) {
|
||||
const qbuf = Buffer.alloc(2);
|
||||
qbuf.writeUInt16LE(quoteTokenIndex);
|
||||
filters.push({
|
||||
memcmp: {
|
||||
bytes: bs58.encode(qbuf),
|
||||
offset: 446,
|
||||
offset: 40,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -1182,14 +1289,14 @@ export class MangoClient {
|
|||
inputToken,
|
||||
amountIn,
|
||||
outputToken,
|
||||
slippage = 0.5,
|
||||
userDefinedInstructions,
|
||||
}: {
|
||||
group: Group;
|
||||
mangoAccount: MangoAccount;
|
||||
inputToken: string;
|
||||
amountIn: number;
|
||||
outputToken: string;
|
||||
slippage: number;
|
||||
userDefinedInstructions: TransactionInstruction[];
|
||||
}): Promise<TransactionSignature> {
|
||||
const inputBank = group.banksMap.get(inputToken);
|
||||
const outputBank = group.banksMap.get(outputToken);
|
||||
|
@ -1260,8 +1367,7 @@ export class MangoClient {
|
|||
}
|
||||
|
||||
/*
|
||||
* Transfer input token to users wallet, then swap with the Jupiter route,
|
||||
* and finally transfer output token from users wallet back to the mango vault
|
||||
* Transfer input token to users wallet, then concat the passed in instructions
|
||||
*/
|
||||
const nativeInputAmount = toU64(
|
||||
amountIn,
|
||||
|
@ -1281,57 +1387,7 @@ export class MangoClient {
|
|||
transferIx.keys[2] = { ...inputBankKey, isWritable: true, isSigner: false };
|
||||
instructions.push(transferIx);
|
||||
|
||||
// TODO: move out of client and into ui
|
||||
// Start Jupiter
|
||||
|
||||
const jupiter = await Jupiter.load({
|
||||
connection: this.program.provider.connection,
|
||||
cluster: 'mainnet-beta',
|
||||
user: mangoAccount.owner, // or public key
|
||||
// platformFeeAndAccounts: NO_PLATFORM_FEE,
|
||||
routeCacheDuration: 10_000, // Will not refetch data on computeRoutes for up to 10 seconds
|
||||
});
|
||||
|
||||
const routes = await jupiter.computeRoutes({
|
||||
inputMint: inputBank.mint, // Mint address of the input token
|
||||
outputMint: outputBank.mint, // Mint address of the output token
|
||||
inputAmount: nativeInputAmount, // raw input amount of tokens
|
||||
slippage, // The slippage in % terms
|
||||
forceFetch: false, // false is the default value => will use cache if not older than routeCacheDuration
|
||||
});
|
||||
|
||||
const routesInfosWithoutRaydium = routes.routesInfos.filter((r) => {
|
||||
if (r.marketInfos.length > 1) {
|
||||
for (const mkt of r.marketInfos) {
|
||||
if (mkt.amm.label === 'Raydium' || mkt.amm.label === 'Serum')
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
const selectedRoute = routesInfosWithoutRaydium[0];
|
||||
|
||||
console.log(
|
||||
`route found: ${selectedRoute.marketInfos[0].amm.label}. generating jup transaction`,
|
||||
);
|
||||
|
||||
const { transactions } = await jupiter.exchange({
|
||||
routeInfo: selectedRoute,
|
||||
});
|
||||
console.log('Jupiter Transactions:', transactions);
|
||||
const { setupTransaction, swapTransaction } = transactions;
|
||||
|
||||
for (const ix of swapTransaction.instructions) {
|
||||
if (
|
||||
ix.programId.toBase58() ===
|
||||
'JUP2jxvXaqu7NQY1GmNF4m1vodw12LVXYxbFL2uJvfo'
|
||||
) {
|
||||
instructions.push(ix);
|
||||
}
|
||||
}
|
||||
|
||||
// End Jupiter
|
||||
instructions.concat(userDefinedInstructions);
|
||||
|
||||
const transferIx2 = Token.createTransferInstruction(
|
||||
TOKEN_PROGRAM_ID,
|
||||
|
@ -1339,12 +1395,12 @@ export class MangoClient {
|
|||
outputBank.vault,
|
||||
mangoAccount.owner,
|
||||
[],
|
||||
selectedRoute.outAmountWithSlippage,
|
||||
0, // todo: use this for testing, this should be the amount to transfer back
|
||||
);
|
||||
instructions.push(transferIx2);
|
||||
|
||||
/*
|
||||
* Build data objects for margin trade instructions
|
||||
* Create object of amounts that will be withdrawn from bank vaults
|
||||
*/
|
||||
const targetRemainingAccounts = instructions
|
||||
.map((ix) => [
|
||||
|
@ -1368,6 +1424,9 @@ export class MangoClient {
|
|||
},
|
||||
];
|
||||
|
||||
/*
|
||||
* Build cpi data objects for instructions
|
||||
*/
|
||||
let cpiDatas = [];
|
||||
for (const [index, ix] of instructions.entries()) {
|
||||
if (index === 0) {
|
||||
|
@ -1385,27 +1444,11 @@ export class MangoClient {
|
|||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
'instructions',
|
||||
instructions.map((i) => ({ ...i, programId: i.programId.toString() })),
|
||||
);
|
||||
console.log('cpiDatas', cpiDatas);
|
||||
console.log(
|
||||
'targetRemainingAccounts',
|
||||
targetRemainingAccounts.map((t) => ({
|
||||
...t,
|
||||
pubkey: t.pubkey.toString(),
|
||||
})),
|
||||
);
|
||||
|
||||
if (setupTransaction) {
|
||||
await this.program.provider.sendAndConfirm(setupTransaction);
|
||||
} else if (preInstructions.length) {
|
||||
if (preInstructions.length) {
|
||||
const tx = new Transaction();
|
||||
for (const ix of preInstructions) {
|
||||
tx.add(ix);
|
||||
}
|
||||
console.log('preInstructions', preInstructions);
|
||||
|
||||
await this.program.provider.sendAndConfirm(tx);
|
||||
}
|
||||
|
@ -1454,6 +1497,7 @@ export class MangoClient {
|
|||
isSigner: false,
|
||||
} as AccountMeta),
|
||||
);
|
||||
console.log('1');
|
||||
|
||||
/*
|
||||
* Find or create associated token accounts
|
||||
|
@ -1500,6 +1544,7 @@ export class MangoClient {
|
|||
),
|
||||
);
|
||||
}
|
||||
console.log('2');
|
||||
|
||||
if (preInstructions.length) {
|
||||
const tx = new Transaction();
|
||||
|
@ -1510,6 +1555,7 @@ export class MangoClient {
|
|||
|
||||
await this.program.provider.sendAndConfirm(tx);
|
||||
}
|
||||
console.log('3');
|
||||
|
||||
const inputBankAccount = {
|
||||
pubkey: inputBank.publicKey,
|
||||
|
@ -1560,6 +1606,9 @@ export class MangoClient {
|
|||
},
|
||||
])
|
||||
.instruction();
|
||||
console.log('4');
|
||||
|
||||
// userDefinedInstructions.push(flashLoanEndIx);
|
||||
|
||||
const flashLoanBeginIx = await this.program.methods
|
||||
.flashLoan3Begin([
|
||||
|
|
|
@ -3,7 +3,9 @@ import { StubOracle } from './accounts/oracle';
|
|||
import { MangoClient } from './client';
|
||||
import { MANGO_V4_ID } from './constants';
|
||||
|
||||
export * from './accounts/bank';
|
||||
export * from './accounts/I80F48';
|
||||
export * from './accounts/mangoAccount';
|
||||
export {
|
||||
Serum3Market,
|
||||
Serum3OrderType,
|
||||
|
@ -12,6 +14,4 @@ export {
|
|||
} from './accounts/serum3';
|
||||
export * from './constants';
|
||||
export * from './utils';
|
||||
export * from './accounts/mangoAccount';
|
||||
|
||||
export { Group, StubOracle, MangoClient, MANGO_V4_ID };
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -7,6 +7,10 @@ import { MangoClient } from '../client';
|
|||
import { MANGO_V4_ID, SERUM3_PROGRAM_ID } from '../constants';
|
||||
import { Id } from '../ids';
|
||||
|
||||
//
|
||||
// script to add a group to ids json
|
||||
//
|
||||
|
||||
function replacer(key, value) {
|
||||
if (value instanceof Map) {
|
||||
return Object.fromEntries(value);
|
||||
|
|
|
@ -4,6 +4,10 @@ import fs from 'fs';
|
|||
import { MangoClient } from '../client';
|
||||
import { MANGO_V4_ID } from '../constants';
|
||||
|
||||
//
|
||||
// example script to close accounts - banks, markets, group etc. which require admin to be the signer
|
||||
//
|
||||
|
||||
export const DEVNET_MINTS = new Map([
|
||||
['USDC', '8FRFC6MoGGkMFQwngccyu69VnYbzykGeez7ignHVAFSN'], // use devnet usdc
|
||||
]);
|
||||
|
@ -40,7 +44,7 @@ async function main() {
|
|||
const usdcDevnetOracle = (
|
||||
await client.getStubOracle(group, usdcDevnetMint)
|
||||
)[0];
|
||||
sig = await client.closeStubOracle(group, usdcDevnetOracle.publicKey);
|
||||
sig = await client.stubOracleClose(group, usdcDevnetOracle.publicKey);
|
||||
console.log(
|
||||
`Closed USDC stub oracle, sig https://explorer.solana.com/tx/${sig}?cluster=devnet`,
|
||||
);
|
||||
|
@ -71,7 +75,7 @@ async function main() {
|
|||
|
||||
// finally, close the group
|
||||
|
||||
sig = await client.closeGroup(group);
|
||||
sig = await client.groupClose(group);
|
||||
console.log(
|
||||
`Closed group, sig https://explorer.solana.com/tx/${sig}?cluster=devnet`,
|
||||
);
|
||||
|
|
|
@ -4,6 +4,16 @@ import fs from 'fs';
|
|||
import { MangoClient } from '../client';
|
||||
import { MANGO_V4_ID } from '../constants';
|
||||
|
||||
//
|
||||
// An example for admins based on high level api i.e. the client
|
||||
// Depoys a new mango group to devnet, registers 4 tokens, and 1 serum3 spot market
|
||||
//
|
||||
// process.env.ADMIN_KEYPAIR - group admin keypair path
|
||||
// to create a new admin keypair:
|
||||
// * solana-keygen new --outfile ~/.config/solana/admin.json
|
||||
// * solana airdrop 1 -k ~/.config/solana/admin.json
|
||||
//
|
||||
|
||||
const DEVNET_SERUM3_MARKETS = new Map([
|
||||
['BTC/USDC', 'DW83EpHFywBxCHmyARxwj3nzxJd7MUdSeznmrdzZKNZB'],
|
||||
['SOL/USDC', '5xWpt56U1NCuHoAEtpLeUrQcxDkEpNfScjfLFaRzLPgR'],
|
||||
|
@ -20,15 +30,6 @@ const DEVNET_ORACLES = new Map([
|
|||
['ORCA', 'A1WttWF7X3Rg6ZRpB2YQUFHCRh1kiXV8sKKLV3S9neJV'],
|
||||
]);
|
||||
|
||||
//
|
||||
// An example for admins based on high level api i.e. the client
|
||||
// Depoys a new mango group to devnet, registers 2 tokens, and 1 serum3 spot market
|
||||
//
|
||||
// process.env.ADMIN_KEYPAIR - group admin keypair path
|
||||
// to create a new admin keypair:
|
||||
// * solana-keygen new --outfile ~/.config/solana/admin.json
|
||||
// * solana airdrop 1 -k ~/.config/solana/admin.json
|
||||
//
|
||||
async function main() {
|
||||
const options = AnchorProvider.defaultOptions();
|
||||
const connection = new Connection(
|
||||
|
@ -52,15 +53,16 @@ async function main() {
|
|||
|
||||
// group
|
||||
console.log(`Creating Group...`);
|
||||
const insuranceMint = new PublicKey(DEVNET_MINTS.get('USDC')!);
|
||||
try {
|
||||
await client.createGroup(0, true);
|
||||
await client.groupCreate(0, true, insuranceMint);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
const group = await client.getGroupForAdmin(admin.publicKey);
|
||||
console.log(`...registered group ${group.publicKey}`);
|
||||
|
||||
// register token 0
|
||||
// register token 1
|
||||
console.log(`Registering BTC...`);
|
||||
const btcDevnetMint = new PublicKey(DEVNET_MINTS.get('BTC')!);
|
||||
const btcDevnetOracle = new PublicKey(DEVNET_ORACLES.get('BTC')!);
|
||||
|
@ -70,8 +72,9 @@ async function main() {
|
|||
btcDevnetMint,
|
||||
btcDevnetOracle,
|
||||
0.1,
|
||||
0,
|
||||
1, // tokenIndex
|
||||
'BTC',
|
||||
0.01,
|
||||
0.4,
|
||||
0.07,
|
||||
0.8,
|
||||
|
@ -90,11 +93,11 @@ async function main() {
|
|||
console.log(error);
|
||||
}
|
||||
|
||||
// stub oracle + register token 1
|
||||
// stub oracle + register token 0
|
||||
console.log(`Registering USDC...`);
|
||||
const usdcDevnetMint = new PublicKey(DEVNET_MINTS.get('USDC')!);
|
||||
try {
|
||||
await client.createStubOracle(group, usdcDevnetMint, 1.0);
|
||||
await client.stubOracleCreate(group, usdcDevnetMint, 1.0);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
@ -108,8 +111,9 @@ async function main() {
|
|||
usdcDevnetMint,
|
||||
usdcDevnetOracle.publicKey,
|
||||
0.1,
|
||||
1,
|
||||
0, // tokenIndex
|
||||
'USDC',
|
||||
0.01,
|
||||
0.4,
|
||||
0.07,
|
||||
0.8,
|
||||
|
@ -138,6 +142,7 @@ async function main() {
|
|||
0.1,
|
||||
2, // tokenIndex
|
||||
'SOL',
|
||||
0.01,
|
||||
0.4,
|
||||
0.07,
|
||||
0.8,
|
||||
|
@ -168,6 +173,7 @@ async function main() {
|
|||
0.1,
|
||||
3, // tokenIndex
|
||||
'ORCA',
|
||||
0.01,
|
||||
0.4,
|
||||
0.07,
|
||||
0.8,
|
||||
|
@ -227,7 +233,7 @@ async function main() {
|
|||
0,
|
||||
'BTC-PERP',
|
||||
0.1,
|
||||
0,
|
||||
1,
|
||||
6,
|
||||
1,
|
||||
10,
|
||||
|
@ -250,10 +256,121 @@ async function main() {
|
|||
const perpMarkets = await client.perpGetMarkets(
|
||||
group,
|
||||
group.banksMap.get('BTC')?.tokenIndex,
|
||||
group.banksMap.get('USDC')?.tokenIndex,
|
||||
);
|
||||
console.log(`...created perp market ${perpMarkets[0].publicKey}`);
|
||||
|
||||
//
|
||||
// edit
|
||||
//
|
||||
|
||||
console.log(`Editing USDC...`);
|
||||
try {
|
||||
let sig = await client.tokenEdit(
|
||||
group,
|
||||
'USDC',
|
||||
btcDevnetOracle,
|
||||
0.1,
|
||||
0.01,
|
||||
0.3,
|
||||
0.08,
|
||||
0.81,
|
||||
0.91,
|
||||
0.75,
|
||||
0.0007,
|
||||
1.7,
|
||||
0.9,
|
||||
0.7,
|
||||
1.3,
|
||||
1.5,
|
||||
0.04,
|
||||
);
|
||||
console.log(`https://explorer.solana.com/tx/${sig}?cluster=devnet`);
|
||||
await group.reloadAll(client);
|
||||
console.log(group.banksMap.get('USDC').toString());
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
console.log(`Resetting USDC...`);
|
||||
try {
|
||||
let sig = await client.tokenEdit(
|
||||
group,
|
||||
'USDC',
|
||||
usdcDevnetOracle.publicKey,
|
||||
0.1,
|
||||
0.01,
|
||||
0.4,
|
||||
0.07,
|
||||
0.8,
|
||||
0.9,
|
||||
1.5,
|
||||
0.0005,
|
||||
1.5,
|
||||
0.8,
|
||||
0.6,
|
||||
1.2,
|
||||
1.4,
|
||||
0.02,
|
||||
);
|
||||
console.log(`https://explorer.solana.com/tx/${sig}?cluster=devnet`);
|
||||
await group.reloadAll(client);
|
||||
console.log(group.banksMap.get('USDC').toString());
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
console.log(`Editing perp market...`);
|
||||
try {
|
||||
let sig = await client.perpEditMarket(
|
||||
group,
|
||||
'BTC-PERP',
|
||||
btcDevnetOracle,
|
||||
0.2,
|
||||
0,
|
||||
6,
|
||||
0.9,
|
||||
0.9,
|
||||
1.035,
|
||||
1.06,
|
||||
0.013,
|
||||
0.0003,
|
||||
0.1,
|
||||
0.07,
|
||||
0.07,
|
||||
1001,
|
||||
);
|
||||
console.log(`https://explorer.solana.com/tx/${sig}?cluster=devnet`);
|
||||
await group.reloadAll(client);
|
||||
console.log(group.perpMarketsMap.get('BTC-PERP').toString());
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
console.log(`Resetting perp market...`);
|
||||
try {
|
||||
let sig = await client.perpEditMarket(
|
||||
group,
|
||||
'BTC-PERP',
|
||||
btcDevnetOracle,
|
||||
0.1,
|
||||
1,
|
||||
6,
|
||||
1,
|
||||
0.95,
|
||||
1.025,
|
||||
1.05,
|
||||
0.012,
|
||||
0.0002,
|
||||
0.0,
|
||||
0.05,
|
||||
0.05,
|
||||
100,
|
||||
);
|
||||
console.log(`https://explorer.solana.com/tx/${sig}?cluster=devnet`);
|
||||
await group.reloadAll(client);
|
||||
console.log(group.perpMarketsMap.get('BTC-PERP').toString());
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
process.exit();
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,10 @@ import fs from 'fs';
|
|||
import { MangoClient } from '../client';
|
||||
import { MANGO_V4_ID } from '../constants';
|
||||
|
||||
//
|
||||
// (unfinished?) script which shows how to use the flash loan 1 ix
|
||||
//
|
||||
|
||||
async function main() {
|
||||
const options = AnchorProvider.defaultOptions();
|
||||
const connection = new Connection(
|
||||
|
|
|
@ -3,6 +3,9 @@ import { Connection, Keypair } from '@solana/web3.js';
|
|||
import fs from 'fs';
|
||||
import { MangoClient } from '../client';
|
||||
|
||||
//
|
||||
// script which shows example usage of ids json (saves having to do gpa)
|
||||
//
|
||||
async function main() {
|
||||
const options = AnchorProvider.defaultOptions();
|
||||
const connection = new Connection(
|
||||
|
|
|
@ -5,6 +5,10 @@ import { Serum3Side } from '../accounts/serum3';
|
|||
import { MangoClient } from '../client';
|
||||
import { MANGO_V4_ID } from '../constants';
|
||||
|
||||
//
|
||||
// script which shows how to close a mango account cleanly i.e. close all active positions, withdraw all tokens, etc.
|
||||
//
|
||||
|
||||
// note: either use finalized or expect closing certain things to fail and having to runs scrript multiple times
|
||||
async function main() {
|
||||
const options = AnchorProvider.defaultOptions();
|
||||
|
|
|
@ -0,0 +1,167 @@
|
|||
import { AnchorProvider, Wallet } from '@project-serum/anchor';
|
||||
import { Connection, Keypair } from '@solana/web3.js';
|
||||
import fs from 'fs';
|
||||
import { OrderType, Side } from '../accounts/perp';
|
||||
import {
|
||||
Serum3OrderType,
|
||||
Serum3SelfTradeBehavior,
|
||||
Serum3Side,
|
||||
} from '../accounts/serum3';
|
||||
import { MangoClient } from '../client';
|
||||
import { MANGO_V4_ID } from '../constants';
|
||||
|
||||
//
|
||||
// An example for users based on high level api i.e. the client
|
||||
// Create
|
||||
// process.env.USER_KEYPAIR - mango account owner keypair path
|
||||
// process.env.ADMIN_KEYPAIR - group admin keypair path (useful for automatically finding the group)
|
||||
//
|
||||
// This script deposits some tokens, places some serum orders, cancels them, places some perp orders
|
||||
//
|
||||
|
||||
async function main() {
|
||||
const options = AnchorProvider.defaultOptions();
|
||||
const connection = new Connection(
|
||||
'https://mango.devnet.rpcpool.com',
|
||||
options,
|
||||
);
|
||||
|
||||
// mango account owner
|
||||
const user = Keypair.fromSecretKey(
|
||||
Buffer.from(
|
||||
JSON.parse(fs.readFileSync(process.env.USER_KEYPAIR!, 'utf-8')),
|
||||
),
|
||||
);
|
||||
const userWallet = new Wallet(user);
|
||||
const userProvider = new AnchorProvider(connection, userWallet, options);
|
||||
const client = await MangoClient.connect(
|
||||
userProvider,
|
||||
'devnet',
|
||||
MANGO_V4_ID['devnet'],
|
||||
);
|
||||
console.log(`User ${userWallet.publicKey.toBase58()}`);
|
||||
|
||||
// delegate
|
||||
const delegate = Keypair.fromSecretKey(
|
||||
Buffer.from(JSON.parse(fs.readFileSync(process.env.DELEGATE!, 'utf-8'))),
|
||||
);
|
||||
const delegateWallet = new Wallet(delegate);
|
||||
const delegateProvider = new AnchorProvider(
|
||||
connection,
|
||||
delegateWallet,
|
||||
options,
|
||||
);
|
||||
// Note: simply create a client with delegate and use this client to execute ixs
|
||||
const delegateClient = await MangoClient.connect(
|
||||
delegateProvider,
|
||||
'devnet',
|
||||
MANGO_V4_ID['devnet'],
|
||||
);
|
||||
console.log(`Delegate ${delegateWallet.publicKey.toBase58()}`);
|
||||
|
||||
// fetch group
|
||||
const admin = Keypair.fromSecretKey(
|
||||
Buffer.from(
|
||||
JSON.parse(fs.readFileSync(process.env.ADMIN_KEYPAIR!, 'utf-8')),
|
||||
),
|
||||
);
|
||||
const group = await delegateClient.getGroupForAdmin(admin.publicKey, 0);
|
||||
console.log(group.toString());
|
||||
|
||||
// fetch mango account using owners pubkey
|
||||
console.log(`Fetching mangoaccount...`);
|
||||
const mangoAccount = (
|
||||
await delegateClient.getMangoAccountForOwner(group, user.publicKey)
|
||||
)[0];
|
||||
console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`);
|
||||
console.log(mangoAccount.toString());
|
||||
|
||||
if (true) {
|
||||
// set delegate, and change name
|
||||
console.log(`...changing mango account name, and setting a delegate`);
|
||||
await client.editMangoAccount(group, 'my_changed_name', delegate.publicKey);
|
||||
await mangoAccount.reload(client, group);
|
||||
console.log(mangoAccount.toString());
|
||||
}
|
||||
|
||||
if (true) {
|
||||
// deposit
|
||||
console.log(`...depositing 50 USDC`);
|
||||
await client.tokenDeposit(group, mangoAccount, 'USDC', 50);
|
||||
await mangoAccount.reload(client, group);
|
||||
|
||||
console.log(`...depositing 0.0005 BTC`);
|
||||
await client.tokenDeposit(group, mangoAccount, 'BTC', 0.0005);
|
||||
await mangoAccount.reload(client, group);
|
||||
|
||||
// serum3
|
||||
console.log(`...placing serum3 bid`);
|
||||
await delegateClient.serum3PlaceOrder(
|
||||
group,
|
||||
mangoAccount,
|
||||
'BTC/USDC',
|
||||
Serum3Side.bid,
|
||||
20,
|
||||
0.0001,
|
||||
Serum3SelfTradeBehavior.decrementTake,
|
||||
Serum3OrderType.limit,
|
||||
Date.now(),
|
||||
10,
|
||||
);
|
||||
await mangoAccount.reload(delegateClient, group);
|
||||
|
||||
console.log(`...current own orders on OB`);
|
||||
let orders = await delegateClient.getSerum3Orders(
|
||||
group,
|
||||
|
||||
'BTC/USDC',
|
||||
);
|
||||
for (const order of orders) {
|
||||
console.log(
|
||||
` - order orderId ${order.orderId}, ${order.side}, ${order.price}, ${order.size}`,
|
||||
);
|
||||
console.log(` - cancelling order with ${order.orderId}`);
|
||||
await delegateClient.serum3CancelOrder(
|
||||
group,
|
||||
mangoAccount,
|
||||
'BTC/USDC',
|
||||
order.side === 'buy' ? Serum3Side.bid : Serum3Side.ask,
|
||||
order.orderId,
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`...settling funds`);
|
||||
await delegateClient.serum3SettleFunds(
|
||||
group,
|
||||
mangoAccount,
|
||||
|
||||
'BTC/USDC',
|
||||
);
|
||||
}
|
||||
|
||||
if (true) {
|
||||
// perps
|
||||
console.log(`...placing perp bid`);
|
||||
try {
|
||||
await delegateClient.perpPlaceOrder(
|
||||
group,
|
||||
mangoAccount,
|
||||
'BTC-PERP',
|
||||
Side.bid,
|
||||
30000,
|
||||
0.000001,
|
||||
30000 * 0.000001,
|
||||
Math.floor(Math.random() * 99999),
|
||||
OrderType.limit,
|
||||
0,
|
||||
1,
|
||||
);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
process.exit();
|
||||
}
|
||||
|
||||
main();
|
|
@ -1,6 +1,7 @@
|
|||
import { AnchorProvider, Wallet } from '@project-serum/anchor';
|
||||
import { Connection, Keypair } from '@solana/web3.js';
|
||||
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
|
||||
import fs from 'fs';
|
||||
import { HealthType } from '../accounts/mangoAccount';
|
||||
import { OrderType, Side } from '../accounts/perp';
|
||||
import {
|
||||
Serum3OrderType,
|
||||
|
@ -17,6 +18,9 @@ import { toUiDecimals } from '../utils';
|
|||
// process.env.USER_KEYPAIR - mango account owner keypair path
|
||||
// process.env.ADMIN_KEYPAIR - group admin keypair path (useful for automatically finding the group)
|
||||
//
|
||||
// This script deposits some tokens, places some serum orders, cancels them, places some perp orders
|
||||
//
|
||||
|
||||
async function main() {
|
||||
const options = AnchorProvider.defaultOptions();
|
||||
const connection = new Connection(
|
||||
|
@ -58,32 +62,57 @@ async function main() {
|
|||
console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`);
|
||||
console.log(mangoAccount.toString());
|
||||
|
||||
await mangoAccount.reloadAccountData(client, group);
|
||||
|
||||
if (true) {
|
||||
// deposit and withdraw
|
||||
console.log(`Depositing...50 USDC`);
|
||||
await client.tokenDeposit(group, mangoAccount, 'USDC', 50);
|
||||
await mangoAccount.reload(client, group);
|
||||
|
||||
console.log(`Depositing...0.0005 BTC`);
|
||||
await client.tokenDeposit(group, mangoAccount, 'BTC', 0.0005);
|
||||
await mangoAccount.reload(client, group);
|
||||
|
||||
console.log(`Withdrawing...0.1 ORCA`);
|
||||
await client.tokenWithdraw2(
|
||||
// set delegate, and change name
|
||||
console.log(`...changing mango account name, and setting a delegate`);
|
||||
const randomKey = new PublicKey(
|
||||
'4ZkS7ZZkxfsC3GtvvsHP3DFcUeByU9zzZELS4r8HCELo',
|
||||
);
|
||||
await client.editMangoAccount(
|
||||
group,
|
||||
mangoAccount,
|
||||
'ORCA',
|
||||
0.1 * Math.pow(10, group.banksMap.get('ORCA').mintDecimals),
|
||||
true,
|
||||
'my_changed_name',
|
||||
randomKey,
|
||||
);
|
||||
await mangoAccount.reload(client, group);
|
||||
console.log(mangoAccount.toString());
|
||||
|
||||
console.log(`...resetting mango account name, and re-setting a delegate`);
|
||||
await client.editMangoAccount(
|
||||
group,
|
||||
mangoAccount,
|
||||
'my_mango_account',
|
||||
PublicKey.default,
|
||||
);
|
||||
await mangoAccount.reload(client, group);
|
||||
console.log(mangoAccount.toString());
|
||||
}
|
||||
|
||||
if (true) {
|
||||
// deposit and withdraw
|
||||
console.log(`...depositing 50 USDC`);
|
||||
await client.tokenDeposit(group, mangoAccount, 'USDC', 50);
|
||||
await mangoAccount.reload(client, group);
|
||||
|
||||
console.log(`...depositing 0.0005 BTC`);
|
||||
await client.tokenDeposit(group, mangoAccount, 'BTC', 0.0005);
|
||||
await mangoAccount.reload(client, group);
|
||||
|
||||
// witdrawing fails if no (other) user has deposited ORCA in the group
|
||||
// console.log(`Withdrawing...0.1 ORCA`);
|
||||
// await client.tokenWithdraw2(
|
||||
// group,
|
||||
// mangoAccount,
|
||||
// 'ORCA',
|
||||
// 0.1 * Math.pow(10, group.banksMap.get('ORCA').mintDecimals),
|
||||
// true,
|
||||
// );
|
||||
// await mangoAccount.reload(client, group);
|
||||
// console.log(mangoAccount.toString());
|
||||
|
||||
// serum3
|
||||
console.log(
|
||||
`Placing serum3 bid which would not be settled since its relatively low then midprice...`,
|
||||
`...placing serum3 bid which would not be settled since its relatively low then midprice`,
|
||||
);
|
||||
await client.serum3PlaceOrder(
|
||||
group,
|
||||
|
@ -100,7 +129,7 @@ async function main() {
|
|||
);
|
||||
await mangoAccount.reload(client, group);
|
||||
|
||||
console.log(`Placing serum3 bid way above midprice...`);
|
||||
console.log(`...placing serum3 bid way above midprice`);
|
||||
await client.serum3PlaceOrder(
|
||||
group,
|
||||
mangoAccount,
|
||||
|
@ -116,7 +145,7 @@ async function main() {
|
|||
);
|
||||
await mangoAccount.reload(client, group);
|
||||
|
||||
console.log(`Placing serum3 ask way below midprice...`);
|
||||
console.log(`...placing serum3 ask way below midprice`);
|
||||
await client.serum3PlaceOrder(
|
||||
group,
|
||||
mangoAccount,
|
||||
|
@ -131,7 +160,7 @@ async function main() {
|
|||
10,
|
||||
);
|
||||
|
||||
console.log(`Current own orders on OB...`);
|
||||
console.log(`...current own orders on OB`);
|
||||
let orders = await client.getSerum3Orders(
|
||||
group,
|
||||
|
||||
|
@ -139,9 +168,9 @@ async function main() {
|
|||
);
|
||||
for (const order of orders) {
|
||||
console.log(
|
||||
` - Order orderId ${order.orderId}, ${order.side}, ${order.price}, ${order.size}`,
|
||||
` - order orderId ${order.orderId}, ${order.side}, ${order.price}, ${order.size}`,
|
||||
);
|
||||
console.log(` - Cancelling order with ${order.orderId}`);
|
||||
console.log(` - cancelling order with ${order.orderId}`);
|
||||
await client.serum3CancelOrder(
|
||||
group,
|
||||
mangoAccount,
|
||||
|
@ -152,7 +181,7 @@ async function main() {
|
|||
);
|
||||
}
|
||||
|
||||
console.log(`Current own orders on OB...`);
|
||||
console.log(`...current own orders on OB`);
|
||||
orders = await client.getSerum3Orders(
|
||||
group,
|
||||
|
||||
|
@ -162,7 +191,7 @@ async function main() {
|
|||
console.log(order);
|
||||
}
|
||||
|
||||
console.log(`Settling funds...`);
|
||||
console.log(`...settling funds`);
|
||||
await client.serum3SettleFunds(
|
||||
group,
|
||||
mangoAccount,
|
||||
|
@ -174,23 +203,31 @@ async function main() {
|
|||
if (true) {
|
||||
await mangoAccount.reload(client, group);
|
||||
console.log(
|
||||
'mangoAccount.getEquity() ' +
|
||||
'...mangoAccount.getEquity() ' +
|
||||
toUiDecimals(mangoAccount.getEquity().toNumber()),
|
||||
);
|
||||
console.log(
|
||||
'mangoAccount.getCollateralValue() ' +
|
||||
'...mangoAccount.getCollateralValue() ' +
|
||||
toUiDecimals(mangoAccount.getCollateralValue().toNumber()),
|
||||
);
|
||||
console.log(
|
||||
'mangoAccount.getAssetsVal() ' +
|
||||
'...mangoAccount.accountData["healthCache"].health(HealthType.init) ' +
|
||||
toUiDecimals(
|
||||
mangoAccount.accountData['healthCache']
|
||||
.health(HealthType.init)
|
||||
.toNumber(),
|
||||
),
|
||||
);
|
||||
console.log(
|
||||
'...mangoAccount.getAssetsVal() ' +
|
||||
toUiDecimals(mangoAccount.getAssetsVal().toNumber()),
|
||||
);
|
||||
console.log(
|
||||
'mangoAccount.getLiabsVal() ' +
|
||||
'...mangoAccount.getLiabsVal() ' +
|
||||
toUiDecimals(mangoAccount.getLiabsVal().toNumber()),
|
||||
);
|
||||
console.log(
|
||||
"mangoAccount.getMaxWithdrawWithBorrowForToken(group, 'SOL') " +
|
||||
'...mangoAccount.getMaxWithdrawWithBorrowForToken(group, "SOL") ' +
|
||||
toUiDecimals(
|
||||
(
|
||||
await mangoAccount.getMaxWithdrawWithBorrowForToken(group, 'SOL')
|
||||
|
@ -198,7 +235,7 @@ async function main() {
|
|||
),
|
||||
);
|
||||
console.log(
|
||||
"mangoAccount.getSerum3MarketMarginAvailable(group, 'BTC/USDC') " +
|
||||
"...mangoAccount.getSerum3MarketMarginAvailable(group, 'BTC/USDC') " +
|
||||
toUiDecimals(
|
||||
mangoAccount
|
||||
.getSerum3MarketMarginAvailable(group, 'BTC/USDC')
|
||||
|
@ -206,7 +243,7 @@ async function main() {
|
|||
),
|
||||
);
|
||||
console.log(
|
||||
"mangoAccount.getPerpMarketMarginAvailable(group, 'BTC-PERP') " +
|
||||
"...mangoAccount.getPerpMarketMarginAvailable(group, 'BTC-PERP') " +
|
||||
toUiDecimals(
|
||||
mangoAccount
|
||||
.getPerpMarketMarginAvailable(group, 'BTC-PERP')
|
||||
|
@ -217,7 +254,7 @@ async function main() {
|
|||
|
||||
if (true) {
|
||||
// perps
|
||||
console.log(`Placing perp bid...`);
|
||||
console.log(`...placing perp bid`);
|
||||
try {
|
||||
await client.perpPlaceOrder(
|
||||
group,
|
||||
|
@ -236,7 +273,7 @@ async function main() {
|
|||
console.log(error);
|
||||
}
|
||||
|
||||
console.log(`Placing perp ask...`);
|
||||
console.log(`...placing perp ask`);
|
||||
await client.perpPlaceOrder(
|
||||
group,
|
||||
mangoAccount,
|
||||
|
@ -254,7 +291,7 @@ async function main() {
|
|||
while (true) {
|
||||
// TODO: quotePositionNative might be buggy on program side, investigate...
|
||||
console.log(
|
||||
`Waiting for self trade to consume (note: make sure keeper crank is running)...`,
|
||||
`...waiting for self trade to consume (note: make sure keeper crank is running)`,
|
||||
);
|
||||
await mangoAccount.reload(client, group);
|
||||
console.log(mangoAccount.toString());
|
||||
|
|
|
@ -7,6 +7,10 @@ import { MangoClient } from '../client';
|
|||
import { MANGO_V4_ID, SERUM3_PROGRAM_ID } from '../constants';
|
||||
import { Id } from '../ids';
|
||||
|
||||
//
|
||||
// script to add a group to ids json
|
||||
//
|
||||
|
||||
function replacer(key, value) {
|
||||
if (value instanceof Map) {
|
||||
return Object.fromEntries(value);
|
||||
|
|
|
@ -4,6 +4,10 @@ import fs from 'fs';
|
|||
import { MangoClient } from '../client';
|
||||
import { MANGO_V4_ID } from '../constants';
|
||||
|
||||
//
|
||||
// example script to close accounts - banks, markets, group etc. which require admin to be the signer
|
||||
//
|
||||
|
||||
const MAINNET_MINTS = new Map([
|
||||
['USDC', 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'],
|
||||
['BTC', '9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E'],
|
||||
|
@ -40,7 +44,7 @@ async function main() {
|
|||
const usdcMainnetBetaOracle = (
|
||||
await client.getStubOracle(group, usdcMainnetBetaMint)
|
||||
)[0];
|
||||
sig = await client.closeStubOracle(group, usdcMainnetBetaOracle.publicKey);
|
||||
sig = await client.stubOracleClose(group, usdcMainnetBetaOracle.publicKey);
|
||||
console.log(
|
||||
`Closed USDC stub oracle, sig https://explorer.solana.com/tx/${sig}`,
|
||||
);
|
||||
|
@ -70,7 +74,7 @@ async function main() {
|
|||
}
|
||||
|
||||
// finally, close the group
|
||||
sig = await client.closeGroup(group);
|
||||
sig = await client.groupClose(group);
|
||||
console.log(`Closed group, sig https://explorer.solana.com/tx/${sig}`);
|
||||
|
||||
process.exit();
|
||||
|
|
|
@ -4,6 +4,10 @@ import fs from 'fs';
|
|||
import { MangoClient } from '../client';
|
||||
import { MANGO_V4_ID } from '../constants';
|
||||
|
||||
//
|
||||
// Script which depoys a new mango group, and registers 3 tokens
|
||||
//
|
||||
|
||||
const MAINNET_MINTS = new Map([
|
||||
['USDC', 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'],
|
||||
['BTC', '9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E'],
|
||||
|
@ -37,7 +41,7 @@ async function main() {
|
|||
// group
|
||||
console.log(`Creating Group...`);
|
||||
try {
|
||||
await client.createGroup(0, true);
|
||||
await client.groupCreate(0, true);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
@ -56,6 +60,7 @@ async function main() {
|
|||
0.1,
|
||||
0,
|
||||
'BTC',
|
||||
0.01,
|
||||
0.4,
|
||||
0.07,
|
||||
0.8,
|
||||
|
@ -78,7 +83,7 @@ async function main() {
|
|||
console.log(`Creating USDC stub oracle...`);
|
||||
const usdcMainnetMint = new PublicKey(MAINNET_MINTS.get('USDC')!);
|
||||
try {
|
||||
await client.createStubOracle(group, usdcMainnetMint, 1.0);
|
||||
await client.stubOracleCreate(group, usdcMainnetMint, 1.0);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
@ -96,6 +101,7 @@ async function main() {
|
|||
0.1,
|
||||
1,
|
||||
'USDC',
|
||||
0.01,
|
||||
0.4,
|
||||
0.07,
|
||||
0.8,
|
||||
|
@ -126,6 +132,7 @@ async function main() {
|
|||
0.1,
|
||||
2, // tokenIndex
|
||||
'SOL',
|
||||
0.01,
|
||||
0.4,
|
||||
0.07,
|
||||
0.8,
|
||||
|
|
|
@ -4,6 +4,9 @@ import fs from 'fs';
|
|||
import { Serum3Side } from '../accounts/serum3';
|
||||
import { MangoClient } from '../client';
|
||||
|
||||
//
|
||||
// (untested?) script which closes a mango account cleanly, first closes all positions, withdraws all tokens and then closes it
|
||||
//
|
||||
async function main() {
|
||||
const options = AnchorProvider.defaultOptions();
|
||||
const connection = new Connection(process.env.CLUSTER_URL, options);
|
||||
|
|
|
@ -5,6 +5,10 @@ import { HealthType } from '../accounts/mangoAccount';
|
|||
import { MangoClient } from '../client';
|
||||
import { toUiDecimals } from '../utils';
|
||||
|
||||
//
|
||||
// example script shows usage of ids json (saves havint to do gpa)
|
||||
//
|
||||
|
||||
async function main() {
|
||||
const options = AnchorProvider.defaultOptions();
|
||||
const connection = new Connection(process.env.CLUSTER_URL, options);
|
||||
|
|
|
@ -20,6 +20,9 @@ const MANGO_MAINNET_PAYER_KEYPAIR =
|
|||
process.env.MANGO_MAINNET_PAYER_KEYPAIR ||
|
||||
'/Users/tylershipe/.config/solana/deploy.json';
|
||||
|
||||
//
|
||||
// example script which shows usage of flash loan 3 ix using a jupiter swap
|
||||
//
|
||||
// NOTE: we assume that ATA for source and target already exist for wallet
|
||||
async function main() {
|
||||
const options = AnchorProvider.defaultOptions();
|
||||
|
|
|
@ -110,7 +110,7 @@ async function main() {
|
|||
'devnet',
|
||||
MANGO_V4_ID['devnet'],
|
||||
);
|
||||
await client.setStubOracle(group, group.banksMap.get('USDC')?.oracle!, 0.5);
|
||||
await client.stubOracleSet(group, group.banksMap.get('USDC')?.oracle!, 0.5);
|
||||
|
||||
process.exit();
|
||||
}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"extends": "./tsconfig.build",
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"declarationDir": "dist/types",
|
||||
"module": "esnext",
|
||||
"outDir": "dist/esm"
|
||||
}
|
||||
}
|
|
@ -10,6 +10,11 @@
|
|||
"skipLibCheck": true,
|
||||
"target": "es2019"
|
||||
},
|
||||
"include": ["ts/client/src"],
|
||||
"exclude": ["./ts/**/*.test.js", "node_modules", "**/node_modules"]
|
||||
"include": ["ts/client/src", "ts/client/scripts", "ts/client/scripts"],
|
||||
"exclude": [
|
||||
"./ts/**/*.test.js",
|
||||
"node_modules",
|
||||
"**/node_modules",
|
||||
"./ts/client/src/scripts"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -12,4 +12,4 @@ anchor build --skip-lint
|
|||
# update types in ts client package
|
||||
cp -v ./target/types/mango_v4.ts ./ts/client/src/mango_v4.ts
|
||||
|
||||
(cd ./ts/client && tsc)
|
||||
(cd ./ts/client && yarn tsc)
|
||||
|
|
77
yarn.lock
77
yarn.lock
|
@ -111,16 +111,16 @@
|
|||
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45"
|
||||
integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==
|
||||
|
||||
"@jup-ag/core@^1.0.0-beta.27":
|
||||
version "1.0.0-beta.27"
|
||||
resolved "https://registry.yarnpkg.com/@jup-ag/core/-/core-1.0.0-beta.27.tgz#e5d8ca75ef827b5cb809384c139c371b579a29c5"
|
||||
integrity sha512-AIGhvDkh16bfagE3IT/DDORAM7Fak+RWPMoHSdCfRE5tqFjyqnJM8c+zbEh3+lhOWIs7iEMi/buiGKamNzx9fA==
|
||||
"@jup-ag/core@^1.0.0-beta.28":
|
||||
version "1.0.0-beta.28"
|
||||
resolved "https://registry.yarnpkg.com/@jup-ag/core/-/core-1.0.0-beta.28.tgz#0c15f940c6049f8b8570d6667d716f1bb1010434"
|
||||
integrity sha512-96gb77z34jAn0WSQDqdrS8qQdCBRVFFv4UP5/N0d+oyPbK0jpIpAL7TIGN8+RCuutJB3nLLAmiNNVWYf+aJaFg==
|
||||
dependencies:
|
||||
"@jup-ag/crema-sdk" "2.0.7"
|
||||
"@jup-ag/cykura-sdk" "0.1.25"
|
||||
"@jup-ag/cykura-sdk-core" "0.1.8"
|
||||
"@jup-ag/lifinity-sdk" "0.1.72"
|
||||
"@jup-ag/math" "1.0.0-beta.27"
|
||||
"@jup-ag/math" "1.0.0-beta.28"
|
||||
"@jup-ag/whirlpool-sdk" "0.1.1"
|
||||
"@mercurial-finance/optimist" "0.1.4"
|
||||
"@project-serum/anchor" "0.23.0"
|
||||
|
@ -173,10 +173,10 @@
|
|||
"@solana/web3.js" "1.31.0"
|
||||
decimal.js "^10.3.1"
|
||||
|
||||
"@jup-ag/math@1.0.0-beta.27":
|
||||
version "1.0.0-beta.27"
|
||||
resolved "https://registry.yarnpkg.com/@jup-ag/math/-/math-1.0.0-beta.27.tgz#5d3f0f822586ee86b2c46bce392d3801bb3fda9b"
|
||||
integrity sha512-jJaMRYjVG0oo9js/BkILVNPkBHnzUW9sQDtu9pH/JXis+VPz99Ud+wZV3mXYEo/kHinvJTsb5QuPAhtM32w+TA==
|
||||
"@jup-ag/math@1.0.0-beta.28":
|
||||
version "1.0.0-beta.28"
|
||||
resolved "https://registry.yarnpkg.com/@jup-ag/math/-/math-1.0.0-beta.28.tgz#52300d7470eee7c2cdac01b27b2f0c16062ad48a"
|
||||
integrity sha512-GQkziJQ5kJwYgYNvK8ZFpW+d92R90ihwiQ2n1BunKakeTvPdwt7BQKAOj4KRgIb9/458zMCuYVZvdF1cAWFA/g==
|
||||
dependencies:
|
||||
decimal.js "10.3.1"
|
||||
jsbi "4.3.0"
|
||||
|
@ -367,19 +367,19 @@
|
|||
"@solana/web3.js" "^1.30.2"
|
||||
buffer "^6.0.1"
|
||||
|
||||
"@saberhq/option-utils@^1.13.30":
|
||||
version "1.13.30"
|
||||
resolved "https://registry.yarnpkg.com/@saberhq/option-utils/-/option-utils-1.13.30.tgz#96d76dc87fc137bc7ae84cf515fbd758df88734d"
|
||||
integrity sha512-/YtbfR1pUqHD9umByNdM5Qf5u2p2TcAa4RKpq2dnBhf56bk/N1e2n2h0UJ+cZXKH6WAqTaN/cuoh13QTtE1Y5w==
|
||||
"@saberhq/option-utils@^1.13.32":
|
||||
version "1.13.32"
|
||||
resolved "https://registry.yarnpkg.com/@saberhq/option-utils/-/option-utils-1.13.32.tgz#143eb446c2c8d743690a900ae1688c9209fbcd05"
|
||||
integrity sha512-xkpXZ82EGPJwT+CGjyhZ6mAQW0KpY+3hBaXAhrkv6M0IPgj2xGRYJ+WdwBvF/WeTgmFxGHCp7o1DmwjR2/XXbA==
|
||||
dependencies:
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@saberhq/solana-contrib@^1.12.66", "@saberhq/solana-contrib@^1.13.30", "@saberhq/solana-contrib@^1.13.6":
|
||||
version "1.13.30"
|
||||
resolved "https://registry.yarnpkg.com/@saberhq/solana-contrib/-/solana-contrib-1.13.30.tgz#3b35ab351f33522bcae0ae3b3e231fec07dbfd35"
|
||||
integrity sha512-4itF/Dw8O3AP/51vDoxExEF8WD7xcnXGKGWo6D3ITQlrLPZd3PboZHQVuhByoz0VB2j9hZeP3sLCtN9vh9Nd7w==
|
||||
"@saberhq/solana-contrib@^1.12.66", "@saberhq/solana-contrib@^1.13.32", "@saberhq/solana-contrib@^1.13.6":
|
||||
version "1.13.32"
|
||||
resolved "https://registry.yarnpkg.com/@saberhq/solana-contrib/-/solana-contrib-1.13.32.tgz#ace608df953c1e92b8cad388835dce5d1729f7a7"
|
||||
integrity sha512-W0F5W1CJjk2ACuAYjGxr/nB+pDHsmg2A6F+d+XRM6/EES/9ZoIINHCA1dnPsTa0p4PnC1S7BtCrv9aERzwRKhg==
|
||||
dependencies:
|
||||
"@saberhq/option-utils" "^1.13.30"
|
||||
"@saberhq/option-utils" "^1.13.32"
|
||||
"@solana/buffer-layout" "^4.0.0"
|
||||
"@types/promise-retry" "^1.1.3"
|
||||
"@types/retry" "^0.12.2"
|
||||
|
@ -401,11 +401,11 @@
|
|||
tslib "^2.4.0"
|
||||
|
||||
"@saberhq/token-utils@^1.12.66", "@saberhq/token-utils@^1.13.6":
|
||||
version "1.13.30"
|
||||
resolved "https://registry.yarnpkg.com/@saberhq/token-utils/-/token-utils-1.13.30.tgz#9f3ad35f21ddc6ee6f2b77fd6719cf2aec72af96"
|
||||
integrity sha512-ZpzYJ/tkWmgEnIZGTPONtmKuoJjT+mpzi/8jIl2xoQjeg37S3/iuthQO/Pgw2O9UK/4j249tqqG0V/NFvPMM5w==
|
||||
version "1.13.32"
|
||||
resolved "https://registry.yarnpkg.com/@saberhq/token-utils/-/token-utils-1.13.32.tgz#2acc98bd4d3732b826396a70b958198e0d20dee8"
|
||||
integrity sha512-n5ECiw82IQJwyq9bTkcrbNWVi+lAQoQlJlTmIye8odUQATBsqOWN+clqfrFkn/UMmezO60bo34bUaM0Oir7Pew==
|
||||
dependencies:
|
||||
"@saberhq/solana-contrib" "^1.13.30"
|
||||
"@saberhq/solana-contrib" "^1.13.32"
|
||||
"@solana/buffer-layout" "^4.0.0"
|
||||
"@solana/spl-token" "^0.1.8"
|
||||
"@ubeswap/token-math" "^5.1.6"
|
||||
|
@ -540,9 +540,9 @@
|
|||
tweetnacl "^1.0.0"
|
||||
|
||||
"@solana/web3.js@^1.32.0":
|
||||
version "1.44.2"
|
||||
resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.44.2.tgz#5303efd94a7f2d6054a1483a4b4db4a26eb2a392"
|
||||
integrity sha512-DvrJMoKonLuaX0/KyyJXcP/+w+9q8mve4gN3hC2Ptg51K/Gi1/cx6oQN2lbRZb4wYPBd2s2GDAJAJUAwZGsEug==
|
||||
version "1.47.3"
|
||||
resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.47.3.tgz#ec13f2cf4f9f54cc4fbd26d20be1e026c6e2279c"
|
||||
integrity sha512-TQJulaN/+b0xXq5EhQAYFwVyOORxSyVJn1EiXupClZm8DY7f9EeUG6vl0FzSAgwEAwXKsgK3sVs/3px2e7H7dQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.12.5"
|
||||
"@ethersproject/sha2" "^5.5.0"
|
||||
|
@ -556,7 +556,7 @@
|
|||
jayson "^3.4.4"
|
||||
js-sha3 "^0.8.0"
|
||||
node-fetch "2"
|
||||
rpc-websockets "^7.4.2"
|
||||
rpc-websockets "^7.5.0"
|
||||
secp256k1 "^4.0.2"
|
||||
superstruct "^0.14.2"
|
||||
tweetnacl "^1.0.0"
|
||||
|
@ -567,9 +567,9 @@
|
|||
integrity sha512-2xN+iGTbPBEzGSnVp/Hd64vKJCJWxsi9gfs88x4PPMyEjHJoA3o5BY9r5OLPHIZU2pAQxkSAsJFqn6itClP8mQ==
|
||||
|
||||
"@types/big.js@^6.1.3":
|
||||
version "6.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/big.js/-/big.js-6.1.3.tgz#c008dec4dae24c7a338ebb4521c46e9609020807"
|
||||
integrity sha512-fHh2h1cFlvGP0kFCqoAsnuQoM0n3xHB6HxgZvELt7dji+BtK/j938MRL0nG5AA45EgibuFcPjgLlkqfUPCyoKw==
|
||||
version "6.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/big.js/-/big.js-6.1.5.tgz#ff06b43a4c0c4002522e0fd7fc28bb963722ab01"
|
||||
integrity sha512-UiWyJ6TLWoHeHZ8VUyngzCOwJDVxTsPnqfAMR/85X93rkRk5A4T2U42BCx0wCmZdtMHGHN/utJ8ft5xWu0V1bA==
|
||||
|
||||
"@types/bn.js@^4.11.5":
|
||||
version "4.11.6"
|
||||
|
@ -1981,9 +1981,9 @@ lru-cache@^6.0.0:
|
|||
yallist "^4.0.0"
|
||||
|
||||
lru-cache@^7.9.0:
|
||||
version "7.10.1"
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.10.1.tgz#db577f42a94c168f676b638d15da8fb073448cab"
|
||||
integrity sha512-BQuhQxPuRl79J5zSXRP+uNzPOyZw2oFI9JLRQ80XswSvg21KMKNtQza9eF42rfI/3Z40RvzBdXgziEkudzjo8A==
|
||||
version "7.12.0"
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.12.0.tgz#be2649a992c8a9116efda5c487538dcf715f3476"
|
||||
integrity sha512-OIP3DwzRZDfLg9B9VP/huWBlpvbkmbfiBy8xmsXp4RPmE4A3MhwNozc5ZJ3fWnSg8fDcdlE/neRTPG2ycEKliw==
|
||||
|
||||
lunr@^2.3.9:
|
||||
version "2.3.9"
|
||||
|
@ -2354,6 +2354,19 @@ rpc-websockets@^7.4.2:
|
|||
bufferutil "^4.0.1"
|
||||
utf-8-validate "^5.0.2"
|
||||
|
||||
rpc-websockets@^7.5.0:
|
||||
version "7.5.0"
|
||||
resolved "https://registry.yarnpkg.com/rpc-websockets/-/rpc-websockets-7.5.0.tgz#bbeb87572e66703ff151e50af1658f98098e2748"
|
||||
integrity sha512-9tIRi1uZGy7YmDjErf1Ax3wtqdSSLIlnmL5OtOzgd5eqPKbsPpwDP5whUDO2LQay3Xp0CcHlcNSGzacNRluBaQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.17.2"
|
||||
eventemitter3 "^4.0.7"
|
||||
uuid "^8.3.2"
|
||||
ws "^8.5.0"
|
||||
optionalDependencies:
|
||||
bufferutil "^4.0.1"
|
||||
utf-8-validate "^5.0.2"
|
||||
|
||||
run-parallel@^1.1.9:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"
|
||||
|
|
Loading…
Reference in New Issue