The GPX V4 hook on Base rebalanced the protocol's liquidity inside beforeSwap without isolating its own burn credit from outstanding PositionManager debt in the same unlock, so an attacker parked an unsettled mint debt and skimmed 114,999.999186 USDC out of the PoolManager.
Security ·
Logged as “2026-10-02: The Gpx V4 Hook Rebalances Protocol Liquidity Inside Befor”.
The GoldPesa GPX V4 hook rebalances the protocol's own liquidity on an hourly cadence from inside beforeSwap, and does so using PositionManager.modifyLiquiditiesWithoutUnlock(BURN_POSITION + TAKE_PAIR). That call runs inside whatever PoolManager.unlock the caller already has open. The hook never checks that the PositionManager's own GPX/USDC position is fully settled before the burn, so its +148,868.6 USDC burn credit is split between the hook and any other PositionManager delta parked in that same unlock. On 2 October 2026, an attacker on Base opened their own unlock, parked a -114,999.999186 USDC phantom debt on the PositionManager, triggered the hook's rebalance inside it, then burned their own position to take the unmatched +114,999.999186 USDC straight out of the PoolManager. DeFiHackLabs records the loss as exactly 114,999.999186 USDC, attributed.
The vulnerable contract is the verified GPXHooks implementation, which owns the GPX/USDC V4 pool and calls reBalanceRoutine() from beforeSwap whenever block.timestamp - lastRebalance >= 1 hours. The rebalance burns the protocol's old GPX/USDC position with PositionManager.modifyLiquiditiesWithoutUnlock(BURN_POSITION, TAKE_PAIR); TAKE_PAIR withdraws the PositionManager's NET POSITIVE USDC delta for the whole shared unlock. The hook trusts that net to be solely its own, but the underlying call does not isolate the hook's burn credit from anything else open in the same unlock.
The exploit is one transaction (0x5c1febd...ad2c9, block 52,078,502, Base). The attacker funded it with a Morpho Blue flash loan of 175,000 USDC, then opened a PoolManager.unlock and:
PositionManager.modifyLiquiditiesWithoutUnlock(MINT_POSITION) on the WETH/USDC V4 pool at ticks [-210000, -209990], liquidity 8,346,512,432,733,306,153, with amount0Max = amount1Max = type(uint128).max. The range sits entirely below spot, so the position is pure currency1 (USDC), leaving the PositionManager with a -114,999.999186 USDC phantom debt in this shared unlock. No SETTLE_PAIR was issued.checkRebalance.beforeSwap fired the hourly rebalance, whose BURN_POSITION + TAKE_PAIR ran in the same unlock. The hook's +148,868.602188 USDC burn credit was netted against the attacker's -114,999.999186 USDC phantom debt, and TAKE_PAIR only delivered the difference: 33,868.603002 USDC, paid to the hook. The attacker's debt was silently cancelled by the hook's burn credit.modifyLiquiditiesWithoutUnlock(BURN_POSITION, TAKE_PAIR) on their own WETH/USDC position, with recipient = attacker. The PositionManager's delta was now positive by exactly 114,999.999186 USDC, and TAKE_PAIR sent it straight out to the attacker.The PoC reproduces the exact per-leg amounts and asserts an attacker stolenUSDC of 114,999.999186 USDC to the micro-unit. The hook's burn credit and the attacker's mint debt split exactly: 33,868.603002 + 114,999.999186 = 148,868.602188.
The hook's rebalance needed to either open its own PoolManager.unlock (so its burn is isolated from any other delta on the PositionManager), or perform a SETTLE_PAIR for the protocol's own position before doing the burn, so its TAKE_PAIR could only withdraw what was genuinely its. A _clearPositionDeltas() step right before BURN_POSITION would have closed the shared-unlock window. The hourly cadence is itself fine: the bug is that beforeSwap runs the rebalance in the attacker's unlock context.
Query every incident yourself — free, no signup
Updated 11 Nov 2026. Figures are as reported by the sources listed above and are not independently verified.