
Date: June 25, 2026
Chain: Polygon
Loss: 127,861 mOCEAN, about $11.9K at the time
Class: pool join and exit formulas that stopped being inverses once a staking helper joined in
Three and a half minutes was all the wallet at 0x3Fa8...a6cf needed on Polygon. It had been funded through the Relay router, and at 05:50:31 UTC on June 25, 2026, it sent a single contract creation transaction. At 05:54:08 UTC it sent its second and last one, a bridge withdrawal that carried 127,861.01 mOCEAN, Ocean Protocol's token on Polygon, back to Ethereum.
The mOCEAN came out of eight liquidity pools built on Ocean Protocol's V4 contracts, and DARKNAVY and Defimon Alerts valued it at about $11,900 at the time. Nobody had to steal a key or fool a user. According to DARKNAVY's analysis, the pool's formula for adding liquidity with one token and its formula for removing liquidity to one token stopped mirroring each other whenever Ocean's staking helper took part, so a deposit followed by a withdrawal paid back more than it put in.
Ocean Protocol's V4 release paired each published dataset's datatoken with OCEAN in an automated market maker pool, a BPool derived from Balancer's weighted pools. Ocean's own 2022 post on the design explains why the pools worked differently from a normal two-token pool. Liquidity providers deposited only OCEAN, and a bot run by the protocol supplied the datatoken side, minting enough datatokens on each stake to keep the price ratio constant and taking them back on each unstake. Ocean called this one-sided staking, and it was meant to stop publishers from holding a pile of datatokens they could dump on stakers.
In the contracts DARKNAVY analyzed, that staking side is handled by a SideStaking contract on Polygon at 0x3EFD...60f4, and the eight affected pools are minimal-proxy clones of one BPool implementation at 0xbb30...dcfd. Ocean's post now carries a notice that the features it describes are no longer available in Ocean Market, but the pools themselves stayed on-chain with OCEAN still in them.
Two functions handled the one-token path. joinswapExternAmountIn takes a deposit of a single token and mints pool shares for it, priced by a helper called calcPoolOutGivenSingleIn. exitswapPoolAmountIn takes pool shares back and pays out a single token, priced by calcSingleOutGivenPoolIn. With SideStaking enabled, each of those calls also tells the bot to stake or unstake its datatoken side, through the Stake and UnStake hooks.
A join and an exit at the same pool state are supposed to undo each other. Put a token in, take the shares straight back out, and the most anyone should get is what they deposited, minus fees. That property is the whole of what keeps a single-sided path from becoming a withdrawal for free.
DARKNAVY found that the property broke once SideStaking took part. On the join side, the pool priced the new shares from the OCEAN deposit alone, and SideStaking then added its matching datatokens and received pool shares of its own. The transaction's token transfers show it directly: on the first pool, each join minted the same number of pool shares twice, one batch going to SideStaking alongside its datatokens and one going to the attacker's contract. On the exit side, DARKNAVY reports that the redemption math effectively counted the share amount twice while the bot's datatoken side was unwound too, so the OCEAN paid out reflected more than the depositor's own contribution. The two calculations were each reasonable on their own and stopped being inverses only when the hook ran between them.
The lesson for builders is the round trip. When a deposit path and a withdrawal path are written as separate formulas, and a helper contract can change pool state in between, the pair has to be tested as a pair. A cashier who rings up a return at a different price than the sale can lose money on every customer without a single wrong entry in either ledger.
The contract creation at 05:50:31 UTC, in Polygon block 89107757, deployed a bootstrap contract at 0xefd1...9b76, which deployed an exploit runner at 0xdd4b...bee5. The runner borrowed 1,261.59 mOCEAN as a flash swap from a Uniswap V2 pair holding mOCEAN and MegaDoge, a loan that has to be repaid before the transaction ends.
According to DARKNAVY's reading of the trace, the runner then worked through eight BPool clones, making 65 single-sided joins and 40 single-sided exits, with SideStaking staking on 65 of those calls and unstaking on 40. DARKNAVY found that each cycle redeemed more mOCEAN than it contributed, and that repeating the cycle across pools inside one transaction compounded the gain. The three largest drains were 58,172.92 mOCEAN from 0x95f5...c0dd, 29,151.07 from 0x56a5...e522, and 25,247.74 from 0x193f...01d6.
At the end of the transaction, the runner repaid the flash swap with 1,265.39 mOCEAN, and 127,861.01 mOCEAN ended up with the attacker's wallet. Defimon Alerts flagged the drain the same day and classified it as a logic error in asymmetric single-sided join and exit math, and DARKNAVY noted that the flash swap was only the source of starting capital, with the accounting error doing the rest.
At 05:54:08 UTC, in block 89107902, the attacker's wallet called the withdraw function on the mOCEAN token contract, burning all 127,861.01 mOCEAN on Polygon to release the same amount on Ethereum through the Polygon bridge. The wallet made no other transactions on Polygon.
In the three weeks after the attack, Ocean Protocol's X account posted about its compute network and its arbitration with Fetch.ai, and didn't post about the drained pools. DARKNAVY recommended making the single-sided join and exit formulas true inverses with SideStaking enabled, or turning off SideStaking's mirroring for single-asset operations, and adding invariant tests that run join and exit sequences in a round trip.
Olympix reads a contract the way an attacker does, as states, transitions and entry points, and it takes in the surrounding design, including the helper contracts a pool calls, to infer the invariants the system relies on but never writes down. In a BPool with SideStaking, the state that matters is the pool's token balances against its outstanding shares, and joinswapExternAmountIn and exitswapPoolAmountIn are the entry points that move both, with the Stake and UnStake hooks running inside them.
A pool's state space looks infinite, but it isn't, and the engine uses signals to decide which paths deserve an attack. Olympix's static analysis surfaces the signal here: a pair of mint and burn paths priced by two separately written formulas, with an external hook that mints or burns shares for another account between the pricing and the transfer. Olympix has flagged this class before in other codebases, where share accounting could be moved by balances that the depositor didn't contribute. From that signal, the engine narrows from every path, to the feasible join and exit sequences, to the ones where SideStaking participates, because those are the only ones that can touch the invariant.
The invariant fits in one sentence: a single-sided join followed by an exit at the same pool state should never return more of a token than was deposited. BugPoCer takes that rule and runs Counterexample-Guided Invariant Refutation over the pool and SideStaking together, executing join and exit sequences until one ends with the caller holding more OCEAN than it started with.
The counterexample it returns is the June 25 transaction in miniature: a join that mints shares to both the caller and SideStaking, an exit that pays out on a share amount the formula counts twice, and a balance that grows from one cycle to the next. It comes with the minimal conditions, a pool with SideStaking enabled and any caller with a modest OCEAN balance, plus a generated Forge test that replays the sequence. Because the engine runs automatically in development, at every merge and before release, that test would have existed when SideStaking was first wired into the single-sided paths.
Ocean's pools lost their OCEAN through two formulas that each did their job and disagreed only when a helper stepped in between them. Any protocol that prices entry and exit separately needs a round-trip rule that holds with every hook enabled, and that rule is something an engine can check long before a pool holds real liquidity. To see how Olympix runs your deposit and withdrawal paths against each other and returns a runnable counterexample for any round trip that pays out, book a technical walkthrough.
Protocol: Ocean Protocol V4 BPool with SideStaking
Chain: Polygon
Date: June 25, 2026, 05:50:31 UTC
Loss: 127,861.01 mOCEAN, about $11.9K at the time
Attacker EOA: 0x3Fa8...a6cf
Bootstrap contract: 0xefd1...9b76
Exploit runner: 0xdd4b...bee5
Vulnerable BPool implementation: 0xbb30...dcfd
SideStaking: 0x3EFD...60f4
Largest drained pool (58,172.92 mOCEAN): 0x95f5...c0dd
Second-largest drained pool (29,151.07 mOCEAN): 0x56a5...e522
Third-largest drained pool (25,247.74 mOCEAN): 0x193f...01d6
Flash-swap source (Uniswap V2 mOCEAN/MegaDoge): 0xEC55...E296
mOCEAN token: 0x282d...a1A1
Exploit tx: 0x6dc8...5796
Block: 89107757
Bridge withdrawal to Ethereum (127,861.01 mOCEAN): 0xee7c...c771
DARKNAVY: Ocean Protocol BPool Single-Sided Join/Exit Math Drain