GoldPesa runs an immutable Uniswap v4 hook on Base to manage fees, initial liquidity, and rebalancing for the GPX/USDC pool. On 2 October 2026 that protocol-owned liquidity was reduced by roughly 115,000 USDC through a netting trick. The bug is not in the v4 core. The hook treated a shared, flash-accounted PositionManager as if it were a private ledger.
GPXHooks (0x4519e2b040ff1B64fa03aBe2AeF0BC99D7CcEaA8) is the orchestration layer between GoldPesa and Uniswap v4. The contract comments describe it as ownerless, unpausable, and non-upgradeable. Pool initialization, fee splits, and rebalancing are hardcoded.
It does five things. It deploys and initializes the GPX-USDC pool. Hooks block unauthorized pool creation and external liquidity changes. Every trade pays a 1% fee, split into four 0.25% shares (Mines, Treasury, Pawn, and GPX Owner). mintInitialLiquidity() runs once, after which liquidity is locked. The contract watches circulating supply and price, and when conditions are met it burns the old position and mints a new one at updated ticks. Liquidity changes are restricted to the internal reBalance() path; direct external calls are rejected.
Rebalancing depends on the PositionManager. In v4, PosM creates and manages liquidity positions, each represented by an ERC-721. The hook does not hold the underlying token accounting itself. It burns, takes tokens, and opens a new position through PosM’s modifyLiquidities / modifyLiquiditiesWithoutUnlock.
The loss only makes sense once unlock accounting is clear. Inside a PoolManager unlock, token movements are not settled immediately. They are recorded as currency deltas. A positive delta is credit that can be taken. A negative delta is debt that must be repaid. Before the unlock ends, relevant deltas must return to zero or the transaction reverts.
The PositionManager is a shared contract. Every mint, burn, and take that goes through it in the same unlock lands on the same delta set. TAKE_PAIR withdraws the PositionManager’s net balance for that pair, not the amount produced by the latest burn alone. If a negative delta is already sitting on PosM when rebalance starts, the new positive credit pays that debt first.
That is the path GPXHooks uses. In source, reBalance() packs BURN_POSITION + TAKE_PAIR and calls positionManager.modifyLiquiditiesWithoutUnlock(...). It does not assert that GPX/USDC deltas are zero in the current unlock.
The missing isolation is the root cause.
Rebalance assumes it is the only party touching the PositionManager in that unlock. It is not. Once an hour has passed since the last rebalance, beforeSwap can be entered by any swap on the GPX pool. An attacker can open their own PoolManager unlock, leave unsettled debt on the shared PosM, then send the swap that triggers rebalance. The credit from burning the real position and the attacker’s negative delta net on the same ledger. TAKE_PAIR only returns the net positive amount. The protocol believes it recovered the full position; it only received the remainder.
This is not a general Uniswap v4 vulnerability. Flash accounting and net settlement are the design. The flaw is the hook using shared state as private state without a zero-delta check. The comments say only the contract may change liquidity, but the tool that changes liquidity is an external shared PosM. The permission boundary and the accounting boundary do not match.
The transaction is 0x5c1febd5047c2a15c37988b6abd5c8b984236dddf6fd24eed96b0f43951ad2c9, 2026-10-02 13:05 UTC, Base block 52078502, status success. The caller is 0x4a5FD2e9357cC87DF4cD6A1808174DBc8646899F. Execution runs through 0x3d69591a3868FE77B531187486862c459Cce4e86.
beforeSwap enters reBalance().BURN_POSITION + TAKE_PAIR, burning its real position and creating about +148,868.60 USDC of credit on PosM.TAKE_PAIR sends the hook only the net positive amount, about 33,868.60 USDC. The difference stays on the attacker’s side.What the protocol lost is the burn credit that should have returned in full to the hook. The size matches SlowMist’s figure of roughly $114,900. The hook was paid; it was paid the residual after netting.
“No owner, cannot pause” addresses governance risk. It does not address ledger isolation. A v4 hook that calls a shared PositionManager from a callback has to treat the current unlock as untrusted context: assert that relevant currency deltas are zero before reBalance, or burn and take inside an unlock the hook itself opens and can keep clean. modifyLiquiditiesWithoutUnlock skips an outer unlock and also skips a natural settlement boundary, so the caller has to supply the assertion.
This class of bug is easy to treat as a call-order detail in review, because unit tests usually start from a clean delta. At Panda Security we treat shared PosM and PoolManager deltas as explicit invariants on v4 hooks: before any internal liquidity operation, deltas for the currencies involved must be zero, or the call reverts. GoldPesa’s loss shows that correct access control is not enough. If the books are not isolated, a perfectly valid unlock can still empty protocol-owned liquidity.
UNCX's token vesting contract lost roughly $87.7K when an attacker exploited a share-accounting flaw in release(), topUpVesting(), and cancel(). We break down the root cause, the exact attack sequence, and what builders should take away from it — without the jargon.
Autheo’s THEO token lost 92% after attackers used a Hyperlane TrustedRelayerIsm misconfiguration to mint 19 million unbacked tokens on Base and extract roughly $1.76 million. Here’s how the attack worked and why single-key security models remain dangerous for cross-chain assets.
Uniswap v4 hooks explained: the PoolManager singleton, flash accounting, and what the Cork and Bunni exploits teach us about hook security. A PandaSecurity deep dive.
An attacker forced a Uniswap V4 vault into a zero-liquidity state, redeemed one minimal share unit, and withdrew roughly 14,838.47 DAI. Panda Security breaks down the transaction, vulnerable code, root cause, and practical fixes.