From 6cfc17de351bd965a5bc37a972acc380c64f5fda Mon Sep 17 00:00:00 2001 From: Michael Zhu Date: Mon, 23 Sep 2019 16:12:20 -0700 Subject: [PATCH] MixinStakeStorage unit tests --- .../contracts/test/TestMixinStakeStorage.sol | 98 ++++++++ contracts/staking/package.json | 2 +- contracts/staking/src/artifacts.ts | 2 + contracts/staking/src/wrappers.ts | 1 + .../unit_tests/mixin_stake_storage_test.ts | 213 ++++++++++++++++++ contracts/staking/test/utils/types.ts | 7 + contracts/staking/tsconfig.json | 1 + 7 files changed, 323 insertions(+), 1 deletion(-) create mode 100644 contracts/staking/contracts/test/TestMixinStakeStorage.sol create mode 100644 contracts/staking/test/unit_tests/mixin_stake_storage_test.ts diff --git a/contracts/staking/contracts/test/TestMixinStakeStorage.sol b/contracts/staking/contracts/test/TestMixinStakeStorage.sol new file mode 100644 index 0000000000..e3eabbfa39 --- /dev/null +++ b/contracts/staking/contracts/test/TestMixinStakeStorage.sol @@ -0,0 +1,98 @@ +/* + + 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 "../src/interfaces/IStructs.sol"; +import "../src/stake/MixinStakeStorage.sol"; + + +contract TestMixinStakeStorage is + MixinStakeStorage +{ + mapping (uint256 => IStructs.StoredBalance) public testBalances; + + function setCurrentEpoch(uint256 newEpoch) + external + { + currentEpoch = newEpoch; + } + + function moveStake( + uint256 fromIndex, + uint256 toIndex, + uint256 amount + ) + external + { + return _moveStake( + testBalances[fromIndex], + testBalances[toIndex], + amount + ); + } + + function increaseCurrentAndNextBalance(uint256 index, uint256 amount) + external + { + return _increaseCurrentAndNextBalance(testBalances[index], amount); + } + + function decreaseCurrentAndNextBalance(uint256 index, uint256 amount) + external + { + _decreaseCurrentAndNextBalance(testBalances[index], amount); + } + + function increaseNextBalance(uint256 index, uint256 amount) + external + { + _increaseNextBalance(testBalances[index], amount); + } + + function decreaseNextBalance(uint256 index, uint256 amount) + external + { + _decreaseNextBalance(testBalances[index], amount); + } + + function loadSyncedBalance(uint256 index) + external + returns (IStructs.StoredBalance memory balance) + { + return _loadSyncedBalance(testBalances[index]); + } + + function loadUnsyncedBalance(uint256 index) + external + view + returns (IStructs.StoredBalance memory balance) + { + return _loadUnsyncedBalance(testBalances[index]); + } + + function setStoredBalance( + IStructs.StoredBalance memory balance, + uint256 index + ) + public + { + testBalances[index] = balance; + } +} diff --git a/contracts/staking/package.json b/contracts/staking/package.json index 5784ca33b4..0e6a2ed35c 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|LibProxy|LibSafeDowncast|LibStakingRichErrors|MixinAbstract|MixinConstants|MixinCumulativeRewards|MixinDeploymentConstants|MixinExchangeFees|MixinExchangeManager|MixinFinalizer|MixinParams|MixinScheduler|MixinStake|MixinStakeBalances|MixinStakeStorage|MixinStakingPool|MixinStakingPoolRewards|MixinStorage|ReadOnlyProxy|Staking|StakingProxy|TestAssertStorageParams|TestCobbDouglas|TestCumulativeRewardTracking|TestDelegatorRewards|TestExchangeManager|TestFinalizer|TestInitTarget|TestLibFixedMath|TestLibProxy|TestLibProxyReceiver|TestLibSafeDowncast|TestProtocolFees|TestStaking|TestStakingNoWETH|TestStakingProxy|TestStorageLayoutAndConstants|ZrxVault).json" + "abis": "./generated-artifacts/@(IStaking|IStakingEvents|IStakingProxy|IStorage|IStorageInit|IStructs|IZrxVault|LibCobbDouglas|LibFixedMath|LibFixedMathRichErrors|LibProxy|LibSafeDowncast|LibStakingRichErrors|MixinAbstract|MixinConstants|MixinCumulativeRewards|MixinDeploymentConstants|MixinExchangeFees|MixinExchangeManager|MixinFinalizer|MixinParams|MixinScheduler|MixinStake|MixinStakeBalances|MixinStakeStorage|MixinStakingPool|MixinStakingPoolRewards|MixinStorage|ReadOnlyProxy|Staking|StakingProxy|TestAssertStorageParams|TestCobbDouglas|TestCumulativeRewardTracking|TestDelegatorRewards|TestExchangeManager|TestFinalizer|TestInitTarget|TestLibFixedMath|TestLibProxy|TestLibProxyReceiver|TestLibSafeDowncast|TestMixinStakeStorage|TestProtocolFees|TestStaking|TestStakingNoWETH|TestStakingProxy|TestStorageLayoutAndConstants|ZrxVault).json" }, "repository": { "type": "git", diff --git a/contracts/staking/src/artifacts.ts b/contracts/staking/src/artifacts.ts index effc576939..9e27fb471e 100644 --- a/contracts/staking/src/artifacts.ts +++ b/contracts/staking/src/artifacts.ts @@ -47,6 +47,7 @@ import * as TestLibFixedMath from '../generated-artifacts/TestLibFixedMath.json' import * as TestLibProxy from '../generated-artifacts/TestLibProxy.json'; import * as TestLibProxyReceiver from '../generated-artifacts/TestLibProxyReceiver.json'; import * as TestLibSafeDowncast from '../generated-artifacts/TestLibSafeDowncast.json'; +import * as TestMixinStakeStorage from '../generated-artifacts/TestMixinStakeStorage.json'; import * as TestProtocolFees from '../generated-artifacts/TestProtocolFees.json'; import * as TestStaking from '../generated-artifacts/TestStaking.json'; import * as TestStakingNoWETH from '../generated-artifacts/TestStakingNoWETH.json'; @@ -97,6 +98,7 @@ export const artifacts = { TestLibProxy: TestLibProxy as ContractArtifact, TestLibProxyReceiver: TestLibProxyReceiver as ContractArtifact, TestLibSafeDowncast: TestLibSafeDowncast as ContractArtifact, + TestMixinStakeStorage: TestMixinStakeStorage as ContractArtifact, TestProtocolFees: TestProtocolFees as ContractArtifact, TestStaking: TestStaking as ContractArtifact, TestStakingNoWETH: TestStakingNoWETH as ContractArtifact, diff --git a/contracts/staking/src/wrappers.ts b/contracts/staking/src/wrappers.ts index 684ab66a60..2c7589178b 100644 --- a/contracts/staking/src/wrappers.ts +++ b/contracts/staking/src/wrappers.ts @@ -45,6 +45,7 @@ export * from '../generated-wrappers/test_lib_fixed_math'; export * from '../generated-wrappers/test_lib_proxy'; export * from '../generated-wrappers/test_lib_proxy_receiver'; export * from '../generated-wrappers/test_lib_safe_downcast'; +export * from '../generated-wrappers/test_mixin_stake_storage'; export * from '../generated-wrappers/test_protocol_fees'; export * from '../generated-wrappers/test_staking'; export * from '../generated-wrappers/test_staking_no_w_e_t_h'; diff --git a/contracts/staking/test/unit_tests/mixin_stake_storage_test.ts b/contracts/staking/test/unit_tests/mixin_stake_storage_test.ts new file mode 100644 index 0000000000..d4487c4366 --- /dev/null +++ b/contracts/staking/test/unit_tests/mixin_stake_storage_test.ts @@ -0,0 +1,213 @@ +import { blockchainTests, expect, Numberish } from '@0x/contracts-test-utils'; +import { StakingRevertErrors } from '@0x/order-utils'; +import { BigNumber } from '@0x/utils'; + +import { StoredBalance } from '../utils/types'; + +import { artifacts, TestMixinStakeStorageContract } from '../../src'; +import { constants } from '../utils/constants'; + +blockchainTests.resets('MixinStakeStorage unit tests', env => { + let testContract: TestMixinStakeStorageContract; + let defaultUninitializedBalance: StoredBalance; + let defaultSyncedBalance: StoredBalance; + let defaultUnsyncedBalance: StoredBalance; + + const CURRENT_EPOCH = new BigNumber(5); + const INDEX_ZERO = new BigNumber(0); + const INDEX_ONE = new BigNumber(1); + + before(async () => { + testContract = await TestMixinStakeStorageContract.deployFrom0xArtifactAsync( + artifacts.TestMixinStakeStorage, + env.provider, + env.txDefaults, + artifacts, + ); + await testContract.setCurrentEpoch.awaitTransactionSuccessAsync(CURRENT_EPOCH); + defaultUninitializedBalance = { + isInitialized: false, + currentEpoch: constants.INITIAL_EPOCH, + currentEpochBalance: new BigNumber(0), + nextEpochBalance: new BigNumber(0), + }; + defaultSyncedBalance = { + isInitialized: true, + currentEpoch: CURRENT_EPOCH, + currentEpochBalance: new BigNumber(16), + nextEpochBalance: new BigNumber(16), + }; + defaultUnsyncedBalance = { + isInitialized: true, + currentEpoch: CURRENT_EPOCH.minus(1), + currentEpochBalance: new BigNumber(10), + nextEpochBalance: new BigNumber(16), + }; + }); + + async function getTestBalancesAsync(index: Numberish): Promise { + const storedBalance: Partial = {}; + [ + storedBalance.isInitialized, + storedBalance.currentEpoch, + storedBalance.currentEpochBalance, + storedBalance.nextEpochBalance, + ] = await testContract.testBalances.callAsync(new BigNumber(index)); + return storedBalance as StoredBalance; + } + + describe('Move stake', () => { + async function moveStakeAndVerifyBalancesAsync( + fromBalance: StoredBalance, + toBalance: StoredBalance, + amount: BigNumber, + ): Promise { + await testContract.setStoredBalance.awaitTransactionSuccessAsync(fromBalance, INDEX_ZERO); + await testContract.setStoredBalance.awaitTransactionSuccessAsync(toBalance, INDEX_ONE); + await testContract.moveStake.awaitTransactionSuccessAsync(INDEX_ZERO, INDEX_ONE, amount); + + const actualBalances = await Promise.all([ + getTestBalancesAsync(INDEX_ZERO), + getTestBalancesAsync(INDEX_ONE), + ]); + expect(actualBalances[0]).to.deep.equal({ + isInitialized: true, + currentEpoch: CURRENT_EPOCH, + currentEpochBalance: fromBalance.currentEpochBalance, + nextEpochBalance: fromBalance.nextEpochBalance.minus(amount), + }); + expect(actualBalances[1]).to.deep.equal({ + isInitialized: true, + currentEpoch: CURRENT_EPOCH, + currentEpochBalance: toBalance.currentEpochBalance, + nextEpochBalance: toBalance.nextEpochBalance.plus(amount), + }); + } + + it('Updates balances to reflect move', async () => { + await moveStakeAndVerifyBalancesAsync( + defaultSyncedBalance, + defaultSyncedBalance, + defaultSyncedBalance.nextEpochBalance.dividedToIntegerBy(2), + ); + }); + it('Can move amount equal to next epoch balance', async () => { + await moveStakeAndVerifyBalancesAsync( + defaultSyncedBalance, + defaultSyncedBalance, + defaultSyncedBalance.nextEpochBalance, + ); + }); + it('Moves to and initializes a previously uninitalized balance', async () => { + await moveStakeAndVerifyBalancesAsync( + defaultSyncedBalance, + defaultUninitializedBalance, + defaultSyncedBalance.nextEpochBalance.dividedToIntegerBy(2), + ); + }); + it('Noop if pointers are equal', async () => { + await testContract.setStoredBalance.awaitTransactionSuccessAsync(defaultSyncedBalance, INDEX_ZERO); + // If the pointers weren't equal, this would revert with InsufficientBalanceError + await testContract.moveStake.awaitTransactionSuccessAsync( + INDEX_ZERO, + INDEX_ZERO, + defaultSyncedBalance.nextEpochBalance.plus(1), + ); + const actualBalance = await getTestBalancesAsync(INDEX_ZERO); + expect(actualBalance).to.deep.equal(defaultSyncedBalance); + }); + it("Reverts if attempting to move more than next epoch's balance", async () => { + await testContract.setStoredBalance.awaitTransactionSuccessAsync(defaultSyncedBalance, INDEX_ZERO); + const amount = defaultSyncedBalance.nextEpochBalance.plus(1); + const tx = testContract.moveStake.awaitTransactionSuccessAsync(INDEX_ZERO, INDEX_ONE, amount); + await expect(tx).to.revertWith( + new StakingRevertErrors.InsufficientBalanceError(amount, defaultSyncedBalance.nextEpochBalance), + ); + }); + }); + + describe('Load balance', () => { + it('_loadSyncedBalance does not change state if balance was previously synced in the current epoch', async () => { + await testContract.setStoredBalance.awaitTransactionSuccessAsync(defaultSyncedBalance, INDEX_ZERO); + const actualBalance = await testContract.loadSyncedBalance.callAsync(INDEX_ZERO); + expect(actualBalance).to.deep.equal(defaultSyncedBalance); + }); + it('_loadSyncedBalance updates current epoch fields if the balance has not yet been synced in the current epoch', async () => { + await testContract.setStoredBalance.awaitTransactionSuccessAsync(defaultUnsyncedBalance, INDEX_ZERO); + const actualBalance = await testContract.loadSyncedBalance.callAsync(INDEX_ZERO); + expect(actualBalance).to.deep.equal(defaultSyncedBalance); + }); + it('_loadUnsyncedBalance loads unsynced balance from storage without changing fields', async () => { + await testContract.setStoredBalance.awaitTransactionSuccessAsync(defaultUnsyncedBalance, INDEX_ZERO); + const actualBalance = await testContract.loadUnsyncedBalance.callAsync(INDEX_ZERO); + expect(actualBalance).to.deep.equal(defaultUnsyncedBalance); + }); + it('_loadUnsyncedBalance loads synced balance from storage without changing fields', async () => { + await testContract.setStoredBalance.awaitTransactionSuccessAsync(defaultSyncedBalance, INDEX_ZERO); + const actualBalance = await testContract.loadUnsyncedBalance.callAsync(INDEX_ZERO); + expect(actualBalance).to.deep.equal(defaultSyncedBalance); + }); + }); + + describe('Increase/decrease balance', () => { + it('_increaseCurrentAndNextBalance', async () => { + await testContract.setStoredBalance.awaitTransactionSuccessAsync(defaultUnsyncedBalance, INDEX_ZERO); + const amount = defaultUnsyncedBalance.currentEpochBalance.dividedToIntegerBy(2); + await testContract.increaseCurrentAndNextBalance.awaitTransactionSuccessAsync(INDEX_ZERO, amount); + const actualBalance = await getTestBalancesAsync(INDEX_ZERO); + expect(actualBalance).to.deep.equal({ + ...defaultSyncedBalance, + currentEpochBalance: defaultSyncedBalance.currentEpochBalance.plus(amount), + nextEpochBalance: defaultSyncedBalance.nextEpochBalance.plus(amount), + }); + }); + it('_increaseCurrentAndNextBalance (previously uninitialized)', async () => { + await testContract.setStoredBalance.awaitTransactionSuccessAsync(defaultUninitializedBalance, INDEX_ZERO); + const amount = defaultSyncedBalance.currentEpochBalance; + await testContract.increaseCurrentAndNextBalance.awaitTransactionSuccessAsync(INDEX_ZERO, amount); + const actualBalance = await getTestBalancesAsync(INDEX_ZERO); + expect(actualBalance).to.deep.equal(defaultSyncedBalance); + }); + it('_decreaseCurrentAndNextBalance', async () => { + await testContract.setStoredBalance.awaitTransactionSuccessAsync(defaultUnsyncedBalance, INDEX_ZERO); + const amount = defaultUnsyncedBalance.currentEpochBalance.dividedToIntegerBy(2); + await testContract.decreaseCurrentAndNextBalance.awaitTransactionSuccessAsync(INDEX_ZERO, amount); + const actualBalance = await getTestBalancesAsync(INDEX_ZERO); + expect(actualBalance).to.deep.equal({ + ...defaultSyncedBalance, + currentEpochBalance: defaultSyncedBalance.currentEpochBalance.minus(amount), + nextEpochBalance: defaultSyncedBalance.nextEpochBalance.minus(amount), + }); + }); + it('_increaseNextBalance', async () => { + await testContract.setStoredBalance.awaitTransactionSuccessAsync(defaultUnsyncedBalance, INDEX_ZERO); + const amount = defaultUnsyncedBalance.currentEpochBalance.dividedToIntegerBy(2); + await testContract.increaseNextBalance.awaitTransactionSuccessAsync(INDEX_ZERO, amount); + const actualBalance = await getTestBalancesAsync(INDEX_ZERO); + expect(actualBalance).to.deep.equal({ + ...defaultSyncedBalance, + nextEpochBalance: defaultSyncedBalance.nextEpochBalance.plus(amount), + }); + }); + it('_increaseCurrentAndNextBalance (previously uninitialized)', async () => { + await testContract.setStoredBalance.awaitTransactionSuccessAsync(defaultUninitializedBalance, INDEX_ZERO); + const amount = defaultSyncedBalance.currentEpochBalance; + await testContract.increaseNextBalance.awaitTransactionSuccessAsync(INDEX_ZERO, amount); + const actualBalance = await getTestBalancesAsync(INDEX_ZERO); + expect(actualBalance).to.deep.equal({ + ...defaultSyncedBalance, + currentEpochBalance: new BigNumber(0), + }); + }); + it('_decreaseNextBalance', async () => { + await testContract.setStoredBalance.awaitTransactionSuccessAsync(defaultUnsyncedBalance, INDEX_ZERO); + const amount = defaultUnsyncedBalance.currentEpochBalance.dividedToIntegerBy(2); + await testContract.decreaseNextBalance.awaitTransactionSuccessAsync(INDEX_ZERO, amount); + const actualBalance = await getTestBalancesAsync(INDEX_ZERO); + expect(actualBalance).to.deep.equal({ + ...defaultSyncedBalance, + nextEpochBalance: defaultSyncedBalance.nextEpochBalance.minus(amount), + }); + }); + }); +}); diff --git a/contracts/staking/test/utils/types.ts b/contracts/staking/test/utils/types.ts index 1216d45999..d50d62549e 100644 --- a/contracts/staking/test/utils/types.ts +++ b/contracts/staking/test/utils/types.ts @@ -59,6 +59,13 @@ export interface EndOfEpochInfo { totalWeightedStake: BigNumber; } +export interface StoredBalance { + isInitialized: boolean; + currentEpoch: number | BigNumber; + currentEpochBalance: BigNumber; + nextEpochBalance: BigNumber; +} + export interface StakeBalance { currentEpochBalance: BigNumber; nextEpochBalance: BigNumber; diff --git a/contracts/staking/tsconfig.json b/contracts/staking/tsconfig.json index 2e0a90ed6d..9e51fd5193 100644 --- a/contracts/staking/tsconfig.json +++ b/contracts/staking/tsconfig.json @@ -45,6 +45,7 @@ "generated-artifacts/TestLibProxy.json", "generated-artifacts/TestLibProxyReceiver.json", "generated-artifacts/TestLibSafeDowncast.json", + "generated-artifacts/TestMixinStakeStorage.json", "generated-artifacts/TestProtocolFees.json", "generated-artifacts/TestStaking.json", "generated-artifacts/TestStakingNoWETH.json",