Skip to content

Creator fee accumulator overflow permanently halts trading on burned admin markets #421

Description

@v1ktorrr0x

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]"
    );
}

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions