When closing account in a resolved market, its calculations don't account for A-factor, so several last closing accounts of such market would fail to be closed due to insufficient asset.oi_eff_..._q
Example scenario for two accounts:
- Deposit long and short accounts
- Long and short open a trade for 100k
- Long account accrues some maintenance fee and gets liquidated by close_q = 25k. asset.a_short becomes 75%
- Market gets into resolved state by admin invoking
resolve_market_not_atomic
- Short account attempts to call close_resolved_account_not_atomic and ends up failing with CounterUnderflow due to the fact that
asset.oi_eff_short_q=75k, while the actual leg.basis_pos_q=100k and it is never scaled by a_basis
In more realistic scenario most users on that side would quickly drain available stored OI until the last ones are left with insufficient OI to fulfil their closing legs basis_pos_q.
Test for `v16_spec_tests.rs`
#[test]
fn v16_fuzz_regression_drain_only_residual_leg_underflows_resolved_close() {
let (mut header, mut markets) = market_fixture(1, 1);
header.config.initial_margin_bps = V16PodU64::new(500);
header.config.maintenance_margin_bps = V16PodU64::new(250);
header.config.max_price_move_bps_per_slot = V16PodU64::new(100);
let mut long_header = account_fixture(1, 213);
let mut short_header = account_fixture(1, 214);
let stranded_q = 10_751 * POS_SCALE;
let residual_q = POS_SCALE;
{
let mut market = MarketGroupV16ViewMut::new(&mut header, &mut markets);
let mut long = PortfolioV16ViewMut::new(&mut long_header);
let mut short = PortfolioV16ViewMut::new(&mut short_header);
market.deposit_not_atomic(&mut long, 1_000).unwrap();
market.deposit_not_atomic(&mut short, 1_000).unwrap();
market
.execute_trade_with_fee_loss_stale_scoped_not_atomic(
&mut long,
&mut short,
TradeRequestV16 {
asset_index: 0,
size_q: signed_q(stranded_q),
exec_price: 1,
fee_bps: 0,
},
)
.unwrap();
}
{
let mut market = MarketGroupV16ViewMut::new(&mut header, &mut markets);
let mut long = PortfolioV16ViewMut::new(&mut long_header);
market
.rebalance_reduce_position_not_atomic(
&mut long,
RebalanceRequestV16 {
asset_index: 0,
reduce_q: stranded_q - residual_q,
},
)
.unwrap();
}
let asset = markets[0].engine.asset.try_to_runtime().unwrap();
let short_leg = short_header.legs[0].try_to_runtime().unwrap();
assert_eq!(asset.oi_eff_long_q, residual_q);
assert_eq!(asset.oi_eff_short_q, residual_q);
assert_eq!(asset.mode_short, SideModeV16::DrainOnly);
assert_eq!(short_leg.basis_pos_q, -signed_q(stranded_q));
{
let mut market = MarketGroupV16ViewMut::new(&mut header, &mut markets);
market
.resolve_market_not_atomic(market.header.current_slot.get())
.unwrap();
}
let mut market = MarketGroupV16ViewMut::new(&mut header, &mut markets);
let mut short = PortfolioV16ViewMut::new(&mut short_header);
market.validate_shape().unwrap();
short.validate_with_market(&market.as_view()).unwrap();
assert_eq!(
market
.close_resolved_account_not_atomic(&mut short, 0)
.map(|_| ()),
Err(V16Error::CounterUnderflow),
"fuzz regression: resolved close tries to subtract the full stranded \
DrainOnly leg from the one-lot residual short OI"
);
}
When closing account in a resolved market, its calculations don't account for A-factor, so several last closing accounts of such market would fail to be closed due to insufficient
asset.oi_eff_..._qExample scenario for two accounts:
resolve_market_not_atomicasset.oi_eff_short_q=75k, while the actualleg.basis_pos_q=100kand it is never scaled bya_basisIn more realistic scenario most users on that side would quickly drain available stored OI until the last ones are left with insufficient OI to fulfil their closing legs
basis_pos_q.Test for `v16_spec_tests.rs`