diff --git a/contracts/staking/CHANGELOG.json b/contracts/staking/CHANGELOG.json index 58226795f7..cd30ad7573 100644 --- a/contracts/staking/CHANGELOG.json +++ b/contracts/staking/CHANGELOG.json @@ -25,6 +25,10 @@ { "note": "Fix overflow w/ `LibFixedMath._mul(-1, -2*255)", "pr": 2311 + }, + { + "note": "Unit tests for MixinScheduler", + "pr": 2314 } ] }, diff --git a/contracts/staking/contracts/test/TestMixinScheduler.sol b/contracts/staking/contracts/test/TestMixinScheduler.sol new file mode 100644 index 0000000000..ac5d545be3 --- /dev/null +++ b/contracts/staking/contracts/test/TestMixinScheduler.sol @@ -0,0 +1,96 @@ +/* + + Copyright 2019 ZeroEx Intl. + + 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.5.9; +pragma experimental ABIEncoderV2; + +import "./TestStaking.sol"; + + +contract TestMixinScheduler is + TestStaking +{ + uint256 public testDeployedTimestamp; + + event GoToNextEpochTestInfo( + uint256 oldEpoch, + uint256 blockTimestamp + ); + + constructor( + address wethAddress, + address zrxVaultAddress + ) + public + TestStaking( + wethAddress, + zrxVaultAddress + ) + { + _addAuthorizedAddress(msg.sender); + init(); + _removeAuthorizedAddressAtIndex(msg.sender, 0); + + // Record time of deployment + // solhint-disable-next-line not-rely-on-time + testDeployedTimestamp = block.timestamp; + } + + /// @dev Tests `_goToNextEpoch`. + /// Configures internal variables such taht `epochEndTime` will be + /// less-than, equal-to, or greater-than the block timestamp. + /// @param epochEndTimeDelta Set to desired `epochEndTime - block.timestamp` + function goToNextEpochTest(int256 epochEndTimeDelta) + public + { + // solhint-disable-next-line not-rely-on-time + uint256 blockTimestamp = block.timestamp; + + // Emit info used by client-side test code + emit GoToNextEpochTestInfo( + currentEpoch, + blockTimestamp + ); + + // (i) In `_goToNextEpoch` we compute: + // `epochEndTime = currentEpochStartTimeInSeconds + epochDurationInSeconds` + // (ii) We want adjust internal state such that: + // `epochEndTime - block.timestamp = epochEndTimeDelta`, or + // `currentEpochStartTimeInSeconds + epochDurationInSeconds - block.timestamp = epochEndTimeDelta` + // + // To do this, we: + // (i) Set `epochDurationInSeconds` to a constant value of 1, and + // (ii) Rearrange the eqn above to get: + // `currentEpochStartTimeInSeconds = epochEndTimeDelta + block.timestamp - epochDurationInSeconds` + epochDurationInSeconds = 1; + currentEpochStartTimeInSeconds = + uint256(epochEndTimeDelta + int256(blockTimestamp) - int256(epochDurationInSeconds)); + + // Test internal function + _goToNextEpoch(); + } + + /// @dev Tests `_initMixinScheduler` + /// @param _currentEpochStartTimeInSeconds Sets `currentEpochStartTimeInSeconds` to this value before test. + function initMixinSchedulerTest(uint256 _currentEpochStartTimeInSeconds) + public + { + currentEpochStartTimeInSeconds = _currentEpochStartTimeInSeconds; + _initMixinScheduler(); + } +} \ No newline at end of file diff --git a/contracts/staking/package.json b/contracts/staking/package.json index dc92524bfe..86c3f5e1fc 100644 --- a/contracts/staking/package.json +++ b/contracts/staking/package.json @@ -37,7 +37,7 @@ }, "config": { "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.", - "abis": "./generated-artifacts/@(IStaking|IStakingEvents|IStakingProxy|IStorage|IStorageInit|IStructs|IZrxVault|LibCobbDouglas|LibFixedMath|LibFixedMathRichErrors|LibSafeDowncast|LibStakingRichErrors|MixinAbstract|MixinConstants|MixinCumulativeRewards|MixinDeploymentConstants|MixinExchangeFees|MixinExchangeManager|MixinFinalizer|MixinParams|MixinScheduler|MixinStake|MixinStakeBalances|MixinStakeStorage|MixinStakingPool|MixinStakingPoolRewards|MixinStorage|Staking|StakingProxy|TestAssertStorageParams|TestCobbDouglas|TestCumulativeRewardTracking|TestDelegatorRewards|TestExchangeManager|TestFinalizer|TestInitTarget|TestLibFixedMath|TestLibSafeDowncast|TestMixinParams|TestMixinStake|TestMixinStakeBalances|TestMixinStakeStorage|TestMixinStakingPool|TestMixinStakingPoolRewards|TestProtocolFees|TestProxyDestination|TestStaking|TestStakingNoWETH|TestStakingProxy|TestStakingProxyUnit|TestStorageLayoutAndConstants|ZrxVault).json" + "abis": "./generated-artifacts/@(IStaking|IStakingEvents|IStakingProxy|IStorage|IStorageInit|IStructs|IZrxVault|LibCobbDouglas|LibFixedMath|LibFixedMathRichErrors|LibSafeDowncast|LibStakingRichErrors|MixinAbstract|MixinConstants|MixinCumulativeRewards|MixinDeploymentConstants|MixinExchangeFees|MixinExchangeManager|MixinFinalizer|MixinParams|MixinScheduler|MixinStake|MixinStakeBalances|MixinStakeStorage|MixinStakingPool|MixinStakingPoolRewards|MixinStorage|Staking|StakingProxy|TestAssertStorageParams|TestCobbDouglas|TestCumulativeRewardTracking|TestDelegatorRewards|TestExchangeManager|TestFinalizer|TestInitTarget|TestLibFixedMath|TestLibSafeDowncast|TestMixinParams|TestMixinScheduler|TestMixinStake|TestMixinStakeBalances|TestMixinStakeStorage|TestMixinStakingPool|TestMixinStakingPoolRewards|TestProtocolFees|TestProxyDestination|TestStaking|TestStakingNoWETH|TestStakingProxy|TestStakingProxyUnit|TestStorageLayoutAndConstants|ZrxVault).json" }, "repository": { "type": "git", diff --git a/contracts/staking/src/artifacts.ts b/contracts/staking/src/artifacts.ts index b9bbf14441..27cdc0604e 100644 --- a/contracts/staking/src/artifacts.ts +++ b/contracts/staking/src/artifacts.ts @@ -44,6 +44,7 @@ import * as TestInitTarget from '../generated-artifacts/TestInitTarget.json'; import * as TestLibFixedMath from '../generated-artifacts/TestLibFixedMath.json'; import * as TestLibSafeDowncast from '../generated-artifacts/TestLibSafeDowncast.json'; import * as TestMixinParams from '../generated-artifacts/TestMixinParams.json'; +import * as TestMixinScheduler from '../generated-artifacts/TestMixinScheduler.json'; import * as TestMixinStake from '../generated-artifacts/TestMixinStake.json'; import * as TestMixinStakeBalances from '../generated-artifacts/TestMixinStakeBalances.json'; import * as TestMixinStakeStorage from '../generated-artifacts/TestMixinStakeStorage.json'; @@ -98,6 +99,7 @@ export const artifacts = { TestLibFixedMath: TestLibFixedMath as ContractArtifact, TestLibSafeDowncast: TestLibSafeDowncast as ContractArtifact, TestMixinParams: TestMixinParams as ContractArtifact, + TestMixinScheduler: TestMixinScheduler as ContractArtifact, TestMixinStake: TestMixinStake as ContractArtifact, TestMixinStakeBalances: TestMixinStakeBalances as ContractArtifact, TestMixinStakeStorage: TestMixinStakeStorage as ContractArtifact, diff --git a/contracts/staking/src/wrappers.ts b/contracts/staking/src/wrappers.ts index f532e731c3..ff10601900 100644 --- a/contracts/staking/src/wrappers.ts +++ b/contracts/staking/src/wrappers.ts @@ -42,6 +42,7 @@ export * from '../generated-wrappers/test_init_target'; export * from '../generated-wrappers/test_lib_fixed_math'; export * from '../generated-wrappers/test_lib_safe_downcast'; export * from '../generated-wrappers/test_mixin_params'; +export * from '../generated-wrappers/test_mixin_scheduler'; export * from '../generated-wrappers/test_mixin_stake'; export * from '../generated-wrappers/test_mixin_stake_balances'; export * from '../generated-wrappers/test_mixin_stake_storage'; diff --git a/contracts/staking/test/unit_tests/mixin_scheduler_test.ts b/contracts/staking/test/unit_tests/mixin_scheduler_test.ts new file mode 100644 index 0000000000..9838c00c71 --- /dev/null +++ b/contracts/staking/test/unit_tests/mixin_scheduler_test.ts @@ -0,0 +1,120 @@ +import { blockchainTests, constants, expect, verifyEventsFromLogs } from '@0x/contracts-test-utils'; +import { StakingRevertErrors } from '@0x/order-utils'; +import { BigNumber } from '@0x/utils'; +import { LogWithDecodedArgs } from 'ethereum-types'; + +import { + artifacts, + TestMixinSchedulerContract, + TestMixinSchedulerEvents, + TestMixinSchedulerGoToNextEpochTestInfoEventArgs, +} from '../../src'; + +import { constants as stakingConstants } from '../utils/constants'; + +blockchainTests.resets('MixinScheduler unit tests', env => { + let testContract: TestMixinSchedulerContract; + + before(async () => { + // Deploy contracts + testContract = await TestMixinSchedulerContract.deployFrom0xArtifactAsync( + artifacts.TestMixinScheduler, + env.provider, + env.txDefaults, + artifacts, + stakingConstants.NIL_ADDRESS, + stakingConstants.NIL_ADDRESS, + ); + }); + + describe('getCurrentEpochEarliestEndTimeInSeconds', () => { + it('Should return the sum of `epoch start time + epoch duration`', async () => { + const testDeployedTimestamp = await testContract.testDeployedTimestamp.callAsync(); + const epochDurationInSeconds = await testContract.epochDurationInSeconds.callAsync(); + const expectedCurrentEpochEarliestEndTimeInSeconds = testDeployedTimestamp.plus(epochDurationInSeconds); + const currentEpochEarliestEndTimeInSeconds = await testContract.getCurrentEpochEarliestEndTimeInSeconds.callAsync(); + expect(currentEpochEarliestEndTimeInSeconds).to.bignumber.equal( + expectedCurrentEpochEarliestEndTimeInSeconds, + ); + }); + }); + + describe('_initMixinScheduler', () => { + it('Should succeed if scheduler is not yet initialized (`currentEpochStartTimeInSeconds == 0`)', async () => { + const initCurrentEpochStartTimeInSeconds = constants.ZERO_AMOUNT; + const txReceipt = await testContract.initMixinSchedulerTest.awaitTransactionSuccessAsync( + initCurrentEpochStartTimeInSeconds, + ); + // Assert `currentEpochStartTimeInSeconds` was properly initialized + const blockTimestamp = await env.web3Wrapper.getBlockTimestampAsync(txReceipt.blockNumber); + const currentEpochStartTimeInSeconds = await testContract.currentEpochStartTimeInSeconds.callAsync(); + expect(currentEpochStartTimeInSeconds).to.bignumber.equal(blockTimestamp); + // Assert `currentEpoch` was properly initialized + const currentEpoch = await testContract.currentEpoch.callAsync(); + expect(currentEpoch).to.bignumber.equal(1); + }); + + it('Should revert if scheduler is already initialized (`currentEpochStartTimeInSeconds != 0`)', async () => { + const initCurrentEpochStartTimeInSeconds = new BigNumber(10); + const tx = testContract.initMixinSchedulerTest.awaitTransactionSuccessAsync( + initCurrentEpochStartTimeInSeconds, + ); + return expect(tx).to.revertWith( + new StakingRevertErrors.InitializationError( + StakingRevertErrors.InitializationErrorCodes.MixinSchedulerAlreadyInitialized, + ), + ); + }); + }); + + describe('_goToNextEpoch', () => { + it('Should succeed if epoch end time is strictly less than to block timestamp', async () => { + const epochEndTimeDelta = new BigNumber(-10); + const txReceipt = await testContract.goToNextEpochTest.awaitTransactionSuccessAsync(epochEndTimeDelta); + const currentEpoch = await testContract.currentEpoch.callAsync(); + const currentEpochStartTimeInSeconds = await testContract.currentEpochStartTimeInSeconds.callAsync(); + verifyEventsFromLogs( + txReceipt.logs, + [ + { + oldEpoch: currentEpoch.minus(1), + blockTimestamp: currentEpochStartTimeInSeconds, + }, + ], + TestMixinSchedulerEvents.GoToNextEpochTestInfo, + ); + }); + + it('Should succeed if epoch end time is equal to block timestamp', async () => { + const epochEndTimeDelta = constants.ZERO_AMOUNT; + const txReceipt = await testContract.goToNextEpochTest.awaitTransactionSuccessAsync(epochEndTimeDelta); + // tslint:disable-next-line no-unnecessary-type-assertion + const testLog: TestMixinSchedulerGoToNextEpochTestInfoEventArgs = (txReceipt.logs[0] as LogWithDecodedArgs< + TestMixinSchedulerGoToNextEpochTestInfoEventArgs + >).args; + const currentEpoch = await testContract.currentEpoch.callAsync(); + const currentEpochStartTimeInSeconds = await testContract.currentEpochStartTimeInSeconds.callAsync(); + expect(currentEpoch).to.bignumber.equal(testLog.oldEpoch.plus(1)); + expect(currentEpochStartTimeInSeconds).to.bignumber.equal(testLog.blockTimestamp); + }); + + it('Should revert if epoch end time is strictly greater than block timestamp', async () => { + const epochEndTimeDelta = new BigNumber(10); + const tx = testContract.goToNextEpochTest.awaitTransactionSuccessAsync(epochEndTimeDelta); + try { + await tx; + // tslint:disable-next-line no-empty + } catch (e) {} + + // Mine the block that this tx would've been in. + await env.web3Wrapper.mineBlockAsync(); + const blockNumber = await env.web3Wrapper.getBlockNumberAsync(); + const blockTimestampAsNumber = await env.web3Wrapper.getBlockTimestampAsync(blockNumber); + const blockTimestamp = new BigNumber(blockTimestampAsNumber); + const epochEndTime = blockTimestamp.plus(epochEndTimeDelta); + return expect(tx).to.revertWith( + new StakingRevertErrors.BlockTimestampTooLowError(epochEndTime, blockTimestamp), + ); + }); + }); +}); diff --git a/contracts/staking/tsconfig.json b/contracts/staking/tsconfig.json index 0905fc08d5..281e750670 100644 --- a/contracts/staking/tsconfig.json +++ b/contracts/staking/tsconfig.json @@ -42,6 +42,7 @@ "generated-artifacts/TestLibFixedMath.json", "generated-artifacts/TestLibSafeDowncast.json", "generated-artifacts/TestMixinParams.json", + "generated-artifacts/TestMixinScheduler.json", "generated-artifacts/TestMixinStake.json", "generated-artifacts/TestMixinStakeBalances.json", "generated-artifacts/TestMixinStakeStorage.json",