https://github.com/hackenproof-public/tokenomics_contract
emergency_withdraw removes only the requested NFT from user_tokens but zeroes the entire recorded balance and subtracts that full amount from total_supply.
https://github.com/hackenproof-public/tokenomics_contract/blob/cc3983b3791b373e3632f9b5bf88bd2e669378af/sources/gauge_clmm.move#L428
If the user has multiple staked NFTs, the first emergency withdrawal burns their whole balance, so a second call fails the *balance > 0 check and the remaining NFTs stay locked in the gauge. total_supply ends up too small as well, so reward math for everyone is wrong.
The deposit path (https://github.com/hackenproof-public/tokenomics_contract/blob/cc3983b3791b373e3632f9b5bf88bd2e669378af/sources/gauge_clmm.move#L329) explicitly allows one user to hold multiple NFTs (pushes each into a per-user vector and accumulates their liquidity into a single balance), so the emergency path’s “one position per user” assumption is incorrect.
As soon as emergency mode is used by any multi-position staker, the extra NFTs become permanently unrecoverable and accounting diverges. This is a critical severity issue
Rework emergency_withdraw to either loop through and return every NFT a user still has recorded (and decrement balances incrementally), or require callers to specify which NFT to exit while only decrementing by that NFT’s liquidity.
Place test in guage_clmm_test.move and run
#[test(dev = @dexlyn_clmm)]
#[
expected_failure(
abort_code = gauge_clmm::ERROR_INSUFFICIENT_BALANCE, location = gauge_clmm
)
]
fun test_clmm_emergency_withdraw_multiple_positions_bug(dev: &signer) {
let (_, _, _, pool, _) = setup_test_without_genesis_with_register_lp();
let dev_address = address_of(dev);
let external_bribe = voter::get_external_bribe_address(pool);
let gauge = gauge_clmm::get_gauge_address(pool);
gauge_clmm::test_create_gauge(dev_address, external_bribe, pool);
let position_ids = vector[1u64, 2u64];
let token_addresses = dexlyn_clmm::pool::generate_token_addresses(pool, position_ids);
let first_token = *vector::borrow(&token_addresses, 0);
let second_token = *vector::borrow(&token_addresses, 1);
assert!(
object::owner<Token>(address_to_object<Token>(first_token)) == dev_address,
600
);
assert!(
object::owner<Token>(address_to_object<Token>(second_token)) == dev_address,
601
);
let first_liquidity = gauge_clmm::get_liquidity(pool, first_token);
let second_liquidity = gauge_clmm::get_liquidity(pool, second_token);
gauge_clmm::deposit(dev, gauge, first_token);
gauge_clmm::deposit(dev, gauge, second_token);
let total_supply_after_deposit = gauge_clmm::total_supply(gauge);
assert!(total_supply_after_deposit == first_liquidity + second_liquidity, 602);
let dev_balance_after_deposit = gauge_clmm::balance_of(gauge, dev_address);
assert!(dev_balance_after_deposit == first_liquidity + second_liquidity, 603);
let dev2 = &create_signer_for_test(@dexlyn_tokenomics);
gauge_clmm::update_emergency_mode(dev2, gauge, true);
gauge_clmm::emergency_withdraw(dev, gauge, first_token);
let total_supply_after_emergency = gauge_clmm::total_supply(gauge);
assert!(total_supply_after_emergency == 0, 604);
let dev_balance_after_emergency = gauge_clmm::balance_of(gauge, dev_address);
assert!(dev_balance_after_emergency == 0, 605);
gauge_clmm::emergency_withdraw(dev, gauge, second_token);
}