pragma solidity ^0.5.16; // Inheritance import "./Owned.sol"; import "./MixinResolver.sol"; import "./MixinSystemSettings.sol"; import "./interfaces/IIssuer.sol"; // Libraries import "./SafeCast.sol"; import "./SafeDecimalMath.sol"; // Internal references import "./interfaces/ISynth.sol"; import "./interfaces/IOikos.sol"; import "./interfaces/IFeePool.sol"; import "./interfaces/IOikosDebtShare.sol"; import "./interfaces/IExchanger.sol"; import "./interfaces/IDelegateApprovals.sol"; import "./interfaces/IExchangeRates.sol"; import "./interfaces/IHasBalance.sol"; import "./interfaces/IERC20.sol"; import "./interfaces/ILiquidations.sol"; import "./interfaces/IRewardEscrowV2.sol"; import "./interfaces/ISynthRedeemer.sol"; import "./interfaces/ISystemStatus.sol"; import "./Proxyable.sol"; import "@chainlink/contracts-0.0.10/src/v0.5/interfaces/AggregatorV2V3Interface.sol"; interface IProxy { function target() external view returns (address); } interface IIssuerInternalDebtCache { function updateCachedSynthDebtWithRate(bytes32 currencyKey, uint currencyRate) external; function updateCachedSynthDebtsWithRates(bytes32[] calldata currencyKeys, uint[] calldata currencyRates) external; function updateDebtCacheValidity(bool currentlyInvalid) external; function totalNonOksBackedDebt() external view returns (uint excludedDebt, bool isInvalid); function cacheInfo() external view returns ( uint cachedDebt, uint timestamp, bool isInvalid, bool isStale ); function updateCachedoUSDDebt(int amount) external; } // https://docs.oikos.cash/contracts/source/contracts/issuer contract Issuer is Owned, MixinSystemSettings, IIssuer { using SafeMath for uint; using SafeDecimalMath for uint; bytes32 public constant CONTRACT_NAME = "Issuer"; // SIP-165: Circuit breaker for Debt Synthesis uint public constant CIRCUIT_BREAKER_SUSPENSION_REASON = 165; // Available Synths which can be used with the system ISynth[] public availableSynths; mapping(bytes32 => ISynth) public synths; mapping(address => bytes32) public synthsByAddress; uint public lastDebtRatio; /* ========== ENCODED NAMES ========== */ bytes32 internal constant oUSD = "oUSD"; bytes32 internal constant oETH = "oETH"; bytes32 internal constant OKS = "OKS"; // Flexible storage names bytes32 internal constant LAST_ISSUE_EVENT = "lastIssueEvent"; /* ========== ADDRESS RESOLVER CONFIGURATION ========== */ bytes32 private constant CONTRACT_SYNTHETIX = "Oikos"; bytes32 private constant CONTRACT_EXCHANGER = "Exchanger"; bytes32 private constant CONTRACT_EXRATES = "ExchangeRates"; bytes32 private constant CONTRACT_SYNTHETIXDEBTSHARE = "OikosDebtShare"; bytes32 private constant CONTRACT_FEEPOOL = "FeePool"; bytes32 private constant CONTRACT_DELEGATEAPPROVALS = "DelegateApprovals"; bytes32 private constant CONTRACT_REWARDESCROW_V2 = "RewardEscrowV2"; bytes32 private constant CONTRACT_SYNTHETIXESCROW = "OikosEscrow"; bytes32 private constant CONTRACT_LIQUIDATIONS = "Liquidations"; bytes32 private constant CONTRACT_DEBTCACHE = "DebtCache"; bytes32 private constant CONTRACT_SYNTHREDEEMER = "SynthRedeemer"; bytes32 private constant CONTRACT_SYSTEMSTATUS = "SystemStatus"; bytes32 private constant CONTRACT_EXT_AGGREGATOR_ISSUED_SYNTHS = "ext:AggregatorIssuedSynths"; bytes32 private constant CONTRACT_EXT_AGGREGATOR_DEBT_RATIO = "ext:AggregatorDebtRatio"; 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[](14); newAddresses[0] = CONTRACT_SYNTHETIX; newAddresses[1] = CONTRACT_EXCHANGER; newAddresses[2] = CONTRACT_EXRATES; newAddresses[3] = CONTRACT_SYNTHETIXDEBTSHARE; newAddresses[4] = CONTRACT_FEEPOOL; newAddresses[5] = CONTRACT_DELEGATEAPPROVALS; newAddresses[6] = CONTRACT_REWARDESCROW_V2; newAddresses[7] = CONTRACT_SYNTHETIXESCROW; newAddresses[8] = CONTRACT_LIQUIDATIONS; newAddresses[9] = CONTRACT_DEBTCACHE; newAddresses[10] = CONTRACT_SYNTHREDEEMER; newAddresses[11] = CONTRACT_SYSTEMSTATUS; newAddresses[12] = CONTRACT_EXT_AGGREGATOR_ISSUED_SYNTHS; newAddresses[13] = CONTRACT_EXT_AGGREGATOR_DEBT_RATIO; return combineArrays(existingAddresses, newAddresses); } function oikos() internal view returns (IOikos) { return IOikos(requireAndGetAddress(CONTRACT_SYNTHETIX)); } function exchanger() internal view returns (IExchanger) { return IExchanger(requireAndGetAddress(CONTRACT_EXCHANGER)); } function exchangeRates() internal view returns (IExchangeRates) { return IExchangeRates(requireAndGetAddress(CONTRACT_EXRATES)); } function oikosDebtShare() internal view returns (IOikosDebtShare) { return IOikosDebtShare(requireAndGetAddress(CONTRACT_SYNTHETIXDEBTSHARE)); } function feePool() internal view returns (IFeePool) { return IFeePool(requireAndGetAddress(CONTRACT_FEEPOOL)); } function liquidations() internal view returns (ILiquidations) { return ILiquidations(requireAndGetAddress(CONTRACT_LIQUIDATIONS)); } function delegateApprovals() internal view returns (IDelegateApprovals) { return IDelegateApprovals(requireAndGetAddress(CONTRACT_DELEGATEAPPROVALS)); } function rewardEscrowV2() internal view returns (IRewardEscrowV2) { return IRewardEscrowV2(requireAndGetAddress(CONTRACT_REWARDESCROW_V2)); } function oikosEscrow() internal view returns (IHasBalance) { return IHasBalance(requireAndGetAddress(CONTRACT_SYNTHETIXESCROW)); } function debtCache() internal view returns (IIssuerInternalDebtCache) { return IIssuerInternalDebtCache(requireAndGetAddress(CONTRACT_DEBTCACHE)); } function synthRedeemer() internal view returns (ISynthRedeemer) { return ISynthRedeemer(requireAndGetAddress(CONTRACT_SYNTHREDEEMER)); } function systemStatus() internal view returns (ISystemStatus) { return ISystemStatus(requireAndGetAddress(CONTRACT_SYSTEMSTATUS)); } function allNetworksDebtInfo() public view returns (uint256 debt, uint256 sharesSupply, bool isStale) { (, int256 rawIssuedSynths, , uint issuedSynthsUpdatedAt, ) = AggregatorV2V3Interface(requireAndGetAddress(CONTRACT_EXT_AGGREGATOR_ISSUED_SYNTHS)) .latestRoundData(); (, int256 rawRatio, , uint ratioUpdatedAt, ) = AggregatorV2V3Interface(requireAndGetAddress(CONTRACT_EXT_AGGREGATOR_DEBT_RATIO)) .latestRoundData(); debt = uint(rawIssuedSynths); sharesSupply = rawRatio == 0 ? 0 : debt.divideDecimalRoundPrecise(uint(rawRatio)); isStale = block.timestamp - getRateStalePeriod() > issuedSynthsUpdatedAt || block.timestamp - getRateStalePeriod() > ratioUpdatedAt; } function issuanceRatio() external view returns (uint) { return getIssuanceRatio(); } function _debtSharesToIssuedSynth( uint debtAmount, uint totalSystemValue, uint totalDebtShares ) internal pure returns (uint) { return debtAmount.multiplyDecimalRound(totalSystemValue).divideDecimalRound(totalDebtShares); } function _issuedSynthToDebtShares( uint sharesAmount, uint totalSystemValue, uint totalDebtShares ) internal pure returns (uint) { return sharesAmount.multiplyDecimalRound(totalDebtShares).divideDecimalRound(totalSystemValue); } function _availableCurrencyKeysWithOptionalOKS(bool withOKS) internal view returns (bytes32[] memory) { bytes32[] memory currencyKeys = new bytes32[](availableSynths.length + (withOKS ? 1 : 0)); for (uint i = 0; i < availableSynths.length; i++) { currencyKeys[i] = synthsByAddress[address(availableSynths[i])]; } if (withOKS) { currencyKeys[availableSynths.length] = OKS; } return currencyKeys; } // Returns the total value of the debt pool in currency specified by `currencyKey`. // To return only the OKS-backed debt, set `excludeCollateral` to true. function _totalIssuedSynths(bytes32 currencyKey, bool excludeCollateral) internal view returns (uint totalIssued, bool anyRateIsInvalid) { (uint debt, , bool cacheIsInvalid, bool cacheIsStale) = debtCache().cacheInfo(); anyRateIsInvalid = cacheIsInvalid || cacheIsStale; IExchangeRates exRates = exchangeRates(); // Add total issued synths from non oks collateral back into the total if not excluded if (!excludeCollateral) { (uint nonOksDebt, bool invalid) = debtCache().totalNonOksBackedDebt(); debt = debt.add(nonOksDebt); anyRateIsInvalid = anyRateIsInvalid || invalid; } if (currencyKey == oUSD) { return (debt, anyRateIsInvalid); } (uint currencyRate, bool currencyRateInvalid) = exRates.rateAndInvalid(currencyKey); return (debt.divideDecimalRound(currencyRate), anyRateIsInvalid || currencyRateInvalid); } function _debtBalanceOfAndTotalDebt(uint debtShareBalance, bytes32 currencyKey) internal view returns ( uint debtBalance, uint totalSystemValue, bool anyRateIsInvalid ) { // What's the total value of the system excluding ETH backed synths in their requested currency? (uint oksBackedAmount, uint debtSharesAmount, bool debtInfoStale) = allNetworksDebtInfo(); if (debtShareBalance == 0) { return (0, oksBackedAmount, debtInfoStale); } // existing functionality requires for us to convert into the exchange rate specified by `currencyKey` (uint currencyRate, bool currencyRateInvalid) = exchangeRates().rateAndInvalid(currencyKey); debtBalance = _debtSharesToIssuedSynth(debtShareBalance, oksBackedAmount, debtSharesAmount).divideDecimalRound(currencyRate); totalSystemValue = oksBackedAmount; anyRateIsInvalid = currencyRateInvalid || debtInfoStale; } function _canBurnSynths(address account) internal view returns (bool) { return now >= _lastIssueEvent(account).add(getMinimumStakeTime()); } function _lastIssueEvent(address account) internal view returns (uint) { // Get the timestamp of the last issue this account made return flexibleStorage().getUIntValue(CONTRACT_NAME, keccak256(abi.encodePacked(LAST_ISSUE_EVENT, account))); } function _remainingIssuableSynths(address _issuer) internal view returns ( uint maxIssuable, uint alreadyIssued, uint totalSystemDebt, bool anyRateIsInvalid ) { (alreadyIssued, totalSystemDebt, anyRateIsInvalid) = _debtBalanceOfAndTotalDebt( oikosDebtShare().balanceOf(_issuer), oUSD ); (uint issuable, bool isInvalid) = _maxIssuableSynths(_issuer); maxIssuable = issuable; anyRateIsInvalid = anyRateIsInvalid || isInvalid; if (alreadyIssued >= maxIssuable) { maxIssuable = 0; } else { maxIssuable = maxIssuable.sub(alreadyIssued); } } function _oksToUSD(uint amount, uint oksRate) internal pure returns (uint) { return amount.multiplyDecimalRound(oksRate); } function _usdToOks(uint amount, uint oksRate) internal pure returns (uint) { return amount.divideDecimalRound(oksRate); } function _maxIssuableSynths(address _issuer) internal view returns (uint, bool) { // What is the value of their OKS balance in oUSD (uint oksRate, bool isInvalid) = exchangeRates().rateAndInvalid(OKS); uint destinationValue = _oksToUSD(_collateral(_issuer), oksRate); // They're allowed to issue up to issuanceRatio of that value return (destinationValue.multiplyDecimal(getIssuanceRatio()), isInvalid); } function _collateralisationRatio(address _issuer) internal view returns (uint, bool) { uint totalOwnedOikos = _collateral(_issuer); (uint debtBalance, , bool anyRateIsInvalid) = _debtBalanceOfAndTotalDebt(oikosDebtShare().balanceOf(_issuer), OKS); // it's more gas intensive to put this check here if they have 0 OKS, but it complies with the interface if (totalOwnedOikos == 0) return (0, anyRateIsInvalid); return (debtBalance.divideDecimalRound(totalOwnedOikos), anyRateIsInvalid); } function _collateral(address account) internal view returns (uint) { uint balance = IERC20(address(oikos())).balanceOf(account); if (address(oikosEscrow()) != address(0)) { balance = balance.add(oikosEscrow().balanceOf(account)); } if (address(rewardEscrowV2()) != address(0)) { balance = balance.add(rewardEscrowV2().balanceOf(account)); } return balance; } function minimumStakeTime() external view returns (uint) { return getMinimumStakeTime(); } function canBurnSynths(address account) external view returns (bool) { return _canBurnSynths(account); } function availableCurrencyKeys() external view returns (bytes32[] memory) { return _availableCurrencyKeysWithOptionalOKS(false); } function availableSynthCount() external view returns (uint) { return availableSynths.length; } function anySynthOrOKSRateIsInvalid() external view returns (bool anyRateInvalid) { (, anyRateInvalid) = exchangeRates().ratesAndInvalidForCurrencies(_availableCurrencyKeysWithOptionalOKS(true)); } function totalIssuedSynths(bytes32 currencyKey, bool excludeOtherCollateral) external view returns (uint totalIssued) { (totalIssued, ) = _totalIssuedSynths(currencyKey, excludeOtherCollateral); } function lastIssueEvent(address account) external view returns (uint) { return _lastIssueEvent(account); } function collateralisationRatio(address _issuer) external view returns (uint cratio) { (cratio, ) = _collateralisationRatio(_issuer); } function collateralisationRatioAndAnyRatesInvalid(address _issuer) external view returns (uint cratio, bool anyRateIsInvalid) { return _collateralisationRatio(_issuer); } function collateral(address account) external view returns (uint) { return _collateral(account); } function debtBalanceOf(address _issuer, bytes32 currencyKey) external view returns (uint debtBalance) { IOikosDebtShare sds = oikosDebtShare(); // What was their initial debt ownership? uint debtShareBalance = sds.balanceOf(_issuer); // If it's zero, they haven't issued, and they have no debt. if (debtShareBalance == 0) return 0; (debtBalance, , ) = _debtBalanceOfAndTotalDebt(debtShareBalance, currencyKey); } function remainingIssuableSynths(address _issuer) external view returns ( uint maxIssuable, uint alreadyIssued, uint totalSystemDebt ) { (maxIssuable, alreadyIssued, totalSystemDebt, ) = _remainingIssuableSynths(_issuer); } function maxIssuableSynths(address _issuer) external view returns (uint) { (uint maxIssuable, ) = _maxIssuableSynths(_issuer); return maxIssuable; } function transferableOikosAndAnyRateIsInvalid(address account, uint balance) external view returns (uint transferable, bool anyRateIsInvalid) { // How many OKS do they have, excluding escrow? // Note: We're excluding escrow here because we're interested in their transferable amount // and escrowed OKS are not transferable. // How many of those will be locked by the amount they've issued? // Assuming issuance ratio is 20%, then issuing 20 OKS of value would require // 100 OKS to be locked in their wallet to maintain their collateralisation ratio // The locked oikos value can exceed their balance. uint debtBalance; (debtBalance, , anyRateIsInvalid) = _debtBalanceOfAndTotalDebt(oikosDebtShare().balanceOf(account), OKS); uint lockedOikosValue = debtBalance.divideDecimalRound(getIssuanceRatio()); // If we exceed the balance, no OKS are transferable, otherwise the difference is. if (lockedOikosValue >= balance) { transferable = 0; } else { transferable = balance.sub(lockedOikosValue); } } function getSynths(bytes32[] calldata currencyKeys) external view returns (ISynth[] memory) { uint numKeys = currencyKeys.length; ISynth[] memory addresses = new ISynth[](numKeys); for (uint i = 0; i < numKeys; i++) { addresses[i] = synths[currencyKeys[i]]; } return addresses; } /* ========== MUTATIVE FUNCTIONS ========== */ function _addSynth(ISynth synth) internal { bytes32 currencyKey = synth.currencyKey(); require(synths[currencyKey] == ISynth(0), "Synth exists"); require(synthsByAddress[address(synth)] == bytes32(0), "Synth address already exists"); availableSynths.push(synth); synths[currencyKey] = synth; synthsByAddress[address(synth)] = currencyKey; emit SynthAdded(currencyKey, address(synth)); } function addSynth(ISynth synth) external onlyOwner { _addSynth(synth); // Invalidate the cache to force a snapshot to be recomputed. If a synth were to be added // back to the system and it still somehow had cached debt, this would force the value to be // updated. debtCache().updateDebtCacheValidity(true); } function addSynths(ISynth[] calldata synthsToAdd) external onlyOwner { uint numSynths = synthsToAdd.length; for (uint i = 0; i < numSynths; i++) { _addSynth(synthsToAdd[i]); } // Invalidate the cache to force a snapshot to be recomputed. debtCache().updateDebtCacheValidity(true); } function _removeSynth(bytes32 currencyKey) internal { address synthToRemove = address(synths[currencyKey]); require(synthToRemove != address(0), "Synth does not exist"); require(currencyKey != oUSD, "Cannot remove synth"); uint synthSupply = IERC20(synthToRemove).totalSupply(); if (synthSupply > 0) { (uint amountOfoUSD, uint rateToRedeem, ) = exchangeRates().effectiveValueAndRates(currencyKey, synthSupply, "oUSD"); require(rateToRedeem > 0, "Cannot remove synth to redeem without rate"); ISynthRedeemer _synthRedeemer = synthRedeemer(); synths[oUSD].issue(address(_synthRedeemer), amountOfoUSD); // ensure the debt cache is aware of the new oUSD issued debtCache().updateCachedoUSDDebt(SafeCast.toInt256(amountOfoUSD)); _synthRedeemer.deprecate(IERC20(address(Proxyable(address(synthToRemove)).proxy())), rateToRedeem); } // Remove the synth from the availableSynths array. for (uint i = 0; i < availableSynths.length; i++) { if (address(availableSynths[i]) == synthToRemove) { delete availableSynths[i]; // Copy the last synth into the place of the one we just deleted // If there's only one synth, this is synths[0] = synths[0]. // If we're deleting the last one, it's also a NOOP in the same way. availableSynths[i] = availableSynths[availableSynths.length - 1]; // Decrease the size of the array by one. availableSynths.length--; break; } } // And remove it from the synths mapping delete synthsByAddress[synthToRemove]; delete synths[currencyKey]; emit SynthRemoved(currencyKey, synthToRemove); } function removeSynth(bytes32 currencyKey) external onlyOwner { // Remove its contribution from the debt pool snapshot, and // invalidate the cache to force a new snapshot. IIssuerInternalDebtCache cache = debtCache(); cache.updateCachedSynthDebtWithRate(currencyKey, 0); cache.updateDebtCacheValidity(true); _removeSynth(currencyKey); } function removeSynths(bytes32[] calldata currencyKeys) external onlyOwner { uint numKeys = currencyKeys.length; // Remove their contributions from the debt pool snapshot, and // invalidate the cache to force a new snapshot. IIssuerInternalDebtCache cache = debtCache(); uint[] memory zeroRates = new uint[](numKeys); cache.updateCachedSynthDebtsWithRates(currencyKeys, zeroRates); cache.updateDebtCacheValidity(true); for (uint i = 0; i < numKeys; i++) { _removeSynth(currencyKeys[i]); } } function issueSynths(address from, uint amount) external onlyOikos { require(amount > 0, "Issuer: cannot issue 0 synths"); _issueSynths(from, amount, false); } function issueMaxSynths(address from) external onlyOikos { _issueSynths(from, 0, true); } function issueSynthsOnBehalf( address issueForAddress, address from, uint amount ) external onlyOikos { _requireCanIssueOnBehalf(issueForAddress, from); _issueSynths(issueForAddress, amount, false); } function issueMaxSynthsOnBehalf(address issueForAddress, address from) external onlyOikos { _requireCanIssueOnBehalf(issueForAddress, from); _issueSynths(issueForAddress, 0, true); } function burnSynths(address from, uint amount) external onlyOikos { _voluntaryBurnSynths(from, amount, false); } function burnSynthsOnBehalf( address burnForAddress, address from, uint amount ) external onlyOikos { _requireCanBurnOnBehalf(burnForAddress, from); _voluntaryBurnSynths(burnForAddress, amount, false); } function burnSynthsToTarget(address from) external onlyOikos { _voluntaryBurnSynths(from, 0, true); } function burnSynthsToTargetOnBehalf(address burnForAddress, address from) external onlyOikos { _requireCanBurnOnBehalf(burnForAddress, from); _voluntaryBurnSynths(burnForAddress, 0, true); } function burnForRedemption( address deprecatedSynthProxy, address account, uint balance ) external onlySynthRedeemer { ISynth(IProxy(deprecatedSynthProxy).target()).burn(account, balance); } function liquidateDelinquentAccount( address account, uint susdAmount, address liquidator ) external onlyOikos returns (uint totalRedeemed, uint amountToLiquidate) { // Ensure waitingPeriod and oUSD balance is settled as burning impacts the size of debt pool require(!exchanger().hasWaitingPeriodOrSettlementOwing(liquidator, oUSD), "oUSD needs to be settled"); // Check account is liquidation open require(liquidations().isOpenForLiquidation(account), "Account not open for liquidation"); // require liquidator has enough oUSD require(IERC20(address(synths[oUSD])).balanceOf(liquidator) >= susdAmount, "Not enough oUSD"); uint liquidationPenalty = liquidations().liquidationPenalty(); // What is their debt in oUSD? (uint debtBalance, uint totalDebtIssued, bool anyRateIsInvalid) = _debtBalanceOfAndTotalDebt(oikosDebtShare().balanceOf(account), oUSD); (uint oksRate, bool oksRateInvalid) = exchangeRates().rateAndInvalid(OKS); _requireRatesNotInvalid(anyRateIsInvalid || oksRateInvalid); uint collateralForAccount = _collateral(account); uint amountToFixRatio = liquidations().calculateAmountToFixCollateral(debtBalance, _oksToUSD(collateralForAccount, oksRate)); // Cap amount to liquidate to repair collateral ratio based on issuance ratio amountToLiquidate = amountToFixRatio < susdAmount ? amountToFixRatio : susdAmount; // what's the equivalent amount of oks for the amountToLiquidate? uint oksRedeemed = _usdToOks(amountToLiquidate, oksRate); // Add penalty totalRedeemed = oksRedeemed.multiplyDecimal(SafeDecimalMath.unit().add(liquidationPenalty)); // if total OKS to redeem is greater than account's collateral // account is under collateralised, liquidate all collateral and reduce oUSD to burn if (totalRedeemed > collateralForAccount) { // set totalRedeemed to all transferable collateral totalRedeemed = collateralForAccount; // whats the equivalent oUSD to burn for all collateral less penalty amountToLiquidate = _oksToUSD( collateralForAccount.divideDecimal(SafeDecimalMath.unit().add(liquidationPenalty)), oksRate ); } // burn oUSD from messageSender (liquidator) and reduce account's debt _burnSynths(account, liquidator, amountToLiquidate, debtBalance, totalDebtIssued); // Remove liquidation flag if amount liquidated fixes ratio if (amountToLiquidate == amountToFixRatio) { // Remove liquidation liquidations().removeAccountInLiquidation(account); } } function setCurrentPeriodId(uint128 periodId) external { require(msg.sender == address(feePool()), "Must be fee pool"); IOikosDebtShare sds = oikosDebtShare(); if (sds.currentPeriodId() < periodId) { sds.takeSnapshot(periodId); } } function setLastDebtRatio(uint256 ratio) external onlyOwner { lastDebtRatio = ratio; } /* ========== INTERNAL FUNCTIONS ========== */ function _requireRatesNotInvalid(bool anyRateIsInvalid) internal pure { require(!anyRateIsInvalid, "A synth or OKS rate is invalid"); } function _requireCanIssueOnBehalf(address issueForAddress, address from) internal view { require(delegateApprovals().canIssueFor(issueForAddress, from), "Not approved to act on behalf"); } function _requireCanBurnOnBehalf(address burnForAddress, address from) internal view { require(delegateApprovals().canBurnFor(burnForAddress, from), "Not approved to act on behalf"); } function _issueSynths( address from, uint amount, bool issueMax ) internal { // check breaker if (!_verifyCircuitBreaker()) { return; } (uint maxIssuable, , uint totalSystemDebt, bool anyRateIsInvalid) = _remainingIssuableSynths(from); _requireRatesNotInvalid(anyRateIsInvalid); if (!issueMax) { require(amount <= maxIssuable, "Amount too large"); } else { amount = maxIssuable; } // Keep track of the debt they're about to create _addToDebtRegister(from, amount, totalSystemDebt); // record issue timestamp _setLastIssueEvent(from); // Create their synths synths[oUSD].issue(from, amount); // Account for the issued debt in the cache debtCache().updateCachedoUSDDebt(SafeCast.toInt256(amount)); } function _burnSynths( address debtAccount, address burnAccount, uint amount, uint existingDebt, uint totalDebtIssued ) internal returns (uint amountBurnt) { // check breaker if (!_verifyCircuitBreaker()) { return 0; } // liquidation requires oUSD to be already settled / not in waiting period // If they're trying to burn more debt than they actually owe, rather than fail the transaction, let's just // clear their debt and leave them be. amountBurnt = existingDebt < amount ? existingDebt : amount; // Remove liquidated debt from the ledger _removeFromDebtRegister(debtAccount, amountBurnt, existingDebt, totalDebtIssued); // synth.burn does a safe subtraction on balance (so it will revert if there are not enough synths). synths[oUSD].burn(burnAccount, amountBurnt); // Account for the burnt debt in the cache. debtCache().updateCachedoUSDDebt(-SafeCast.toInt256(amountBurnt)); } // If burning to target, `amount` is ignored, and the correct quantity of oUSD is burnt to reach the target // c-ratio, allowing fees to be claimed. In this case, pending settlements will be skipped as the user // will still have debt remaining after reaching their target. function _voluntaryBurnSynths( address from, uint amount, bool burnToTarget ) internal { // check breaker if (!_verifyCircuitBreaker()) { return; } if (!burnToTarget) { // If not burning to target, then burning requires that the minimum stake time has elapsed. require(_canBurnSynths(from), "Minimum stake time not reached"); // First settle anything pending into oUSD as burning or issuing impacts the size of the debt pool (, uint refunded, uint numEntriesSettled) = exchanger().settle(from, oUSD); if (numEntriesSettled > 0) { amount = exchanger().calculateAmountAfterSettlement(from, oUSD, amount, refunded); } } (uint existingDebt, uint totalSystemValue, bool anyRateIsInvalid) = _debtBalanceOfAndTotalDebt(oikosDebtShare().balanceOf(from), oUSD); (uint maxIssuableSynthsForAccount, bool oksRateInvalid) = _maxIssuableSynths(from); _requireRatesNotInvalid(anyRateIsInvalid || oksRateInvalid); require(existingDebt > 0, "No debt to forgive"); if (burnToTarget) { amount = existingDebt.sub(maxIssuableSynthsForAccount); } uint amountBurnt = _burnSynths(from, from, amount, existingDebt, totalSystemValue); // Check and remove liquidation if existingDebt after burning is <= maxIssuableSynths // Issuance ratio is fixed so should remove any liquidations if (existingDebt.sub(amountBurnt) <= maxIssuableSynthsForAccount) { liquidations().removeAccountInLiquidation(from); } } function _setLastIssueEvent(address account) internal { // Set the timestamp of the last issueSynths flexibleStorage().setUIntValue( CONTRACT_NAME, keccak256(abi.encodePacked(LAST_ISSUE_EVENT, account)), block.timestamp ); } function _addToDebtRegister( address from, uint amount, uint totalDebtIssued ) internal { IOikosDebtShare sds = oikosDebtShare(); // it is possible (eg in tests, system initialized with extra debt) to have issued debt without any shares issued // in which case, the first account to mint gets the debt. yw. if (sds.totalSupply() == 0) { sds.mintShare(from, amount); } else { sds.mintShare(from, _issuedSynthToDebtShares(amount, totalDebtIssued, sds.totalSupply())); } } function _removeFromDebtRegister( address from, uint debtToRemove, uint existingDebt, uint totalDebtIssued ) internal { IOikosDebtShare sds = oikosDebtShare(); uint currentDebtShare = sds.balanceOf(from); if (debtToRemove == existingDebt) { sds.burnShare(from, currentDebtShare); } else { uint balanceToRemove = _issuedSynthToDebtShares(debtToRemove, totalDebtIssued, sds.totalSupply()); sds.burnShare(from, balanceToRemove < currentDebtShare ? balanceToRemove : currentDebtShare); } } function _verifyCircuitBreaker() internal returns (bool) { (, int256 rawRatio, , uint ratioUpdatedAt, ) = AggregatorV2V3Interface(requireAndGetAddress(CONTRACT_EXT_AGGREGATOR_DEBT_RATIO)) .latestRoundData(); uint deviation = _calculateDeviation(lastDebtRatio, uint(rawRatio)); if (deviation >= getPriceDeviationThresholdFactor()) { systemStatus().suspendIssuance(CIRCUIT_BREAKER_SUSPENSION_REASON); return false; } lastDebtRatio = uint(rawRatio); return true; } function _calculateDeviation( uint last, uint fresh ) internal pure returns (uint deviation) { if (last == 0) { deviation = 1; } else if (fresh == 0) { deviation = uint(-1); } else if (last > fresh) { deviation = last.divideDecimal(fresh); } else { deviation = fresh.divideDecimal(last); } } /* ========== MODIFIERS ========== */ function _onlyOikos() internal view { require(msg.sender == address(oikos()), "Issuer: Only the oikos contract can perform this action"); } modifier onlyOikos() { _onlyOikos(); // Use an internal function to save code size. _; } function _onlySynthRedeemer() internal view { require(msg.sender == address(synthRedeemer()), "Issuer: Only the SynthRedeemer contract can perform this action"); } modifier onlySynthRedeemer() { _onlySynthRedeemer(); _; } /* ========== EVENTS ========== */ event SynthAdded(bytes32 currencyKey, address synth); event SynthRemoved(bytes32 currencyKey, address synth); }