From 8c4c3d56c68c11ce0168a99ae3c11d849e00c9db Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Thu, 31 Oct 2019 15:53:46 -0400 Subject: [PATCH 1/6] `@0x/contracts-staking`: Create `MixinStakingPoolRewards` unit tests. --- .../test/TestMixinStakingPoolRewards.sol | 181 ++++++++++++++++++ contracts/staking/package.json | 2 +- contracts/staking/src/artifacts.ts | 2 + contracts/staking/src/wrappers.ts | 1 + .../unit_tests/mixin_staking_pool_rewards.ts | 35 ++++ contracts/staking/tsconfig.json | 1 + 6 files changed, 221 insertions(+), 1 deletion(-) create mode 100644 contracts/staking/contracts/test/TestMixinStakingPoolRewards.sol create mode 100644 contracts/staking/test/unit_tests/mixin_staking_pool_rewards.ts diff --git a/contracts/staking/contracts/test/TestMixinStakingPoolRewards.sol b/contracts/staking/contracts/test/TestMixinStakingPoolRewards.sol new file mode 100644 index 0000000000..43b9a99fce --- /dev/null +++ b/contracts/staking/contracts/test/TestMixinStakingPoolRewards.sol @@ -0,0 +1,181 @@ +/* + + 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 TestMixinStakingPoolRewards is + TestStakingNoWETH +{ + event UpdateCumulativeReward( + bytes32 poolId + ); + + struct UnfinalizedPoolReward { + uint256 reward; + uint256 membersStake; + } + + constructor() public { + _addAuthorizedAddress(msg.sender); + init(); + _removeAuthorizedAddressAtIndex(msg.sender, 0); + } + + // Rewards returned by `_computeMemberRewardOverInterval()`, indexed + // by `_getMemberRewardOverIntervalHash()`. + mapping (bytes32 => uint256) private _memberRewardsOverInterval; + // Rewards returned by `_getUnfinalizedPoolRewards()`, indexed by pool ID. + mapping (bytes32 => UnfinalizedPoolReward) private _unfinalizedPoolRewards; + + // Set the rewards returned by a call to `_computeMemberRewardOverInterval()`. + function setMemberRewardsOverInterval( + bytes32 poolId, + uint256 memberStakeOverInterval, + uint256 beginEpoch, + uint256 endEpoch, + uint256 reward + ) + external + { + bytes32 rewardHash = _getMemberRewardOverIntervalHash( + poolId, + memberStakeOverInterval, + beginEpoch, + endEpoch + ); + _memberRewardsOverInterval[rewardHash] = reward; + } + + // Set the rewards returned by `_getUnfinalizedPoolRewards()`. + function setUnfinalizedPoolRewards( + bytes32 poolId, + uint256 reward, + uint256 membersStake + ) + external + { + _unfinalizedPoolRewards[poolId] = UnfinalizedPoolReward( + reward, + membersStake + ); + } + + // Advance the epoch. + function advanceEpoch() external { + currentEpoch += 1; + } + + // Set `_delegatedStakeToPoolByOwner` + function setDelegatedStakeToPoolByOwner( + address member, + bytes32 poolId, + IStructs.StoredBalance memory balance + ) + public + { + _delegatedStakeToPoolByOwner[member][poolId] = balance; + } + + // Set `_poolById`. + function setPool( + bytes32 poolId, + IStructs.Pool memory pool + ) + public + { + _poolById[poolId] = pool; + } + + // Overridden to use `_memberRewardsOverInterval` + function _computeMemberRewardOverInterval( + bytes32 poolId, + uint256 memberStakeOverInterval, + uint256 beginEpoch, + uint256 endEpoch + ) + internal + view + returns (uint256 reward) + { + bytes32 rewardHash = _getMemberRewardOverIntervalHash( + poolId, + memberStakeOverInterval, + beginEpoch, + endEpoch + ); + return _memberRewardsOverInterval[rewardHash]; + } + + // Overridden to use `_unfinalizedPoolRewards` + function _getUnfinalizedPoolRewards( + bytes32 poolId + ) + internal + view + returns (uint256 reward, uint256 membersStake) + { + (reward, membersStake) = ( + _unfinalizedPoolRewards[poolId].reward, + _unfinalizedPoolRewards[poolId].membersStake + ); + } + + // Overridden to revert if a pool has unfinalized rewards. + function _assertPoolFinalizedLastEpoch(bytes32 poolId) + internal + view + { + require( + _unfinalizedPoolRewards[poolId].membersStake == 0, + "POOL_NOT_FINALIZED" + ); + } + + // Overridden to just emit an event. + function _updateCumulativeReward(bytes32 poolId) + internal + { + emit UpdateCumulativeReward(poolId); + } + + // Compute a hash to index `_memberRewardsOverInterval` + function _getMemberRewardOverIntervalHash( + bytes32 poolId, + uint256 memberStakeOverInterval, + uint256 beginEpoch, + uint256 endEpoch + ) + private + pure + returns (bytes32 rewardHash) + { + return keccak256( + abi.encode( + poolId, + memberStakeOverInterval, + beginEpoch, + endEpoch + ) + ); + } +} diff --git a/contracts/staking/package.json b/contracts/staking/package.json index b15f0786a0..1d87f81e1c 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|TestProtocolFees|TestProxyDestination|TestStaking|TestStakingNoWETH|TestStakingProxy|TestStakingProxyUnit|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|Staking|StakingProxy|TestAssertStorageParams|TestCobbDouglas|TestCumulativeRewardTracking|TestDelegatorRewards|TestExchangeManager|TestFinalizer|TestInitTarget|TestLibFixedMath|TestLibProxy|TestLibProxyReceiver|TestLibSafeDowncast|TestMixinParams|TestMixinStake|TestMixinStakeStorage|TestMixinStakingPool|TestMixinStakingPoolRewards|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 db17417685..b9bbf14441 100644 --- a/contracts/staking/src/artifacts.ts +++ b/contracts/staking/src/artifacts.ts @@ -48,6 +48,7 @@ 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 TestMixinStakingPoolRewards from '../generated-artifacts/TestMixinStakingPoolRewards.json'; import * as TestProtocolFees from '../generated-artifacts/TestProtocolFees.json'; import * as TestProxyDestination from '../generated-artifacts/TestProxyDestination.json'; import * as TestStaking from '../generated-artifacts/TestStaking.json'; @@ -101,6 +102,7 @@ export const artifacts = { TestMixinStakeBalances: TestMixinStakeBalances as ContractArtifact, TestMixinStakeStorage: TestMixinStakeStorage as ContractArtifact, TestMixinStakingPool: TestMixinStakingPool as ContractArtifact, + TestMixinStakingPoolRewards: TestMixinStakingPoolRewards as ContractArtifact, TestProtocolFees: TestProtocolFees as ContractArtifact, TestProxyDestination: TestProxyDestination as ContractArtifact, TestStaking: TestStaking as ContractArtifact, diff --git a/contracts/staking/src/wrappers.ts b/contracts/staking/src/wrappers.ts index df1b36a8ab..f532e731c3 100644 --- a/contracts/staking/src/wrappers.ts +++ b/contracts/staking/src/wrappers.ts @@ -46,6 +46,7 @@ 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_mixin_staking_pool_rewards'; export * from '../generated-wrappers/test_protocol_fees'; export * from '../generated-wrappers/test_proxy_destination'; export * from '../generated-wrappers/test_staking'; diff --git a/contracts/staking/test/unit_tests/mixin_staking_pool_rewards.ts b/contracts/staking/test/unit_tests/mixin_staking_pool_rewards.ts new file mode 100644 index 0000000000..8e10465352 --- /dev/null +++ b/contracts/staking/test/unit_tests/mixin_staking_pool_rewards.ts @@ -0,0 +1,35 @@ +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, + TestMixinStakingPoolRewardsContract, + TestMixinStakingPoolRewardsEvents, + TestMixinStakingPoolRewardsUpdateCumulativeRewardEventArgs as UpdateCumulativeReward, +} from '../../src'; +import { constants } from '../utils/constants'; + +blockchainTests.resets.only('MixinStakingPoolRewards unit tests', env => { + let testContract: TestMixinStakingPoolRewardsContract; + + const INITIAL_EPOCH = 0; + const NEXT_EPOCH = 1; + + before(async () => { + testContract = await TestMixinStakingPoolRewardsContract.deployFrom0xArtifactAsync( + artifacts.TestMixinStakingPoolRewards, + env.provider, + env.txDefaults, + artifacts, + ); + }); + + describe('withdrawAndSyncDelegatorRewards()', () => { + it('poop', async () => { + // no-op + }); + }); +}); diff --git a/contracts/staking/tsconfig.json b/contracts/staking/tsconfig.json index 3383d877ae..0905fc08d5 100644 --- a/contracts/staking/tsconfig.json +++ b/contracts/staking/tsconfig.json @@ -46,6 +46,7 @@ "generated-artifacts/TestMixinStakeBalances.json", "generated-artifacts/TestMixinStakeStorage.json", "generated-artifacts/TestMixinStakingPool.json", + "generated-artifacts/TestMixinStakingPoolRewards.json", "generated-artifacts/TestProtocolFees.json", "generated-artifacts/TestProxyDestination.json", "generated-artifacts/TestStaking.json", From 28a2e56003f964a553eea4bdb83659e06fb22554 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Fri, 1 Nov 2019 12:00:10 -0400 Subject: [PATCH 2/6] `@0x/contracts-staking`: Add more `MixinStakingPoolRewards` unit tests. --- .../test/TestMixinStakingPoolRewards.sol | 56 ++- contracts/staking/package.json | 3 +- .../unit_tests/mixin_staking_pool_rewards.ts | 416 +++++++++++++++++- 3 files changed, 456 insertions(+), 19 deletions(-) diff --git a/contracts/staking/contracts/test/TestMixinStakingPoolRewards.sol b/contracts/staking/contracts/test/TestMixinStakingPoolRewards.sol index 43b9a99fce..b11b42b0fc 100644 --- a/contracts/staking/contracts/test/TestMixinStakingPoolRewards.sol +++ b/contracts/staking/contracts/test/TestMixinStakingPoolRewards.sol @@ -47,6 +47,25 @@ contract TestMixinStakingPoolRewards is // Rewards returned by `_getUnfinalizedPoolRewards()`, indexed by pool ID. mapping (bytes32 => UnfinalizedPoolReward) private _unfinalizedPoolRewards; + // Set pool `rewardsByPoolId`. + function setPoolRewards( + bytes32 poolId, + uint256 _rewardsByPoolId + ) + external + { + rewardsByPoolId[poolId] = _rewardsByPoolId; + } + + // Set `wethReservedForPoolRewards`. + function setWethReservedForPoolRewards( + uint256 _wethReservedForPoolRewards + ) + external + { + wethReservedForPoolRewards = _wethReservedForPoolRewards; + } + // Set the rewards returned by a call to `_computeMemberRewardOverInterval()`. function setMemberRewardsOverInterval( bytes32 poolId, @@ -80,9 +99,30 @@ contract TestMixinStakingPoolRewards is ); } - // Advance the epoch. - function advanceEpoch() external { - currentEpoch += 1; + // Set `currentEpoch`. + function setCurrentEpoch(uint256 epoch) external { + currentEpoch = epoch; + } + + // Expose `_syncPoolRewards()` for testing. + function syncPoolRewards( + bytes32 poolId, + uint256 reward, + uint256 membersStake + ) + external + returns (uint256 operatorReward, uint256 membersReward) + { + return _syncPoolRewards(poolId, reward, membersStake); + } + + // Access `_delegatedStakeToPoolByOwner` + function delegatedStakeToPoolByOwner(address member, bytes32 poolId) + external + view + returns (IStructs.StoredBalance memory balance) + { + return _delegatedStakeToPoolByOwner[member][poolId]; } // Set `_delegatedStakeToPoolByOwner` @@ -140,6 +180,16 @@ contract TestMixinStakingPoolRewards is ); } + // Overridden to just increase `currentEpoch`. + function _loadCurrentBalance(IStructs.StoredBalance storage balancePtr) + internal + view + returns (IStructs.StoredBalance memory balance) + { + balance = balancePtr; + balance.currentEpoch += 1; + } + // Overridden to revert if a pool has unfinalized rewards. function _assertPoolFinalizedLastEpoch(bytes32 poolId) internal diff --git a/contracts/staking/package.json b/contracts/staking/package.json index 1d87f81e1c..dc92524bfe 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|Staking|StakingProxy|TestAssertStorageParams|TestCobbDouglas|TestCumulativeRewardTracking|TestDelegatorRewards|TestExchangeManager|TestFinalizer|TestInitTarget|TestLibFixedMath|TestLibProxy|TestLibProxyReceiver|TestLibSafeDowncast|TestMixinParams|TestMixinStake|TestMixinStakeStorage|TestMixinStakingPool|TestMixinStakingPoolRewards|TestProtocolFees|TestStaking|TestStakingNoWETH|TestStakingProxy|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|TestMixinStake|TestMixinStakeBalances|TestMixinStakeStorage|TestMixinStakingPool|TestMixinStakingPoolRewards|TestProtocolFees|TestProxyDestination|TestStaking|TestStakingNoWETH|TestStakingProxy|TestStakingProxyUnit|TestStorageLayoutAndConstants|ZrxVault).json" }, "repository": { "type": "git", @@ -50,6 +50,7 @@ "homepage": "https://github.com/0xProject/0x-monorepo/contracts/tokens/README.md", "devDependencies": { "@0x/abi-gen": "^4.3.0-beta.0", + "@0x/contracts-exchange-libs": "^3.1.0-beta.0", "@0x/contracts-gen": "^1.1.0-beta.0", "@0x/contracts-test-utils": "^3.2.0-beta.0", "@0x/dev-utils": "^2.4.0-beta.0", diff --git a/contracts/staking/test/unit_tests/mixin_staking_pool_rewards.ts b/contracts/staking/test/unit_tests/mixin_staking_pool_rewards.ts index 8e10465352..6c196ebff4 100644 --- a/contracts/staking/test/unit_tests/mixin_staking_pool_rewards.ts +++ b/contracts/staking/test/unit_tests/mixin_staking_pool_rewards.ts @@ -1,22 +1,31 @@ -import { blockchainTests, expect, Numberish } from '@0x/contracts-test-utils'; -import { StakingRevertErrors } from '@0x/order-utils'; +import { ReferenceFunctions } from '@0x/contracts-exchange-libs'; +import { + blockchainTests, + constants, + expect, + getRandomInteger, + getRandomPortion, + hexRandom, + Numberish, + randomAddress, + TransactionHelper, + verifyEventsFromLogs, +} from '@0x/contracts-test-utils'; import { BigNumber } from '@0x/utils'; +import { LogEntry, TransactionReceiptWithDecodedLogs } from 'ethereum-types'; import { StoredBalance } from '../utils/types'; -import { - artifacts, - TestMixinStakingPoolRewardsContract, - TestMixinStakingPoolRewardsEvents, - TestMixinStakingPoolRewardsUpdateCumulativeRewardEventArgs as UpdateCumulativeReward, -} from '../../src'; -import { constants } from '../utils/constants'; +import { artifacts, TestMixinStakingPoolRewardsContract, TestMixinStakingPoolRewardsEvents as Events } from '../../src'; -blockchainTests.resets.only('MixinStakingPoolRewards unit tests', env => { +blockchainTests.resets('MixinStakingPoolRewards unit tests', env => { let testContract: TestMixinStakingPoolRewardsContract; + let txHelper: TransactionHelper; - const INITIAL_EPOCH = 0; - const NEXT_EPOCH = 1; + const POOL_ID = hexRandom(); + const OPERATOR = randomAddress(); + const OPERATOR_SHARE = getRandomInteger(1, constants.PPM_100_PERCENT); + let caller: string; before(async () => { testContract = await TestMixinStakingPoolRewardsContract.deployFrom0xArtifactAsync( @@ -25,11 +34,388 @@ blockchainTests.resets.only('MixinStakingPoolRewards unit tests', env => { env.txDefaults, artifacts, ); + await testContract.setPool.awaitTransactionSuccessAsync(POOL_ID, { + operator: OPERATOR, + operatorShare: OPERATOR_SHARE, + }); + [caller] = await env.getAccountAddressesAsync(); + txHelper = new TransactionHelper(env.web3Wrapper, artifacts); }); - describe('withdrawAndSyncDelegatorRewards()', () => { - it('poop', async () => { - // no-op + async function setUnfinalizedPoolRewardsAsync( + poolId: string, + reward: Numberish, + membersStake: Numberish, + ): Promise { + await testContract.setUnfinalizedPoolRewards.awaitTransactionSuccessAsync( + poolId, + new BigNumber(reward), + new BigNumber(membersStake), + ); + } + + // Set the delegated stake of a delegator in a pool. + // Omitted fields will be randomly generated. + async function setStakeAsync( + poolId: string, + delegator: string, + stake?: Partial, + ): Promise { + const _stake = { + currentEpoch: getRandomInteger(1, 4e9), + currentEpochBalance: getRandomInteger(1, 1e18), + nextEpochBalance: getRandomInteger(1, 1e18), + ...stake, + }; + await testContract.setDelegatedStakeToPoolByOwner.awaitTransactionSuccessAsync(delegator, poolId, { + currentEpoch: _stake.currentEpoch, + currentEpochBalance: _stake.currentEpochBalance, + nextEpochBalance: _stake.nextEpochBalance, + }); + return _stake; + } + + // Sets up state for a call to `_computeDelegatorReward()` and return the + // finalized rewards it will compute. + async function setComputeDelegatorRewardStateAsync( + poolId: string, + delegator: string, + finalizedReward?: Numberish, + ): Promise { + const stake = await testContract.delegatedStakeToPoolByOwner.callAsync(delegator, poolId); + // Split the rewards up across the two calls to `_computeMemberRewardOverInterval()` + const reward = finalizedReward === undefined ? getRandomInteger(1, 1e18) : new BigNumber(finalizedReward); + const oldRewards = getRandomPortion(reward); + await testContract.setMemberRewardsOverInterval.awaitTransactionSuccessAsync( + poolId, + stake.currentEpochBalance, + stake.currentEpoch, + stake.currentEpoch.plus(1), + oldRewards, + ); + const newRewards = reward.minus(oldRewards); + await testContract.setMemberRewardsOverInterval.awaitTransactionSuccessAsync( + poolId, + stake.nextEpochBalance, + stake.currentEpoch.plus(1), + await testContract.currentEpoch.callAsync(), + newRewards, + ); + return reward; + } + + function toOperatorPortion(operatorShare: Numberish, reward: Numberish): BigNumber { + return ReferenceFunctions.getPartialAmountCeil( + new BigNumber(operatorShare), + new BigNumber(constants.PPM_DENOMINATOR), + new BigNumber(reward), + ); + } + + function toMembersPortion(operatorShare: Numberish, reward: Numberish): BigNumber { + return new BigNumber(reward).minus(toOperatorPortion(operatorShare, reward)); + } + + describe('withdrawDelegatorRewards()', () => { + const POOL_REWARD = getRandomInteger(1, 100e18); + const WETH_RESERVED_FOR_POOL_REWARDS = POOL_REWARD.plus(getRandomInteger(1, 100e18)); + let stake: StoredBalance; + + before(async () => { + stake = await setStakeAsync(POOL_ID, caller); + await testContract.setPoolRewards.awaitTransactionSuccessAsync(POOL_ID, POOL_REWARD); + await testContract.setWethReservedForPoolRewards.awaitTransactionSuccessAsync( + WETH_RESERVED_FOR_POOL_REWARDS, + ); + }); + + async function withdrawDelegatorRewardsAsync(): Promise { + return testContract.withdrawDelegatorRewards.awaitTransactionSuccessAsync(POOL_ID); + } + + it('reverts if the pool is not finalized', async () => { + await setUnfinalizedPoolRewardsAsync(POOL_ID, 0, 1); + const tx = withdrawDelegatorRewardsAsync(); + return expect(tx).to.revertWith('POOL_NOT_FINALIZED'); + }); + it('calls `_updateCumulativeReward()`', async () => { + const { logs } = await withdrawDelegatorRewardsAsync(); + verifyEventsFromLogs(logs, [{ poolId: POOL_ID }], Events.UpdateCumulativeReward); + }); + it('transfers finalized rewards to the sender', async () => { + const finalizedReward = getRandomPortion(POOL_REWARD); + await setComputeDelegatorRewardStateAsync(POOL_ID, caller, finalizedReward); + const { logs } = await withdrawDelegatorRewardsAsync(); + verifyEventsFromLogs( + logs, + [{ _from: testContract.address, _to: caller, _value: finalizedReward }], + Events.Transfer, + ); + }); + it('reduces `rewardsByPoolId` for the pool', async () => { + const finalizedReward = getRandomPortion(POOL_REWARD); + await setComputeDelegatorRewardStateAsync(POOL_ID, caller, finalizedReward); + await withdrawDelegatorRewardsAsync(); + const poolReward = await testContract.rewardsByPoolId.callAsync(POOL_ID); + expect(poolReward).to.bignumber.eq(POOL_REWARD.minus(finalizedReward)); + }); + it('reduces `wethReservedForPoolRewards` for the pool', async () => { + const finalizedReward = getRandomPortion(POOL_REWARD); + await setComputeDelegatorRewardStateAsync(POOL_ID, caller, finalizedReward); + await withdrawDelegatorRewardsAsync(); + const wethReserved = await testContract.wethReservedForPoolRewards.callAsync(); + expect(wethReserved).to.bignumber.eq(WETH_RESERVED_FOR_POOL_REWARDS.minus(finalizedReward)); + }); + it('syncs `_delegatedStakeToPoolByOwner`', async () => { + await setComputeDelegatorRewardStateAsync(POOL_ID, caller, getRandomPortion(POOL_REWARD)); + await withdrawDelegatorRewardsAsync(); + const stakeAfter = await testContract.delegatedStakeToPoolByOwner.callAsync(caller, POOL_ID); + // `_loadCurrentBalance` is overridden to just increment `currentEpoch`. + expect(stakeAfter).to.deep.eq({ + currentEpoch: stake.currentEpoch.plus(1), + currentEpochBalance: stake.currentEpochBalance, + nextEpochBalance: stake.nextEpochBalance, + }); + }); + it('does not transfer zero rewards', async () => { + await setComputeDelegatorRewardStateAsync(POOL_ID, caller, 0); + const { logs } = await withdrawDelegatorRewardsAsync(); + verifyEventsFromLogs(logs, [], Events.Transfer); + }); + it('no rewards if the delegated stake epoch == current epoch', async () => { + // Set some finalized rewards that should be ignored. + await setComputeDelegatorRewardStateAsync(POOL_ID, caller, getRandomInteger(1, POOL_REWARD)); + await testContract.setCurrentEpoch.awaitTransactionSuccessAsync(stake.currentEpoch); + const { logs } = await withdrawDelegatorRewardsAsync(); + // There will be no Transfer events if computed rewards are zero. + verifyEventsFromLogs(logs, [], Events.Transfer); + }); + }); + + describe('computeRewardBalanceOfOperator()', () => { + async function computeRewardBalanceOfOperatorAsync(): Promise { + return testContract.computeRewardBalanceOfOperator.callAsync(POOL_ID); + } + + it('returns only unfinalized rewards', async () => { + const unfinalizedReward = getRandomInteger(1, 1e18); + await setUnfinalizedPoolRewardsAsync(POOL_ID, unfinalizedReward, getRandomInteger(1, 1e18)); + // Set some unfinalized state for a call to `_computeDelegatorReward()`, + // which should not be called. + await setComputeDelegatorRewardStateAsync(POOL_ID, OPERATOR, getRandomInteger(1, 1e18)); + const reward = await computeRewardBalanceOfOperatorAsync(); + const expectedReward = toOperatorPortion(OPERATOR_SHARE, unfinalizedReward); + expect(reward).to.bignumber.eq(expectedReward); + }); + it('returns operator portion of unfinalized rewards', async () => { + const unfinalizedReward = getRandomInteger(1, 1e18); + await setUnfinalizedPoolRewardsAsync(POOL_ID, unfinalizedReward, getRandomInteger(1, 1e18)); + const reward = await computeRewardBalanceOfOperatorAsync(); + const expectedReward = toOperatorPortion(OPERATOR_SHARE, unfinalizedReward); + expect(reward).to.bignumber.eq(expectedReward); + }); + it('returns zero if no unfinalized rewards', async () => { + await setUnfinalizedPoolRewardsAsync(POOL_ID, 0, getRandomInteger(1, 1e18)); + const reward = await computeRewardBalanceOfOperatorAsync(); + expect(reward).to.bignumber.eq(0); + }); + it('returns all unfinalized reward if member stake is zero', async () => { + const unfinalizedReward = getRandomInteger(1, 1e18); + await setUnfinalizedPoolRewardsAsync(POOL_ID, unfinalizedReward, 0); + const reward = await computeRewardBalanceOfOperatorAsync(); + expect(reward).to.bignumber.eq(unfinalizedReward); + }); + it('returns no reward if operator share is zero', async () => { + await testContract.setPool.awaitTransactionSuccessAsync(POOL_ID, { + operator: OPERATOR, + operatorShare: constants.ZERO_AMOUNT, + }); + await setUnfinalizedPoolRewardsAsync(POOL_ID, getRandomInteger(1, 1e18), getRandomInteger(1, 1e18)); + const reward = await computeRewardBalanceOfOperatorAsync(); + expect(reward).to.bignumber.eq(0); + }); + it('returns all unfinalized reward if operator share is 100%', async () => { + await testContract.setPool.awaitTransactionSuccessAsync(POOL_ID, { + operator: OPERATOR, + operatorShare: constants.PPM_100_PERCENT, + }); + const unfinalizedReward = getRandomInteger(1, 1e18); + await setUnfinalizedPoolRewardsAsync(POOL_ID, unfinalizedReward, getRandomInteger(1, 1e18)); + const reward = await computeRewardBalanceOfOperatorAsync(); + expect(reward).to.bignumber.eq(unfinalizedReward); + }); + }); + + describe('computeRewardBalanceOfDelegator()', () => { + const DELEGATOR = randomAddress(); + let currentEpoch: BigNumber; + let stake: StoredBalance; + + before(async () => { + currentEpoch = await testContract.currentEpoch.callAsync(); + stake = await setStakeAsync(POOL_ID, DELEGATOR); + }); + + async function computeRewardBalanceOfDelegatorAsync(): Promise { + return testContract.computeRewardBalanceOfDelegator.callAsync(POOL_ID, DELEGATOR); + } + + function getDelegatorPortionOfUnfinalizedReward( + unfinalizedReward: Numberish, + unfinalizedMembersStake: Numberish, + ): BigNumber { + const unfinalizedStakeBalance = stake.currentEpoch.gte(currentEpoch) + ? stake.currentEpochBalance + : stake.nextEpochBalance; + return ReferenceFunctions.getPartialAmountFloor( + unfinalizedStakeBalance, + new BigNumber(unfinalizedMembersStake), + toMembersPortion(OPERATOR_SHARE, unfinalizedReward), + ); + } + + it('returns zero when no finalized or unfinalized rewards', async () => { + const reward = await computeRewardBalanceOfDelegatorAsync(); + expect(reward).to.bignumber.eq(0); + }); + it('returns only unfinalized rewards when no finalized rewards', async () => { + const unfinalizedReward = getRandomInteger(1, 1e18); + const unfinalizedMembersStake = getRandomInteger(1, 1e18); + await setUnfinalizedPoolRewardsAsync(POOL_ID, unfinalizedReward, unfinalizedMembersStake); + const expectedReward = getDelegatorPortionOfUnfinalizedReward(unfinalizedReward, unfinalizedMembersStake); + const reward = await computeRewardBalanceOfDelegatorAsync(); + expect(reward).to.bignumber.eq(expectedReward); + }); + it("returns zero when delegator's synced stake was zero in the last epoch and no finalized rewards", async () => { + await setStakeAsync(POOL_ID, DELEGATOR, { + ...stake, + currentEpoch: currentEpoch.minus(1), + currentEpochBalance: constants.ZERO_AMOUNT, + }); + await setUnfinalizedPoolRewardsAsync(POOL_ID, getRandomInteger(1, 1e18), getRandomInteger(1, 1e18)); + const reward = await computeRewardBalanceOfDelegatorAsync(); + expect(reward).to.bignumber.eq(0); + }); + it("returns zero when delegator's unsynced stake was zero in the last epoch and no finalized rewards", async () => { + const epoch = 2; + await setStakeAsync(POOL_ID, DELEGATOR, { + ...stake, + currentEpoch: new BigNumber(epoch - 2), + nextEpochBalance: constants.ZERO_AMOUNT, + }); + await testContract.setCurrentEpoch.awaitTransactionSuccessAsync(new BigNumber(epoch)); + await setUnfinalizedPoolRewardsAsync(POOL_ID, getRandomInteger(1, 1e18), getRandomInteger(1, 1e18)); + const reward = await computeRewardBalanceOfDelegatorAsync(); + expect(reward).to.bignumber.eq(0); + }); + it('returns only finalized rewards when no unfinalized rewards', async () => { + const finalizedReward = getRandomInteger(1, 1e18); + await setComputeDelegatorRewardStateAsync(POOL_ID, DELEGATOR, finalizedReward); + const reward = await computeRewardBalanceOfDelegatorAsync(); + expect(reward).to.bignumber.eq(finalizedReward); + }); + it('returns both unfinalized and finalized rewards', async () => { + const unfinalizedReward = getRandomInteger(1, 1e18); + const unfinalizedMembersStake = getRandomInteger(1, 1e18); + await setUnfinalizedPoolRewardsAsync(POOL_ID, unfinalizedReward, unfinalizedMembersStake); + const finalizedReward = getRandomInteger(1, 1e18); + await setComputeDelegatorRewardStateAsync(POOL_ID, DELEGATOR, finalizedReward); + const delegatorUnfinalizedReward = getDelegatorPortionOfUnfinalizedReward( + unfinalizedReward, + unfinalizedMembersStake, + ); + const expectedReward = delegatorUnfinalizedReward.plus(finalizedReward); + const reward = await computeRewardBalanceOfDelegatorAsync(); + expect(reward).to.bignumber.eq(expectedReward); + }); + }); + + describe('_syncPoolRewards()', async () => { + const POOL_REWARD = getRandomInteger(1, 100e18); + const WETH_RESERVED_FOR_POOL_REWARDS = POOL_REWARD.plus(getRandomInteger(1, 100e18)); + + before(async () => { + await testContract.setPoolRewards.awaitTransactionSuccessAsync(POOL_ID, POOL_REWARD); + await testContract.setWethReservedForPoolRewards.awaitTransactionSuccessAsync( + WETH_RESERVED_FOR_POOL_REWARDS, + ); + }); + + async function syncPoolRewardsAsync( + reward: Numberish, + membersStake: Numberish, + ): Promise<[[BigNumber, BigNumber], LogEntry[]]> { + const [result, receipt] = await txHelper.getResultAndReceiptAsync( + testContract.syncPoolRewards, + POOL_ID, + new BigNumber(reward), + new BigNumber(membersStake), + ); + return [result, receipt.logs]; + } + + it("transfers operator's portion of the reward to the operator", async () => { + const totalReward = getRandomInteger(1, 1e18); + const membersStake = getRandomInteger(1, 1e18); + const [, logs] = await syncPoolRewardsAsync(totalReward, membersStake); + const expectedOperatorReward = toOperatorPortion(OPERATOR_SHARE, totalReward); + verifyEventsFromLogs( + logs, + [{ _from: testContract.address, _to: OPERATOR, _value: expectedOperatorReward }], + Events.Transfer, + ); + }); + it("increases `rewardsByPoolId` with members' portion of rewards", async () => { + const totalReward = getRandomInteger(1, 1e18); + const membersStake = getRandomInteger(1, 1e18); + await syncPoolRewardsAsync(totalReward, membersStake); + const expectedMembersReward = toMembersPortion(OPERATOR_SHARE, totalReward); + const poolReward = await testContract.rewardsByPoolId.callAsync(POOL_ID); + expect(poolReward).to.bignumber.eq(POOL_REWARD.plus(expectedMembersReward)); + }); + it("increases `wethReservedForPoolRewards` with members' portion of rewards", async () => { + const totalReward = getRandomInteger(1, 1e18); + const membersStake = getRandomInteger(1, 1e18); + await syncPoolRewardsAsync(totalReward, membersStake); + const expectedMembersReward = toMembersPortion(OPERATOR_SHARE, totalReward); + const wethReserved = await testContract.wethReservedForPoolRewards.callAsync(); + expect(wethReserved).to.bignumber.eq(WETH_RESERVED_FOR_POOL_REWARDS.plus(expectedMembersReward)); + }); + it("returns operator and members' portion of the reward", async () => { + const totalReward = getRandomInteger(1, 1e18); + const membersStake = getRandomInteger(1, 1e18); + const [[operatorReward, membersReward]] = await syncPoolRewardsAsync(totalReward, membersStake); + const expectedOperatorReward = toOperatorPortion(OPERATOR_SHARE, totalReward); + const expectedMembersReward = toMembersPortion(OPERATOR_SHARE, totalReward); + expect(operatorReward).to.bignumber.eq(expectedOperatorReward); + expect(membersReward).to.bignumber.eq(expectedMembersReward); + }); + it("gives all rewards to operator if members' stake is zero", async () => { + const totalReward = getRandomInteger(1, 1e18); + const [[operatorReward, membersReward], logs] = await syncPoolRewardsAsync(totalReward, 0); + expect(operatorReward).to.bignumber.eq(totalReward); + expect(membersReward).to.bignumber.eq(0); + verifyEventsFromLogs( + logs, + [{ _from: testContract.address, _to: OPERATOR, _value: totalReward }], + Events.Transfer, + ); + }); + it("gives all rewards to members if operator's share is zero", async () => { + const totalReward = getRandomInteger(1, 1e18); + await testContract.setPool.awaitTransactionSuccessAsync(POOL_ID, { + operator: OPERATOR, + operatorShare: constants.ZERO_AMOUNT, + }); + const [[operatorReward, membersReward], logs] = await syncPoolRewardsAsync( + totalReward, + getRandomInteger(1, 1e18), + ); + expect(operatorReward).to.bignumber.eq(0); + expect(membersReward).to.bignumber.eq(totalReward); + // Should be no transfer to the operator. + verifyEventsFromLogs(logs, [], Events.Transfer); }); }); }); +// tslint:disable: max-file-line-count From cba72c811d4ad1248d9dec9378f7ec96fa25e7f3 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Mon, 4 Nov 2019 10:06:01 -0500 Subject: [PATCH 3/6] `@0x/contracts-staking`: Add `_computePoolRewardsSplit()` tests to `MixinStakingPoolRewards` unit tests. --- .../test/TestMixinStakingPoolRewards.sol | 17 +++++ .../unit_tests/mixin_staking_pool_rewards.ts | 63 +++++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/contracts/staking/contracts/test/TestMixinStakingPoolRewards.sol b/contracts/staking/contracts/test/TestMixinStakingPoolRewards.sol index b11b42b0fc..5b6cd3a8e0 100644 --- a/contracts/staking/contracts/test/TestMixinStakingPoolRewards.sol +++ b/contracts/staking/contracts/test/TestMixinStakingPoolRewards.sol @@ -116,6 +116,23 @@ contract TestMixinStakingPoolRewards is return _syncPoolRewards(poolId, reward, membersStake); } + // Expose `_computePoolRewardsSplit()` for testing. + function computePoolRewardsSplit( + uint32 operatorShare, + uint256 totalReward, + uint256 membersStake + ) + external + pure + returns (uint256 operatorReward, uint256 membersReward) + { + return _computePoolRewardsSplit( + operatorShare, + totalReward, + membersStake + ); + } + // Access `_delegatedStakeToPoolByOwner` function delegatedStakeToPoolByOwner(address member, bytes32 poolId) external diff --git a/contracts/staking/test/unit_tests/mixin_staking_pool_rewards.ts b/contracts/staking/test/unit_tests/mixin_staking_pool_rewards.ts index 6c196ebff4..39fe97ace1 100644 --- a/contracts/staking/test/unit_tests/mixin_staking_pool_rewards.ts +++ b/contracts/staking/test/unit_tests/mixin_staking_pool_rewards.ts @@ -417,5 +417,68 @@ blockchainTests.resets('MixinStakingPoolRewards unit tests', env => { verifyEventsFromLogs(logs, [], Events.Transfer); }); }); + + describe('_computePoolRewardsSplit', () => { + it("gives all rewards to operator if members' stake is zero", async () => { + const operatorShare = getRandomPortion(constants.PPM_100_PERCENT); + const totalReward = getRandomInteger(1, 1e18); + const membersStake = constants.ZERO_AMOUNT; + const [operatorReward, membersReward] = await testContract.computePoolRewardsSplit.callAsync( + operatorShare, + totalReward, + membersStake, + ); + expect(operatorReward).to.bignumber.eq(totalReward); + expect(membersReward).to.bignumber.eq(0); + }); + it("gives all rewards to operator if members' stake is zero and operator share is zero", async () => { + const operatorShare = constants.ZERO_AMOUNT; + const totalReward = getRandomInteger(1, 1e18); + const membersStake = constants.ZERO_AMOUNT; + const [operatorReward, membersReward] = await testContract.computePoolRewardsSplit.callAsync( + operatorShare, + totalReward, + membersStake, + ); + expect(operatorReward).to.bignumber.eq(totalReward); + expect(membersReward).to.bignumber.eq(0); + }); + it('gives all rewards to operator if operator share is 100%', async () => { + const operatorShare = constants.PPM_100_PERCENT; + const totalReward = getRandomInteger(1, 1e18); + const membersStake = getRandomInteger(1, 1e18); + const [operatorReward, membersReward] = await testContract.computePoolRewardsSplit.callAsync( + operatorShare, + totalReward, + membersStake, + ); + expect(operatorReward).to.bignumber.eq(totalReward); + expect(membersReward).to.bignumber.eq(0); + }); + it('gives all rewards to members if operator share is 0%', async () => { + const operatorShare = constants.ZERO_AMOUNT; + const totalReward = getRandomInteger(1, 1e18); + const membersStake = getRandomInteger(1, 1e18); + const [operatorReward, membersReward] = await testContract.computePoolRewardsSplit.callAsync( + operatorShare, + totalReward, + membersStake, + ); + expect(operatorReward).to.bignumber.eq(0); + expect(membersReward).to.bignumber.eq(totalReward); + }); + it('splits rewards between operator and members based on operator share', async () => { + const operatorShare = getRandomPortion(constants.PPM_100_PERCENT); + const totalReward = getRandomInteger(1, 1e18); + const membersStake = getRandomInteger(1, 1e18); + const [operatorReward, membersReward] = await testContract.computePoolRewardsSplit.callAsync( + operatorShare, + totalReward, + membersStake, + ); + expect(operatorReward).to.bignumber.eq(toOperatorPortion(operatorShare, totalReward)); + expect(membersReward).to.bignumber.eq(toMembersPortion(operatorShare, totalReward)); + }); + }); }); // tslint:disable: max-file-line-count From 15c3c8074c7915810f68c6e1dc242f7e109cbb0f Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Mon, 4 Nov 2019 10:24:07 -0500 Subject: [PATCH 4/6] `@0x/contracts-staking`: Add separate unit tests for `withdrawDelegatorRewards()` and `_withdrawAndSyncDelegatorRewards()`. --- .../test/TestMixinStakingPoolRewards.sol | 33 +++++++++++++ .../unit_tests/mixin_staking_pool_rewards.ts | 46 +++++++++++-------- 2 files changed, 60 insertions(+), 19 deletions(-) diff --git a/contracts/staking/contracts/test/TestMixinStakingPoolRewards.sol b/contracts/staking/contracts/test/TestMixinStakingPoolRewards.sol index 5b6cd3a8e0..14400d95af 100644 --- a/contracts/staking/contracts/test/TestMixinStakingPoolRewards.sol +++ b/contracts/staking/contracts/test/TestMixinStakingPoolRewards.sol @@ -26,10 +26,16 @@ import "./TestStakingNoWETH.sol"; contract TestMixinStakingPoolRewards is TestStakingNoWETH { + // solhint-disable no-simple-event-func-name event UpdateCumulativeReward( bytes32 poolId ); + event WithdrawAndSyncDelegatorRewards( + bytes32 poolId, + address member + ); + struct UnfinalizedPoolReward { uint256 reward; uint256 membersStake; @@ -116,6 +122,19 @@ contract TestMixinStakingPoolRewards is return _syncPoolRewards(poolId, reward, membersStake); } + // Expose `_withdrawAndSyncDelegatorRewards()` for testing. + function withdrawAndSyncDelegatorRewards( + bytes32 poolId, + address member + ) + external + { + return _withdrawAndSyncDelegatorRewards( + poolId, + member + ); + } + // Expose `_computePoolRewardsSplit()` for testing. function computePoolRewardsSplit( uint32 operatorShare, @@ -163,6 +182,20 @@ contract TestMixinStakingPoolRewards is _poolById[poolId] = pool; } + // Overridden to emit an event. + function _withdrawAndSyncDelegatorRewards( + bytes32 poolId, + address member + ) + internal + { + emit WithdrawAndSyncDelegatorRewards(poolId, member); + return MixinStakingPoolRewards._withdrawAndSyncDelegatorRewards( + poolId, + member + ); + } + // Overridden to use `_memberRewardsOverInterval` function _computeMemberRewardOverInterval( bytes32 poolId, diff --git a/contracts/staking/test/unit_tests/mixin_staking_pool_rewards.ts b/contracts/staking/test/unit_tests/mixin_staking_pool_rewards.ts index 39fe97ace1..782936d13a 100644 --- a/contracts/staking/test/unit_tests/mixin_staking_pool_rewards.ts +++ b/contracts/staking/test/unit_tests/mixin_staking_pool_rewards.ts @@ -117,59 +117,67 @@ blockchainTests.resets('MixinStakingPoolRewards unit tests', env => { } describe('withdrawDelegatorRewards()', () => { + it('calls `_withdrawAndSyncDelegatorRewards()` with the sender as the member', async () => { + const { logs } = await testContract.withdrawDelegatorRewards.awaitTransactionSuccessAsync(POOL_ID); + verifyEventsFromLogs(logs, [{ poolId: POOL_ID, member: caller }], Events.WithdrawAndSyncDelegatorRewards); + }); + }); + + describe('_withdrawAndSyncDelegatorRewards()', () => { const POOL_REWARD = getRandomInteger(1, 100e18); const WETH_RESERVED_FOR_POOL_REWARDS = POOL_REWARD.plus(getRandomInteger(1, 100e18)); + const DELEGATOR = randomAddress(); let stake: StoredBalance; before(async () => { - stake = await setStakeAsync(POOL_ID, caller); + stake = await setStakeAsync(POOL_ID, DELEGATOR); await testContract.setPoolRewards.awaitTransactionSuccessAsync(POOL_ID, POOL_REWARD); await testContract.setWethReservedForPoolRewards.awaitTransactionSuccessAsync( WETH_RESERVED_FOR_POOL_REWARDS, ); }); - async function withdrawDelegatorRewardsAsync(): Promise { - return testContract.withdrawDelegatorRewards.awaitTransactionSuccessAsync(POOL_ID); + async function withdrawAndSyncDelegatorRewardsAsync(): Promise { + return testContract.withdrawAndSyncDelegatorRewards.awaitTransactionSuccessAsync(POOL_ID, DELEGATOR); } it('reverts if the pool is not finalized', async () => { await setUnfinalizedPoolRewardsAsync(POOL_ID, 0, 1); - const tx = withdrawDelegatorRewardsAsync(); + const tx = withdrawAndSyncDelegatorRewardsAsync(); return expect(tx).to.revertWith('POOL_NOT_FINALIZED'); }); it('calls `_updateCumulativeReward()`', async () => { - const { logs } = await withdrawDelegatorRewardsAsync(); + const { logs } = await withdrawAndSyncDelegatorRewardsAsync(); verifyEventsFromLogs(logs, [{ poolId: POOL_ID }], Events.UpdateCumulativeReward); }); it('transfers finalized rewards to the sender', async () => { const finalizedReward = getRandomPortion(POOL_REWARD); - await setComputeDelegatorRewardStateAsync(POOL_ID, caller, finalizedReward); - const { logs } = await withdrawDelegatorRewardsAsync(); + await setComputeDelegatorRewardStateAsync(POOL_ID, DELEGATOR, finalizedReward); + const { logs } = await withdrawAndSyncDelegatorRewardsAsync(); verifyEventsFromLogs( logs, - [{ _from: testContract.address, _to: caller, _value: finalizedReward }], + [{ _from: testContract.address, _to: DELEGATOR, _value: finalizedReward }], Events.Transfer, ); }); it('reduces `rewardsByPoolId` for the pool', async () => { const finalizedReward = getRandomPortion(POOL_REWARD); - await setComputeDelegatorRewardStateAsync(POOL_ID, caller, finalizedReward); - await withdrawDelegatorRewardsAsync(); + await setComputeDelegatorRewardStateAsync(POOL_ID, DELEGATOR, finalizedReward); + await withdrawAndSyncDelegatorRewardsAsync(); const poolReward = await testContract.rewardsByPoolId.callAsync(POOL_ID); expect(poolReward).to.bignumber.eq(POOL_REWARD.minus(finalizedReward)); }); it('reduces `wethReservedForPoolRewards` for the pool', async () => { const finalizedReward = getRandomPortion(POOL_REWARD); - await setComputeDelegatorRewardStateAsync(POOL_ID, caller, finalizedReward); - await withdrawDelegatorRewardsAsync(); + await setComputeDelegatorRewardStateAsync(POOL_ID, DELEGATOR, finalizedReward); + await withdrawAndSyncDelegatorRewardsAsync(); const wethReserved = await testContract.wethReservedForPoolRewards.callAsync(); expect(wethReserved).to.bignumber.eq(WETH_RESERVED_FOR_POOL_REWARDS.minus(finalizedReward)); }); it('syncs `_delegatedStakeToPoolByOwner`', async () => { - await setComputeDelegatorRewardStateAsync(POOL_ID, caller, getRandomPortion(POOL_REWARD)); - await withdrawDelegatorRewardsAsync(); - const stakeAfter = await testContract.delegatedStakeToPoolByOwner.callAsync(caller, POOL_ID); + await setComputeDelegatorRewardStateAsync(POOL_ID, DELEGATOR, getRandomPortion(POOL_REWARD)); + await withdrawAndSyncDelegatorRewardsAsync(); + const stakeAfter = await testContract.delegatedStakeToPoolByOwner.callAsync(DELEGATOR, POOL_ID); // `_loadCurrentBalance` is overridden to just increment `currentEpoch`. expect(stakeAfter).to.deep.eq({ currentEpoch: stake.currentEpoch.plus(1), @@ -178,15 +186,15 @@ blockchainTests.resets('MixinStakingPoolRewards unit tests', env => { }); }); it('does not transfer zero rewards', async () => { - await setComputeDelegatorRewardStateAsync(POOL_ID, caller, 0); - const { logs } = await withdrawDelegatorRewardsAsync(); + await setComputeDelegatorRewardStateAsync(POOL_ID, DELEGATOR, 0); + const { logs } = await withdrawAndSyncDelegatorRewardsAsync(); verifyEventsFromLogs(logs, [], Events.Transfer); }); it('no rewards if the delegated stake epoch == current epoch', async () => { // Set some finalized rewards that should be ignored. - await setComputeDelegatorRewardStateAsync(POOL_ID, caller, getRandomInteger(1, POOL_REWARD)); + await setComputeDelegatorRewardStateAsync(POOL_ID, DELEGATOR, getRandomInteger(1, POOL_REWARD)); await testContract.setCurrentEpoch.awaitTransactionSuccessAsync(stake.currentEpoch); - const { logs } = await withdrawDelegatorRewardsAsync(); + const { logs } = await withdrawAndSyncDelegatorRewardsAsync(); // There will be no Transfer events if computed rewards are zero. verifyEventsFromLogs(logs, [], Events.Transfer); }); From c15c5e12b08d86d6416c9ba240b31750acf6e4cf Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Mon, 4 Nov 2019 11:09:21 -0500 Subject: [PATCH 5/6] `@0x/contracts-staking`: Fix event name collision in `MixinStakingPoolRewards` unit tests. --- .../staking/contracts/test/TestMixinStakingPoolRewards.sol | 2 +- contracts/staking/test/unit_tests/mixin_staking_pool_rewards.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/staking/contracts/test/TestMixinStakingPoolRewards.sol b/contracts/staking/contracts/test/TestMixinStakingPoolRewards.sol index 14400d95af..3bda161aac 100644 --- a/contracts/staking/contracts/test/TestMixinStakingPoolRewards.sol +++ b/contracts/staking/contracts/test/TestMixinStakingPoolRewards.sol @@ -33,7 +33,7 @@ contract TestMixinStakingPoolRewards is event WithdrawAndSyncDelegatorRewards( bytes32 poolId, - address member + address delegator ); struct UnfinalizedPoolReward { diff --git a/contracts/staking/test/unit_tests/mixin_staking_pool_rewards.ts b/contracts/staking/test/unit_tests/mixin_staking_pool_rewards.ts index 782936d13a..ab6f332488 100644 --- a/contracts/staking/test/unit_tests/mixin_staking_pool_rewards.ts +++ b/contracts/staking/test/unit_tests/mixin_staking_pool_rewards.ts @@ -119,7 +119,7 @@ blockchainTests.resets('MixinStakingPoolRewards unit tests', env => { describe('withdrawDelegatorRewards()', () => { it('calls `_withdrawAndSyncDelegatorRewards()` with the sender as the member', async () => { const { logs } = await testContract.withdrawDelegatorRewards.awaitTransactionSuccessAsync(POOL_ID); - verifyEventsFromLogs(logs, [{ poolId: POOL_ID, member: caller }], Events.WithdrawAndSyncDelegatorRewards); + verifyEventsFromLogs(logs, [{ poolId: POOL_ID, delegator: caller }], Events.WithdrawAndSyncDelegatorRewards); }); }); From c957b48281a193f1f1eaf3f0d5641fb3201c7ac7 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Mon, 4 Nov 2019 11:10:07 -0500 Subject: [PATCH 6/6] `@0x/contracts-staking`: Run prettier. --- .../staking/test/unit_tests/mixin_staking_pool_rewards.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/contracts/staking/test/unit_tests/mixin_staking_pool_rewards.ts b/contracts/staking/test/unit_tests/mixin_staking_pool_rewards.ts index ab6f332488..88e344f3be 100644 --- a/contracts/staking/test/unit_tests/mixin_staking_pool_rewards.ts +++ b/contracts/staking/test/unit_tests/mixin_staking_pool_rewards.ts @@ -119,7 +119,11 @@ blockchainTests.resets('MixinStakingPoolRewards unit tests', env => { describe('withdrawDelegatorRewards()', () => { it('calls `_withdrawAndSyncDelegatorRewards()` with the sender as the member', async () => { const { logs } = await testContract.withdrawDelegatorRewards.awaitTransactionSuccessAsync(POOL_ID); - verifyEventsFromLogs(logs, [{ poolId: POOL_ID, delegator: caller }], Events.WithdrawAndSyncDelegatorRewards); + verifyEventsFromLogs( + logs, + [{ poolId: POOL_ID, delegator: caller }], + Events.WithdrawAndSyncDelegatorRewards, + ); }); });