On June 25, 2026 at 05:50:31 UTC, an attacker exploited Ocean Protocol BPool clones on Polygon through a logic error in the protocol’s single-sided join and exit accounting. The attacker flash-borrowed 1,261.592155 mOCEAN, cycled it through eight SideStaking-controlled pools, and extracted a net 127,861.011181 mOCEAN profit after repaying the flash swap. The vulnerable path combined asymmetric joinswapExternAmountIn and exitswapPoolAmountIn math with automatic SideStaking co-stake and co-unstake flows, letting each cycle mint pool shares too cheaply and redeem more base token than it contributed. At contemporaneous market pricing, the loss was roughly $11.9K.

Root Cause

Vulnerable Contract

BPool implementation at 0xbb3051df2d3e408dae6e6daa2296bc6215f0dcfd, reached through multiple minimal-proxy Ocean pool clones including 0xe7832a036da14dc3bbcec5f73a8193221e9f0da5, 0x2dd64ba8d9b9b1bb402aa70214e1fb1d7af314a1, 0x25faf893edcef3b1c94029f01a088448669fcb9a, 0x1f5927cb77ea8449f0281ed14847a70d7a4f7053, 0x56a5cf2fb3f5b12e6c4bc4c0f100800d3735e522, 0x569c692125cf32baf19e4ce713f9cf43e4c18c2c, 0x95f57249e6dd394318025068a8bfc841ac6ec0dd, and 0x193f1ce9108644cd4d09c769d8dcd100f2b901d6. The source is verified, and the execution trace shows those clones repeatedly delegate into the shared implementation.

Vulnerable Function

The vulnerable execution path is the pairing of joinswapExternAmountIn(uint256,uint256) (0x8329ab33) with exitswapPoolAmountIn(uint256,uint256) (0x12989260). The join path prices pool-share minting from a base-token-only deposit, then also triggers SideStaking to contribute datatokens and mint matching pool shares to the controller. The exit path later redeems base token using the same pool-share amount while also unwinding the controller’s datatoken side, so the attacker can repeatedly round-trip a smaller mOCEAN input into a larger mOCEAN output.

Vulnerable Code

function calcPoolOutGivenSingleIn(
    uint tokenBalanceIn,
    uint poolSupply,
    uint tokenAmountIn
) internal pure returns (uint poolAmountOut) {
    uint tokenAmountInAfterFee = bmul(tokenAmountIn, BONE);
    uint newTokenBalanceIn = badd(tokenBalanceIn, tokenAmountInAfterFee);
    uint tokenInRatio = bdiv(newTokenBalanceIn, tokenBalanceIn);
    uint poolRatio = bsub(tokenInRatio, BONE);
    uint newPoolSupply = bmul(poolRatio, poolSupply);
    newPoolSupply = newPoolSupply / 2; // <-- VULNERABILITY: single-sided join mints shares from only the base-token leg
    return newPoolSupply;
}

function joinswapExternAmountIn(uint256 tokenAmountIn, uint256 minPoolAmountOut)
    external
    returns (uint256 poolAmountOut)
{
    poolAmountOut = calcPoolOutGivenSingleIn(inRecord.balance, _totalSupply, tokenAmountIn);
    uint256 ssAmountIn = calcSingleInGivenPoolOut(ssInRecord.balance, _totalSupply, poolAmountOut);

    if (ssContract.canStake(_datatokenAddress, ssAmountIn)) {
        _mintPoolShare(poolAmountOut);
        _pushPoolShare(_controller, poolAmountOut);
        _pullUnderlying(_datatokenAddress, _controller, ssAmountIn);
    }

    _mintPoolShare(poolAmountOut);
    _pushPoolShare(msg.sender, poolAmountOut); // <-- VULNERABILITY: attacker receives shares while controller also mirrors the join
    _pullUnderlying(_baseTokenAddress, msg.sender, tokenAmountIn);
}

function calcSingleOutGivenPoolIn(
    uint tokenSupply,
    uint poolSupply,
    uint poolAmountIn
) internal pure returns (uint tokenAmountOut) {
    poolAmountIn = poolAmountIn * 2; // <-- VULNERABILITY: exit redeems against a doubled share amount
    uint newPoolSupply = bsub(poolSupply, poolAmountIn);
    uint poolRatio = bdiv(newPoolSupply, poolSupply);
    uint tokenOutRatio = bsub(BONE, poolRatio);
    return bmul(tokenOutRatio, tokenSupply);
}

function exitswapPoolAmountIn(uint256 poolAmountIn, uint256 minAmountOut)
    external
    returns (uint256 tokenAmountOut)
{
    tokenAmountOut = calcSingleOutGivenPoolIn(outRecord.balance, _totalSupply, poolAmountIn);
    if (ssContract.canUnStake(_datatokenAddress, poolAmountIn)) {
        uint256 ssAmountOut = calcSingleOutGivenPoolIn(ssOutRecord.balance, _totalSupply, poolAmountIn);
        _burnPoolShare(poolAmountIn);
        _pushUnderlying(_datatokenAddress, _controller, ssAmountOut);
        ssContract.UnStake(_datatokenAddress, ssAmountOut, poolAmountIn);
    }
    _burnPoolShare(poolAmountIn);
    _pushUnderlying(_baseTokenAddress, msg.sender, tokenAmountOut); // <-- VULNERABILITY: attacker redeems more mOCEAN than the paired join funded
}

Why It’s Vulnerable

Expected behavior: A single-sided base-token join followed by a single-sided base-token exit should be economically neutral apart from fees and slippage, even when SideStaking mirrors the datatoken side. The combined pool-share minting and burning logic should preserve the invariant that the attacker cannot withdraw more mOCEAN than the cycle contributed.

