diff --git a/contracts/FuturesMarket.sol b/contracts/FuturesMarket.sol index 5223f03d2d..6f17b5f6d0 100644 --- a/contracts/FuturesMarket.sol +++ b/contracts/FuturesMarket.sol @@ -13,6 +13,7 @@ import "./SignedSafeDecimalMath.sol"; // Internal references import "./interfaces/IExchangeRates.sol"; +import "./interfaces/ISystemStatus.sol"; import "./interfaces/IERC20.sol"; /* @@ -139,6 +140,7 @@ contract FuturesMarket is Owned, Proxyable, MixinFuturesMarketSettings, IFutures bytes32 internal constant CONTRACT_EXRATES = "ExchangeRates"; bytes32 internal constant CONTRACT_FUTURESMARKETMANAGER = "FuturesMarketManager"; bytes32 internal constant CONTRACT_FUTURESMARKETSETTINGS = "FuturesMarketSettings"; + bytes32 internal constant CONTRACT_SYSTEMSTATUS = "SystemStatus"; /* ========== CONSTRUCTOR ========== */ @@ -172,10 +174,11 @@ contract FuturesMarket is Owned, Proxyable, MixinFuturesMarketSettings, IFutures function resolverAddressesRequired() public view returns (bytes32[] memory addresses) { bytes32[] memory existingAddresses = MixinFuturesMarketSettings.resolverAddressesRequired(); - bytes32[] memory newAddresses = new bytes32[](3); + bytes32[] memory newAddresses = new bytes32[](4); newAddresses[0] = CONTRACT_EXRATES; newAddresses[1] = CONTRACT_FUTURESMARKETMANAGER; newAddresses[2] = CONTRACT_FUTURESMARKETSETTINGS; + newAddresses[3] = CONTRACT_SYSTEMSTATUS; addresses = combineArrays(existingAddresses, newAddresses); } @@ -183,6 +186,10 @@ contract FuturesMarket is Owned, Proxyable, MixinFuturesMarketSettings, IFutures return IExchangeRates(requireAndGetAddress(CONTRACT_EXRATES)); } + function systemStatus() internal view returns (ISystemStatus) { + return ISystemStatus(requireAndGetAddress(CONTRACT_SYSTEMSTATUS)); + } + function _manager() internal view returns (IFuturesMarketManagerInternal) { return IFuturesMarketManagerInternal(requireAndGetAddress(CONTRACT_FUTURESMARKETMANAGER)); } @@ -195,14 +202,18 @@ contract FuturesMarket is Owned, Proxyable, MixinFuturesMarketSettings, IFutures function _assetPrice(IExchangeRates exchangeRates) internal view returns (uint price, bool invalid) { (uint _price, bool _invalid) = exchangeRates.rateAndInvalid(baseAsset); - // Ensure we catch uninitialised rates - return (_price, _invalid || _price == 0); + // Ensure we catch uninitialised rates or suspended state / synth + _invalid = _invalid || _price == 0 || systemStatus().synthSuspended(baseAsset); + return (_price, _invalid); } /* - * The current base price, reverting if it is invalid. + * The current base price, reverting if it is invalid, or if system or synth is suspended. */ - function _assetPriceRequireNotInvalid() internal view returns (uint) { + function _assetPriceRequireChecks() internal view returns (uint) { + // check that synth is active, and wasn't suspended, revert with appropriate message + systemStatus().requireSynthActive(baseAsset); + // check if price is invalid (uint price, bool invalid) = _assetPrice(_exchangeRates()); _revertIfError(invalid, Status.InvalidPrice); return price; @@ -920,7 +931,7 @@ contract FuturesMarket is Owned, Proxyable, MixinFuturesMarketSettings, IFutures */ function recomputeFunding() external returns (uint lastIndex) { _revertIfError(msg.sender != _settings(), Status.NotPermitted); - return _recomputeFunding(_assetPriceRequireNotInvalid()); + return _recomputeFunding(_assetPriceRequireChecks()); } /* @@ -1026,7 +1037,7 @@ contract FuturesMarket is Owned, Proxyable, MixinFuturesMarketSettings, IFutures * Reverts on withdrawal if the amount to be withdrawn would expose an open position to liquidation. */ function transferMargin(int marginDelta) external optionalProxy { - uint price = _assetPriceRequireNotInvalid(); + uint price = _assetPriceRequireChecks(); uint fundingIndex = _recomputeFunding(price); _transferMargin(marginDelta, price, fundingIndex, messageSender); } @@ -1037,7 +1048,7 @@ contract FuturesMarket is Owned, Proxyable, MixinFuturesMarketSettings, IFutures */ function withdrawAllMargin() external optionalProxy { address sender = messageSender; - uint price = _assetPriceRequireNotInvalid(); + uint price = _assetPriceRequireChecks(); uint fundingIndex = _recomputeFunding(price); int marginDelta = -int(_accessibleMargin(positions[sender], fundingIndex, price)); _transferMargin(marginDelta, price, fundingIndex, sender); @@ -1102,7 +1113,7 @@ contract FuturesMarket is Owned, Proxyable, MixinFuturesMarketSettings, IFutures * Reverts if the resulting position is too large, outside the max leverage, or is liquidating. */ function modifyPosition(int sizeDelta) external optionalProxy { - uint price = _assetPriceRequireNotInvalid(); + uint price = _assetPriceRequireChecks(); uint fundingIndex = _recomputeFunding(price); _modifyPosition(sizeDelta, price, fundingIndex, messageSender); } @@ -1126,7 +1137,7 @@ contract FuturesMarket is Owned, Proxyable, MixinFuturesMarketSettings, IFutures uint minPrice, uint maxPrice ) external optionalProxy { - uint price = _assetPriceRequireNotInvalid(); + uint price = _assetPriceRequireChecks(); _revertIfPriceOutsideBounds(price, minPrice, maxPrice); uint fundingIndex = _recomputeFunding(price); _modifyPosition(sizeDelta, price, fundingIndex, messageSender); @@ -1138,7 +1149,7 @@ contract FuturesMarket is Owned, Proxyable, MixinFuturesMarketSettings, IFutures function closePosition() external optionalProxy { int size = positions[messageSender].size; _revertIfError(size == 0, Status.NoPositionOpen); - uint price = _assetPriceRequireNotInvalid(); + uint price = _assetPriceRequireChecks(); _modifyPosition(-size, price, _recomputeFunding(price), messageSender); } @@ -1148,7 +1159,7 @@ contract FuturesMarket is Owned, Proxyable, MixinFuturesMarketSettings, IFutures function closePositionWithPriceBounds(uint minPrice, uint maxPrice) external optionalProxy { int size = positions[messageSender].size; _revertIfError(size == 0, Status.NoPositionOpen); - uint price = _assetPriceRequireNotInvalid(); + uint price = _assetPriceRequireChecks(); _revertIfPriceOutsideBounds(price, minPrice, maxPrice); _modifyPosition(-size, price, _recomputeFunding(price), messageSender); } @@ -1193,7 +1204,7 @@ contract FuturesMarket is Owned, Proxyable, MixinFuturesMarketSettings, IFutures * Upon liquidation, the position will be closed, and the liquidation fee minted into the liquidator's account. */ function liquidatePosition(address account) external optionalProxy { - uint price = _assetPriceRequireNotInvalid(); + uint price = _assetPriceRequireChecks(); uint fundingIndex = _recomputeFunding(price); uint liquidationFee = _liquidationFee(); diff --git a/contracts/SystemStatus.sol b/contracts/SystemStatus.sol index a9c65d1253..40dc267fbf 100644 --- a/contracts/SystemStatus.sol +++ b/contracts/SystemStatus.sol @@ -33,6 +33,10 @@ contract SystemStatus is Owned, ISystemStatus { _internalRequireSystemActive(); } + function systemSuspended() external view returns (bool) { + return systemSuspension.suspended; + } + function requireIssuanceActive() external view { // Issuance requires the system be active _internalRequireSystemActive(); @@ -55,6 +59,10 @@ contract SystemStatus is Owned, ISystemStatus { _internalRequireSynthExchangeActive(currencyKey); } + function synthSuspended(bytes32 currencyKey) external view returns (bool) { + return systemSuspension.suspended || synthSuspension[currencyKey].suspended; + } + function requireSynthActive(bytes32 currencyKey) external view { // Synth exchange and transfer requires the system be active _internalRequireSystemActive(); diff --git a/contracts/interfaces/ISystemStatus.sol b/contracts/interfaces/ISystemStatus.sol index 737a19bcd2..5a02d96db3 100644 --- a/contracts/interfaces/ISystemStatus.sol +++ b/contracts/interfaces/ISystemStatus.sol @@ -19,6 +19,8 @@ interface ISystemStatus { function requireSystemActive() external view; + function systemSuspended() external view returns (bool); + function requireIssuanceActive() external view; function requireExchangeActive() external view; @@ -27,6 +29,8 @@ interface ISystemStatus { function requireSynthActive(bytes32 currencyKey) external view; + function synthSuspended(bytes32 currencyKey) external view returns (bool); + function requireSynthsActive(bytes32 sourceCurrencyKey, bytes32 destinationCurrencyKey) external view; function systemSuspension() external view returns (bool suspended, uint248 reason); diff --git a/publish/releases.json b/publish/releases.json index 2cd419f721..f9e0d86995 100644 --- a/publish/releases.json +++ b/publish/releases.json @@ -61,7 +61,7 @@ }, { "sip": 80, - "layer": "ovm", + "layer": "kovan-ovm", "released": false, "sources": [ "FuturesMarketBTC", diff --git a/test/contracts/FuturesMarket.js b/test/contracts/FuturesMarket.js index f797fc0d7a..80dfde0e82 100644 --- a/test/contracts/FuturesMarket.js +++ b/test/contracts/FuturesMarket.js @@ -44,7 +44,8 @@ contract('FuturesMarket', accounts => { sUSD, synthetix, feePool, - debtCache; + debtCache, + systemStatus; const owner = accounts[1]; const trader = accounts[2]; @@ -105,6 +106,7 @@ contract('FuturesMarket', accounts => { Synthetix: synthetix, FeePool: feePool, DebtCache: debtCache, + SystemStatus: systemStatus, } = await setupAllContracts({ accounts, synths: ['sUSD'], @@ -132,6 +134,15 @@ contract('FuturesMarket', accounts => { for (const t of [trader, trader2, trader3]) { await sUSD.issue(t, traderInitialBalance); } + + // allow ownder to suspend system or synths + await systemStatus.updateAccessControls( + [toBytes32('System'), toBytes32('Synth')], + [owner, owner], + [true, true], + [true, true], + { from: owner } + ); }); addSnapshotBeforeRestoreAfterEach(); @@ -970,6 +981,51 @@ contract('FuturesMarket', accounts => { }); }); + it('Reverts if the price is invalid', async () => { + await futuresMarket.transferMargin(toUnit('1000'), { from: trader }); + await fastForward(7 * 24 * 60 * 60); + await assert.revert( + futuresMarket.transferMargin(toUnit('-1000'), { from: trader }), + 'Invalid price' + ); + }); + + it('Reverts if the system is suspended', async () => { + await futuresMarket.transferMargin(toUnit('1000'), { from: trader }); + + // suspend + await systemStatus.suspendSystem('3', { from: owner }); + // should revert + await assert.revert( + futuresMarket.transferMargin(toUnit('-1000'), { from: trader }), + 'Synthetix is suspended' + ); + + // resume + await systemStatus.resumeSystem({ from: owner }); + // should work now + await futuresMarket.transferMargin(toUnit('-1000'), { from: trader }); + assert.bnClose((await futuresMarket.accessibleMargin(trader))[0], toBN('0'), toUnit('0.1')); + }); + + it('Reverts if the synth is suspended', async () => { + await futuresMarket.transferMargin(toUnit('1000'), { from: trader }); + + // suspend + await systemStatus.suspendSynth(baseAsset, 65, { from: owner }); + // should revert + await assert.revert( + futuresMarket.transferMargin(toUnit('-1000'), { from: trader }), + 'Synth is suspended' + ); + + // resume + await systemStatus.resumeSynth(baseAsset, { from: owner }); + // should work now + await futuresMarket.transferMargin(toUnit('-1000'), { from: trader }); + assert.bnClose((await futuresMarket.accessibleMargin(trader))[0], toBN('0'), toUnit('0.1')); + }); + describe('No position', async () => { it('New margin', async () => { assert.bnEqual((await futuresMarket.positions(trader)).margin, toBN(0)); @@ -1092,6 +1148,54 @@ contract('FuturesMarket', accounts => { await assert.revert(futuresMarket.modifyPosition(size, { from: trader }), 'Invalid price'); }); + it('Cannot modify a position if the system is suspended', async () => { + const margin = toUnit('1000'); + await futuresMarket.transferMargin(margin, { from: trader }); + const size = toUnit('10'); + const price = toUnit('200'); + await setPrice(baseAsset, price); + + // suspend + await systemStatus.suspendSystem('3', { from: owner }); + // should revert modifying position + await assert.revert( + futuresMarket.modifyPosition(size, { from: trader }), + 'Synthetix is suspended' + ); + + // resume + await systemStatus.resumeSystem({ from: owner }); + // should work now + await futuresMarket.modifyPosition(size, { from: trader }); + const position = await futuresMarket.positions(trader); + assert.bnEqual(position.size, size); + assert.bnEqual(position.lastPrice, price); + }); + + it('Cannot modify a position if the synth is suspended', async () => { + const margin = toUnit('1000'); + await futuresMarket.transferMargin(margin, { from: trader }); + const size = toUnit('10'); + const price = toUnit('200'); + await setPrice(baseAsset, price); + + // suspend + await systemStatus.suspendSynth(baseAsset, 65, { from: owner }); + // should revert modifying position + await assert.revert( + futuresMarket.modifyPosition(size, { from: trader }), + 'Synth is suspended' + ); + + // resume + await systemStatus.resumeSynth(baseAsset, { from: owner }); + // should work now + await futuresMarket.modifyPosition(size, { from: trader }); + const position = await futuresMarket.positions(trader); + assert.bnEqual(position.size, size); + assert.bnEqual(position.lastPrice, price); + }); + it('Empty orders fail', async () => { const margin = toUnit('1000'); await futuresMarket.transferMargin(margin, { from: trader }); @@ -2214,6 +2318,50 @@ contract('FuturesMarket', accounts => { await assert.revert(futuresMarket.withdrawAllMargin({ from: trader }), 'Invalid price'); }); + it('Reverts if the system is suspended', async () => { + await futuresMarket.transferMargin(toUnit('1000'), { from: trader }); + + // suspend + await systemStatus.suspendSystem('3', { from: owner }); + // should revert + await assert.revert( + futuresMarket.withdrawAllMargin({ from: trader }), + 'Synthetix is suspended' + ); + + // resume + await systemStatus.resumeSystem({ from: owner }); + // should work now + await futuresMarket.withdrawAllMargin({ from: trader }); + assert.bnClose( + (await futuresMarket.accessibleMargin(trader))[0], + toBN('0'), + toUnit('0.1') + ); + }); + + it('Reverts if the synth is suspended', async () => { + await futuresMarket.transferMargin(toUnit('1000'), { from: trader }); + + // suspend + await systemStatus.suspendSynth(baseAsset, 65, { from: owner }); + // should revert + await assert.revert( + futuresMarket.withdrawAllMargin({ from: trader }), + 'Synth is suspended' + ); + + // resume + await systemStatus.resumeSynth(baseAsset, { from: owner }); + // should work now + await futuresMarket.withdrawAllMargin({ from: trader }); + assert.bnClose( + (await futuresMarket.accessibleMargin(trader))[0], + toBN('0'), + toUnit('0.1') + ); + }); + it('allows users to withdraw all their margin', async () => { await futuresMarket.transferMargin(toUnit('1000'), { from: trader }); await futuresMarket.transferMargin(toUnit('3000'), { from: trader2 }); @@ -3088,6 +3236,40 @@ contract('FuturesMarket', accounts => { await fastForward(60 * 60 * 24 * 7); // Stale the price assert.isFalse(await futuresMarket.canLiquidate(trader)); }); + + it('No liquidations while the system is suspended', async () => { + await setPrice(baseAsset, toUnit('250')); + await futuresMarket.transferMargin(toUnit('1000'), { from: trader }); + await futuresMarket.modifyPosition(toUnit('20'), { from: trader }); + await setPrice(baseAsset, toUnit('25')); + assert.isTrue(await futuresMarket.canLiquidate(trader)); + + // suspend + await systemStatus.suspendSystem('3', { from: owner }); + assert.isFalse(await futuresMarket.canLiquidate(trader)); + + // resume + await systemStatus.resumeSystem({ from: owner }); + // should work now + assert.isTrue(await futuresMarket.canLiquidate(trader)); + }); + + it('No liquidations while the synth is suspended', async () => { + await setPrice(baseAsset, toUnit('250')); + await futuresMarket.transferMargin(toUnit('1000'), { from: trader }); + await futuresMarket.modifyPosition(toUnit('20'), { from: trader }); + await setPrice(baseAsset, toUnit('25')); + assert.isTrue(await futuresMarket.canLiquidate(trader)); + + // suspend + await systemStatus.suspendSynth(baseAsset, 65, { from: owner }); + assert.isFalse(await futuresMarket.canLiquidate(trader)); + + // resume + await systemStatus.resumeSynth(baseAsset, { from: owner }); + // should work now + assert.isTrue(await futuresMarket.canLiquidate(trader)); + }); }); describe('liquidatePosition', () => { diff --git a/test/contracts/SystemStatus.js b/test/contracts/SystemStatus.js index 270c0be909..94614f37c6 100644 --- a/test/contracts/SystemStatus.js +++ b/test/contracts/SystemStatus.js @@ -103,6 +103,12 @@ contract('SystemStatus', async accounts => { await systemStatus.requireSynthsActive(toBytes32('sBTC'), toBytes32('sETH')); }); + it('and all the bool views are correct', async () => { + assert.isFalse(await systemStatus.systemSuspended()); + assert.isFalse(await systemStatus.synthSuspended(toBytes32('sETH'))); + assert.isFalse(await systemStatus.synthSuspended(toBytes32('sBTC'))); + }); + it('can only be invoked by the owner initially', async () => { await onlyGivenAddressCanInvoke({ fnc: systemStatus.suspendSystem, @@ -145,6 +151,12 @@ contract('SystemStatus', async accounts => { reason ); }); + + it('and all the bool views are correct', async () => { + assert.isTrue(await systemStatus.systemSuspended()); + assert.isTrue(await systemStatus.synthSuspended(toBytes32('sETH'))); + assert.isTrue(await systemStatus.synthSuspended(toBytes32('sBTC'))); + }); }); describe('when the owner adds an address to suspend only', () => { @@ -1023,7 +1035,13 @@ contract('SystemStatus', async accounts => { 'Synth is suspended. Operation prohibited' ); }); - it('but not the others', async () => { + it('and the synth bool view is as expected', async () => { + assert.isTrue(await systemStatus.synthSuspended(sBTC)); + }); + it('but not other synth bool view', async () => { + assert.isFalse(await systemStatus.synthSuspended(toBytes32('sETH'))); + }); + it('but others do not revert', async () => { await systemStatus.requireSystemActive(); await systemStatus.requireIssuanceActive(); });