/
Liquidation.sol
288 lines (194 loc) · 12 KB
/
Liquidation.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
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;
import "../BaseLogic.sol";
/// @notice Liquidate users who are in collateral violation to protect lenders
contract Liquidation is BaseLogic {
constructor(bytes32 moduleGitCommit_) BaseLogic(MODULEID__LIQUIDATION, moduleGitCommit_) {}
// How much of a liquidation is credited to the underlying's reserves.
uint public constant UNDERLYING_RESERVES_FEE = 0.02 * 1e18;
// Maximum discount that can be awarded under any conditions.
uint public constant MAXIMUM_DISCOUNT = 0.20 * 1e18;
// How much faster the booster grows for a fully funded supplier. Partially-funded suppliers
// have this scaled proportional to their free-liquidity divided by the violator's liability.
uint public constant DISCOUNT_BOOSTER_SLOPE = 2 * 1e18;
// How much booster discount can be awarded beyond the base discount.
uint public constant MAXIMUM_BOOSTER_DISCOUNT = 0.025 * 1e18;
// Post-liquidation target health score that limits maximum liquidation sizes. Must be >= 1.
uint public constant TARGET_HEALTH = 1.25 * 1e18;
/// @notice Information about a prospective liquidation opportunity
struct LiquidationOpportunity {
uint repay;
uint yield;
uint healthScore;
// Only populated if repay > 0:
uint baseDiscount;
uint discount;
uint conversionRate;
}
struct LiquidationLocals {
address liquidator;
address violator;
address underlying;
address collateral;
uint underlyingPrice;
uint collateralPrice;
LiquidationOpportunity liqOpp;
uint repayPreFees;
}
function computeLiqOpp(LiquidationLocals memory liqLocs) private {
require(!isSubAccountOf(liqLocs.violator, liqLocs.liquidator), "e/liq/self-liquidation");
require(isEnteredInMarket(liqLocs.violator, liqLocs.underlying), "e/liq/violator-not-entered-underlying");
require(isEnteredInMarket(liqLocs.violator, liqLocs.collateral), "e/liq/violator-not-entered-collateral");
liqLocs.underlyingPrice = getAssetPrice(liqLocs.underlying);
liqLocs.collateralPrice = getAssetPrice(liqLocs.collateral);
LiquidationOpportunity memory liqOpp = liqLocs.liqOpp;
AssetStorage storage underlyingAssetStorage = eTokenLookup[underlyingLookup[liqLocs.underlying].eTokenAddress];
AssetCache memory underlyingAssetCache = loadAssetCache(liqLocs.underlying, underlyingAssetStorage);
AssetStorage storage collateralAssetStorage = eTokenLookup[underlyingLookup[liqLocs.collateral].eTokenAddress];
AssetCache memory collateralAssetCache = loadAssetCache(liqLocs.collateral, collateralAssetStorage);
liqOpp.repay = liqOpp.yield = 0;
(uint collateralValue, uint liabilityValue) = getAccountLiquidity(liqLocs.violator);
if (liabilityValue == 0) {
liqOpp.healthScore = type(uint).max;
return; // no violation
}
liqOpp.healthScore = collateralValue * 1e18 / liabilityValue;
if (collateralValue >= liabilityValue) {
return; // no violation
}
// At this point healthScore must be < 1 since collateral < liability
// Compute discount
{
uint baseDiscount = UNDERLYING_RESERVES_FEE + (1e18 - liqOpp.healthScore);
uint discountBooster = computeDiscountBooster(liqLocs.liquidator, liabilityValue);
uint discount = baseDiscount * discountBooster / 1e18;
if (discount > (baseDiscount + MAXIMUM_BOOSTER_DISCOUNT)) discount = baseDiscount + MAXIMUM_BOOSTER_DISCOUNT;
if (discount > MAXIMUM_DISCOUNT) discount = MAXIMUM_DISCOUNT;
liqOpp.baseDiscount = baseDiscount;
liqOpp.discount = discount;
liqOpp.conversionRate = liqLocs.underlyingPrice * 1e18 / liqLocs.collateralPrice * 1e18 / (1e18 - discount);
}
// Determine amount to repay to bring user to target health
if (liqLocs.underlying == liqLocs.collateral) {
liqOpp.repay = type(uint).max;
} else {
AssetConfig memory collateralConfig = resolveAssetConfig(liqLocs.collateral);
AssetConfig memory underlyingConfig = resolveAssetConfig(liqLocs.underlying);
uint collateralFactor = collateralConfig.collateralFactor;
uint borrowFactor = underlyingConfig.borrowFactor;
uint liabilityValueTarget = liabilityValue * TARGET_HEALTH / 1e18;
// These factors are first converted into standard 1e18-scale fractions, then adjusted according to TARGET_HEALTH and the discount:
uint borrowAdj = borrowFactor != 0 ? TARGET_HEALTH * CONFIG_FACTOR_SCALE / borrowFactor : MAX_SANE_DEBT_AMOUNT;
uint collateralAdj = 1e18 * uint(collateralFactor) / CONFIG_FACTOR_SCALE * 1e18 / (1e18 - liqOpp.discount);
if (borrowAdj <= collateralAdj) {
liqOpp.repay = type(uint).max;
} else {
// liabilityValueTarget >= liabilityValue > collateralValue
uint maxRepayInReference = (liabilityValueTarget - collateralValue) * 1e18 / (borrowAdj - collateralAdj);
liqOpp.repay = maxRepayInReference * 1e18 / liqLocs.underlyingPrice;
}
}
// Limit repay to current owed
// This can happen when there are multiple borrows and liquidating this one won't bring the violator back to solvency
{
uint currentOwed = getCurrentOwed(underlyingAssetStorage, underlyingAssetCache, liqLocs.violator);
if (liqOpp.repay > currentOwed) liqOpp.repay = currentOwed;
}
// Limit yield to borrower's available collateral, and reduce repay if necessary
// This can happen when borrower has multiple collaterals and seizing all of this one won't bring the violator back to solvency
liqOpp.yield = liqOpp.repay * liqOpp.conversionRate / 1e18;
{
uint collateralBalance = balanceToUnderlyingAmount(collateralAssetCache, collateralAssetStorage.users[liqLocs.violator].balance);
if (collateralBalance < liqOpp.yield) {
liqOpp.repay = collateralBalance * 1e18 / liqOpp.conversionRate;
liqOpp.yield = collateralBalance;
}
}
// Adjust repay to account for reserves fee
liqLocs.repayPreFees = liqOpp.repay;
liqOpp.repay = liqOpp.repay * (1e18 + UNDERLYING_RESERVES_FEE) / 1e18;
}
// Returns 1e18-scale fraction > 1 representing how much faster the booster grows for this liquidator
function computeDiscountBooster(address liquidator, uint violatorLiabilityValue) private returns (uint) {
uint booster = getUpdatedAverageLiquidityWithDelegate(liquidator) * 1e18 / violatorLiabilityValue;
if (booster > 1e18) booster = 1e18;
booster = booster * (DISCOUNT_BOOSTER_SLOPE - 1e18) / 1e18;
return booster + 1e18;
}
/// @notice Checks to see if a liquidation would be profitable, without actually doing anything
/// @param liquidator Address that will initiate the liquidation
/// @param violator Address that may be in collateral violation
/// @param underlying Token that is to be repayed
/// @param collateral Token that is to be seized
/// @return liqOpp The details about the liquidation opportunity
function checkLiquidation(address liquidator, address violator, address underlying, address collateral) external nonReentrant returns (LiquidationOpportunity memory liqOpp) {
LiquidationLocals memory liqLocs;
liqLocs.liquidator = liquidator;
liqLocs.violator = violator;
liqLocs.underlying = underlying;
liqLocs.collateral = collateral;
computeLiqOpp(liqLocs);
return liqLocs.liqOpp;
}
/// @notice Attempts to perform a liquidation
/// @param violator Address that may be in collateral violation
/// @param underlying Token that is to be repayed
/// @param collateral Token that is to be seized
/// @param repay The amount of underlying DTokens to be transferred from violator to sender, in units of underlying
/// @param minYield The minimum acceptable amount of collateral ETokens to be transferred from violator to sender, in units of collateral
function liquidate(address violator, address underlying, address collateral, uint repay, uint minYield) external nonReentrant {
require(accountLookup[violator].deferLiquidityStatus == DEFERLIQUIDITY__NONE, "e/liq/violator-liquidity-deferred");
address liquidator = unpackTrailingParamMsgSender();
emit RequestLiquidate(liquidator, violator, underlying, collateral, repay, minYield);
updateAverageLiquidity(liquidator);
updateAverageLiquidity(violator);
LiquidationLocals memory liqLocs;
liqLocs.liquidator = liquidator;
liqLocs.violator = violator;
liqLocs.underlying = underlying;
liqLocs.collateral = collateral;
computeLiqOpp(liqLocs);
executeLiquidation(liqLocs, repay, minYield);
}
function executeLiquidation(LiquidationLocals memory liqLocs, uint desiredRepay, uint minYield) private {
require(desiredRepay <= liqLocs.liqOpp.repay, "e/liq/excessive-repay-amount");
uint repay;
{
AssetStorage storage underlyingAssetStorage = eTokenLookup[underlyingLookup[liqLocs.underlying].eTokenAddress];
AssetCache memory underlyingAssetCache = loadAssetCache(liqLocs.underlying, underlyingAssetStorage);
if (desiredRepay == liqLocs.liqOpp.repay) repay = liqLocs.repayPreFees;
else repay = desiredRepay * (1e18 * 1e18 / (1e18 + UNDERLYING_RESERVES_FEE)) / 1e18;
{
uint repayExtra = desiredRepay - repay;
// Liquidator takes on violator's debt:
transferBorrow(underlyingAssetStorage, underlyingAssetCache, underlyingAssetStorage.dTokenAddress, liqLocs.violator, liqLocs.liquidator, repay);
// Extra debt is minted and assigned to liquidator:
increaseBorrow(underlyingAssetStorage, underlyingAssetCache, underlyingAssetStorage.dTokenAddress, liqLocs.liquidator, repayExtra);
// The underlying's reserve is credited to compensate for this extra debt:
{
uint poolAssets = underlyingAssetCache.poolSize + (underlyingAssetCache.totalBorrows / INTERNAL_DEBT_PRECISION);
uint newTotalBalances = poolAssets * underlyingAssetCache.totalBalances / (poolAssets - repayExtra);
increaseReserves(underlyingAssetStorage, underlyingAssetCache, newTotalBalances - underlyingAssetCache.totalBalances);
}
}
logAssetStatus(underlyingAssetCache);
}
uint yield;
{
AssetStorage storage collateralAssetStorage = eTokenLookup[underlyingLookup[liqLocs.collateral].eTokenAddress];
AssetCache memory collateralAssetCache = loadAssetCache(liqLocs.collateral, collateralAssetStorage);
yield = repay * liqLocs.liqOpp.conversionRate / 1e18;
require(yield >= minYield, "e/liq/min-yield");
// Liquidator gets violator's collateral:
address eTokenAddress = underlyingLookup[collateralAssetCache.underlying].eTokenAddress;
transferBalance(collateralAssetStorage, collateralAssetCache, eTokenAddress, liqLocs.violator, liqLocs.liquidator, underlyingAmountToBalance(collateralAssetCache, yield));
logAssetStatus(collateralAssetCache);
}
// Since liquidator is taking on new debt, liquidity must be checked:
checkLiquidity(liqLocs.liquidator);
emitLiquidationLog(liqLocs, repay, yield);
}
function emitLiquidationLog(LiquidationLocals memory liqLocs, uint repay, uint yield) private {
emit Liquidation(liqLocs.liquidator, liqLocs.violator, liqLocs.underlying, liqLocs.collateral, repay, yield, liqLocs.liqOpp.healthScore, liqLocs.liqOpp.baseDiscount, liqLocs.liqOpp.discount);
}
}