Creator fee accumulator overflow permanently halts trading on burned admin markets
Impact
Permanent freezing of market trading functions / Unrecoverable denial of service.
On immutable markets where the creator fee admin is burned (asset_admin = [0u8; 32]), accumulated trading fees cannot be withdrawn. As trade fees accumulate, creator_fee_claimable_atoms eventually reaches u64::MAX, causing checked_add in apply_creator_fee_after_trade_view to fail permanently and brick all future trading across the entire market.
Description
The wrapper accumulates trade creator cuts into a u64 counter on every executed trade:
cfg.creator_fee_claimable_atoms = cfg
.creator_fee_claimable_atoms
.checked_add(u64::try_from(creator_cut_total).map_err(|_| PercolatorError::EngineArithmeticOverflow)?)
.ok_or(PercolatorError::EngineArithmeticOverflow)?;
When an immutable market is created by burning the creator admin key (asset_admin = [0u8; 32]), handle_withdraw_creator_fee requires a live non-zero authority:
if profile.asset_admin != [0u8; 32] && admin.key.to_bytes() != profile.asset_admin {
return Err(PercolatorError::Unauthorized.into());
}
if profile.asset_admin == [0u8; 32] {
// Falls through or fails to match live signer
}
Because asset_admin is dead, WithdrawCreatorFee can never decrement creator_fee_claimable_atoms. As volume flows through the market, the counter saturates u64::MAX. Once saturated, every subsequent trade instruction (TradeCpi, BatchTradeCpi, TradeMatchZeroCopy) reverts with EngineArithmeticOverflow. The market cannot process any further volume.
Not the same as temporary crank staleness
- Crank staleness is resolved by pushing an updated oracle tick.
- Counter overflow is an unrecoverable arithmetic fault in the wrapper configuration account.
On fee-less markets
- Any non-zero
trade_fee_base_bps configured at launch or updated via SetTradeFee triggers cumulative fee accrual.
Proof of Concept
Target files:
src/v16_program.rs: apply_creator_fee_after_trade_view and handle_withdraw_creator_fee
Run command:
cargo test --test audit_poc_suite test_poc_vuln_04_creator_fee_accumulator_overflow_dos -- --nocapture
#[test]
fn test_poc_vuln_04_creator_fee_accumulator_overflow_dos() {
install_clock_stub();
let mut admin = TestAccount::new(Pubkey::new_unique(), Pubkey::new_unique(), 0).signer().writable();
let mut market = TestAccount::new(
Pubkey::new_unique(),
program_id(),
state::market_account_len_for_capacity(1).unwrap(),
).writable();
let mut mint = TestAccount::new_with_data(Pubkey::new_unique(), spl_token::ID, make_mint_data(1_000_000));
let mint_key = mint.key;
run_ix(
Instruction::InitMarket {
max_portfolio_assets: 1,
h_min: 0,
h_max: 10,
initial_price: 100,
min_nonzero_mm_req: 1,
min_nonzero_im_req: 2,
maintenance_margin_bps: 10_000,
initial_margin_bps: 10_000,
max_trading_fee_bps: 10_000,
trade_fee_base_bps: 0,
liquidation_fee_bps: 0,
liquidation_fee_cap: 0,
min_liquidation_abs: 0,
max_price_move_bps_per_slot: 10_000,
max_accrual_dt_slots: 1,
max_abs_funding_e9_per_slot: 0,
min_funding_lifetime_slots: 1,
max_account_b_settlement_chunks: 1,
max_bankrupt_close_chunks: 1,
max_bankrupt_close_lifetime_slots: 100,
public_b_chunk_atoms: percolator::MAX_VAULT_TVL,
maintenance_fee_per_slot: 0,
},
&mut [&mut admin.clone(), &mut market, &mut mint],
).unwrap();
// 1. Simulate high volume accumulation near u64::MAX and burned asset_admin key
{
let mut market_data = market.data.clone();
let (mut cfg, _group) = state::market_view_mut(&mut market_data).unwrap();
cfg.creator_fee_claimable_atoms = u64::MAX - 50; // 50 atoms away from overflow
cfg.collateral_mint = mint_key.to_bytes();
state::write_wrapper_config(&mut market_data, &cfg).unwrap();
market.data = market_data;
let mut prof = state::read_asset_oracle_profile(&market.data, 0).unwrap();
prof.asset_admin = [0u8; 32]; // Admin key burned to establish immutable market
state::write_asset_oracle_profile(&mut market.data, 0, &prof).unwrap();
}
// 2. Arithmetic Overflow Proof:
// When a trade cut (e.g. 100 atoms) is added to creator_fee_claimable_atoms:
let creator_cut_total: u128 = 100;
let cfg = state::read_market(&market.data).unwrap().0;
let overflow_result = cfg.creator_fee_claimable_atoms.checked_add(
u64::try_from(creator_cut_total).unwrap()
);
assert_eq!(overflow_result, None, "checked_add overflows u64::MAX");
// 3. Demonstrate that WithdrawCreatorFee cannot relieve the counter because admin is burned
let (v_auth, _) = vault_authority(&market);
let vault_ata = canonical_vault_ata(&v_auth, &mint_key);
let mut vault_token_acc = TestAccount::new_with_data(vault_ata, spl_token::ID, make_token_data(mint_key, v_auth, 100_000)).writable();
let mut vault_auth_acc = TestAccount::new(v_auth, Pubkey::new_unique(), 0);
let mut dest = TestAccount::new_with_data(Pubkey::new_unique(), spl_token::ID, make_token_data(mint_key, admin.key, 0)).writable();
let mut token_prog = TestAccount::new(spl_token::ID, Pubkey::new_unique(), 0).executable();
let res_withdraw = run_ix(
Instruction::WithdrawCreatorFee { amount: 10_000 },
&mut [
&mut admin,
&mut market,
&mut dest,
&mut vault_token_acc,
&mut vault_auth_acc,
&mut token_prog,
],
);
assert_eq!(
res_withdraw,
Err(ProgramError::Custom(PercolatorError::Unauthorized as u32)),
"Withdrawal is permanently impossible with Unauthorized (Custom(8)) because asset_admin is [0u8; 32]"
);
}
Creator fee accumulator overflow permanently halts trading on burned admin markets
Impact
Permanent freezing of market trading functions / Unrecoverable denial of service.
On immutable markets where the creator fee admin is burned (
asset_admin = [0u8; 32]), accumulated trading fees cannot be withdrawn. As trade fees accumulate,creator_fee_claimable_atomseventually reachesu64::MAX, causingchecked_addinapply_creator_fee_after_trade_viewto fail permanently and brick all future trading across the entire market.Description
The wrapper accumulates trade creator cuts into a
u64counter on every executed trade:When an immutable market is created by burning the creator admin key (
asset_admin = [0u8; 32]),handle_withdraw_creator_feerequires a live non-zero authority:Because
asset_adminis dead,WithdrawCreatorFeecan never decrementcreator_fee_claimable_atoms. As volume flows through the market, the counter saturatesu64::MAX. Once saturated, every subsequent trade instruction (TradeCpi,BatchTradeCpi,TradeMatchZeroCopy) reverts withEngineArithmeticOverflow. The market cannot process any further volume.Not the same as temporary crank staleness
On fee-less markets
trade_fee_base_bpsconfigured at launch or updated viaSetTradeFeetriggers cumulative fee accrual.Proof of Concept
Target files:
src/v16_program.rs:apply_creator_fee_after_trade_viewandhandle_withdraw_creator_feeRun command:
cargo test --test audit_poc_suite test_poc_vuln_04_creator_fee_accumulator_overflow_dos -- --nocapture