program: add a min health check (#913)

add a min health check
This commit is contained in:
Serge Farny 2024-03-12 08:27:40 +01:00 committed by GitHub
parent 4fcaf09c09
commit b3b4cc8223
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 560 additions and 2 deletions

View File

@ -1790,6 +1790,36 @@
}
]
},
{
"name": "healthCheck",
"accounts": [
{
"name": "group",
"isMut": false,
"isSigner": false
},
{
"name": "account",
"isMut": true,
"isSigner": false,
"relations": [
"group"
]
}
],
"args": [
{
"name": "minHealthValue",
"type": "f64"
},
{
"name": "checkKind",
"type": {
"defined": "HealthCheckKind"
}
}
]
},
{
"name": "stubOracleCreate",
"accounts": [
@ -10674,6 +10704,32 @@
]
}
},
{
"name": "HealthCheckKind",
"type": {
"kind": "enum",
"variants": [
{
"name": "Maint"
},
{
"name": "Init"
},
{
"name": "LiquidationEnd"
},
{
"name": "MaintRatio"
},
{
"name": "InitRatio"
},
{
"name": "LiquidationEndRatio"
}
]
}
},
{
"name": "Serum3SelfTradeBehavior",
"docs": [
@ -11031,6 +11087,9 @@
},
{
"name": "SequenceCheck"
},
{
"name": "HealthCheck"
}
]
}
@ -14383,6 +14442,11 @@
"code": 6071,
"name": "InvalidSequenceNumber",
"msg": "invalid sequence number"
},
{
"code": 6072,
"name": "InvalidHealth",
"msg": "invalid health"
}
]
}

View File

@ -0,0 +1,30 @@
use crate::error::*;
use crate::state::*;
use anchor_lang::prelude::*;
use num_enum::{IntoPrimitive, TryFromPrimitive};
#[derive(Clone, Copy, TryFromPrimitive, IntoPrimitive, AnchorSerialize, AnchorDeserialize)]
#[repr(u8)]
pub enum HealthCheckKind {
Maint = 0b0000,
Init = 0b0010,
LiquidationEnd = 0b0100,
MaintRatio = 0b0001,
InitRatio = 0b0011,
LiquidationEndRatio = 0b0101,
}
#[derive(Accounts)]
pub struct HealthCheck<'info> {
#[account(
constraint = group.load()?.is_ix_enabled(IxGate::SequenceCheck) @ MangoError::IxIsDisabled,
)]
pub group: AccountLoader<'info, Group>,
#[account(
mut,
has_one = group,
constraint = account.load()?.is_operational() @ MangoError::AccountIsFrozen
)]
pub account: AccountLoader<'info, MangoAccountFixed>,
}

View File

@ -16,6 +16,7 @@ pub use group_close::*;
pub use group_create::*;
pub use group_edit::*;
pub use group_withdraw_insurance_fund::*;
pub use health_check::*;
pub use health_region::*;
pub use ix_gate_set::*;
pub use openbook_v2_cancel_order::*;
@ -95,6 +96,7 @@ mod group_close;
mod group_create;
mod group_edit;
mod group_withdraw_insurance_fund;
mod health_check;
mod health_region;
mod ix_gate_set;
mod openbook_v2_cancel_order;

View File

