pragma solidity ^0.5.16; // Inheritance import "./Owned.sol"; import "./MixinResolver.sol"; import "./interfaces/ILiquidations.sol"; // Libraries import "./SafeDecimalMath.sol"; // Internal references import "./EternalStorage.sol"; import "./interfaces/IOikos.sol"; import "./interfaces/IOikosState.sol"; import "./interfaces/IExchangeRates.sol"; import "./interfaces/IIssuer.sol"; import "./interfaces/ISystemStatus.sol"; // https://docs.oikos.cash/contracts/Liquidations contract Liquidations is Owned, MixinResolver, ILiquidations { 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_OIKOS = "Oikos"; bytes32 private constant CONTRACT_ETERNALSTORAGE_LIQUIDATIONS = "EternalStorageLiquidations"; bytes32 private constant CONTRACT_OIKOSSTATE = "OikosState"; bytes32 private constant CONTRACT_ISSUER = "Issuer"; bytes32 private constant CONTRACT_EXRATES = "ExchangeRates"; bytes32[24] private addressesToCache = [ CONTRACT_SYSTEMSTATUS, CONTRACT_OIKOS, CONTRACT_ETERNALSTORAGE_LIQUIDATIONS, CONTRACT_OIKOSSTATE, CONTRACT_ISSUER, CONTRACT_EXRATES ]; /* ========== CONSTANTS ========== */ uint public constant MAX_LIQUIDATION_RATIO = 1e18; // 100% issuance ratio uint public constant MAX_LIQUIDATION_PENALTY = 1e18 / 4; // Max 25% liquidation penalty / bonus uint public constant RATIO_FROM_TARGET_BUFFER = 2e18; // 200% - mininimum buffer between issuance ratio and liquidation ratio uint public constant MAX_LIQUIDATION_DELAY = 30 days; uint public constant MIN_LIQUIDATION_DELAY = 1 days; // Storage keys bytes32 public constant LIQUIDATION_DEADLINE = "LiquidationDeadline"; bytes32 public constant LIQUIDATION_CALLER = "LiquidationCaller"; /* ========== STATE VARIABLES ========== */ uint public liquidationDelay = 2 weeks; // liquidation time delay after address flagged uint public liquidationRatio = 1e18 / 2; // 0.5 issuance ratio when account can be flagged for liquidation uint public liquidationPenalty = 1e18 / 10; // 10% constructor(address _owner, address _resolver) public Owned(_owner) MixinResolver(_resolver, addressesToCache) {} /* ========== VIEWS ========== */ function oikos() internal view returns (IOikos) { return IOikos(resolver.requireAndGetAddress(CONTRACT_OIKOS, "Missing Oikos address")); } function oikosState() internal view returns (IOikosState) { return IOikosState(resolver.requireAndGetAddress(CONTRACT_OIKOSSTATE, "Missing OikosState address")); } function systemStatus() internal view returns (ISystemStatus) { return ISystemStatus(resolver.requireAndGetAddress(CONTRACT_SYSTEMSTATUS, "Missing SystemStatus address")); } function issuer() internal view returns (IIssuer) { return IIssuer(resolver.requireAndGetAddress(CONTRACT_ISSUER, "Missing Issuer address")); } function exchangeRates() internal view returns (IExchangeRates) { return IExchangeRates(resolver.requireAndGetAddress(CONTRACT_EXRATES, "Missing ExchangeRates address")); } // refactor to oikos storage eternal storage contract once that's ready function eternalStorageLiquidations() internal view returns (EternalStorage) { return EternalStorage( resolver.requireAndGetAddress(CONTRACT_ETERNALSTORAGE_LIQUIDATIONS, "Missing EternalStorageLiquidations address") ); } /* ========== VIEWS ========== */ function liquidationCollateralRatio() external view returns (uint) { return SafeDecimalMath.unit().divideDecimalRound(liquidationRatio); } function getLiquidationDeadlineForAccount(address account) external view returns (uint) { LiquidationEntry memory liquidation = _getLiquidationEntryForAccount(account); return liquidation.deadline; } function isOpenForLiquidation(address account) external view returns (bool) { uint accountCollateralisationRatio = oikos().collateralisationRatio(account); // Liquidation closed if collateral ratio less than or equal target issuance Ratio // Account with no oks collateral will also not be open for liquidation (ratio is 0) if (accountCollateralisationRatio <= oikosState().issuanceRatio()) { return false; } LiquidationEntry memory liquidation = _getLiquidationEntryForAccount(account); // liquidation cap at issuanceRatio is checked above //if (_deadlinePassed(liquidation.deadline)) { return true; //} //return false; } 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; } /** * r = target issuance ratio * D = debt balance * V = Collateral * P = liquidation penalty * Calculates amount of synths = (D - V * r) / (1 - (1 + P) * r) */ function calculateAmountToFixCollateral(uint debtBalance, uint collateral) external view returns (uint) { uint ratio = oikosState().issuanceRatio(); uint unit = SafeDecimalMath.unit(); uint dividend = debtBalance.sub(collateral.multiplyDecimal(ratio)); uint divisor = unit.sub(unit.add(liquidationPenalty).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 = eternalStorageLiquidations().getUIntValue(_getKey(LIQUIDATION_DEADLINE, account)); // liquidation caller not used _liquidation.caller = address(0); } function _getKey(bytes32 _scope, address _account) internal pure returns (bytes32) { return keccak256(abi.encodePacked(_scope, _account)); } /* ========== SETTERS ========== */ function setLiquidationDelay(uint time) external onlyOwner { require(time <= MAX_LIQUIDATION_DELAY, "Must be less than 30 days"); require(time >= MIN_LIQUIDATION_DELAY, "Must be greater than 1 day"); liquidationDelay = time; emit LiquidationDelayUpdated(time); } // Accounts Collateral/Issuance ratio is higher when there is less collateral backing their debt // Upper bound liquidationRatio is 1 + penalty (100% + 10% = 110%) to allow collateral to cover debt and penalty function setLiquidationRatio(uint _liquidationRatio) external onlyOwner { require( _liquidationRatio <= MAX_LIQUIDATION_RATIO.divideDecimal(SafeDecimalMath.unit().add(liquidationPenalty)), "liquidationRatio > MAX_LIQUIDATION_RATIO / (1 + penalty)" ); // MIN_LIQUIDATION_RATIO is a product of target issuance ratio * RATIO_FROM_TARGET_BUFFER // Ensures that liquidation ratio is set so that there is a buffer between the issuance ratio and liquidation ratio. uint MIN_LIQUIDATION_RATIO = oikosState().issuanceRatio().multiplyDecimal(RATIO_FROM_TARGET_BUFFER); require(_liquidationRatio >= MIN_LIQUIDATION_RATIO, "liquidationRatio < MIN_LIQUIDATION_RATIO"); liquidationRatio = _liquidationRatio; emit LiquidationRatioUpdated(_liquidationRatio); } function setLiquidationPenalty(uint penalty) external onlyOwner { require(penalty <= MAX_LIQUIDATION_PENALTY, "penalty > MAX_LIQUIDATION_PENALTY"); liquidationPenalty = penalty; emit LiquidationPenaltyUpdated(penalty); } /* ========== MUTATIVE FUNCTIONS ========== */ // totalIssuedSynths checks synths for staleness // check oks rate is not stale function flagAccountForLiquidation(address account) external rateNotStale("OKS") { systemStatus().requireSystemActive(); LiquidationEntry memory liquidation = _getLiquidationEntryForAccount(account); require(liquidation.deadline == 0, "Account already flagged for liquidation"); uint accountsCollateralisationRatio = oikos().collateralisationRatio(account); // if accounts issuance ratio is greater than or equal to liquidation ratio set liquidation entry require(accountsCollateralisationRatio >= liquidationRatio, "Account issuance ratio is less than liquidation ratio"); uint deadline = now.add(liquidationDelay); _storeLiquidationEntry(account, deadline, msg.sender); emit AccountFlaggedForLiquidation(account, deadline); } // Internal function to remove account from liquidations // Does not check collateral ratio is fixed function removeAccountInLiquidation(address account) external onlyIssuer { LiquidationEntry memory liquidation = _getLiquidationEntryForAccount(account); if (liquidation.deadline > 0) { _removeLiquidationEntry(account); } } // Public function to allow an account to remove from liquidations // Checks collateral ratio is fixed - below target issuance ratio // Check OKS rate is not stale function checkAndRemoveAccountInLiquidation(address account) external rateNotStale("OKS") { systemStatus().requireSystemActive(); LiquidationEntry memory liquidation = _getLiquidationEntryForAccount(account); require(liquidation.deadline > 0, "Account has no liquidation set"); uint accountsCollateralisationRatio = oikos().collateralisationRatio(account); // Remove from liquidations if accountsCollateralisationRatio is fixed (less than equal target issuance ratio) if (accountsCollateralisationRatio <= oikosState().issuanceRatio()) { _removeLiquidationEntry(account); } } function _storeLiquidationEntry( address _account, uint _deadline, address _caller ) internal { // record liquidation deadline eternalStorageLiquidations().setUIntValue(_getKey(LIQUIDATION_DEADLINE, _account), _deadline); eternalStorageLiquidations().setAddressValue(_getKey(LIQUIDATION_CALLER, _account), _caller); } function _removeLiquidationEntry(address _account) internal { // delete liquidation deadline eternalStorageLiquidations().deleteUIntValue(_getKey(LIQUIDATION_DEADLINE, _account)); // delete liquidation caller eternalStorageLiquidations().deleteAddressValue(_getKey(LIQUIDATION_CALLER, _account)); emit AccountRemovedFromLiquidation(_account, now); } /* ========== MODIFIERS ========== */ modifier onlyIssuer() { require(msg.sender == address(issuer()), "Liquidations: Only the Issuer contract can perform this action"); _; } modifier rateNotStale(bytes32 currencyKey) { require(!exchangeRates().rateIsStale(currencyKey), "Rate stale or not a synth"); _; } /* ========== EVENTS ========== */ event AccountFlaggedForLiquidation(address indexed account, uint deadline); event AccountRemovedFromLiquidation(address indexed account, uint time); event LiquidationDelayUpdated(uint newDelay); event LiquidationRatioUpdated(uint newRatio); event LiquidationPenaltyUpdated(uint newPenalty); }