October 20, 2026
|
Exploit Postmortem
Cover image for "GoldPesa's $115K Loss and How Olympix Would Have Prevented It"

GoldPesa's $115K Loss and How Olympix Would Have Prevented It

Date: October 2, 2026

Chain: Base

Loss: 114,999.999186 USDC pulled from the Uniswap v4 PoolManager, about $115K

Class: Uniswap v4 hook settlement that collected a shared net balance, letting another caller's debt be paid from the hook's proceeds

When GoldPesa's hook burned its own liquidity position on October 2, 2026, it released a real credit of 148,868.60 USDC and then collected what it was owed. It received 33,868.60 USDC. The other 115,000 or so had already gone to pay off a liquidity position someone else had opened earlier in the same transaction, without paying for it.

That someone walked away with 114,999.999186 USDC out of the Uniswap v4 PoolManager in a single transaction on Base. It was possible because the hook collected its proceeds by withdrawing the net balance of everything happening inside the current PoolManager session, and it never checked that the balance belonged only to it.

A Hook That Rebalances Itself

GoldPesa describes GPX as a fixed-supply token run by ownerless, immutable contracts, with permanently locked liquidity. Its protocol-owned GPX/USDC liquidity sits in a Uniswap v4 pool managed by a custom hook contract called GPXHooks. A hook in Uniswap v4 is a contract that the pool calls at set moments, such as just before a swap, so a protocol can run its own logic there.

GPXHooks uses that moment to rebalance. Inside beforeSwap, once an hour has passed since the last rebalance, it calls PositionManager.modifyLiquiditiesWithoutUnlock with two actions: BURN_POSITION, which removes the protocol's liquidity and creates a credit, and TAKE_PAIR, which withdraws that credit. As Defimon put it, any swap can trigger the rebalance once the hour is up.

One Session, Shared Balances

Uniswap v4 batches activity inside an unlock, a session in which the PoolManager lets callers run up debts and credits and only requires that everything nets to zero before the session closes. The PositionManager is a single shared contract that many users act through, and Defimon noted that it nets outstanding balances across everything happening in the same PoolManager session.

According to the DeFiHackLabs reproduction, which was checked against the verified GPXHooks source, TAKE_PAIR can only withdraw the PositionManager's net positive balance for each currency across the whole unlock, and the hook never checked that its own GPX/USDC balances were isolated and actually zero before relying on that withdrawal. Whenever the hook ran inside a session it had opened itself, the net balance was its own. Inside a session someone else had opened, the net included whatever that someone owed.

The lesson for builders is that in a system where balances are netted, a contract has to confirm what it actually received instead of trusting that a withdrawal equals its own proceeds. Two roommates who settle bills through one joint account learn this the first month one of them leaves a charge unpaid.

Paying Someone Else's Tab

The attack ran in transaction 0x5c1f...d2c9, confirmed in block 52,078,502 at 13:05:51 UTC on October 2. The attacker borrowed 175,000 USDC from Morpho through an attack contract and opened a single PoolManager unlock.

Inside that unlock, it minted a WETH/USDC position through the PositionManager with MINT_POSITION and didn't pay for it, which left the PositionManager carrying a debt of 114,999.999186 USDC. It then made two 5,500 USDC buys on the GPX pool, and those swaps triggered GPXHooks' hourly rebalance inside the same unlock.

The hook's BURN_POSITION released its real 148,868.602188 USDC credit. TAKE_PAIR then withdrew only the net positive balance of the whole unlock, so the hook received 33,868.603002 USDC and the rest was netted against the attacker's unpaid mint. With its position now paid for, the attacker burned it and pulled 114,999.999186 USDC out of the PoolManager. Defimon summed it up plainly: the hook paid for the attacker's position.

Repaid, Swapped and Bridged

The attacker repaid Morpho and converted the remaining funds into USDT, and DeFiHackLabs puts its net profit at 114,428.509331 USDC. BaseScan's transfer list for the exploit transaction shows the attack contract sending 114,428.509331 USDC to an unlabeled contract at 0xD56d...5DA1, receiving 96,388.996061 USDT back from it, and passing that USDT to the attacker's wallet. The Crypto Times reported that the USDT was then bridged to Solana and on to BNB Chain, where Defimon's alert names 0xE38e...6fBF as the address the funds were laundered to. GoldPesa hadn't published a response at the time of reporting.

How Olympix Checks Who a Settlement Really Pays

Olympix reads a protocol the way an attacker does, as states, transitions and entry points, and it takes in the context around the code, which here includes how the Uniswap v4 PoolManager and the shared PositionManager net balances across an unlock. The states that matter are the hook's own credits and the PositionManager's net balances, and the entry points are any swap that can trigger beforeSwap, plus the permissionless mint and burn actions any user can call in the same session.

The signal is a hook that withdraws a shared net balance as if it were its own proceeds, with no check of what it actually received. No single line of GPXHooks is wrong in isolation, which is why this isn't a pattern a static detector targets. BugPoCer approaches it from the other end, starting with what the PoolManager session as a whole has to balance and asking whether the hook's share of it can ever come out wrong, which is the question its Counterexample-Guided Invariant Refutation is built to answer. Olympix has flagged related rebalance settlement issues before in other codebases, though not this exact cross-caller netting.

The invariant is one sentence: a rebalance must collect exactly what its own BURN_POSITION released, no matter what else is open in the same unlock. BugPoCer would plausibly reach the path that breaks it, because the path is short: an unpaid mint opened first, followed by a swap that triggers the rebalance inside the same unlock.

That path is the counterexample: the sequence of balance changes, the minimal conditions that trigger it and a generated Forge test that replays it, which is the October 2 transaction produced in development instead of on mainnet. Because the engine runs automatically at every merge and before release, it puts a hook's settlement assumptions under test while the hook is still being written.

Net Balances Need an Owner

GPXHooks did its own accounting correctly and still paid for a stranger's position, because the number it trusted was a total that other callers could add to. Contracts built on netted settlement have an invariant about whose balance they're collecting, and that invariant is something a tool can infer and try to break before launch. If you're building hooks or anything else that settles inside a shared Uniswap v4 unlock, book a technical walkthrough.

On-Chain References

Protocol: GoldPesa (GPX)

Chain: Base

Date: October 2, 2026, 13:05:51 UTC

Loss: 114,999.999186 USDC pulled from the PoolManager (about $115K)

Attacker EOA: 0x4a5F...899F

Attack contract: 0x3d69...4e86

GPXHooks (vulnerable hook): 0x4519...EaA8

GPX token: 0x454F...3133

Uniswap v4 PoolManager: 0x4985...2b2b

Uniswap v4 PositionManager: 0x7C5f...9bDc

Flash loan source (Morpho Blue): 0xBBBB...FFCb

Exploit tx: 0x5c1f...d2c9

Block: 52,078,502

Laundered to (BNB Chain, per Defimon): 0xE38e...6fBF

Sources

DeFiHackLabs PR #1288: GoldPesa (GPX) exploit PoC

DeFiHackLabs: GoldPesa_exp.sol

Defimon Alerts: GoldPesa exploit alert

The Crypto Times: GoldPesa's GPXHooks Allegedly Drained for $114K in Base Exploit

GoldPesa GitBook: The GoldPesa Token (GPX)

More from Olympix:

No items found.

Ready to Shift Security Assurance In-House? Talk to Our Security Experts Today.