@ -149,6 +149,8 @@ pub enum MangoError {
BorrowsRequireHealthAccountBank,
#[msg("invalid sequence number")]
InvalidSequenceNumber,
#[msg("invalid health")]
InvalidHealth,
}
impl MangoError {

View File

@ -0,0 +1,49 @@
use anchor_lang::prelude::*;
use fixed::types::I80F48;
use crate::accounts_ix::*;
use crate::error::{Contextable, MangoError};
use crate::health::{
new_fixed_order_account_retriever_with_optional_banks,
new_health_cache_skipping_missing_banks_and_bad_oracles, HealthType,
};
use crate::state::*;
use crate::util::clock_now;
pub fn health_check(
ctx: Context<HealthCheck>,
min_value: f64,
health_check_kind: HealthCheckKind,
) -> Result<()> {
let account = ctx.accounts.account.load_full_mut()?;
let (now_ts, now_slot) = clock_now();
let retriever = new_fixed_order_account_retriever_with_optional_banks(
ctx.remaining_accounts,
&account.borrow(),
now_slot,
)?;
let health_cache = new_health_cache_skipping_missing_banks_and_bad_oracles(
&account.borrow(),
&retriever,
now_ts,
)
.context("health_check health cache")?;
let min_value = I80F48::from_num(min_value);
let actual_value = match health_check_kind {
HealthCheckKind::Maint => health_cache.health(HealthType::Maint),
HealthCheckKind::Init => health_cache.health(HealthType::Init),
HealthCheckKind::LiquidationEnd => health_cache.health(HealthType::LiquidationEnd),
HealthCheckKind::MaintRatio => health_cache.health_ratio(HealthType::Maint),
HealthCheckKind::InitRatio => health_cache.health_ratio(HealthType::Init),
HealthCheckKind::LiquidationEndRatio => {
health_cache.health_ratio(HealthType::LiquidationEnd)
}
};
// msg!("{}", actual_value);
require_gte!(actual_value, min_value, MangoError::InvalidHealth);
Ok(())
}

View File

@ -97,6 +97,7 @@ pub fn ix_gate_set(ctx: Context<IxGateSet>, ix_gate: u128) -> Result<()> {
log_if_changed(&group, ix_gate, IxGate::Serum3PlaceOrderV2);
log_if_changed(&group, ix_gate, IxGate::TokenForceWithdraw);
log_if_changed(&group, ix_gate, IxGate::SequenceCheck);
log_if_changed(&group, ix_gate, IxGate::HealthCheck);
group.ix_gate = ix_gate;

View File

@ -16,6 +16,7 @@ pub use group_close::*;
pub use group_create::*;
pub use group_edit::*;
pub use group_withdraw_insurance_fund::*;
pub use health_check::*;
pub use health_region::*;
pub use ix_gate_set::*;
pub use perp_cancel_all_orders::*;
@ -86,6 +87,7 @@ mod group_close;
mod group_create;
mod group_edit;
mod group_withdraw_insurance_fund;
mod health_check;
mod health_region;
mod ix_gate_set;
mod perp_cancel_all_orders;

View File

@ -464,6 +464,16 @@ pub mod mango_v4 {
Ok(())
}
pub fn health_check(
ctx: Context<HealthCheck>,
min_health_value: f64,
check_kind: HealthCheckKind,
) -> Result<()> {
#[cfg(feature = "enable-gpl")]
instructions::health_check(ctx, min_health_value, check_kind)?;
Ok(())
}
// todo:
// ckamm: generally, using an I80F48 arg will make it harder to call
// because generic anchor clients won't know how to deal with it

View File

@ -247,6 +247,7 @@ pub enum IxGate {
Serum3PlaceOrderV2 = 71,
TokenForceWithdraw = 72,
SequenceCheck = 73,
HealthCheck = 74,
// NOTE: Adding new variants requires matching changes in ts and the ix_gate_set instruction.
}

View File

@ -22,6 +22,7 @@ mod test_collateral_fees;
mod test_delegate;
mod test_fees_buyback_with_mngo;
mod test_force_close;
mod test_health_check;
mod test_health_compute;
mod test_health_region;
mod test_ix_gate_set;

View File

@ -0,0 +1,178 @@
use crate::cases::{
create_funded_account, mango_setup, send_tx, tokio, HealthAccountSkipping,
HealthCheckInstruction, TestContext, TestKeypair, TokenWithdrawInstruction,
};
use crate::send_tx_expect_error;
use mango_v4::accounts_ix::{HealthCheck, HealthCheckKind};
use mango_v4::error::MangoError;
use solana_sdk::transport::TransportError;
// TODO FAS
#[tokio::test]
async fn test_health_check() -> Result<(), TransportError> {
let context = TestContext::new().await;
let solana = &context.solana.clone();
let admin = TestKeypair::new();
let owner = context.users[0].key;
let payer = context.users[1].key;
let payer_token_accounts = &context.users[1].token_accounts;
let mints = &context.mints[0..3];
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
admin,
payer,
mints: mints.to_vec(),
zero_token_is_quote: true,
..mango_setup::GroupWithTokensConfig::default()
}
.create(solana)
.await;
// Funding to fill the vaults
create_funded_account(
&solana,
group,
owner,
0,
&context.users[1],
&mints,
1_000_000,
0,
)
.await;
let account = create_funded_account(
&solana,
group,
owner,
1,
&context.users[1],
&mints[0..2],
1000,
0,
)
.await;
send_tx(
solana,
TokenWithdrawInstruction {
amount: 775,
allow_borrow: true,
account,
owner,
token_account: payer_token_accounts[2],
bank_index: 0,
},
)
.await
.unwrap();
//
// TEST (Health is about 93% with all banks, 7% without banks 1)
//
send_tx(
solana,
HealthCheckInstruction {
account,
owner,
min_health_value: 20.0,
check_kind: HealthCheckKind::MaintRatio,
},
)
.await
.unwrap();
send_tx(
solana,
HealthCheckInstruction {
account,
owner,
min_health_value: 500.0,
check_kind: HealthCheckKind::Init,
},
)
.await
.unwrap();
send_tx_expect_error!(
solana,
HealthCheckInstruction {
owner,
account,
min_health_value: 600.0,
check_kind: HealthCheckKind::Init,
},
MangoError::InvalidHealth
);
send_tx(
solana,
HealthCheckInstruction {
account,
owner,
min_health_value: 800.0,
check_kind: HealthCheckKind::Maint,
},
)
.await
.unwrap();
send_tx_expect_error!(
solana,
HealthCheckInstruction {
owner,
account,
min_health_value: 100.0,
check_kind: HealthCheckKind::MaintRatio,
},
MangoError::InvalidHealth
);
send_tx(
solana,
HealthAccountSkipping {
inner: HealthCheckInstruction {
owner,
account,
min_health_value: 5.0,
check_kind: HealthCheckKind::MaintRatio,
},
skip_banks: vec![tokens[1].bank],
},
)
.await
.unwrap();
send_tx_expect_error!(
solana,
HealthAccountSkipping {
inner: HealthCheckInstruction {
owner,
account,
min_health_value: 10.0,
check_kind: HealthCheckKind::MaintRatio,
},
skip_banks: vec![tokens[1].bank],
},
MangoError::InvalidHealth
);
send_tx_expect_error!(
solana,
HealthAccountSkipping {
inner: HealthCheckInstruction {
owner,
account,
min_health_value: 10.0,
check_kind: HealthCheckKind::MaintRatio,
},
skip_banks: vec![tokens[2].bank],
},
MangoError::InvalidBank
);
Ok(())
}

View File

@ -7,7 +7,7 @@ use anchor_spl::token::{Token, TokenAccount};
use fixed::types::I80F48;
use itertools::Itertools;
use mango_v4::accounts_ix::{
InterestRateParams, Serum3OrderType, Serum3SelfTradeBehavior, Serum3Side,
HealthCheckKind, InterestRateParams, Serum3OrderType, Serum3SelfTradeBehavior, Serum3Side,
};
use mango_v4::state::{MangoAccount, MangoAccountValue};
use solana_program::instruction::Instruction;
@ -5208,3 +5208,52 @@ impl ClientInstruction for SequenceCheckInstruction {
vec![self.owner]
}
}
pub struct HealthCheckInstruction {
pub account: Pubkey,
pub owner: TestKeypair,
pub min_health_value: f64,
pub check_kind: HealthCheckKind,
}
#[async_trait::async_trait(?Send)]
impl ClientInstruction for HealthCheckInstruction {
type Accounts = mango_v4::accounts::HealthCheck;
type Instruction = mango_v4::instruction::HealthCheck;
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 {
min_health_value: self.min_health_value,
check_kind: self.check_kind,
};
let account = account_loader
.load_mango_account(&self.account)
.await
.unwrap();
let accounts = Self::Accounts {
group: account.fixed.group,
account: self.account,
};
let health_check_metas = derive_health_check_remaining_account_metas(
account_loader,
&account,
None,
false,
None,
)
.await;
let mut instruction = make_instruction(program_id, &accounts, &instruction);
instruction.accounts.extend(health_check_metas.into_iter());
(accounts, instruction)
}
fn signers(&self) -> Vec<TestKeypair> {
vec![]
}
}

