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
- 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.
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.
- 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)
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: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_pnlinget_pool_value,libs/market_utils/src/lib.rs:574-575) has grown large enough thatpool_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_valuebecomes<= 0.get_market_token_pricehandles 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), andget_pool_value's own doc comment separately notes "Nomax_pnl_factorcap is applied ... this function returns the raw net PnL without capping" as a known simplification — so nothing currently preventspool_valuefrom actually reaching<= 0in the first place.Failure scenario
net_pnlexceedslong_usd + short_usd + impact_pool_usd.get_pool_valuereturnspool_value <= 0. LP tokens are still outstanding from earlier, healthy deposits — their true backing is now ≤ $0/share.deposit_handler::execute_depositcallsget_market_token_price(..., false)to price the mint (contracts/deposit_handler/src/lib.rs:431-442). Becausepool_value <= 0, it receivesFLOAT_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.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 becausepool_amountis 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 fromdeposit_handler::execute_depositinstead 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 themax_pnl_factorcapget_pool_value's own doc comment says is missing would also help preventpool_valuefrom reaching this state at all.Scope
libs/market_utils/src/lib.rs::get_market_token_price/get_pool_valuecontracts/deposit_handler/src/lib.rs::execute_deposit(the consumer that mints against this price)