Skip to content

get_market_token_price resets to $1/share for an insolvent pool (supply>0, pool_value<=0), same fallback as a genuine first deposit #622

Description

@abayomicornelius

Problem

gmx_market_utils::get_market_token_price (libs/market_utils/src/lib.rs:601-630) conflates two very different situations under the same fallback:

pub fn get_market_token_price(...) -> i128 {
    let supply = MarketTokenClient::new(env, &market.market_token).total_supply();
    if supply <= 0 {
        return FLOAT_PRECISION;   // case A: genuine first deposit, no LP tokens exist yet
    }

    let info = get_pool_value(env, ds, market, long_token_price, short_token_price, index_token_price, maximize);
    if info.pool_value <= 0 {
        return FLOAT_PRECISION;   // case B: LP tokens DO exist, but the pool is insolvent
    }

    mul_div_wide(env, info.pool_value, TOKEN_PRECISION, supply)
}

Case A (supply <= 0) is the legitimate "no LP tokens exist yet" bootstrap case — $1/share is a reasonable default. Case B is different: LP tokens are outstanding and represent real economic claims, but aggregate trader PnL (net_pnl in get_pool_value, libs/market_utils/src/lib.rs:574-575) has grown large enough that pool_value = long_usd + short_usd + impact_pool_usd - net_pnl <= 0. This is an insolvent-pool state, not a bootstrap state, yet it is handled identically: the function still reports $1/share.

This is not merely a hypothetical — docs/SECURITY_REVIEW.md §7.2 already documents the consequence and treats it as an accepted assumption rather than a tracked defect: "If aggregate trader PnL is overwhelmingly positive and exceeds total pool assets, pool_value becomes <= 0. get_market_token_price handles this by resetting the LP price to FLOAT_PRECISION ($1)... Subsequent deposits into the bankrupted pool will mint LP tokens at the fresh $1 rate... a bankrupt pool effectively 'reboots' its LP pricing." No GitHub issue exists for this (grep -iE "bankrupt|pool_value.*negative|net_pnl.*cap" over all issue titles returns nothing), and get_pool_value's own doc comment separately notes "No max_pnl_factor cap is applied ... this function returns the raw net PnL without capping" as a known simplification — so nothing currently prevents pool_value from actually reaching <= 0 in the first place.

Failure scenario

  1. A market accumulates large, still-unrealised long-side profit (e.g. index price rallies hard against a heavily long-skewed open interest book) such that net_pnl exceeds long_usd + short_usd + impact_pool_usd. get_pool_value returns pool_value <= 0. LP tokens are still outstanding from earlier, healthy deposits — their true backing is now ≤ $0/share.
  2. deposit_handler::execute_deposit calls get_market_token_price(..., false) to price the mint (contracts/deposit_handler/src/lib.rs:431-442). Because pool_value <= 0, it receives FLOAT_PRECISION ($1/share) instead of the true (≤$0) price, and mints new LP tokens against the depositor's real, incoming dollars at that fictitious $1 rate.
  3. Those freshly minted tokens are fungible with the existing (economically worthless) LP supply. The new depositor's real capital is immediately diluted across all LP holders — including whichever winning traders have not yet realised/claimed their PnL against the pool — at a price that has no relationship to the pool's actual ≤$0 backing. Existing LPs are effectively subsidized by the new depositor's principal rather than the new depositor receiving a fair, proportional share.

Suggested fix

Distinguish the two cases explicitly rather than sharing one fallback:

  • supply <= 0 (case A): keep the $1 bootstrap price — this is correct and, per §7.1 of the security review, already immune to the classic first-depositor donation attack because pool_amount is tracked internally rather than read from the raw token balance.
  • supply > 0 && pool_value <= 0 (case B): either reject new deposits into an insolvent pool outright (surface a dedicated error from deposit_handler::execute_deposit instead of silently minting), or price new LP tokens at a floor that reflects the pool's actual (zero or negative) backing rather than resetting to par. Whichever direction is chosen, it should be a deliberate, tested behavior rather than the same one-line fallback used for a real bootstrap deposit. Applying the max_pnl_factor cap get_pool_value's own doc comment says is missing would also help prevent pool_value from reaching this state at all.

Scope

  • libs/market_utils/src/lib.rs::get_market_token_price / get_pool_value
  • contracts/deposit_handler/src/lib.rs::execute_deposit (the consumer that mints against this price)

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions