Skip to content

Commit

Permalink
program: amm drawdown check (#865)
Browse files Browse the repository at this point in the history
* bigz/amm-drawdown-guardrail

* add test

* add amm pause from drawdown sdk check

* update/cleanup switch statement and magintude of drawdown quote

* cleanup whitespace

* fix lint

* fix sdk bug

* CHANGELOG

---------

Co-authored-by: Chris Heaney <[email protected]>
  • Loading branch information
0xbigz and crispheaney committed Feb 15, 2024
1 parent 26d2170 commit aa5a65c
Show file tree
Hide file tree
Showing 6 changed files with 181 additions and 7 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Features

- program: amm drawdown check ([#865](https://github.com/drift-labs/protocol-v2/pull/865))
- program: relax oracle guardrail validity check for init margin calc for positive pnl ([#876](https://github.com/drift-labs/protocol-v2/pull/876))
- program: add more max spread baselines ([#858](https://github.com/drift-labs/protocol-v2/pull/858))

Expand Down
1 change: 1 addition & 0 deletions programs/drift/src/controller/orders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -970,6 +970,7 @@ pub fn fill_perp_order(
{
let market = &mut perp_market_map.get_ref_mut(&market_index)?;
amm_is_available &= !market.is_operation_paused(PerpOperation::AmmFill);
amm_is_available &= !market.has_too_much_drawdown()?;
validation::perp_market::validate_perp_market(market)?;
validate!(
!market.is_in_settlement(now),
Expand Down
64 changes: 63 additions & 1 deletion programs/drift/src/math/amm/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,68 @@ use crate::state::oracle::HistoricalOracleData;
use crate::state::perp_market::PerpMarket;
use crate::state::user::PerpPosition;

#[test]
fn calculate_amm_available_guards() {
let prev = 1656682258;
let _now = prev + 2;

// let px = 32 * PRICE_PRECISION;

let mut market: PerpMarket = PerpMarket::default_btc_test();

let _oracle_price_data = OraclePriceData {
price: (34 * PRICE_PRECISION) as i64,
confidence: PRICE_PRECISION_U64 / 100,
delay: 1,
has_sufficient_number_of_data_points: true,
};

assert_eq!(market.amm.net_revenue_since_last_funding, 0);
assert_eq!(market.amm.total_fee_minus_distributions, 0);

assert_eq!(!market.has_too_much_drawdown().unwrap(), true);

market.amm.net_revenue_since_last_funding = -100_000_000_000;
market.amm.total_fee_minus_distributions = 100_000_000_000;

assert_eq!(!market.has_too_much_drawdown().unwrap(), false);

market.amm.net_revenue_since_last_funding = -10_000_000_000;
market.amm.total_fee_minus_distributions = 100_000_000_000;

assert_eq!(!market.has_too_much_drawdown().unwrap(), false);

market.amm.net_revenue_since_last_funding = -5_000_000_000;
market.amm.total_fee_minus_distributions = 100_000_000_000;

assert_eq!(!market.has_too_much_drawdown().unwrap(), false);

market.amm.net_revenue_since_last_funding = -1_000_000_000;
market.amm.total_fee_minus_distributions = 100_000_000_000;

assert_eq!(!market.has_too_much_drawdown().unwrap(), true);

market.amm.net_revenue_since_last_funding = -1_000_000_000;
market.amm.total_fee_minus_distributions = 1_000_000;

assert_eq!(!market.has_too_much_drawdown().unwrap(), true);

market.amm.net_revenue_since_last_funding = -6_000_000_000;
market.amm.total_fee_minus_distributions = 1_000_000;

assert_eq!(!market.has_too_much_drawdown().unwrap(), false);

market.amm.net_revenue_since_last_funding = -5_000;
market.amm.total_fee_minus_distributions = -9279797219;

assert_eq!(!market.has_too_much_drawdown().unwrap(), true); // too small net_revenue_since_last_funding drawdown

market.amm.net_revenue_since_last_funding = -88_000_000_000;
market.amm.total_fee_minus_distributions = -9279797219;

assert_eq!(!market.has_too_much_drawdown().unwrap(), false); // too small net_revenue_since_last_funding drawdown
}

#[test]
fn calculate_net_user_pnl_test() {
let prev = 1656682258;
Expand Down Expand Up @@ -40,7 +102,7 @@ fn calculate_net_user_pnl_test() {
let net_user_pnl = calculate_net_user_pnl(&amm, oracle_price_data.price).unwrap();
assert_eq!(net_user_pnl, 0);

let market = PerpMarket::default_btc_test();
let market: PerpMarket = PerpMarket::default_btc_test();
let net_user_pnl = calculate_net_user_pnl(
&market.amm,
market.amm.historical_oracle_data.last_oracle_price,
Expand Down
45 changes: 42 additions & 3 deletions programs/drift/src/state/perp_market.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ use crate::math::constants::{
AMM_RESERVE_PRECISION, MAX_CONCENTRATION_COEFFICIENT, PRICE_PRECISION_I64,
};
use crate::math::constants::{
AMM_RESERVE_PRECISION_I128, BID_ASK_SPREAD_PRECISION, BID_ASK_SPREAD_PRECISION_U128,
AMM_RESERVE_PRECISION_I128, AMM_TO_QUOTE_PRECISION_RATIO, BID_ASK_SPREAD_PRECISION,
BID_ASK_SPREAD_PRECISION_U128, DEFAULT_REVENUE_SINCE_LAST_FUNDING_SPREAD_RETREAT,
LP_FEE_SLICE_DENOMINATOR, LP_FEE_SLICE_NUMERATOR, MARGIN_PRECISION_U128, PERCENTAGE_PRECISION,
SPOT_WEIGHT_PRECISION, TWENTY_FOUR_HOUR,
PERCENTAGE_PRECISION_I128, PRICE_PRECISION, SPOT_WEIGHT_PRECISION, TWENTY_FOUR_HOUR,
};
use crate::math::helpers::get_proportion_i128;

Expand All @@ -28,7 +29,6 @@ use crate::state::events::OrderActionExplanation;
use crate::state::oracle::{HistoricalOracleData, OracleSource};
use crate::state::spot_market::{AssetTier, SpotBalance, SpotBalanceType};
use crate::state::traits::{MarketIndexOffset, Size};
use crate::{AMM_TO_QUOTE_PRECISION_RATIO, PRICE_PRECISION};
use borsh::{BorshDeserialize, BorshSerialize};

use crate::state::paused_operations::PerpOperation;
Expand Down Expand Up @@ -292,6 +292,45 @@ impl PerpMarket {
PerpOperation::is_operation_paused(self.paused_operations, operation)
}

pub fn has_too_much_drawdown(&self) -> DriftResult<bool> {
let quote_drawdown_limit_breached = match self.contract_tier {
ContractTier::A | ContractTier::B => {
self.amm.net_revenue_since_last_funding
<= DEFAULT_REVENUE_SINCE_LAST_FUNDING_SPREAD_RETREAT * 400
}
_ => {
self.amm.net_revenue_since_last_funding
<= DEFAULT_REVENUE_SINCE_LAST_FUNDING_SPREAD_RETREAT * 200
}
};

if quote_drawdown_limit_breached {
let percent_drawdown = self
.amm
.net_revenue_since_last_funding
.cast::<i128>()?
.safe_mul(PERCENTAGE_PRECISION_I128)?
.safe_div(self.amm.total_fee_minus_distributions.max(1))?;

let percent_drawdown_limit_breached = match self.contract_tier {
ContractTier::A => percent_drawdown <= -PERCENTAGE_PRECISION_I128 / 50,
ContractTier::B => percent_drawdown <= -PERCENTAGE_PRECISION_I128 / 33,
ContractTier::C => percent_drawdown <= -PERCENTAGE_PRECISION_I128 / 25,
_ => percent_drawdown <= -PERCENTAGE_PRECISION_I128 / 20,
};

if percent_drawdown_limit_breached {
msg!("AMM has too much on-the-hour drawdown (percentage={}, quote={}) to accept fills",
percent_drawdown,
self.amm.net_revenue_since_last_funding
);
return Ok(true);
}
}

Ok(false)
}

pub fn get_sanitize_clamp_denominator(self) -> DriftResult<Option<i64>> {
Ok(match self.contract_tier {
ContractTier::A => Some(10_i64), // 10%
Expand Down
71 changes: 68 additions & 3 deletions sdk/src/math/exchangeStatus.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import {
DEFAULT_REVENUE_SINCE_LAST_FUNDING_SPREAD_RETREAT,
PERCENTAGE_PRECISION,
ONE,
} from '../constants/numericConstants';
import {
ExchangeStatus,
PerpMarketAccount,
PerpOperation,
SpotMarketAccount,
SpotOperation,
StateAccount,
isVariant,
} from '../types';
import { BN } from '@coral-xyz/anchor';

export function exchangePaused(state: StateAccount): boolean {
return state.exchangeStatus !== ExchangeStatus.ACTIVE;
Expand Down Expand Up @@ -41,10 +48,19 @@ export function ammPaused(
}

if (market.hasOwnProperty('amm')) {
return isOperationPaused(market.pausedOperations, PerpOperation.AMM_FILL);
} else {
return false;
const operationPaused = isOperationPaused(
market.pausedOperations,
PerpOperation.AMM_FILL
);
if (operationPaused) {
return true;
}
if (isAmmDrawdownPause(market as PerpMarketAccount)) {
return true;
}
}

return false;
}

export function isOperationPaused(
Expand All @@ -53,3 +69,52 @@ export function isOperationPaused(
): boolean {
return (pausedOperations & operation) > 0;
}

export function isAmmDrawdownPause(market: PerpMarketAccount): boolean {
let quoteDrawdownLimitBreached: boolean;

if (
isVariant(market.contractTier, 'a') ||
isVariant(market.contractTier, 'b')
) {
quoteDrawdownLimitBreached = market.amm.netRevenueSinceLastFunding.lte(
DEFAULT_REVENUE_SINCE_LAST_FUNDING_SPREAD_RETREAT.muln(400)
);
} else {
quoteDrawdownLimitBreached = market.amm.netRevenueSinceLastFunding.lte(
DEFAULT_REVENUE_SINCE_LAST_FUNDING_SPREAD_RETREAT.muln(200)
);
}

if (quoteDrawdownLimitBreached) {
const percentDrawdown = market.amm.netRevenueSinceLastFunding
.mul(PERCENTAGE_PRECISION)
.div(BN.max(market.amm.totalFeeMinusDistributions, ONE));

let percentDrawdownLimitBreached: boolean;

if (isVariant(market.contractTier, 'a')) {
percentDrawdownLimitBreached = percentDrawdown.lte(
PERCENTAGE_PRECISION.divn(50).neg()
);
} else if (isVariant(market.contractTier, 'b')) {
percentDrawdownLimitBreached = percentDrawdown.lte(
PERCENTAGE_PRECISION.divn(33).neg()
);
} else if (isVariant(market.contractTier, 'c')) {
percentDrawdownLimitBreached = percentDrawdown.lte(
PERCENTAGE_PRECISION.divn(25).neg()
);
} else {
percentDrawdownLimitBreached = percentDrawdown.lte(
PERCENTAGE_PRECISION.divn(20).neg()
);
}

if (percentDrawdownLimitBreached) {
return true;
}
}

return false;
}
6 changes: 6 additions & 0 deletions sdk/tests/dlob/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ export const mockPerpMarkets: Array<PerpMarketAccount> = [
},
quoteSpotMarketIndex: 0,
feeAdjustment: 0,
pausedOperations: 0,
},
{
status: MarketStatus.INITIALIZED,
Expand Down Expand Up @@ -219,6 +220,7 @@ export const mockPerpMarkets: Array<PerpMarketAccount> = [
},
quoteSpotMarketIndex: 0,
feeAdjustment: 0,
pausedOperations: 0,
},
{
status: MarketStatus.INITIALIZED,
Expand Down Expand Up @@ -257,6 +259,7 @@ export const mockPerpMarkets: Array<PerpMarketAccount> = [
},
quoteSpotMarketIndex: 0,
feeAdjustment: 0,
pausedOperations: 0,
},
];

Expand Down Expand Up @@ -341,6 +344,7 @@ export const mockSpotMarkets: Array<SpotMarketAccount> = [
lastIndexPriceTwap5Min: PRICE_PRECISION,
lastIndexPriceTwapTs: new BN(0),
},
pausedOperations: 0,
},
{
status: MarketStatus.ACTIVE,
Expand Down Expand Up @@ -422,6 +426,7 @@ export const mockSpotMarkets: Array<SpotMarketAccount> = [
lastIndexPriceTwap5Min: new BN(0),
lastIndexPriceTwapTs: new BN(0),
},
pausedOperations: 0,
},
{
status: MarketStatus.ACTIVE,
Expand Down Expand Up @@ -503,6 +508,7 @@ export const mockSpotMarkets: Array<SpotMarketAccount> = [
lastIndexPriceTwap5Min: new BN(0),
lastIndexPriceTwapTs: new BN(0),
},
pausedOperations: 0,
},
];

Expand Down

0 comments on commit aa5a65c

Please sign in to comment.