pragma solidity ^0.5.16; // Inheritance import "./Owned.sol"; import "./MixinResolver.sol"; import "./MixinSystemSettings.sol"; import "./interfaces/IIssuer.sol"; // Libraries import "./SafeDecimalMath.sol"; // Internal references import "./interfaces/ISynth.sol"; import "./interfaces/ISynthetix.sol"; import "./interfaces/IFeePool.sol"; import "./interfaces/ISynthetixState.sol"; import "./interfaces/IExchanger.sol"; import "./interfaces/IDelegateApprovals.sol"; import "./interfaces/IExchangeRates.sol"; import "./interfaces/IEtherCollateral.sol"; import "./interfaces/IEtherCollateralsUSD.sol"; import "./interfaces/IHasBalance.sol"; import "./interfaces/IERC20.sol"; import "./interfaces/ILiquidations.sol"; import "./interfaces/ICollateralManager.sol"; import "./interfaces/IDebtCache.sol"; interface IRewardEscrowV2 { // Views function balanceOf(address account) external view returns (uint); } interface IIssuerInternalDebtCache { function updateCachedSynthDebtWithRate(bytes32 currencyKey, uint currencyRate) external; function updateCachedSynthDebtsWithRates(bytes32[] calldata currencyKeys, uint[] calldata currencyRates) external; function updateDebtCacheValidity(bool currentlyInvalid) external; function cacheInfo() external view returns ( uint cachedDebt, uint timestamp, bool isInvalid, bool isStale ); } // https://docs.synthetix.io/contracts/source/contracts/issuer contract Issuer is Owned, MixinSystemSettings, IIssuer { using SafeMath for uint; using SafeDecimalMath for uint; // Available Synths which can be used with the system ISynth[] public availableSynths; mapping(bytes32 => ISynth) public synths; mapping(address => bytes32) public synthsByAddress; /* ========== ENCODED NAMES ========== */ bytes32 internal constant sUSD = "sUSD"; bytes32 internal constant sETH = "sETH"; bytes32 internal constant SNX = "SNX"; // Flexible storage names bytes32 public constant CONTRACT_NAME = "Issuer"; bytes32 internal constant LAST_ISSUE_EVENT = "lastIssueEvent"; /* ========== ADDRESS RESOLVER CONFIGURATION ========== */ bytes32 private constant CONTRACT_SYNTHETIX = "Synthetix"; bytes32 private constant CONTRACT_EXCHANGER = "Exchanger"; bytes32 private constant CONTRACT_EXRATES = "ExchangeRates"; bytes32 private constant CONTRACT_SYNTHETIXSTATE = "SynthetixState"; bytes32 private constant CONTRACT_FEEPOOL = "FeePool"; bytes32 private constant CONTRACT_DELEGATEAPPROVALS = "DelegateApprovals"; bytes32 private constant CONTRACT_ETHERCOLLATERAL = "EtherCollateral"; bytes32 private constant CONTRACT_ETHERCOLLATERAL_SUSD = "EtherCollateralsUSD"; bytes32 private constant CONTRACT_COLLATERALMANAGER = "CollateralManager"; bytes32 private constant CONTRACT_REWARDESCROW_V2 = "RewardEscrowV2"; bytes32 private constant CONTRACT_SYNTHETIXESCROW = "SynthetixEscrow"; bytes32 private constant CONTRACT_LIQUIDATIONS = "Liquidations"; bytes32 private constant CONTRACT_DEBTCACHE = "DebtCache"; 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[](13); newAddresses[0] = CONTRACT_SYNTHETIX; newAddresses[1] = CONTRACT_EXCHANGER; newAddresses[2] = CONTRACT_EXRATES; newAddresses[3] = CONTRACT_SYNTHETIXSTATE; newAddresses[4] = CONTRACT_FEEPOOL; newAddresses[5] = CONTRACT_DELEGATEAPPROVALS; newAddresses[6] = CONTRACT_ETHERCOLLATERAL; newAddresses[7] = CONTRACT_ETHERCOLLATERAL_SUSD; newAddresses[8] = CONTRACT_REWARDESCROW_V2; newAddresses[9] = CONTRACT_SYNTHETIXESCROW; newAddresses[10] = CONTRACT_LIQUIDATIONS; newAddresses[11] = CONTRACT_DEBTCACHE; newAddresses[12] = CONTRACT_COLLATERALMANAGER; return combineArrays(existingAddresses, newAddresses); } function synthetix() internal view returns (ISynthetix) { return ISynthetix(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 synthetixState() internal view returns (ISynthetixState) { return ISynthetixState(requireAndGetAddress(CONTRACT_SYNTHETIXSTATE)); } 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 etherCollateral() internal view returns (IEtherCollateral) { return IEtherCollateral(requireAndGetAddress(CONTRACT_ETHERCOLLATERAL)); } function etherCollateralsUSD() internal view returns (IEtherCollateralsUSD) { return IEtherCollateralsUSD(requireAndGetAddress(CONTRACT_ETHERCOLLATERAL_SUSD)); } function collateralManager() internal view returns (ICollateralManager) { return ICollateralManager(requireAndGetAddress(CONTRACT_COLLATERALMANAGER)); } function rewardEscrowV2() internal view returns (IRewardEscrowV2) { return IRewardEscrowV2(requireAndGetAddress(CONTRACT_REWARDESCROW_V2)); } function synthetixEscrow() internal view returns (IHasBalance) { return IHasBalance(requireAndGetAddress(CONTRACT_SYNTHETIXESCROW)); } function debtCache() internal view returns (IIssuerInternalDebtCache) { return IIssuerInternalDebtCache(requireAndGetAddress(CONTRACT_DEBTCACHE)); } function issuanceRatio() external view returns (uint) { return getIssuanceRatio(); } function _availableCurrencyKeysWithOptionalSNX(bool withSNX) internal view returns (bytes32[] memory) { bytes32[] memory currencyKeys = new bytes32[](availableSynths.length + (withSNX ? 1 : 0)); for (uint i = 0; i < availableSynths.length; i++) { currencyKeys[i] = synthsByAddress[address(availableSynths[i])]; } if (withSNX) { currencyKeys[availableSynths.length] = SNX; } return currencyKeys; } 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 snx collateral back into the total if not excluded if (!excludeCollateral) { // Get the sUSD equivalent amount of all the MC issued synths. (uint nonSnxDebt, bool invalid) = collateralManager().totalLong(); debt = debt.add(nonSnxDebt); anyRateIsInvalid = anyRateIsInvalid || invalid; // Now add the ether collateral stuff as we are still supporting it. debt = debt.add(etherCollateralsUSD().totalIssuedSynths()); // Add ether collateral sETH (uint ethRate, bool ethRateInvalid) = exRates.rateAndInvalid(sETH); uint ethIssuedDebt = etherCollateral().totalIssuedSynths().multiplyDecimalRound(ethRate); debt = debt.add(ethIssuedDebt); anyRateIsInvalid = anyRateIsInvalid || ethRateInvalid; } if (currencyKey == sUSD) { return (debt, anyRateIsInvalid); } (uint currencyRate, bool currencyRateInvalid) = exRates.rateAndInvalid(currencyKey); return (debt.divideDecimalRound(currencyRate), anyRateIsInvalid || currencyRateInvalid); } function _debtBalanceOfAndTotalDebt(address _issuer, bytes32 currencyKey) internal view returns ( uint debtBalance, uint totalSystemValue, bool anyRateIsInvalid ) { ISynthetixState state = synthetixState(); // What was their initial debt ownership? (uint initialDebtOwnership, uint debtEntryIndex) = state.issuanceData(_issuer); // What's the total value of the system excluding ETH backed synths in their requested currency? (totalSystemValue, anyRateIsInvalid) = _totalIssuedSynths(currencyKey, true); // If it's zero, they haven't issued, and they have no debt. // Note: it's more gas intensive to put this check here rather than before _totalIssuedSynths // if they have 0 SNX, but it's a necessary trade-off if (initialDebtOwnership == 0) return (0, totalSystemValue, anyRateIsInvalid); // Figure out the global debt percentage delta from when they entered the system. // This is a high precision integer of 27 (1e27) decimals. uint currentDebtOwnership = state .lastDebtLedgerEntry() .divideDecimalRoundPrecise(state.debtLedger(debtEntryIndex)) .multiplyDecimalRoundPrecise(initialDebtOwnership); // Their debt balance is their portion of the total system value. uint highPrecisionBalance = totalSystemValue.decimalToPreciseDecimal().multiplyDecimalRoundPrecise( currentDebtOwnership ); // Convert back into 18 decimals (1e18) debtBalance = highPrecisionBalance.preciseDecimalToDecimal(); } 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(_issuer, sUSD); (uint issuable, bool isInvalid) = _maxIssuableSynths(_issuer); maxIssuable = issuable; anyRateIsInvalid = anyRateIsInvalid || isInvalid; if (alreadyIssued >= maxIssuable) { maxIssuable = 0; } else { maxIssuable = maxIssuable.sub(alreadyIssued); } } function _snxToUSD(uint amount, uint snxRate) internal pure returns (uint) { return amount.multiplyDecimalRound(snxRate); } function _usdToSnx(uint amount, uint snxRate) internal pure returns (uint) { return amount.divideDecimalRound(snxRate); } function _maxIssuableSynths(address _issuer) internal view returns (uint, bool) { // What is the value of their SNX balance in sUSD (uint snxRate, bool isInvalid) = exchangeRates().rateAndInvalid(SNX); uint destinationValue = _snxToUSD(_collateral(_issuer), snxRate); // 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 totalOwnedSynthetix = _collateral(_issuer); (uint debtBalance, , bool anyRateIsInvalid) = _debtBalanceOfAndTotalDebt(_issuer, SNX); // it's more gas intensive to put this check here if they have 0 SNX, but it complies with the interface if (totalOwnedSynthetix == 0) return (0, anyRateIsInvalid); return (debtBalance.divideDecimalRound(totalOwnedSynthetix), anyRateIsInvalid); } function _collateral(address account) internal view returns (uint) { uint balance = IERC20(address(synthetix())).balanceOf(account); if (address(synthetixEscrow()) != address(0)) { balance = balance.add(synthetixEscrow().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 _availableCurrencyKeysWithOptionalSNX(false); } function availableSynthCount() external view returns (uint) { return availableSynths.length; } function anySynthOrSNXRateIsInvalid() external view returns (bool anyRateInvalid) { (, anyRateInvalid) = exchangeRates().ratesAndInvalidForCurrencies(_availableCurrencyKeysWithOptionalSNX(true)); } function totalIssuedSynths(bytes32 currencyKey, bool excludeEtherCollateral) external view returns (uint totalIssued) { (totalIssued, ) = _totalIssuedSynths(currencyKey, excludeEtherCollateral); } 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) { ISynthetixState state = synthetixState(); // What was their initial debt ownership? (uint initialDebtOwnership, ) = state.issuanceData(_issuer); // If it's zero, they haven't issued, and they have no debt. if (initialDebtOwnership == 0) return 0; (debtBalance, , ) = _debtBalanceOfAndTotalDebt(_issuer, 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 transferableSynthetixAndAnyRateIsInvalid(address account, uint balance) external view returns (uint transferable, bool anyRateIsInvalid) { // How many SNX do they have, excluding escrow? // Note: We're excluding escrow here because we're interested in their transferable amount // and escrowed SNX are not transferable. // How many of those will be locked by the amount they've issued? // Assuming issuance ratio is 20%, then issuing 20 SNX of value would require // 100 SNX to be locked in their wallet to maintain their collateralisation ratio // The locked synthetix value can exceed their balance. uint debtBalance; (debtBalance, , anyRateIsInvalid) = _debtBalanceOfAndTotalDebt(account, SNX); uint lockedSynthetixValue = debtBalance.divideDecimalRound(getIssuanceRatio()); // If we exceed the balance, no SNX are transferable, otherwise the difference is. if (lockedSynthetixValue >= balance) { transferable = 0; } else { transferable = balance.sub(lockedSynthetixValue); } } 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(IERC20(synthToRemove).totalSupply() == 0, "Synth supply exists"); require(currencyKey != sUSD, "Cannot remove synth"); // 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 onlySynthetix { _issueSynths(from, amount, false); } function issueMaxSynths(address from) external onlySynthetix { _issueSynths(from, 0, true); } function issueSynthsOnBehalf( address issueForAddress, address from, uint amount ) external onlySynthetix { _requireCanIssueOnBehalf(issueForAddress, from); _issueSynths(issueForAddress, amount, false); } function issueMaxSynthsOnBehalf(address issueForAddress, address from) external onlySynthetix { _requireCanIssueOnBehalf(issueForAddress, from); _issueSynths(issueForAddress, 0, true); } function burnSynths(address from, uint amount) external onlySynthetix { _voluntaryBurnSynths(from, amount, false); } function burnSynthsOnBehalf( address burnForAddress, address from, uint amount ) external onlySynthetix { _requireCanBurnOnBehalf(burnForAddress, from); _voluntaryBurnSynths(burnForAddress, amount, false); } function burnSynthsToTarget(address from) external onlySynthetix { _voluntaryBurnSynths(from, 0, true); } function burnSynthsToTargetOnBehalf(address burnForAddress, address from) external onlySynthetix { _requireCanBurnOnBehalf(burnForAddress, from); _voluntaryBurnSynths(burnForAddress, 0, true); } function liquidateDelinquentAccount( address account, uint susdAmount, address liquidator ) external onlySynthetix returns (uint totalRedeemed, uint amountToLiquidate) { // Ensure waitingPeriod and sUSD balance is settled as burning impacts the size of debt pool require(!exchanger().hasWaitingPeriodOrSettlementOwing(liquidator, sUSD), "sUSD needs to be settled"); // Check account is liquidation open require(liquidations().isOpenForLiquidation(account), "Account not open for liquidation"); // require liquidator has enough sUSD require(IERC20(address(synths[sUSD])).balanceOf(liquidator) >= susdAmount, "Not enough sUSD"); uint liquidationPenalty = liquidations().liquidationPenalty(); // What is their debt in sUSD? (uint debtBalance, uint totalDebtIssued, bool anyRateIsInvalid) = _debtBalanceOfAndTotalDebt(account, sUSD); (uint snxRate, bool snxRateInvalid) = exchangeRates().rateAndInvalid(SNX); _requireRatesNotInvalid(anyRateIsInvalid || snxRateInvalid); uint collateralForAccount = _collateral(account); uint amountToFixRatio = liquidations().calculateAmountToFixCollateral( debtBalance, _snxToUSD(collateralForAccount, snxRate) ); // Cap amount to liquidate to repair collateral ratio based on issuance ratio amountToLiquidate = amountToFixRatio < susdAmount ? amountToFixRatio : susdAmount; // what's the equivalent amount of snx for the amountToLiquidate? uint snxRedeemed = _usdToSnx(amountToLiquidate, snxRate); // Add penalty totalRedeemed = snxRedeemed.multiplyDecimal(SafeDecimalMath.unit().add(liquidationPenalty)); // if total SNX to redeem is greater than account's collateral // account is under collateralised, liquidate all collateral and reduce sUSD to burn if (totalRedeemed > collateralForAccount) { // set totalRedeemed to all transferable collateral totalRedeemed = collateralForAccount; // whats the equivalent sUSD to burn for all collateral less penalty amountToLiquidate = _snxToUSD( collateralForAccount.divideDecimal(SafeDecimalMath.unit().add(liquidationPenalty)), snxRate ); } // burn sUSD 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); } } /* ========== INTERNAL FUNCTIONS ========== */ function _requireRatesNotInvalid(bool anyRateIsInvalid) internal pure { require(!anyRateIsInvalid, "A synth or SNX 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 { (uint maxIssuable, uint existingDebt, 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, existingDebt, totalSystemDebt); // record issue timestamp _setLastIssueEvent(from); // Create their synths synths[sUSD].issue(from, amount); // Account for the issued debt in the cache debtCache().updateCachedSynthDebtWithRate(sUSD, SafeDecimalMath.unit()); // Store their locked SNX amount to determine their fee % for the period _appendAccountIssuanceRecord(from); } function _burnSynths( address debtAccount, address burnAccount, uint amount, uint existingDebt, uint totalDebtIssued ) internal returns (uint amountBurnt) { // liquidation requires sUSD 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[sUSD].burn(burnAccount, amountBurnt); // Account for the burnt debt in the cache. debtCache().updateCachedSynthDebtWithRate(sUSD, SafeDecimalMath.unit()); // Store their debtRatio against a fee period to determine their fee/rewards % for the period _appendAccountIssuanceRecord(debtAccount); } // If burning to target, `amount` is ignored, and the correct quantity of sUSD 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 { 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 sUSD as burning or issuing impacts the size of the debt pool (, uint refunded, uint numEntriesSettled) = exchanger().settle(from, sUSD); if (numEntriesSettled > 0) { amount = exchanger().calculateAmountAfterSettlement(from, sUSD, amount, refunded); } } (uint existingDebt, uint totalSystemValue, bool anyRateIsInvalid) = _debtBalanceOfAndTotalDebt(from, sUSD); (uint maxIssuableSynthsForAccount, bool snxRateInvalid) = _maxIssuableSynths(from); _requireRatesNotInvalid(anyRateIsInvalid || snxRateInvalid); 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 _appendAccountIssuanceRecord(address from) internal { uint initialDebtOwnership; uint debtEntryIndex; (initialDebtOwnership, debtEntryIndex) = synthetixState().issuanceData(from); feePool().appendAccountIssuanceRecord(from, initialDebtOwnership, debtEntryIndex); } function _addToDebtRegister( address from, uint amount, uint existingDebt, uint totalDebtIssued ) internal { ISynthetixState state = synthetixState(); // What will the new total be including the new value? uint newTotalDebtIssued = amount.add(totalDebtIssued); // What is their percentage (as a high precision int) of the total debt? uint debtPercentage = amount.divideDecimalRoundPrecise(newTotalDebtIssued); // And what effect does this percentage change have on the global debt holding of other issuers? // The delta specifically needs to not take into account any existing debt as it's already // accounted for in the delta from when they issued previously. // The delta is a high precision integer. uint delta = SafeDecimalMath.preciseUnit().sub(debtPercentage); // And what does their debt ownership look like including this previous stake? if (existingDebt > 0) { debtPercentage = amount.add(existingDebt).divideDecimalRoundPrecise(newTotalDebtIssued); } else { // If they have no debt, they're a new issuer; record this. state.incrementTotalIssuerCount(); } // Save the debt entry parameters state.setCurrentIssuanceData(from, debtPercentage); // And if we're the first, push 1 as there was no effect to any other holders, otherwise push // the change for the rest of the debt holders. The debt ledger holds high precision integers. if (state.debtLedgerLength() > 0) { state.appendDebtLedgerValue(state.lastDebtLedgerEntry().multiplyDecimalRoundPrecise(delta)); } else { state.appendDebtLedgerValue(SafeDecimalMath.preciseUnit()); } } function _removeFromDebtRegister( address from, uint debtToRemove, uint existingDebt, uint totalDebtIssued ) internal { ISynthetixState state = synthetixState(); // What will the new total after taking out the withdrawn amount uint newTotalDebtIssued = totalDebtIssued.sub(debtToRemove); uint delta = 0; // What will the debt delta be if there is any debt left? // Set delta to 0 if no more debt left in system after user if (newTotalDebtIssued > 0) { // What is the percentage of the withdrawn debt (as a high precision int) of the total debt after? uint debtPercentage = debtToRemove.divideDecimalRoundPrecise(newTotalDebtIssued); // And what effect does this percentage change have on the global debt holding of other issuers? // The delta specifically needs to not take into account any existing debt as it's already // accounted for in the delta from when they issued previously. delta = SafeDecimalMath.preciseUnit().add(debtPercentage); } // Are they exiting the system, or are they just decreasing their debt position? if (debtToRemove == existingDebt) { state.setCurrentIssuanceData(from, 0); state.decrementTotalIssuerCount(); } else { // What percentage of the debt will they be left with? uint newDebt = existingDebt.sub(debtToRemove); uint newDebtPercentage = newDebt.divideDecimalRoundPrecise(newTotalDebtIssued); // Store the debt percentage and debt ledger as high precision integers state.setCurrentIssuanceData(from, newDebtPercentage); } // Update our cumulative ledger. This is also a high precision integer. state.appendDebtLedgerValue(state.lastDebtLedgerEntry().multiplyDecimalRoundPrecise(delta)); } /* ========== MODIFIERS ========== */ function _onlySynthetix() internal view { require(msg.sender == address(synthetix()), "Issuer: Only the synthetix contract can perform this action"); } modifier onlySynthetix() { _onlySynthetix(); // Use an internal function to save code size. _; } /* ========== EVENTS ========== */ event SynthAdded(bytes32 currencyKey, address synth); event SynthRemoved(bytes32 currencyKey, address synth); }