Actual behavior: The join path computes the attacker’s pool-share output from only the base-token deposit, then mints the same amount of pool shares again for the SideStaking controller when datatokens are auto-added. The exit path values poolAmountIn through calcSingleOutGivenPoolIn, which doubles the share amount during redemption and repeats the same logic for the controller’s datatoken side. That asymmetric accounting makes the join and exit formulas non-inverse once SideStaking participates, so the attacker can accumulate excess mOCEAN across repeated cycles.

The flaw is therefore a logic_error in pool-share accounting, with flash_loan_abuse only serving as the capital source that let the attacker run the loop across many pools in a single transaction.

Attack Execution

High-Level Flow

  1. The attacker EOA deployed a bootstrap contract, which immediately deployed the exploit runner.
  2. The exploit runner initiated a flash swap on the mOCEAN/MegaDoge Uniswap V2 pair and received 1,261.592155 mOCEAN.
  3. Inside the pair callback, the runner repeatedly called joinswapExternAmountIn on eight Ocean pool clones, contributing only mOCEAN while each pool forced SideStaking to mirror the datatoken leg.
  4. The runner then cascaded exitswapPoolAmountIn calls to redeem mOCEAN from those same pools, with each exit also triggering SideStaking unstake callbacks.
  5. After the loop, the runner repaid 1,265.388320 mOCEAN to the pair and forwarded the remaining 127,861.011181 mOCEAN profit to the attacker EOA.

Detailed Call Trace

The transaction begins with 0x3fa8cf7fea68c8e76a9838d77889464ddfb6a6cf creating 0xefd1b12f5e3c35d7dae0d1449674c247566f9b76, which in turn creates the exploit runner at 0xdd4bfd70117b5b6b343fc8d2c8c0075d095dbee5. The runner then calls Uniswap V2 pair 0xec554b30ca0656ea2404e85528c1d5f885e9e296 with swap(uint256,uint256,address,bytes), causing the pair to transfer 1,261.592155 mOCEAN to the runner and invoke the runner’s callback.

Inside that callback, the runner executes 65 joinswapExternAmountIn calls and 40 exitswapPoolAmountIn calls across eight BPool clones. The execution trace also shows 65 canStake checks and 65 Stake calls from those pools into SideStaking controller 0x3efdd8f728c8e774ab81d14d0b2f07a8238960f4, followed by 40 canUnStake checks and 40 UnStake calls during the exit cascade. The first observed joins on pool 0xe7832a036da14dc3bbcec5f73a8193221e9f0da5 deposit 37.265436, 55.898153, and 83.847230 mOCEAN, while the first exits burn 930.848306, 465.424153, and 232.712077 pool-share units to recover base token.

Because every join minted shares for both the attacker and the controller while only the attacker supplied base token, the runner could cycle through the same pool multiple times before moving to the next clone. The largest mOCEAN drains in the trace came from pool clones 0x95f57249e6dd394318025068a8bfc841ac6ec0dd (58,172.918414 mOCEAN), 0x56a5cf2fb3f5b12e6c4bc4c0f100800d3735e522 (29,151.074452 mOCEAN), and 0x193f1ce9108644cd4d09c769d8dcd100f2b901d6 (25,247.737039 mOCEAN). Once the loop finished, the runner repaid the pair and the remaining mOCEAN was transferred out to the attacker EOA.

Financial Impact

The attacker realized a net profit of 127,861.011181 mOCEAN. The flash swap itself was temporary capital: 1,261.592155 mOCEAN was borrowed from the Uniswap V2 pair and 1,265.388320 mOCEAN was repaid before the transaction ended, leaving the net gain entirely sourced from the Ocean BPool clones. The on-chain losses were distributed across eight affected pool clones, with the heaviest impact concentrated in 0x95f57249e6dd394318025068a8bfc841ac6ec0dd, 0x56a5cf2fb3f5b12e6c4bc4c0f100800d3735e522, and 0x193f1ce9108644cd4d09c769d8dcd100f2b901d6. Using contemporaneous market pricing, the extracted mOCEAN was worth roughly $11.9K.

Evidence

  • Transaction: 0x6dc8a7fba1303faef3ec7afa770b90b17ec5ecd73b51229277a9b0492e285796 on Polygon, block 89107757, status success.
  • Attacker: 0x3fa8cf7fea68c8e76a9838d77889464ddfb6a6cf.
  • Vulnerable contract: 0xbb3051df2d3e408dae6e6daa2296bc6215f0dcfd, reached via multiple BPool clones.
  • Impacted pool/token/protocol component: Ocean Protocol SideStaking-managed BPool clones using mOCEAN as the base token.
  • Key on-chain fact: the trace shows exactly 65 joinswapExternAmountIn calls, 40 exitswapPoolAmountIn calls, 65 Stake calls, and 40 UnStake calls within the same transaction.
  • Key on-chain fact: the transaction transferred 1,261.592155 mOCEAN from the Uniswap pair to the runner, repaid 1,265.388320 mOCEAN to the pair, and still delivered 127,861.011181 mOCEAN net to the attacker EOA.

Remediation

Make the single-sided join and exit formulas true inverses under SideStaking participation, or disable SideStaking mirroring for single-asset liquidity operations entirely. In practice, joinswapExternAmountIn must not mint an attacker share amount that can later be redeemed against both the attacker’s base-token leg and the controller’s mirrored datatoken leg, and exitswapPoolAmountIn must be tested against the exact join path it is meant to unwind. Add invariant tests that round-trip single-sided join/exit sequences across pools with SideStaking enabled, and reject any flow where the same notional liquidity cycle increases the caller’s base-token balance before fees.