View File

@ -83,7 +83,7 @@ import {
import { Id } from './ids';
import { IDL, MangoV4 } from './mango_v4';
import { I80F48 } from './numbers/I80F48';
import { FlashLoanType, OracleConfigParams } from './types';
import { FlashLoanType, HealthCheckKind, OracleConfigParams } from './types';
import {
I64_MAX_BN,
U64_MAX_BN,
@ -1048,6 +1048,30 @@ export class MangoClient {
.instruction();
}
public async healthCheckIx(
group: Group,
mangoAccount: MangoAccount,
minHealthValue: number,
checkKind: HealthCheckKind,
): Promise<TransactionInstruction> {
const healthRemainingAccounts: PublicKey[] =
this.buildHealthRemainingAccounts(group, [mangoAccount], [], [], []);
return await this.program.methods
.healthCheck(minHealthValue, checkKind)
.accounts({
group: group.publicKey,
account: mangoAccount.publicKey,
})
.remainingAccounts(
healthRemainingAccounts.map(
(pk) =>
({ pubkey: pk, isWritable: false, isSigner: false } as AccountMeta),
),
)
.instruction();
}
public async getMangoAccount(
mangoAccountPk: PublicKey,
loadSerum3Oo = false,

View File

@ -1790,6 +1790,36 @@ export type MangoV4 = {
}
]
},
{
"name": "healthCheck",
"accounts": [
{
"name": "group",
"isMut": false,
"isSigner": false
},
{
"name": "account",
"isMut": true,
"isSigner": false,
"relations": [
"group"
]
}
],
"args": [
{
"name": "minHealthValue",
"type": "f64"
},
{
"name": "checkKind",
"type": {
"defined": "HealthCheckKind"
}
}
]
},
{
"name": "stubOracleCreate",
"accounts": [
@ -10674,6 +10704,32 @@ export type MangoV4 = {
]
}
},
{
"name": "HealthCheckKind",
"type": {
"kind": "enum",
"variants": [
{
"name": "Maint"
},
{
"name": "Init"
},
{
"name": "LiquidationEnd"
},
{
"name": "MaintRatio"
},
{
"name": "InitRatio"
},
{
"name": "LiquidationEndRatio"
}
]
}
},
{
"name": "Serum3SelfTradeBehavior",
"docs": [
@ -11031,6 +11087,9 @@ export type MangoV4 = {
},
{
"name": "SequenceCheck"
},
{
"name": "HealthCheck"
}
]
}
@ -14383,6 +14442,11 @@ export type MangoV4 = {
"code": 6071,
"name": "InvalidSequenceNumber",
"msg": "invalid sequence number"
},
{
"code": 6072,
"name": "InvalidHealth",
"msg": "invalid health"
}
]
};
@ -16179,6 +16243,36 @@ export const IDL: MangoV4 = {
}
]
},
{
"name": "healthCheck",
"accounts": [
{
"name": "group",
"isMut": false,
"isSigner": false
},
{
"name": "account",
"isMut": true,
"isSigner": false,
"relations": [
"group"
]
}
],
"args": [
{
"name": "minHealthValue",
"type": "f64"
},
{
"name": "checkKind",
"type": {
"defined": "HealthCheckKind"
}
}
]
},
{
"name": "stubOracleCreate",
"accounts": [
@ -25063,6 +25157,32 @@ export const IDL: MangoV4 = {
]
}
},
{
"name": "HealthCheckKind",
"type": {
"kind": "enum",
"variants": [
{
"name": "Maint"
},
{
"name": "Init"
},
{
"name": "LiquidationEnd"
},
{
"name": "MaintRatio"
},
{
"name": "InitRatio"
},
{
"name": "LiquidationEndRatio"
}
]
}
},
{
"name": "Serum3SelfTradeBehavior",
"docs": [
@ -25420,6 +25540,9 @@ export const IDL: MangoV4 = {
},
{
"name": "SequenceCheck"
},
{
"name": "HealthCheck"
}
]
}
@ -28772,6 +28895,11 @@ export const IDL: MangoV4 = {
"code": 6071,
"name": "InvalidSequenceNumber",
"msg": "invalid sequence number"
},
{
"code": 6072,
"name": "InvalidHealth",
"msg": "invalid health"
}
]
};

View File

@ -18,6 +18,23 @@ export namespace FlashLoanType {
export const swapWithoutFee = { swapWithoutFee: {} };
}
export type HealthCheckKind =
| { maint: Record<string, never> }
| { init: Record<string, never> }
| { liquidationEnd: Record<string, never> }
| { maintRatio: Record<string, never> }
| { initRatio: Record<string, never> }
| { liquidationEndRatio: Record<string, never> };
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace HealthCheckKind {
export const maint = { maint: {} };
export const init = { init: {} };
export const liquidationEnd = { liquidationEnd: {} };
export const maintRatio = { maintRatio: {} };
export const initRatio = { initRatio: {} };
export const liquidationEndRatio = { liquidationEndRatio: {} };
}
export class InterestRateParams {
util0: number;
rate0: number;