diff --git a/contracts/core/extensions/CoreAccounting.sol b/contracts/core/extensions/CoreAccounting.sol index 2e80ed73c..695dbb2dc 100644 --- a/contracts/core/extensions/CoreAccounting.sol +++ b/contracts/core/extensions/CoreAccounting.sol @@ -21,6 +21,7 @@ import { SafeMath } from "openzeppelin-solidity/contracts/math/SafeMath.sol"; import { CoreState } from "../lib/CoreState.sol"; import { ITransferProxy } from "../interfaces/ITransferProxy.sol"; import { IVault } from "../interfaces/IVault.sol"; +import { CoreOperationState } from "./CoreOperationState.sol"; /** @@ -32,6 +33,7 @@ import { IVault } from "../interfaces/IVault.sol"; */ contract CoreAccounting is CoreState, + CoreOperationState, ReentrancyGuard { // Use SafeMath library for all uint256 arithmetic @@ -51,6 +53,7 @@ contract CoreAccounting is ) external nonReentrant + whenOperational { // Call internal deposit function depositInternal( @@ -96,6 +99,7 @@ contract CoreAccounting is ) external nonReentrant + whenOperational { // Call internal batch deposit function batchDepositInternal( @@ -144,6 +148,7 @@ contract CoreAccounting is ) external nonReentrant + whenOperational { IVault(state.vault).transferBalance( _token, diff --git a/contracts/core/extensions/CoreIssuance.sol b/contracts/core/extensions/CoreIssuance.sol index 695fdf81d..f8c2e740c 100644 --- a/contracts/core/extensions/CoreIssuance.sol +++ b/contracts/core/extensions/CoreIssuance.sol @@ -22,6 +22,7 @@ import { CoreState } from "../lib/CoreState.sol"; import { ISetToken } from "../interfaces/ISetToken.sol"; import { ITransferProxy } from "../interfaces/ITransferProxy.sol"; import { IVault } from "../interfaces/IVault.sol"; +import { CoreOperationState } from "./CoreOperationState.sol"; /** @@ -32,6 +33,7 @@ import { IVault } from "../interfaces/IVault.sol"; */ contract CoreIssuance is CoreState, + CoreOperationState, ReentrancyGuard { // Use SafeMath library for all uint256 arithmetic @@ -59,6 +61,7 @@ contract CoreIssuance is ) external nonReentrant + whenOperational { issueInternal( msg.sender, diff --git a/contracts/core/extensions/CoreOperationState.sol b/contracts/core/extensions/CoreOperationState.sol new file mode 100644 index 000000000..096c4a8bf --- /dev/null +++ b/contracts/core/extensions/CoreOperationState.sol @@ -0,0 +1,95 @@ +/* + Copyright 2018 Set Labs Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +pragma solidity 0.4.25; + +import { Ownable } from "openzeppelin-solidity/contracts/ownership/Ownable.sol"; +import { CoreState } from "../lib/CoreState.sol"; + + +/** + * @title Core Operation State + * @author Set Protocol + * + * The CoreOperationState contract contains methods to alter state of variables that track + * Core dependency addresses. + */ +contract CoreOperationState is + Ownable, + CoreState +{ + + /* ============ Enum ============ */ + + /** + * Operational: + * All Accounting and Issuance related functions are available for usage during this stage + * + * Shut Down: + * Only functions which allow users to redeem and withdraw funds are allowed during this stage + */ + enum OperationState { + Operational, + ShutDown, + InvalidState + } + + /* ============ Events ============ */ + + event OperationStateChanged( + uint8 _prevState, + uint8 _newState + ); + + /* ============ Modifiers ============ */ + + modifier whenOperational() { + require( + state.operationState == uint8(OperationState.Operational), + "CoreOperationalState.whenOperational: Function is in non-operational state." + ); + _; + } + + /* ============ External Functions ============ */ + + /** + * Updates the operation state of the protocol. + * Can only be called by owner of Core. + * + * @param _operationState Uint8 representing the current protocol operation state + */ + function setOperationState( + uint8 _operationState + ) + external + onlyOwner + { + require( + _operationState < uint8(OperationState.InvalidState), + "CoreOperationalState.setOperationalState: Input is not a valid operation state" + ); + + emit OperationStateChanged( + state.operationState, + _operationState + ); + + state.operationState = _operationState; + } + + +} diff --git a/contracts/core/interfaces/ICoreAccounting.sol b/contracts/core/interfaces/ICoreAccounting.sol index 36cc0eb66..af9d6d155 100644 --- a/contracts/core/interfaces/ICoreAccounting.sol +++ b/contracts/core/interfaces/ICoreAccounting.sol @@ -18,10 +18,10 @@ pragma solidity 0.4.25; /** - * @title ICoreIssuance + * @title ICoreAccounting * @author Set Protocol * - * The ICoreIssuance Contract defines all the functions exposed in the CoreIssuance + * The ICoreAccounting Contract defines all the functions exposed in the CoreIssuance * extension. */ contract ICoreAccounting { diff --git a/contracts/core/lib/CoreState.sol b/contracts/core/lib/CoreState.sol index 8f3a99250..d91ebe838 100644 --- a/contracts/core/lib/CoreState.sol +++ b/contracts/core/lib/CoreState.sol @@ -29,6 +29,9 @@ contract CoreState { /* ============ Structs ============ */ struct State { + // Protocol state of operation + uint8 operationState; + // Mapping of exchange enumeration to address mapping(uint8 => address) exchanges; @@ -72,6 +75,19 @@ contract CoreState { /* ============ Public Getters ============ */ + /** + * Return uint8 representing the operational state of the protocol + * + * @return uint8 Uint8 representing the operational state of the protocol + */ + function operationState() + public + view + returns(uint8) + { + return state.operationState; + } + /** * Return address belonging to given exchangeId. * diff --git a/test/contracts/core/extensions/coreAccounting.spec.ts b/test/contracts/core/extensions/coreAccounting.spec.ts index 89d3afe1b..4dc820831 100644 --- a/test/contracts/core/extensions/coreAccounting.spec.ts +++ b/test/contracts/core/extensions/coreAccounting.spec.ts @@ -26,6 +26,7 @@ import { DEPLOYED_TOKEN_QUANTITY, UNLIMITED_ALLOWANCE_IN_BASE_UNITS, ZERO, + ONE, } from '@utils/constants'; import { ERC20Wrapper } from '@utils/erc20Wrapper'; import { getWeb3 } from '@utils/web3Helper'; @@ -159,6 +160,19 @@ contract('CoreAccounting', accounts => { await expectRevertError(subject()); }); }); + + describe('when the protocol is not in operational state', async () => { + beforeEach(async () => { + await coreWrapper.setOperationStateAsync( + core, + ONE, + ); + }); + + it('should revert', async () => { + await expectRevertError(subject()); + }); + }); }); describe('#withdraw', async () => { @@ -459,6 +473,19 @@ contract('CoreAccounting', accounts => { expect(newOwnerBalance).to.be.bignumber.equal(existingOwnerVaultBalance.add(DEPLOYED_TOKEN_QUANTITY)); }); }); + + describe('when the protocol is not in operational state', async () => { + beforeEach(async () => { + await coreWrapper.setOperationStateAsync( + core, + ONE, + ); + }); + + it('should revert', async () => { + await expectRevertError(subject()); + }); + }); }); describe('#batchWithdraw', async () => { @@ -661,5 +688,18 @@ contract('CoreAccounting', accounts => { await expectRevertError(subject()); }); }); + + describe('when the protocol is not in operational state', async () => { + beforeEach(async () => { + await coreWrapper.setOperationStateAsync( + core, + ONE, + ); + }); + + it('should revert', async () => { + await expectRevertError(subject()); + }); + }); }); }); diff --git a/test/contracts/core/extensions/coreIssuance.spec.ts b/test/contracts/core/extensions/coreIssuance.spec.ts index d5bf2551a..f4eda9138 100644 --- a/test/contracts/core/extensions/coreIssuance.spec.ts +++ b/test/contracts/core/extensions/coreIssuance.spec.ts @@ -28,6 +28,7 @@ import { DEFAULT_GAS, DEPLOYED_TOKEN_QUANTITY, ZERO, + ONE, DEFAULT_UNIT_SHARES, DEFAULT_REBALANCING_NATURAL_UNIT, ONE_DAY_IN_SECONDS @@ -359,6 +360,19 @@ contract('CoreIssuance', accounts => { await assertTokenBalanceAsync(setToken, existingBalance.add(subjectQuantityToIssue), ownerAccount); }); }); + + describe('when the protocol is not in operational state', async () => { + beforeEach(async () => { + await coreWrapper.setOperationStateAsync( + core, + ONE, + ); + }); + + it('should revert', async () => { + await expectRevertError(subject()); + }); + }); }); describe('#issue: RebalancingToken', async () => { diff --git a/test/contracts/core/extensions/coreOperationState.spec.ts b/test/contracts/core/extensions/coreOperationState.spec.ts new file mode 100644 index 000000000..5256d5bf5 --- /dev/null +++ b/test/contracts/core/extensions/coreOperationState.spec.ts @@ -0,0 +1,122 @@ +require('module-alias/register'); + +import * as ABIDecoder from 'abi-decoder'; +import * as chai from 'chai'; +import * as setProtocolUtils from 'set-protocol-utils'; +import { BigNumber } from 'bignumber.js'; +import { Address, Log } from 'set-protocol-utils'; + +import ChaiSetup from '@utils/chaiSetup'; +import { BigNumberSetup } from '@utils/bigNumberSetup'; +import { CoreContract } from '@utils/contracts'; +import { expectRevertError } from '@utils/tokenAssertions'; +import { Blockchain } from '@utils/blockchain'; +import { OperationStateChanged } from '@utils/contract_logs/core'; +import { ZERO, ONE } from '@utils/constants'; +import { CoreWrapper } from '@utils/coreWrapper'; +import { getWeb3 } from '@utils/web3Helper'; + +BigNumberSetup.configure(); +ChaiSetup.configure(); +const web3 = getWeb3(); +const Core = artifacts.require('Core'); +const { SetProtocolTestUtils: SetTestUtils } = setProtocolUtils; +const setTestUtils = new SetTestUtils(web3); +const { expect } = chai; +const blockchain = new Blockchain(web3); + +contract('CoreOperationState', accounts => { + const [ + ownerAccount, + notOwnerAccount, + ] = accounts; + + let core: CoreContract; + + const coreWrapper = new CoreWrapper(ownerAccount, ownerAccount); + + before(async () => { + ABIDecoder.addABI(Core.abi); + }); + + after(async () => { + ABIDecoder.removeABI(Core.abi); + }); + + beforeEach(async () => { + await blockchain.saveSnapshotAsync(); + + core = await coreWrapper.deployCoreAndDependenciesAsync(); + }); + + afterEach(async () => { + await blockchain.revertAsync(); + }); + + describe('#setOperationState', async () => { + let subjectOperationState: BigNumber = ONE; + let subjectCaller: Address = ownerAccount; + + async function subject(): Promise { + return core.setOperationState.sendTransactionAsync( + subjectOperationState, + { from: subjectCaller }, + ); + } + + it('sets the operation state correctly', async () => { + await subject(); + + const currentOperationState = await core.operationState.callAsync(); + expect(currentOperationState).to.bignumber.equal(subjectOperationState); + }); + + it('emits a OperationStateChanged event', async () => { + const txHash = await subject(); + const logs = await setTestUtils.getLogsFromTxHash(txHash); + + const expectedLogs: Log[] = [ + OperationStateChanged( + core.address, + ZERO, + subjectOperationState, + ), + ]; + + await SetTestUtils.assertLogEquivalence(logs, expectedLogs); + }); + + describe('when the operation state input is Zero', async () => { + beforeEach(async () => { + subjectOperationState = ZERO; + }); + + it('sets the operation state correctly', async () => { + await subject(); + + const currentOperationState = await core.operationState.callAsync(); + expect(currentOperationState).to.bignumber.equal(subjectOperationState); + }); + }); + + describe('when the input operation state is the invalid state', async () => { + beforeEach(async () => { + subjectOperationState = new BigNumber(2); + }); + + it('should revert', async () => { + await expectRevertError(subject()); + }); + }); + + describe('when the caller is not the owner', async () => { + beforeEach(async () => { + subjectCaller = notOwnerAccount; + }); + + it('should revert', async () => { + await expectRevertError(subject()); + }); + }); + }); +}); diff --git a/utils/contract_logs/core.ts b/utils/contract_logs/core.ts index 360e11342..00e73ad71 100644 --- a/utils/contract_logs/core.ts +++ b/utils/contract_logs/core.ts @@ -129,6 +129,21 @@ export function ProtocolFeeChanged( }; } +export function OperationStateChanged( + _coreAddress: Address, + _prevState: BigNumber, + _newState: BigNumber, +): Log { + return { + event: 'OperationStateChanged', + address: _coreAddress, + args: { + _prevState, + _newState, + }, + }; +} + export function extractNewSetTokenAddressFromLogs( logs: Log[], ): Address { diff --git a/utils/coreWrapper.ts b/utils/coreWrapper.ts index 262e62c70..b42e9d9bf 100644 --- a/utils/coreWrapper.ts +++ b/utils/coreWrapper.ts @@ -474,6 +474,24 @@ export class CoreWrapper { ); } + /* ============ CoreOperationState Extension ============ */ + + /** + * OperationStates + * 0 = Operational + * 1 = Shut Down + */ + public async setOperationStateAsync( + core: CoreLikeContract, + operationState: BigNumber, + from: Address = this._tokenOwnerAddress, + ) { + await core.setOperationState.sendTransactionAsync( + operationState, + { from } + ); + } + /* ============ CoreExchangeDispatcher Extension ============ */ public async registerDefaultExchanges(