From 6f2e79208a361105ea8bc2862b9fcc1fe2a70d05 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Fri, 25 Oct 2019 08:52:41 -0400 Subject: [PATCH 1/3] `@0x/contracts-staking`: Add `MixinStakeBalances` unit tests. --- .../contracts/test/TestMixinStakeBalances.sol | 122 ++++++++++ contracts/staking/src/artifacts.ts | 2 + contracts/staking/src/wrappers.ts | 1 + .../test/unit_tests/stake_balances_test.ts | 229 ++++++++++++++++++ contracts/staking/tsconfig.json | 1 + 5 files changed, 355 insertions(+) create mode 100644 contracts/staking/contracts/test/TestMixinStakeBalances.sol create mode 100644 contracts/staking/test/unit_tests/stake_balances_test.ts diff --git a/contracts/staking/contracts/test/TestMixinStakeBalances.sol b/contracts/staking/contracts/test/TestMixinStakeBalances.sol new file mode 100644 index 0000000000..7ef21888d4 --- /dev/null +++ b/contracts/staking/contracts/test/TestMixinStakeBalances.sol @@ -0,0 +1,122 @@ +/* + + 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 "./TestStakingNoWETH.sol"; + + +contract TestMixinStakeBalances is + TestStakingNoWETH +{ + uint256 private _balanceOfZrxVault; + mapping (address => uint256) private _zrxBalanceOf; + + function setBalanceOfZrxVault(uint256 balance) + external + { + _balanceOfZrxVault = balance; + } + + function setZrxBalanceOf(address staker, uint256 balance) + external + { + _zrxBalanceOf[staker] = balance; + } + + /// @dev `IZrxVault.balanceOfZrxVault` + function balanceOfZrxVault() + external + view + returns (uint256) + { + return _balanceOfZrxVault; + } + + /// @dev `IZrxVault.balanceOf` + function balanceOf(address staker) + external + view + returns (uint256) + { + return _zrxBalanceOf[staker]; + } + + /// @dev Set `_ownerStakeByStatus` + function setOwnerStakeByStatus( + address owner, + IStructs.StakeStatus status, + IStructs.StoredBalance memory stake + ) + public + { + _ownerStakeByStatus[uint8(status)][owner] = stake; + } + + /// @dev Set `_delegatedStakeToPoolByOwner` + function setDelegatedStakeToPoolByOwner( + address owner, + bytes32 poolId, + IStructs.StoredBalance memory stake + ) + public + { + _delegatedStakeToPoolByOwner[owner][poolId] = stake; + } + + /// @dev Set `_delegatedStakeByPoolId` + function setDelegatedStakeByPoolId( + bytes32 poolId, + IStructs.StoredBalance memory stake + ) + public + { + _delegatedStakeByPoolId[poolId] = stake; + } + + /// @dev Set `_globalStakeByStatus` + function setGlobalStakeByStatus( + IStructs.StakeStatus status, + IStructs.StoredBalance memory stake + ) + public + { + _globalStakeByStatus[uint8(status)] = stake; + } + + /// @dev Overridden to use this contract as the ZRX vault. + function getZrxVault() + public + view + returns (IZrxVault zrxVault) + { + return IZrxVault(address(this)); + } + + /// @dev Overridden to just return the input with the epoch incremented. + function _loadCurrentBalance(IStructs.StoredBalance storage balancePtr) + internal + view + returns (IStructs.StoredBalance memory balance) + { + balance = balancePtr; + balance.currentEpoch += 1; + } +} diff --git a/contracts/staking/src/artifacts.ts b/contracts/staking/src/artifacts.ts index 018d30ba3b..f5674e3b11 100644 --- a/contracts/staking/src/artifacts.ts +++ b/contracts/staking/src/artifacts.ts @@ -45,6 +45,7 @@ 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 TestMixinStake from '../generated-artifacts/TestMixinStake.json'; +import * as TestMixinStakeBalances from '../generated-artifacts/TestMixinStakeBalances.json'; import * as TestMixinStakeStorage from '../generated-artifacts/TestMixinStakeStorage.json'; import * as TestMixinStakingPool from '../generated-artifacts/TestMixinStakingPool.json'; import * as TestProtocolFees from '../generated-artifacts/TestProtocolFees.json'; @@ -95,6 +96,7 @@ export const artifacts = { TestLibSafeDowncast: TestLibSafeDowncast as ContractArtifact, TestMixinParams: TestMixinParams as ContractArtifact, TestMixinStake: TestMixinStake as ContractArtifact, + TestMixinStakeBalances: TestMixinStakeBalances as ContractArtifact, TestMixinStakeStorage: TestMixinStakeStorage as ContractArtifact, TestMixinStakingPool: TestMixinStakingPool as ContractArtifact, TestProtocolFees: TestProtocolFees as ContractArtifact, diff --git a/contracts/staking/src/wrappers.ts b/contracts/staking/src/wrappers.ts index 8d74eb453c..3e2424b760 100644 --- a/contracts/staking/src/wrappers.ts +++ b/contracts/staking/src/wrappers.ts @@ -43,6 +43,7 @@ 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_stake'; +export * from '../generated-wrappers/test_mixin_stake_balances'; export * from '../generated-wrappers/test_mixin_stake_storage'; export * from '../generated-wrappers/test_mixin_staking_pool'; export * from '../generated-wrappers/test_protocol_fees'; diff --git a/contracts/staking/test/unit_tests/stake_balances_test.ts b/contracts/staking/test/unit_tests/stake_balances_test.ts new file mode 100644 index 0000000000..97558731bb --- /dev/null +++ b/contracts/staking/test/unit_tests/stake_balances_test.ts @@ -0,0 +1,229 @@ +import { + blockchainTests, + constants, + expect, + getRandomInteger, + hexRandom, + randomAddress, +} from '@0x/contracts-test-utils'; +import { BigNumber, SafeMathRevertErrors } from '@0x/utils'; +import * as _ from 'lodash'; + +import { artifacts, TestMixinStakeBalancesContract } from '../../src'; +import { constants as stakingConstants } from '../utils/constants'; +import { StakeStatus, StoredBalance } from '../utils/types'; + +blockchainTests.resets.only('MixinStakeBalances unit tests', env => { + let testContract: TestMixinStakeBalancesContract; + const { INITIAL_EPOCH } = stakingConstants; + const CURRENT_EPOCH = INITIAL_EPOCH.plus(1); + const EMPTY_BALANCE = { + currentEpochBalance: constants.ZERO_AMOUNT, + nextEpochBalance: constants.ZERO_AMOUNT, + currentEpoch: new BigNumber(1), + }; + + before(async () => { + testContract = await TestMixinStakeBalancesContract.deployFrom0xArtifactAsync( + artifacts.TestMixinStakeBalances, + env.provider, + env.txDefaults, + artifacts, + ); + }); + + function randomAmount(): BigNumber { + return getRandomInteger(1, 100e18); + } + + function randomStoredBalance(): StoredBalance { + return { + currentEpochBalance: randomAmount(), + nextEpochBalance: randomAmount(), + currentEpoch: INITIAL_EPOCH, + }; + } + + // Mirrors the behavior of the `_loadCurrentBalance()` override in + // `TestMixinStakeBalances`. + function toCurrentBalance(balance: StoredBalance): StoredBalance { + return { + ...balance, + currentEpoch: balance.currentEpoch.plus(1), + }; + } + + describe('getGlobalStakeByStatus()', () => { + const delegatedBalance = randomStoredBalance(); + const zrxVaultBalance = randomAmount().plus( + BigNumber.max(delegatedBalance.currentEpochBalance, delegatedBalance.nextEpochBalance), + ); + + before(async () => { + await testContract.setGlobalStakeByStatus.awaitTransactionSuccessAsync( + StakeStatus.Delegated, + delegatedBalance, + ); + await testContract.setBalanceOfZrxVault.awaitTransactionSuccessAsync(zrxVaultBalance); + }); + + it('undelegated stake is the difference between zrx vault balance and global delegated stake', async () => { + const expectedBalance = { + currentEpoch: CURRENT_EPOCH, + currentEpochBalance: zrxVaultBalance.minus(delegatedBalance.currentEpochBalance), + nextEpochBalance: zrxVaultBalance.minus(delegatedBalance.nextEpochBalance), + }; + const actualBalance = await testContract.getGlobalStakeByStatus.callAsync(StakeStatus.Undelegated); + expect(actualBalance).to.deep.eq(expectedBalance); + }); + + it('delegated stake is the global delegated stake', async () => { + const actualBalance = await testContract.getGlobalStakeByStatus.callAsync(StakeStatus.Delegated); + expect(actualBalance).to.deep.eq(toCurrentBalance(delegatedBalance)); + }); + + it('undelegated stake throws if the zrx vault balance is below the delegated stake balance', async () => { + const _zrxVaultBalance = BigNumber.min( + delegatedBalance.currentEpochBalance, + delegatedBalance.nextEpochBalance, + ).minus(1); + await testContract.setBalanceOfZrxVault.awaitTransactionSuccessAsync(_zrxVaultBalance); + const tx = testContract.getGlobalStakeByStatus.callAsync(StakeStatus.Undelegated); + const expectedError = new SafeMathRevertErrors.Uint256BinOpError( + SafeMathRevertErrors.BinOpErrorCodes.SubtractionUnderflow, + _zrxVaultBalance, + delegatedBalance.currentEpochBalance.gt(_zrxVaultBalance) + ? delegatedBalance.currentEpochBalance + : delegatedBalance.nextEpochBalance, + ); + return expect(tx).to.revertWith(expectedError); + }); + + it('throws if unknown stake status is passed in', async () => { + const tx = testContract.getGlobalStakeByStatus.callAsync(2); + return expect(tx).to.be.rejected(); + }); + }); + + describe('getOwnerStakeByStatus()', () => { + const staker = randomAddress(); + const notStaker = randomAddress(); + const delegatedStake = randomStoredBalance(); + const undelegatedStake = randomStoredBalance(); + + before(async () => { + await testContract.setOwnerStakeByStatus.awaitTransactionSuccessAsync( + staker, + StakeStatus.Delegated, + delegatedStake, + ); + await testContract.setOwnerStakeByStatus.awaitTransactionSuccessAsync( + staker, + StakeStatus.Undelegated, + undelegatedStake, + ); + }); + + it('throws if unknown stake status is passed in', async () => { + const tx = testContract.getOwnerStakeByStatus.callAsync(staker, 2); + return expect(tx).to.be.rejected(); + }); + + it('returns empty delegated stake for an unstaked owner', async () => { + const balance = await testContract.getOwnerStakeByStatus.callAsync(notStaker, StakeStatus.Delegated); + expect(balance).to.deep.eq(EMPTY_BALANCE); + }); + + it('returns empty undelegated stake for an unstaked owner', async () => { + const balance = await testContract.getOwnerStakeByStatus.callAsync(notStaker, StakeStatus.Undelegated); + expect(balance).to.deep.eq(EMPTY_BALANCE); + }); + + it('returns empty undelegated stake for an unstaked owner', async () => { + const balance = await testContract.getOwnerStakeByStatus.callAsync(notStaker, StakeStatus.Undelegated); + expect(balance).to.deep.eq(EMPTY_BALANCE); + }); + + it('returns undelegated stake for a staked owner', async () => { + const balance = await testContract.getOwnerStakeByStatus.callAsync(staker, StakeStatus.Undelegated); + expect(balance).to.deep.eq(toCurrentBalance(undelegatedStake)); + }); + + it('returns delegated stake for a staked owner', async () => { + const balance = await testContract.getOwnerStakeByStatus.callAsync(staker, StakeStatus.Delegated); + expect(balance).to.deep.eq(toCurrentBalance(delegatedStake)); + }); + }); + + describe('getTotalStake()', () => { + const staker = randomAddress(); + const notStaker = randomAddress(); + const stakerAmount = randomAmount(); + + before(async () => { + await testContract.setZrxBalanceOf.awaitTransactionSuccessAsync(staker, stakerAmount); + }); + + it('returns empty for unstaked owner', async () => { + const amount = await testContract.getTotalStake.callAsync(notStaker); + expect(amount).to.bignumber.eq(0); + }); + + it('returns stake for staked owner', async () => { + const amount = await testContract.getTotalStake.callAsync(staker); + expect(amount).to.bignumber.eq(stakerAmount); + }); + }); + + describe('getStakeDelegatedToPoolByOwner()', () => { + const staker = randomAddress(); + const notStaker = randomAddress(); + const poolId = hexRandom(); + const notPoolId = hexRandom(); + const delegatedBalance = randomStoredBalance(); + + before(async () => { + await testContract.setDelegatedStakeToPoolByOwner.awaitTransactionSuccessAsync( + staker, + poolId, + delegatedBalance, + ); + }); + + it('returns empty for unstaked owner', async () => { + const balance = await testContract.getStakeDelegatedToPoolByOwner.callAsync(notStaker, poolId); + expect(balance).to.deep.eq(EMPTY_BALANCE); + }); + + it('returns empty for empty pool', async () => { + const balance = await testContract.getStakeDelegatedToPoolByOwner.callAsync(staker, notPoolId); + expect(balance).to.deep.eq(EMPTY_BALANCE); + }); + + it('returns stake for staked owner in their pool', async () => { + const balance = await testContract.getStakeDelegatedToPoolByOwner.callAsync(staker, poolId); + expect(balance).to.deep.eq(toCurrentBalance(delegatedBalance)); + }); + }); + + describe('getTotalStakeDelegatedToPool()', () => { + const poolId = hexRandom(); + const notPoolId = hexRandom(); + const delegatedBalance = randomStoredBalance(); + + before(async () => { + await testContract.setDelegatedStakeByPoolId.awaitTransactionSuccessAsync(poolId, delegatedBalance); + }); + + it('returns empty for empty pool', async () => { + const balance = await testContract.getTotalStakeDelegatedToPool.callAsync(notPoolId); + expect(balance).to.deep.eq(EMPTY_BALANCE); + }); + + it('returns stake for staked pool', async () => { + const balance = await testContract.getTotalStakeDelegatedToPool.callAsync(poolId); + expect(balance).to.deep.eq(toCurrentBalance(delegatedBalance)); + }); + }); +}); +// tslint:disable: max-file-line-count diff --git a/contracts/staking/tsconfig.json b/contracts/staking/tsconfig.json index 57fb262f2c..f230efb194 100644 --- a/contracts/staking/tsconfig.json +++ b/contracts/staking/tsconfig.json @@ -43,6 +43,7 @@ "generated-artifacts/TestLibSafeDowncast.json", "generated-artifacts/TestMixinParams.json", "generated-artifacts/TestMixinStake.json", + "generated-artifacts/TestMixinStakeBalances.json", "generated-artifacts/TestMixinStakeStorage.json", "generated-artifacts/TestMixinStakingPool.json", "generated-artifacts/TestProtocolFees.json", From 49c67fbb185228baebc6b7cd638d35118bb76a25 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Fri, 25 Oct 2019 11:00:03 -0400 Subject: [PATCH 2/3] `@0x/contracts-staking`: Remove unused lodash import and `only` modifier from `MixinStakeBalances` unit tests. --- contracts/staking/test/unit_tests/stake_balances_test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/contracts/staking/test/unit_tests/stake_balances_test.ts b/contracts/staking/test/unit_tests/stake_balances_test.ts index 97558731bb..f483beed7a 100644 --- a/contracts/staking/test/unit_tests/stake_balances_test.ts +++ b/contracts/staking/test/unit_tests/stake_balances_test.ts @@ -7,13 +7,12 @@ import { randomAddress, } from '@0x/contracts-test-utils'; import { BigNumber, SafeMathRevertErrors } from '@0x/utils'; -import * as _ from 'lodash'; import { artifacts, TestMixinStakeBalancesContract } from '../../src'; import { constants as stakingConstants } from '../utils/constants'; import { StakeStatus, StoredBalance } from '../utils/types'; -blockchainTests.resets.only('MixinStakeBalances unit tests', env => { +blockchainTests.resets('MixinStakeBalances unit tests', env => { let testContract: TestMixinStakeBalancesContract; const { INITIAL_EPOCH } = stakingConstants; const CURRENT_EPOCH = INITIAL_EPOCH.plus(1); From a02892cbc87b4dcf7370e59c3812610083f17f10 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Fri, 25 Oct 2019 15:36:17 -0400 Subject: [PATCH 3/3] `@0x/contracts-staking`: Remove duplicated test case in `MixinStakeBalances` unit tests. --- contracts/staking/test/unit_tests/stake_balances_test.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/contracts/staking/test/unit_tests/stake_balances_test.ts b/contracts/staking/test/unit_tests/stake_balances_test.ts index f483beed7a..019a4487b1 100644 --- a/contracts/staking/test/unit_tests/stake_balances_test.ts +++ b/contracts/staking/test/unit_tests/stake_balances_test.ts @@ -138,11 +138,6 @@ blockchainTests.resets('MixinStakeBalances unit tests', env => { expect(balance).to.deep.eq(EMPTY_BALANCE); }); - it('returns empty undelegated stake for an unstaked owner', async () => { - const balance = await testContract.getOwnerStakeByStatus.callAsync(notStaker, StakeStatus.Undelegated); - expect(balance).to.deep.eq(EMPTY_BALANCE); - }); - it('returns undelegated stake for a staked owner', async () => { const balance = await testContract.getOwnerStakeByStatus.callAsync(staker, StakeStatus.Undelegated); expect(balance).to.deep.eq(toCurrentBalance(undelegatedStake));