forked from Synthetixio/synthetix
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathLiquidator.sol
334 lines (267 loc) · 13.6 KB
/
Liquidator.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
pragma solidity ^0.5.16;
// Inheritance
import "./Owned.sol";
import "./MixinResolver.sol";
import "./MixinSystemSettings.sol";
import "./interfaces/ILiquidator.sol";
// Libraries
import "./SafeDecimalMath.sol";
// Internal references
import "./interfaces/IERC20.sol";
import "./interfaces/ISynthetix.sol";
import "./interfaces/IExchangeRates.sol";
import "./interfaces/IIssuer.sol";
import "./interfaces/ISystemStatus.sol";
import "./interfaces/IHasBalance.sol";
/// @title Upgrade Liquidation Mechanism V2 (SIP-148)
/// @notice This contract is a modification to the existing liquidation mechanism defined in SIP-15
contract Liquidator is Owned, MixinSystemSettings, ILiquidator {
using SafeMath for uint;
using SafeDecimalMath for uint;
struct LiquidationEntry {
uint deadline;
address caller;
}
/* ========== ADDRESS RESOLVER CONFIGURATION ========== */
bytes32 private constant CONTRACT_SYSTEMSTATUS = "SystemStatus";
bytes32 private constant CONTRACT_SYNTHETIX = "Synthetix";
bytes32 private constant CONTRACT_ISSUER = "Issuer";
bytes32 private constant CONTRACT_EXRATES = "ExchangeRates";
bytes32 private constant CONTRACT_SYNTHETIXESCROW = "SynthetixEscrow";
bytes32 private constant CONTRACT_V3_LEGACYMARKET = "LegacyMarket";
/* ========== CONSTANTS ========== */
bytes32 public constant CONTRACT_NAME = "Liquidator";
// Storage keys
bytes32 public constant LIQUIDATION_DEADLINE = "LiquidationDeadline";
bytes32 public constant LIQUIDATION_CALLER = "LiquidationCaller";
constructor(address _owner, address _resolver) public Owned(_owner) MixinSystemSettings(_resolver) {}
/* ========== VIEWS ========== */
function resolverAddressesRequired() public view returns (bytes32[] memory addresses) {
bytes32[] memory existingAddresses = MixinSystemSettings.resolverAddressesRequired();
bytes32[] memory newAddresses = new bytes32[](4);
newAddresses[0] = CONTRACT_SYSTEMSTATUS;
newAddresses[1] = CONTRACT_SYNTHETIX;
newAddresses[2] = CONTRACT_ISSUER;
newAddresses[3] = CONTRACT_EXRATES;
addresses = combineArrays(existingAddresses, newAddresses);
}
function synthetix() internal view returns (ISynthetix) {
return ISynthetix(requireAndGetAddress(CONTRACT_SYNTHETIX));
}
function systemStatus() internal view returns (ISystemStatus) {
return ISystemStatus(requireAndGetAddress(CONTRACT_SYSTEMSTATUS));
}
function issuer() internal view returns (IIssuer) {
return IIssuer(requireAndGetAddress(CONTRACT_ISSUER));
}
function exchangeRates() internal view returns (IExchangeRates) {
return IExchangeRates(requireAndGetAddress(CONTRACT_EXRATES));
}
function issuanceRatio() external view returns (uint) {
return getIssuanceRatio();
}
function liquidationDelay() external view returns (uint) {
return getLiquidationDelay();
}
function liquidationRatio() external view returns (uint) {
return getLiquidationRatio();
}
function liquidationEscrowDuration() external view returns (uint) {
return getLiquidationEscrowDuration();
}
function liquidationPenalty() external view returns (uint) {
// SIP-251: use getSnxLiquidationPenalty instead of getLiquidationPenalty
// which is used for loans / shorts (collateral contracts).
// Keeping the view name because it makes sense in the context of this contract.
return getSnxLiquidationPenalty();
}
function selfLiquidationPenalty() external view returns (uint) {
return getSelfLiquidationPenalty();
}
function liquidateReward() external view returns (uint) {
return getLiquidateReward();
}
function flagReward() external view returns (uint) {
return getFlagReward();
}
function liquidationCollateralRatio() external view returns (uint) {
return SafeDecimalMath.unit().divideDecimalRound(getLiquidationRatio());
}
function getLiquidationDeadlineForAccount(address account) external view returns (uint) {
LiquidationEntry memory liquidation = _getLiquidationEntryForAccount(account);
return liquidation.deadline;
}
function getLiquidationCallerForAccount(address account) external view returns (address) {
LiquidationEntry memory liquidation = _getLiquidationEntryForAccount(account);
return liquidation.caller;
}
/// @notice Determines if an account is eligible for forced or self liquidation
/// @dev An account is eligible to self liquidate if its c-ratio is below the target c-ratio
/// @dev An account with no SNX collateral will not be open for liquidation since the ratio is 0
function isLiquidationOpen(address account, bool isSelfLiquidation) external view returns (bool) {
uint accountCollateralisationRatio = synthetix().collateralisationRatio(account);
// Not open for liquidation if collateral ratio is less than or equal to target issuance ratio
if (accountCollateralisationRatio <= getIssuanceRatio()) {
return false;
}
if (!isSelfLiquidation) {
LiquidationEntry memory liquidation = _getLiquidationEntryForAccount(account);
// Open for liquidation if the deadline has passed and the user has enough SNX collateral.
if (_deadlinePassed(liquidation.deadline) && _hasEnoughSNXForRewards(account)) {
return true;
}
return false;
} else {
// Not open for self-liquidation when the account's collateral value is less than debt issued + forced penalty
uint unit = SafeDecimalMath.unit();
if (accountCollateralisationRatio > (unit.divideDecimal(unit.add(getSnxLiquidationPenalty())))) {
return false;
}
}
return true;
}
/// View for calculating the amounts of collateral (liquid and escrow that will be liquidated), and debt that will
/// be removed.
/// @param account The account to be liquidated
/// @param isSelfLiquidation boolean to determine if this is a forced or self-invoked liquidation
/// @return totalRedeemed the total amount of collateral (SNX) to redeem (liquid and escrow)
/// @return debtToRemove the amount of debt (sUSD) to burn in order to fix the account's c-ratio
/// @return escrowToLiquidate the amount of escrow SNX that will be revoked during liquidation
/// @return initialDebtBalance the amount of initial (sUSD) debt the account has
function liquidationAmounts(address account, bool isSelfLiquidation)
external
view
returns (
uint totalRedeemed,
uint debtToRemove,
uint escrowToLiquidate,
uint initialDebtBalance
)
{
// return zeroes otherwise calculateAmountToFixCollateral reverts with unhelpful underflow error
if (!this.isLiquidationOpen(account, isSelfLiquidation)) {
return (0, 0, 0, issuer().debtBalanceOf(account, "sUSD"));
}
return issuer().liquidationAmounts(account, isSelfLiquidation);
}
function isLiquidationDeadlinePassed(address account) external view returns (bool) {
LiquidationEntry memory liquidation = _getLiquidationEntryForAccount(account);
return _deadlinePassed(liquidation.deadline);
}
function _deadlinePassed(uint deadline) internal view returns (bool) {
// check deadline is set > 0
// check now > deadline
return deadline > 0 && now > deadline;
}
/// @notice Checks if an account has enough SNX balance to be considered open for forced liquidation.
function _hasEnoughSNXForRewards(address account) internal view returns (bool) {
uint balance = issuer().collateral(account);
return balance >= (getLiquidateReward().add(getFlagReward()));
}
/**
* r = target issuance ratio
* D = debt value
* V = collateral value
* P = liquidation penalty
* S = debt amount to redeem
* Calculates amount of synths = (D - V * r) / (1 - (1 + P) * r)
*
* Derivation of the formula:
* Collateral "sold" with penalty: collateral-sold = S * (1 + P)
* After liquidation: new-debt = D - S, new-collateral = V - collateral-sold = V - S * (1 + P)
* Because we fixed the c-ratio, new-debt / new-collateral = c-ratio: (D - S) / (V - S * (1 + P)) = r
* After solving for S we get: S = (D - V * r) / (1 - (1 + P) * r)
* Note: this only returns the amount of debt to remove "assuming the penalty", the penalty still needs to be
* correctly applied when removing collateral.
*/
function calculateAmountToFixCollateral(
uint debtBalance,
uint collateral,
uint penalty
) external view returns (uint) {
uint ratio = getIssuanceRatio();
uint unit = SafeDecimalMath.unit();
uint dividend = debtBalance.sub(collateral.multiplyDecimal(ratio));
uint divisor = unit.sub(unit.add(penalty).multiplyDecimal(ratio));
return dividend.divideDecimal(divisor);
}
// get liquidationEntry for account
// returns deadline = 0 when not set
function _getLiquidationEntryForAccount(address account) internal view returns (LiquidationEntry memory _liquidation) {
_liquidation.deadline = flexibleStorage().getUIntValue(CONTRACT_NAME, _getKey(LIQUIDATION_DEADLINE, account));
// This is used to reward the caller for flagging an account for liquidation.
_liquidation.caller = flexibleStorage().getAddressValue(CONTRACT_NAME, _getKey(LIQUIDATION_CALLER, account));
}
function _getKey(bytes32 _scope, address _account) internal pure returns (bytes32) {
return keccak256(abi.encodePacked(_scope, _account));
}
/* ========== MUTATIVE FUNCTIONS ========== */
// totalIssuedSynths checks synths for staleness
// check snx rate is not stale
function flagAccountForLiquidation(address account) external rateNotInvalid("SNX") {
systemStatus().requireSystemActive();
require(resolver.getAddress(CONTRACT_V3_LEGACYMARKET) == address(0), "Must liquidate using V3");
require(getLiquidationRatio() > 0, "Liquidation ratio not set");
require(getLiquidationDelay() > 0, "Liquidation delay not set");
LiquidationEntry memory liquidation = _getLiquidationEntryForAccount(account);
require(liquidation.deadline == 0, "Account already flagged for liquidation");
uint accountsCollateralisationRatio = synthetix().collateralisationRatio(account);
// if accounts issuance ratio is greater than or equal to liquidation ratio set liquidation entry
require(
accountsCollateralisationRatio >= getLiquidationRatio(),
"Account issuance ratio is less than liquidation ratio"
);
// if account doesn't have enough liquidatable collateral for rewards the liquidation transaction
// is not possible
require(_hasEnoughSNXForRewards(account), "not enough SNX for rewards");
uint deadline = now.add(getLiquidationDelay());
_storeLiquidationEntry(account, deadline, msg.sender);
emit AccountFlaggedForLiquidation(account, deadline);
}
/// @notice This function is called by the Issuer to remove an account's liquidation entry
/// @dev The Issuer must check if the account's c-ratio is fixed before removing
function removeAccountInLiquidation(address account) external onlyIssuer {
LiquidationEntry memory liquidation = _getLiquidationEntryForAccount(account);
if (liquidation.deadline > 0) {
_removeLiquidationEntry(account);
}
}
/// @notice External function to allow anyone to remove an account's liquidation entry
/// @dev This function checks if the account's c-ratio is OK and that the rate of SNX is not stale
function checkAndRemoveAccountInLiquidation(address account) external rateNotInvalid("SNX") {
systemStatus().requireSystemActive();
LiquidationEntry memory liquidation = _getLiquidationEntryForAccount(account);
require(liquidation.deadline > 0, "Account has no liquidation set");
uint accountsCollateralisationRatio = synthetix().collateralisationRatio(account);
// Remove from liquidator if accountsCollateralisationRatio is fixed (less than equal target issuance ratio)
if (accountsCollateralisationRatio <= getIssuanceRatio()) {
_removeLiquidationEntry(account);
}
}
function _storeLiquidationEntry(
address _account,
uint _deadline,
address _caller
) internal {
// record liquidation deadline and caller
flexibleStorage().setUIntValue(CONTRACT_NAME, _getKey(LIQUIDATION_DEADLINE, _account), _deadline);
flexibleStorage().setAddressValue(CONTRACT_NAME, _getKey(LIQUIDATION_CALLER, _account), _caller);
}
/// @notice Only delete the deadline value, keep caller for flag reward payout
function _removeLiquidationEntry(address _account) internal {
flexibleStorage().deleteUIntValue(CONTRACT_NAME, _getKey(LIQUIDATION_DEADLINE, _account));
emit AccountRemovedFromLiquidation(_account, now);
}
/* ========== MODIFIERS ========== */
modifier onlyIssuer() {
require(msg.sender == address(issuer()), "Liquidator: Only the Issuer contract can perform this action");
_;
}
modifier rateNotInvalid(bytes32 currencyKey) {
require(!exchangeRates().rateIsInvalid(currencyKey), "Rate invalid or not a synth");
_;
}
/* ========== EVENTS ========== */
event AccountFlaggedForLiquidation(address indexed account, uint deadline);
event AccountRemovedFromLiquidation(address indexed account, uint time);
}