diff --git a/contracts/integrations/test/framework/actors/keeper.ts b/contracts/integrations/test/framework/actors/keeper.ts index af1f589783..b77376fe29 100644 --- a/contracts/integrations/test/framework/actors/keeper.ts +++ b/contracts/integrations/test/framework/actors/keeper.ts @@ -100,9 +100,9 @@ export function KeeperMixin(Base: TBase): TBase & Con private async *_validEndEpoch(): AsyncIterableIterator { const assertion = validEndEpochAssertion(this.actor.deployment, this.actor.simulationEnvironment!); - const { currentEpoch } = this.actor.simulationEnvironment!; const { stakingWrapper } = this.actor.deployment.staking; while (true) { + const { currentEpoch } = this.actor.simulationEnvironment!; const aggregatedStats = AggregatedStats.fromArray( await stakingWrapper.aggregatedStatsByEpoch(currentEpoch.minus(1)).callAsync(), ); diff --git a/contracts/integrations/test/framework/actors/staker.ts b/contracts/integrations/test/framework/actors/staker.ts index e96ae55666..a1c9bbd876 100644 --- a/contracts/integrations/test/framework/actors/staker.ts +++ b/contracts/integrations/test/framework/actors/staker.ts @@ -71,8 +71,8 @@ export function StakerMixin(Base: TBase): TBase & Con private async *_validStake(): AsyncIterableIterator { const { zrx } = this.actor.deployment.tokens; - const { deployment, balanceStore, globalStake } = this.actor.simulationEnvironment!; - const assertion = validStakeAssertion(deployment, balanceStore, globalStake, this.stake); + const { deployment, balanceStore } = this.actor.simulationEnvironment!; + const assertion = validStakeAssertion(deployment, this.actor.simulationEnvironment!, this.stake); while (true) { await balanceStore.updateErc20BalancesAsync(); @@ -84,8 +84,8 @@ export function StakerMixin(Base: TBase): TBase & Con private async *_validUnstake(): AsyncIterableIterator { const { stakingWrapper } = this.actor.deployment.staking; - const { deployment, balanceStore, globalStake } = this.actor.simulationEnvironment!; - const assertion = validUnstakeAssertion(deployment, balanceStore, globalStake, this.stake); + const { deployment, balanceStore } = this.actor.simulationEnvironment!; + const assertion = validUnstakeAssertion(deployment, this.actor.simulationEnvironment!, this.stake); while (true) { await balanceStore.updateErc20BalancesAsync(); @@ -102,22 +102,23 @@ export function StakerMixin(Base: TBase): TBase & Con } private async *_validMoveStake(): AsyncIterableIterator { - const { deployment, globalStake, stakingPools } = this.actor.simulationEnvironment!; - const assertion = validMoveStakeAssertion(deployment, globalStake, this.stake, stakingPools); + const { deployment, stakingPools } = this.actor.simulationEnvironment!; + const assertion = validMoveStakeAssertion(deployment, this.actor.simulationEnvironment!, this.stake); while (true) { + const { currentEpoch } = this.actor.simulationEnvironment!; const fromPoolId = Pseudorandom.sample( Object.keys(_.omit(this.stake[StakeStatus.Delegated], ['total'])), ); const fromStatus = - fromPoolId === undefined + fromPoolId === undefined || stakingPools[fromPoolId].lastFinalized.isLessThan(currentEpoch.minus(1)) ? StakeStatus.Undelegated : (Pseudorandom.sample([StakeStatus.Undelegated, StakeStatus.Delegated]) as StakeStatus); const from = new StakeInfo(fromStatus, fromPoolId); const toPoolId = Pseudorandom.sample(Object.keys(stakingPools)); const toStatus = - toPoolId === undefined + toPoolId === undefined || stakingPools[toPoolId].lastFinalized.isLessThan(currentEpoch.minus(1)) ? StakeStatus.Undelegated : (Pseudorandom.sample([StakeStatus.Undelegated, StakeStatus.Delegated]) as StakeStatus); const to = new StakeInfo(toStatus, toPoolId); @@ -134,9 +135,17 @@ export function StakerMixin(Base: TBase): TBase & Con private async *_validWithdrawDelegatorRewards(): AsyncIterableIterator { const { stakingPools } = this.actor.simulationEnvironment!; - const assertion = validWithdrawDelegatorRewardsAssertion(this.actor.deployment, this.actor.simulationEnvironment!); + const assertion = validWithdrawDelegatorRewardsAssertion( + this.actor.deployment, + this.actor.simulationEnvironment!, + ); while (true) { - const poolId = Pseudorandom.sample(Object.keys(stakingPools)); + const prevEpoch = this.actor.simulationEnvironment!.currentEpoch.minus(1); + const poolId = Pseudorandom.sample( + Object.keys(stakingPools).filter(poolId => + stakingPools[poolId].lastFinalized.isGreaterThanOrEqualTo(prevEpoch), + ), + ); if (poolId === undefined) { yield; } else { diff --git a/contracts/integrations/test/framework/actors/taker.ts b/contracts/integrations/test/framework/actors/taker.ts index bb7b86589e..5fa803dbe5 100644 --- a/contracts/integrations/test/framework/actors/taker.ts +++ b/contracts/integrations/test/framework/actors/taker.ts @@ -83,11 +83,9 @@ export function TakerMixin(Base: TBase): TBase & Cons token: DummyERC20TokenContract, ): Promise => { let balance = balanceStore.balances.erc20[owner.address][token.address]; - if (balance === undefined || balance.isZero()) { - await owner.configureERC20TokenAsync(token); - balance = balanceStore.balances.erc20[owner.address][token.address] = - constants.INITIAL_ERC20_BALANCE; - } + await owner.configureERC20TokenAsync(token); + balance = balanceStore.balances.erc20[owner.address][token.address] = + constants.INITIAL_ERC20_BALANCE; return Pseudorandom.integer(balance.dividedToIntegerBy(2)); }; diff --git a/contracts/integrations/test/framework/assertions/createStakingPool.ts b/contracts/integrations/test/framework/assertions/createStakingPool.ts index 581f443775..078c748451 100644 --- a/contracts/integrations/test/framework/assertions/createStakingPool.ts +++ b/contracts/integrations/test/framework/assertions/createStakingPool.ts @@ -37,6 +37,9 @@ export function validCreateStakingPoolAssertion( args: [number, boolean], txData: Partial, ) => { + // Ensure that the tx succeeded. + expect(result.success, `Error: ${result.data}`).to.be.true(); + const [operatorShare] = args; // Checks the logs for the new poolId, verifies that it is as expected diff --git a/contracts/integrations/test/framework/assertions/decreaseStakingPoolOperatorShare.ts b/contracts/integrations/test/framework/assertions/decreaseStakingPoolOperatorShare.ts index 2eb15675a3..86899164ab 100644 --- a/contracts/integrations/test/framework/assertions/decreaseStakingPoolOperatorShare.ts +++ b/contracts/integrations/test/framework/assertions/decreaseStakingPoolOperatorShare.ts @@ -17,7 +17,10 @@ export function validDecreaseStakingPoolOperatorShareAssertion( const { stakingWrapper } = deployment.staking; return new FunctionAssertion<[string, number], {}, void>(stakingWrapper, 'decreaseStakingPoolOperatorShare', { - after: async (_beforeInfo, _result: FunctionResult, args: [string, number], _txData: Partial) => { + after: async (_beforeInfo, result: FunctionResult, args: [string, number], _txData: Partial) => { + // Ensure that the tx succeeded. + expect(result.success, `Error: ${result.data}`).to.be.true(); + const [poolId, expectedOperatorShare] = args; // Checks that the on-chain pool's operator share has been updated. diff --git a/contracts/integrations/test/framework/assertions/endEpoch.ts b/contracts/integrations/test/framework/assertions/endEpoch.ts index 33be8c438a..122aa257b0 100644 --- a/contracts/integrations/test/framework/assertions/endEpoch.ts +++ b/contracts/integrations/test/framework/assertions/endEpoch.ts @@ -1,8 +1,14 @@ import { WETH9DepositEventArgs, WETH9Events } from '@0x/contracts-erc20'; -import { AggregatedStats, StakingEvents, StakingEpochEndedEventArgs } from '@0x/contracts-staking'; -import { expect, verifyEventsFromLogs } from '@0x/contracts-test-utils'; +import { + AggregatedStats, + StakingEvents, + StakingEpochEndedEventArgs, + StakingEpochFinalizedEventArgs, +} from '@0x/contracts-staking'; +import { constants, expect, verifyEventsFromLogs } from '@0x/contracts-test-utils'; import { BigNumber } from '@0x/utils'; import { TxData } from 'ethereum-types'; +import * as _ from 'lodash'; import { DeploymentManager } from '../deployment_manager'; import { SimulationEnvironment } from '../simulation'; @@ -25,20 +31,25 @@ export function validEndEpochAssertion( simulationEnvironment: SimulationEnvironment, ): FunctionAssertion<[], EndEpochBeforeInfo, void> { const { stakingWrapper } = deployment.staking; - const { balanceStore, currentEpoch } = simulationEnvironment; + const { balanceStore } = simulationEnvironment; return new FunctionAssertion(stakingWrapper, 'endEpoch', { before: async () => { await balanceStore.updateEthBalancesAsync(); const aggregatedStatsBefore = AggregatedStats.fromArray( - await stakingWrapper.aggregatedStatsByEpoch(currentEpoch).callAsync(), + await stakingWrapper.aggregatedStatsByEpoch(simulationEnvironment.currentEpoch).callAsync(), ); const wethReservedForPoolRewards = await stakingWrapper.wethReservedForPoolRewards().callAsync(); return { wethReservedForPoolRewards, aggregatedStatsBefore }; }, after: async (beforeInfo: EndEpochBeforeInfo, result: FunctionResult, _args: [], _txData: Partial) => { + // Ensure that the tx succeeded. + expect(result.success, `Error: ${result.data}`).to.be.true(); + + const { currentEpoch } = simulationEnvironment; + // Check WETH deposit event - const previousEthBalance = balanceStore.balances.eth[stakingWrapper.address]; + const previousEthBalance = balanceStore.balances.eth[stakingWrapper.address] || constants.ZERO_AMOUNT; if (previousEthBalance.isGreaterThan(0)) { verifyEventsFromLogs( result.receipt!.logs, @@ -56,9 +67,11 @@ export function validEndEpochAssertion( const { wethReservedForPoolRewards, aggregatedStatsBefore } = beforeInfo; const expectedAggregatedStats = { ...aggregatedStatsBefore, - rewardsAvailable: balanceStore.balances.erc20[stakingWrapper.address][ - deployment.tokens.weth.address - ].minus(wethReservedForPoolRewards), + rewardsAvailable: _.get( + balanceStore.balances, + ['erc20', stakingWrapper.address, deployment.tokens.weth.address], + constants.ZERO_AMOUNT, + ).minus(wethReservedForPoolRewards), }; const aggregatedStatsAfter = AggregatedStats.fromArray( @@ -66,22 +79,35 @@ export function validEndEpochAssertion( ); expect(aggregatedStatsAfter).to.deep.equal(expectedAggregatedStats); - const expectedEpochEndedEvents = aggregatedStatsAfter.numPoolsToFinalize.isZero() + verifyEventsFromLogs( + result.receipt!.logs, + [ + { + epoch: currentEpoch, + numPoolsToFinalize: aggregatedStatsAfter.numPoolsToFinalize, + rewardsAvailable: aggregatedStatsAfter.rewardsAvailable, + totalFeesCollected: aggregatedStatsAfter.totalFeesCollected, + totalWeightedStake: aggregatedStatsAfter.totalWeightedStake, + }, + ], + StakingEvents.EpochEnded, + ); + + const expectedEpochFinalizedEvents = aggregatedStatsAfter.numPoolsToFinalize.isZero() ? [ { epoch: currentEpoch, - numPoolsToFinalize: aggregatedStatsAfter.numPoolsToFinalize, - rewardsAvailable: aggregatedStatsAfter.rewardsAvailable, - totalFeesCollected: aggregatedStatsAfter.totalFeesCollected, - totalWeightedStake: aggregatedStatsAfter.totalWeightedStake, + rewardsPaid: constants.ZERO_AMOUNT, + rewardsRemaining: aggregatedStatsAfter.rewardsAvailable, }, ] : []; - verifyEventsFromLogs( + verifyEventsFromLogs( result.receipt!.logs, - expectedEpochEndedEvents, - StakingEvents.EpochEnded, + expectedEpochFinalizedEvents, + StakingEvents.EpochFinalized, ); + expect(result.data, 'endEpoch should return the number of unfinalized pools').to.bignumber.equal( aggregatedStatsAfter.numPoolsToFinalize, ); diff --git a/contracts/integrations/test/framework/assertions/fillOrder.ts b/contracts/integrations/test/framework/assertions/fillOrder.ts index 88f13a47ff..c3d2cf757b 100644 --- a/contracts/integrations/test/framework/assertions/fillOrder.ts +++ b/contracts/integrations/test/framework/assertions/fillOrder.ts @@ -104,7 +104,7 @@ export function validFillOrderAssertion( simulationEnvironment: SimulationEnvironment, ): FunctionAssertion<[Order, BigNumber, string], FillOrderBeforeInfo | void, FillResults> { const { stakingWrapper } = deployment.staking; - const { actors, currentEpoch } = simulationEnvironment; + const { actors } = simulationEnvironment; return new FunctionAssertion<[Order, BigNumber, string], FillOrderBeforeInfo | void, FillResults>( deployment.exchange, @@ -112,6 +112,7 @@ export function validFillOrderAssertion( { before: async (args: [Order, BigNumber, string]) => { const [order] = args; + const { currentEpoch } = simulationEnvironment; const maker = filterActorsByRole(actors, Maker).find(maker => maker.address === order.makerAddress); const poolId = maker!.makerPoolId; @@ -138,12 +139,13 @@ export function validFillOrderAssertion( result: FunctionResult, args: [Order, BigNumber, string], txData: Partial, - ) => { - const [order, fillAmount] = args; - + ) => { // Ensure that the tx succeeded. expect(result.success, `Error: ${result.data}`).to.be.true(); + const [order, fillAmount] = args; + const { currentEpoch } = simulationEnvironment; + // Ensure that the correct events were emitted. verifyFillEvents(txData, order, result.receipt!, deployment, fillAmount); diff --git a/contracts/integrations/test/framework/assertions/finalizePool.ts b/contracts/integrations/test/framework/assertions/finalizePool.ts index f4cea95d68..f8b2f76dec 100644 --- a/contracts/integrations/test/framework/assertions/finalizePool.ts +++ b/contracts/integrations/test/framework/assertions/finalizePool.ts @@ -65,12 +65,12 @@ export function validFinalizePoolAssertion( simulationEnvironment: SimulationEnvironment, ): FunctionAssertion<[string], FinalizePoolBeforeInfo, void> { const { stakingWrapper } = deployment.staking; - const { currentEpoch } = simulationEnvironment; - const prevEpoch = currentEpoch.minus(1); return new FunctionAssertion<[string], FinalizePoolBeforeInfo, void>(stakingWrapper, 'finalizePool', { before: async (args: [string]) => { const [poolId] = args; + const { currentEpoch } = simulationEnvironment; + const prevEpoch = currentEpoch.minus(1); const poolStats = PoolStats.fromArray(await stakingWrapper.poolStatsByEpoch(poolId, prevEpoch).callAsync()); const aggregatedStats = AggregatedStats.fromArray( @@ -90,10 +90,14 @@ export function validFinalizePoolAssertion( }; }, after: async (beforeInfo: FinalizePoolBeforeInfo, result: FunctionResult, args: [string]) => { + // Ensure that the tx succeeded. + expect(result.success, `Error: ${result.data}`).to.be.true(); + // // Compute relevant epochs // uint256 currentEpoch_ = currentEpoch; // uint256 prevEpoch = currentEpoch_.safeSub(1); - const { stakingPools } = simulationEnvironment; + const { stakingPools, currentEpoch } = simulationEnvironment; + const prevEpoch = currentEpoch.minus(1); const [poolId] = args; const pool = stakingPools[poolId]; @@ -155,9 +159,8 @@ export function validFinalizePoolAssertion( expect(events.length, 'Number of RewardsPaid events emitted').to.equal(1); const [rewardsPaidEvent] = events; - expect(rewardsPaidEvent.currentEpoch_, 'RewardsPaid event: currentEpoch_').to.bignumber.equal(currentEpoch); - expect(rewardsPaidEvent.poolId, 'RewardsPaid event: poolId').to.bignumber.equal(poolId); - expect(rewardsPaidEvent.currentEpoch_, 'RewardsPaid event: currentEpoch_').to.bignumber.equal(currentEpoch); + expect(rewardsPaidEvent.poolId, 'RewardsPaid event: poolId').to.equal(poolId); + expect(rewardsPaidEvent.epoch, 'RewardsPaid event: currentEpoch_').to.bignumber.equal(currentEpoch); const { operatorReward, membersReward } = rewardsPaidEvent; const totalReward = operatorReward.plus(membersReward); @@ -189,6 +192,7 @@ export function validFinalizePoolAssertion( }, ] : []; + // Check for WETH transfer event emitted when paying out operator's reward. verifyEventsFromLogs( result.receipt!.logs, diff --git a/contracts/integrations/test/framework/assertions/function_assertion.ts b/contracts/integrations/test/framework/assertions/function_assertion.ts index 44ecd8afa5..0782c1b1e0 100644 --- a/contracts/integrations/test/framework/assertions/function_assertion.ts +++ b/contracts/integrations/test/framework/assertions/function_assertion.ts @@ -79,13 +79,15 @@ export class FunctionAssertion imp // Initialize the callResult so that the default success value is true. const callResult: FunctionResult = { success: true }; + // Log function name, arguments, and txData + logger.logFunctionAssertion(this._functionName, args, txData); + // Try to make the call to the function. If it is successful, pass the // result and receipt to the after condition. try { const functionWithArgs = (this._contractWrapper as any)[this._functionName]( ...args, ) as ContractTxFunctionObj; - logger.logFunctionAssertion(this._functionName, args, txData); callResult.data = await functionWithArgs.callAsync(txData); callResult.receipt = functionWithArgs.awaitTransactionSuccessAsync !== undefined diff --git a/contracts/integrations/test/framework/assertions/joinStakingPool.ts b/contracts/integrations/test/framework/assertions/joinStakingPool.ts index 88795c469a..cafa5ac29c 100644 --- a/contracts/integrations/test/framework/assertions/joinStakingPool.ts +++ b/contracts/integrations/test/framework/assertions/joinStakingPool.ts @@ -15,12 +15,13 @@ export function validJoinStakingPoolAssertion(deployment: DeploymentManager): Fu const { stakingWrapper } = deployment.staking; return new FunctionAssertion<[string], {}, void>(stakingWrapper, 'joinStakingPoolAsMaker', { - after: async (_beforeInfo, _result: FunctionResult, args: [string], txData: Partial) => { + after: async (_beforeInfo, result: FunctionResult, args: [string], txData: Partial) => { + // Ensure that the tx succeeded. + expect(result.success, `Error: ${result.data}`).to.be.true(); + const [poolId] = args; - expect(_result.success).to.be.true(); - - const logs = _result.receipt!.logs; + const logs = result.receipt!.logs; const logArgs = filterLogsToArguments( logs, StakingEvents.MakerStakingPoolSet, diff --git a/contracts/integrations/test/framework/assertions/moveStake.ts b/contracts/integrations/test/framework/assertions/moveStake.ts index 8b478ffa6e..9ca90619da 100644 --- a/contracts/integrations/test/framework/assertions/moveStake.ts +++ b/contracts/integrations/test/framework/assertions/moveStake.ts @@ -1,75 +1,94 @@ -import { - GlobalStakeByStatus, - OwnerStakeByStatus, - StakeInfo, - StakeStatus, - StakingPoolById, - StoredBalance, -} from '@0x/contracts-staking'; +import { OwnerStakeByStatus, StakeInfo, StakeStatus, StoredBalance } from '@0x/contracts-staking'; import { constants, expect } from '@0x/contracts-test-utils'; import { BigNumber } from '@0x/utils'; import { TxData } from 'ethereum-types'; import * as _ from 'lodash'; import { DeploymentManager } from '../deployment_manager'; +import { SimulationEnvironment } from '../simulation'; import { FunctionAssertion, FunctionResult } from './function_assertion'; -function incrementNextEpochBalance(stakeBalance: StoredBalance, amount: BigNumber): void { +function incrementNextEpochBalance(stakeBalance: StoredBalance, amount: BigNumber, currentEpoch: BigNumber): void { + stakeBalance.currentEpochBalance = currentEpoch.isGreaterThan(stakeBalance.currentEpoch) + ? stakeBalance.nextEpochBalance + : stakeBalance.currentEpochBalance; + stakeBalance.currentEpoch = currentEpoch; _.update(stakeBalance, ['nextEpochBalance'], balance => (balance || constants.ZERO_AMOUNT).plus(amount)); } -function decrementNextEpochBalance(stakeBalance: StoredBalance, amount: BigNumber): void { +function decrementNextEpochBalance(stakeBalance: StoredBalance, amount: BigNumber, currentEpoch: BigNumber): void { + stakeBalance.currentEpochBalance = currentEpoch.isGreaterThan(stakeBalance.currentEpoch) + ? stakeBalance.nextEpochBalance + : stakeBalance.currentEpochBalance; + stakeBalance.currentEpoch = currentEpoch; _.update(stakeBalance, ['nextEpochBalance'], balance => (balance || constants.ZERO_AMOUNT).minus(amount)); } +function loadCurrentBalance(balance: StoredBalance, currentEpoch: BigNumber): StoredBalance { + return { + ...balance, + currentEpoch: currentEpoch, + currentEpochBalance: currentEpoch.isGreaterThan(balance.currentEpoch) + ? balance.nextEpochBalance + : balance.currentEpochBalance, + }; +} + function updateNextEpochBalances( - globalStake: GlobalStakeByStatus, ownerStake: OwnerStakeByStatus, - pools: StakingPoolById, from: StakeInfo, to: StakeInfo, amount: BigNumber, + simulationEnvironment: SimulationEnvironment, ): string[] { + const { globalStake, stakingPools, currentEpoch } = simulationEnvironment; + // The on-chain state of these updated pools will be verified in the `after` of the assertion. const updatedPools = []; // Decrement next epoch balances associated with the `from` stake if (from.status === StakeStatus.Undelegated) { // Decrement owner undelegated stake - decrementNextEpochBalance(ownerStake[StakeStatus.Undelegated], amount); + decrementNextEpochBalance(ownerStake[StakeStatus.Undelegated], amount, currentEpoch); // Decrement global undelegated stake - decrementNextEpochBalance(globalStake[StakeStatus.Undelegated], amount); + decrementNextEpochBalance(globalStake[StakeStatus.Undelegated], amount, currentEpoch); } else if (from.status === StakeStatus.Delegated) { // Decrement owner's delegated stake to this pool - decrementNextEpochBalance(ownerStake[StakeStatus.Delegated][from.poolId], amount); + decrementNextEpochBalance(ownerStake[StakeStatus.Delegated][from.poolId], amount, currentEpoch); // Decrement owner's total delegated stake - decrementNextEpochBalance(ownerStake[StakeStatus.Delegated].total, amount); + decrementNextEpochBalance(ownerStake[StakeStatus.Delegated].total, amount, currentEpoch); // Decrement global delegated stake - decrementNextEpochBalance(globalStake[StakeStatus.Delegated], amount); + decrementNextEpochBalance(globalStake[StakeStatus.Delegated], amount, currentEpoch); // Decrement pool's delegated stake - decrementNextEpochBalance(pools[from.poolId].delegatedStake, amount); + decrementNextEpochBalance(stakingPools[from.poolId].delegatedStake, amount, currentEpoch); updatedPools.push(from.poolId); + + // TODO: Check that delegator rewards have been withdrawn/synced } // Increment next epoch balances associated with the `to` stake if (to.status === StakeStatus.Undelegated) { - incrementNextEpochBalance(ownerStake[StakeStatus.Undelegated], amount); - incrementNextEpochBalance(globalStake[StakeStatus.Undelegated], amount); + // Increment owner undelegated stake + incrementNextEpochBalance(ownerStake[StakeStatus.Undelegated], amount, currentEpoch); + // Increment global undelegated stake + incrementNextEpochBalance(globalStake[StakeStatus.Undelegated], amount, currentEpoch); } else if (to.status === StakeStatus.Delegated) { // Initializes the balance for this pool if the user has not previously delegated to it _.defaults(ownerStake[StakeStatus.Delegated], { [to.poolId]: new StoredBalance(), }); // Increment owner's delegated stake to this pool - incrementNextEpochBalance(ownerStake[StakeStatus.Delegated][to.poolId], amount); + incrementNextEpochBalance(ownerStake[StakeStatus.Delegated][to.poolId], amount, currentEpoch); // Increment owner's total delegated stake - incrementNextEpochBalance(ownerStake[StakeStatus.Delegated].total, amount); + incrementNextEpochBalance(ownerStake[StakeStatus.Delegated].total, amount, currentEpoch); // Increment global delegated stake - incrementNextEpochBalance(globalStake[StakeStatus.Delegated], amount); + incrementNextEpochBalance(globalStake[StakeStatus.Delegated], amount, currentEpoch); // Increment pool's delegated stake - incrementNextEpochBalance(pools[to.poolId].delegatedStake, amount); + incrementNextEpochBalance(stakingPools[to.poolId].delegatedStake, amount, currentEpoch); updatedPools.push(to.poolId); + + // TODO: Check that delegator rewards have been withdrawn/synced } return updatedPools; } @@ -80,25 +99,28 @@ function updateNextEpochBalances( /* tslint:disable:no-unnecessary-type-assertion */ export function validMoveStakeAssertion( deployment: DeploymentManager, - globalStake: GlobalStakeByStatus, + simulationEnvironment: SimulationEnvironment, ownerStake: OwnerStakeByStatus, - pools: StakingPoolById, ): FunctionAssertion<[StakeInfo, StakeInfo, BigNumber], {}, void> { - const { stakingWrapper } = deployment.staking; + const { stakingWrapper, zrxVault } = deployment.staking; return new FunctionAssertion<[StakeInfo, StakeInfo, BigNumber], {}, void>(stakingWrapper, 'moveStake', { after: async ( _beforeInfo: {}, - _result: FunctionResult, + result: FunctionResult, args: [StakeInfo, StakeInfo, BigNumber], txData: Partial, ) => { + // Ensure that the tx succeeded. + expect(result.success, `Error: ${result.data}`).to.be.true(); + const [from, to, amount] = args; + const { stakingPools, globalStake, currentEpoch } = simulationEnvironment; const owner = txData.from!; // tslint:disable-line:no-non-null-assertion // Update local balances to match the expected result of this `moveStake` operation - const updatedPools = updateNextEpochBalances(globalStake, ownerStake, pools, from, to, amount); + const updatedPools = updateNextEpochBalances(ownerStake, from, to, amount, simulationEnvironment); // Fetches on-chain owner stake balances and checks against local balances const ownerUndelegatedStake = { @@ -109,16 +131,27 @@ export function validMoveStakeAssertion( ...new StoredBalance(), ...(await stakingWrapper.getOwnerStakeByStatus(owner, StakeStatus.Delegated).callAsync()), }; - expect(ownerUndelegatedStake).to.deep.equal(ownerStake[StakeStatus.Undelegated]); - expect(ownerDelegatedStake).to.deep.equal(ownerStake[StakeStatus.Delegated].total); + expect(ownerUndelegatedStake).to.deep.equal( + loadCurrentBalance(ownerStake[StakeStatus.Undelegated], currentEpoch), + ); + expect(ownerDelegatedStake).to.deep.equal( + loadCurrentBalance(ownerStake[StakeStatus.Delegated].total, currentEpoch), + ); // Fetches on-chain global stake balances and checks against local balances + const globalDelegatedStake = await stakingWrapper.getGlobalStakeByStatus(StakeStatus.Delegated).callAsync(); const globalUndelegatedStake = await stakingWrapper .getGlobalStakeByStatus(StakeStatus.Undelegated) .callAsync(); - const globalDelegatedStake = await stakingWrapper.getGlobalStakeByStatus(StakeStatus.Delegated).callAsync(); - expect(globalUndelegatedStake).to.deep.equal(globalStake[StakeStatus.Undelegated]); - expect(globalDelegatedStake).to.deep.equal(globalStake[StakeStatus.Delegated]); + const totalStake = await zrxVault.balanceOfZrxVault().callAsync(); + expect(globalDelegatedStake).to.deep.equal( + loadCurrentBalance(globalStake[StakeStatus.Delegated], currentEpoch), + ); + expect(globalUndelegatedStake).to.deep.equal({ + currentEpochBalance: totalStake.minus(globalDelegatedStake.currentEpochBalance), + nextEpochBalance: totalStake.minus(globalDelegatedStake.nextEpochBalance), + currentEpoch, + }); // Fetches on-chain pool stake balances and checks against local balances for (const poolId of updatedPools) { @@ -126,8 +159,12 @@ export function validMoveStakeAssertion( .getStakeDelegatedToPoolByOwner(owner, poolId) .callAsync(); const totalStakeDelegated = await stakingWrapper.getTotalStakeDelegatedToPool(poolId).callAsync(); - expect(stakeDelegatedByOwner).to.deep.equal(ownerStake[StakeStatus.Delegated][poolId]); - expect(totalStakeDelegated).to.deep.equal(pools[poolId].delegatedStake); + expect(stakeDelegatedByOwner).to.deep.equal( + loadCurrentBalance(ownerStake[StakeStatus.Delegated][poolId], currentEpoch), + ); + expect(totalStakeDelegated).to.deep.equal( + loadCurrentBalance(stakingPools[poolId].delegatedStake, currentEpoch), + ); } }, }); diff --git a/contracts/integrations/test/framework/assertions/stake.ts b/contracts/integrations/test/framework/assertions/stake.ts index f2ef1f190a..5b78373e33 100644 --- a/contracts/integrations/test/framework/assertions/stake.ts +++ b/contracts/integrations/test/framework/assertions/stake.ts @@ -3,19 +3,23 @@ import { expect } from '@0x/contracts-test-utils'; import { BigNumber } from '@0x/utils'; import { TxData } from 'ethereum-types'; -import { BlockchainBalanceStore } from '../balances/blockchain_balance_store'; import { LocalBalanceStore } from '../balances/local_balance_store'; import { DeploymentManager } from '../deployment_manager'; +import { SimulationEnvironment } from '../simulation'; import { FunctionAssertion, FunctionResult } from './function_assertion'; function expectedUndelegatedStake( initStake: OwnerStakeByStatus | GlobalStakeByStatus, amount: BigNumber, + currentEpoch: BigNumber, ): StoredBalance { return { - currentEpoch: initStake[StakeStatus.Undelegated].currentEpoch, - currentEpochBalance: initStake[StakeStatus.Undelegated].currentEpochBalance.plus(amount), + currentEpoch: currentEpoch, + currentEpochBalance: (currentEpoch.isGreaterThan(initStake[StakeStatus.Undelegated].currentEpoch) + ? initStake[StakeStatus.Undelegated].nextEpochBalance + : initStake[StakeStatus.Undelegated].currentEpochBalance + ).plus(amount), nextEpochBalance: initStake[StakeStatus.Undelegated].nextEpochBalance.plus(amount), }; } @@ -28,8 +32,7 @@ function expectedUndelegatedStake( /* tslint:disable:no-unnecessary-type-assertion */ export function validStakeAssertion( deployment: DeploymentManager, - balanceStore: BlockchainBalanceStore, - globalStake: GlobalStakeByStatus, + simulationEnvironment: SimulationEnvironment, ownerStake: OwnerStakeByStatus, ): FunctionAssertion<[BigNumber], LocalBalanceStore, void> { const { stakingWrapper, zrxVault } = deployment.staking; @@ -37,6 +40,7 @@ export function validStakeAssertion( return new FunctionAssertion(stakingWrapper, 'stake', { before: async (args: [BigNumber], txData: Partial) => { const [amount] = args; + const { balanceStore } = simulationEnvironment; // Simulates the transfer of ZRX from staker to vault const expectedBalances = LocalBalanceStore.create(balanceStore); @@ -50,11 +54,15 @@ export function validStakeAssertion( }, after: async ( expectedBalances: LocalBalanceStore, - _result: FunctionResult, + result: FunctionResult, args: [BigNumber], txData: Partial, ) => { + // Ensure that the tx succeeded. + expect(result.success, `Error: ${result.data}`).to.be.true(); + const [amount] = args; + const { balanceStore, globalStake, currentEpoch } = simulationEnvironment; // Checks that the ZRX transfer updated balances as expected. await balanceStore.updateErc20BalancesAsync(); @@ -64,7 +72,7 @@ export function validStakeAssertion( const ownerUndelegatedStake = await stakingWrapper .getOwnerStakeByStatus(txData.from!, StakeStatus.Undelegated) // tslint:disable-line:no-non-null-assertion .callAsync(); - const expectedOwnerUndelegatedStake = expectedUndelegatedStake(ownerStake, amount); + const expectedOwnerUndelegatedStake = expectedUndelegatedStake(ownerStake, amount, currentEpoch); expect(ownerUndelegatedStake, 'Owner undelegated stake').to.deep.equal(expectedOwnerUndelegatedStake); // Updates local state accordingly ownerStake[StakeStatus.Undelegated] = expectedOwnerUndelegatedStake; @@ -73,7 +81,7 @@ export function validStakeAssertion( const globalUndelegatedStake = await stakingWrapper .getGlobalStakeByStatus(StakeStatus.Undelegated) .callAsync(); - const expectedGlobalUndelegatedStake = expectedUndelegatedStake(globalStake, amount); + const expectedGlobalUndelegatedStake = expectedUndelegatedStake(globalStake, amount, currentEpoch); expect(globalUndelegatedStake, 'Global undelegated stake').to.deep.equal(expectedGlobalUndelegatedStake); // Updates local state accordingly globalStake[StakeStatus.Undelegated] = expectedGlobalUndelegatedStake; diff --git a/contracts/integrations/test/framework/assertions/unstake.ts b/contracts/integrations/test/framework/assertions/unstake.ts index fea2384ee1..261d3bb1f2 100644 --- a/contracts/integrations/test/framework/assertions/unstake.ts +++ b/contracts/integrations/test/framework/assertions/unstake.ts @@ -3,19 +3,23 @@ import { expect } from '@0x/contracts-test-utils'; import { BigNumber } from '@0x/utils'; import { TxData } from 'ethereum-types'; -import { BlockchainBalanceStore } from '../balances/blockchain_balance_store'; import { LocalBalanceStore } from '../balances/local_balance_store'; import { DeploymentManager } from '../deployment_manager'; +import { SimulationEnvironment } from '../simulation'; import { FunctionAssertion, FunctionResult } from './function_assertion'; function expectedUndelegatedStake( initStake: OwnerStakeByStatus | GlobalStakeByStatus, amount: BigNumber, + currentEpoch: BigNumber, ): StoredBalance { return { - currentEpoch: initStake[StakeStatus.Undelegated].currentEpoch, - currentEpochBalance: initStake[StakeStatus.Undelegated].currentEpochBalance.minus(amount), + currentEpoch: currentEpoch, + currentEpochBalance: (currentEpoch.isGreaterThan(initStake[StakeStatus.Undelegated].currentEpoch) + ? initStake[StakeStatus.Undelegated].nextEpochBalance + : initStake[StakeStatus.Undelegated].currentEpochBalance + ).minus(amount), nextEpochBalance: initStake[StakeStatus.Undelegated].nextEpochBalance.minus(amount), }; } @@ -29,8 +33,7 @@ function expectedUndelegatedStake( /* tslint:disable:no-non-null-assertion */ export function validUnstakeAssertion( deployment: DeploymentManager, - balanceStore: BlockchainBalanceStore, - globalStake: GlobalStakeByStatus, + simulationEnvironment: SimulationEnvironment, ownerStake: OwnerStakeByStatus, ): FunctionAssertion<[BigNumber], LocalBalanceStore, void> { const { stakingWrapper, zrxVault } = deployment.staking; @@ -38,6 +41,7 @@ export function validUnstakeAssertion( return new FunctionAssertion(stakingWrapper, 'unstake', { before: async (args: [BigNumber], txData: Partial) => { const [amount] = args; + const { balanceStore } = simulationEnvironment; // Simulates the transfer of ZRX from vault to staker const expectedBalances = LocalBalanceStore.create(balanceStore); @@ -51,11 +55,15 @@ export function validUnstakeAssertion( }, after: async ( expectedBalances: LocalBalanceStore, - _result: FunctionResult, + result: FunctionResult, args: [BigNumber], txData: Partial, ) => { + // Ensure that the tx succeeded. + expect(result.success, `Error: ${result.data}`).to.be.true(); + const [amount] = args; + const { balanceStore, globalStake, currentEpoch } = simulationEnvironment; // Checks that the ZRX transfer updated balances as expected. await balanceStore.updateErc20BalancesAsync(); @@ -65,7 +73,7 @@ export function validUnstakeAssertion( const ownerUndelegatedStake = await stakingWrapper .getOwnerStakeByStatus(txData.from!, StakeStatus.Undelegated) .callAsync(); - const expectedOwnerUndelegatedStake = expectedUndelegatedStake(ownerStake, amount); + const expectedOwnerUndelegatedStake = expectedUndelegatedStake(ownerStake, amount, currentEpoch); expect(ownerUndelegatedStake, 'Owner undelegated stake').to.deep.equal(expectedOwnerUndelegatedStake); // Updates local state accordingly ownerStake[StakeStatus.Undelegated] = expectedOwnerUndelegatedStake; @@ -74,7 +82,7 @@ export function validUnstakeAssertion( const globalUndelegatedStake = await stakingWrapper .getGlobalStakeByStatus(StakeStatus.Undelegated) .callAsync(); - const expectedGlobalUndelegatedStake = expectedUndelegatedStake(globalStake, amount); + const expectedGlobalUndelegatedStake = expectedUndelegatedStake(globalStake, amount, currentEpoch); expect(globalUndelegatedStake, 'Global undelegated stake').to.deep.equal(expectedGlobalUndelegatedStake); // Updates local state accordingly globalStake[StakeStatus.Undelegated] = expectedGlobalUndelegatedStake; diff --git a/contracts/integrations/test/framework/assertions/withdrawDelegatorRewards.ts b/contracts/integrations/test/framework/assertions/withdrawDelegatorRewards.ts index cb62535d92..9f09092a4b 100644 --- a/contracts/integrations/test/framework/assertions/withdrawDelegatorRewards.ts +++ b/contracts/integrations/test/framework/assertions/withdrawDelegatorRewards.ts @@ -1,6 +1,6 @@ import { WETH9TransferEventArgs, WETH9Events } from '@0x/contracts-erc20'; import { StoredBalance } from '@0x/contracts-staking'; -import { expect, verifyEventsFromLogs } from '@0x/contracts-test-utils'; +import { expect, filterLogsToArguments, verifyEventsFromLogs } from '@0x/contracts-test-utils'; import { BigNumber } from '@0x/utils'; import { TxData } from 'ethereum-types'; @@ -13,7 +13,6 @@ interface WithdrawDelegatorRewardsBeforeInfo { delegatorStake: StoredBalance; poolRewards: BigNumber; wethReservedForPoolRewards: BigNumber; - delegatorReward: BigNumber; } /** @@ -27,35 +26,17 @@ export function validWithdrawDelegatorRewardsAssertion( simulationEnvironment: SimulationEnvironment, ): FunctionAssertion<[string], WithdrawDelegatorRewardsBeforeInfo, void> { const { stakingWrapper } = deployment.staking; - const { currentEpoch } = simulationEnvironment; return new FunctionAssertion(stakingWrapper, 'withdrawDelegatorRewards', { before: async (args: [string], txData: Partial) => { const [poolId] = args; + const delegatorStake = await stakingWrapper .getStakeDelegatedToPoolByOwner(txData.from!, poolId) .callAsync(); const poolRewards = await stakingWrapper.rewardsByPoolId(poolId).callAsync(); const wethReservedForPoolRewards = await stakingWrapper.wethReservedForPoolRewards().callAsync(); - const delegatorReward = BigNumber.sum( - await stakingWrapper - .computeMemberRewardOverInterval( - poolId, - delegatorStake.currentEpochBalance, - delegatorStake.currentEpoch, - delegatorStake.currentEpoch.plus(1), - ) - .callAsync(), - await stakingWrapper - .computeMemberRewardOverInterval( - poolId, - delegatorStake.nextEpochBalance, - delegatorStake.currentEpoch.plus(1), - currentEpoch, - ) - .callAsync(), - ); // TODO: Test the reward computation more robustly - return { delegatorStake, poolRewards, wethReservedForPoolRewards, delegatorReward }; + return { delegatorStake, poolRewards, wethReservedForPoolRewards }; }, after: async ( beforeInfo: WithdrawDelegatorRewardsBeforeInfo, @@ -63,7 +44,11 @@ export function validWithdrawDelegatorRewardsAssertion( args: [string], txData: Partial, ) => { + // Ensure that the tx succeeded. + expect(result.success, `Error: ${result.data}`).to.be.true(); + const [poolId] = args; + const { currentEpoch } = simulationEnvironment; const expectedDelegatorStake = { ...beforeInfo.delegatorStake, @@ -77,18 +62,16 @@ export function validWithdrawDelegatorRewardsAssertion( .callAsync(); expect(delegatorStake).to.deep.equal(expectedDelegatorStake); - const expectedPoolRewards = beforeInfo.poolRewards.minus(beforeInfo.delegatorReward); - const poolRewards = await stakingWrapper.rewardsByPoolId(poolId).callAsync(); - expect(poolRewards).to.bignumber.equal(expectedPoolRewards); - - const expectedTransferEvents = beforeInfo.delegatorReward.isZero() - ? [] - : [{ _from: stakingWrapper.address, _to: txData.from!, _value: beforeInfo.delegatorReward }]; - verifyEventsFromLogs( + const transferEvents = filterLogsToArguments( result.receipt!.logs, - expectedTransferEvents, WETH9Events.Transfer, ); + const expectedPoolRewards = + transferEvents.length > 0 + ? beforeInfo.poolRewards.minus(transferEvents[0]._value) + : beforeInfo.poolRewards; + const poolRewards = await stakingWrapper.rewardsByPoolId(poolId).callAsync(); + expect(poolRewards).to.bignumber.equal(expectedPoolRewards); // TODO: Check CR }, diff --git a/contracts/integrations/test/framework/deployment_manager.ts b/contracts/integrations/test/framework/deployment_manager.ts index 6cf0f4305c..c5811e65a3 100644 --- a/contracts/integrations/test/framework/deployment_manager.ts +++ b/contracts/integrations/test/framework/deployment_manager.ts @@ -393,7 +393,16 @@ export class DeploymentManager { stakingLogic.address, ); - const stakingWrapper = new TestStakingContract(stakingProxy.address, environment.provider, txDefaults); + const logDecoderDependencies = _.mapValues( + { ...stakingArtifacts, ...ERC20Artifacts }, + v => v.compilerOutput.abi, + ); + const stakingWrapper = new TestStakingContract( + stakingProxy.address, + environment.provider, + txDefaults, + logDecoderDependencies, + ); // Add the zrx vault and the weth contract to the staking proxy. await stakingWrapper.setWethContract(tokens.weth.address).awaitTransactionSuccessAsync({ from: owner }); diff --git a/contracts/integrations/test/framework/utils/logger.ts b/contracts/integrations/test/framework/utils/logger.ts index 35bb16a0ab..20be7e6d7a 100644 --- a/contracts/integrations/test/framework/utils/logger.ts +++ b/contracts/integrations/test/framework/utils/logger.ts @@ -29,7 +29,7 @@ class Logger { msg: `Function called: ${functionName}(${functionArgs .map(arg => JSON.stringify(arg).replace(/"/g, "'")) .join(', ')})`, - step: this._step++, + step: ++this._step, txData, }), ); diff --git a/contracts/integrations/test/framework/utils/pseudorandom.ts b/contracts/integrations/test/framework/utils/pseudorandom.ts index 93f4e52e57..6f4132650f 100644 --- a/contracts/integrations/test/framework/utils/pseudorandom.ts +++ b/contracts/integrations/test/framework/utils/pseudorandom.ts @@ -3,7 +3,7 @@ import { BigNumber } from '@0x/utils'; import * as seedrandom from 'seedrandom'; class PRNGWrapper { - public readonly seed = process.env.UUID || Math.random().toString(); + public readonly seed = process.env.SEED || Math.random().toString(); private readonly _rng = seedrandom(this.seed); /* diff --git a/contracts/integrations/test/fuzz_tests/staking_rewards_test.ts b/contracts/integrations/test/fuzz_tests/staking_rewards_test.ts index 4d7ddbbce2..ea3cbaa192 100644 --- a/contracts/integrations/test/fuzz_tests/staking_rewards_test.ts +++ b/contracts/integrations/test/fuzz_tests/staking_rewards_test.ts @@ -20,7 +20,6 @@ import { DeploymentManager } from '../framework/deployment_manager'; import { Simulation, SimulationEnvironment } from '../framework/simulation'; import { Pseudorandom } from '../framework/utils/pseudorandom'; -import { PoolManagementSimulation } from './pool_management_test'; import { PoolMembershipSimulation } from './pool_membership_test'; import { StakeManagementSimulation } from './stake_management_test'; @@ -30,7 +29,6 @@ export class StakingRewardsSimulation extends Simulation { const stakers = filterActorsByRole(actors, Staker); const keepers = filterActorsByRole(actors, Keeper); - const poolManagement = new PoolManagementSimulation(this.environment); const poolMembership = new PoolMembershipSimulation(this.environment); const stakeManagement = new StakeManagementSimulation(this.environment); @@ -38,7 +36,6 @@ export class StakingRewardsSimulation extends Simulation { ...stakers.map(staker => staker.simulationActions.validWithdrawDelegatorRewards), ...keepers.map(keeper => keeper.simulationActions.validFinalizePool), ...keepers.map(keeper => keeper.simulationActions.validEndEpoch), - poolManagement.generator, poolMembership.generator, stakeManagement.generator, ]; @@ -66,12 +63,22 @@ blockchainTests('Staking rewards fuzz test', env => { numErc721TokensToDeploy: 0, numErc1155TokensToDeploy: 0, }); + const [ERC20TokenA, ERC20TokenB, ERC20TokenC, ERC20TokenD] = deployment.tokens.erc20; const balanceStore = new BlockchainBalanceStore( { StakingProxy: deployment.staking.stakingProxy.address, ZRXVault: deployment.staking.zrxVault.address, }, - { erc20: { ZRX: deployment.tokens.zrx } }, + { + erc20: { + ZRX: deployment.tokens.zrx, + WETH: deployment.tokens.weth, + ERC20TokenA, + ERC20TokenB, + ERC20TokenC, + ERC20TokenD, + }, + }, ); const simulationEnvironment = new SimulationEnvironment(deployment, balanceStore); diff --git a/contracts/staking/contracts/test/TestStaking.sol b/contracts/staking/contracts/test/TestStaking.sol index 5c5671848a..4a60105d81 100644 --- a/contracts/staking/contracts/test/TestStaking.sol +++ b/contracts/staking/contracts/test/TestStaking.sol @@ -67,69 +67,6 @@ contract TestStaking is cumulativeRewards = _cumulativeRewardsByPool[poolId][lastStoredEpoch]; } - function computeMemberRewardOverInterval( - bytes32 poolId, - uint256 memberStakeOverInterval, - uint256 beginEpoch, - uint256 endEpoch - ) - external - view - returns (uint256 reward) - { - // Sanity check if we can skip computation, as it will result in zero. - if (memberStakeOverInterval == 0 || beginEpoch == endEpoch) { - return 0; - } - - // Sanity check interval - require(beginEpoch < endEpoch, "CR_INTERVAL_INVALID"); - - // Sanity check begin reward - IStructs.Fraction memory beginReward = getCumulativeRewardAtEpoch(poolId, beginEpoch); - IStructs.Fraction memory endReward = getCumulativeRewardAtEpoch(poolId, endEpoch); - - // Compute reward - reward = LibFractions.scaleDifference( - endReward.numerator, - endReward.denominator, - beginReward.numerator, - beginReward.denominator, - memberStakeOverInterval - ); - } - - function getCumulativeRewardAtEpoch(bytes32 poolId, uint256 epoch) - public - view - returns (IStructs.Fraction memory cumulativeReward) - { - // Return CR at `epoch`, given it's set. - cumulativeReward = _cumulativeRewardsByPool[poolId][epoch]; - if (_isCumulativeRewardSet(cumulativeReward)) { - return cumulativeReward; - } - - // Return CR at `epoch-1`, given it's set. - uint256 lastEpoch = epoch.safeSub(1); - cumulativeReward = _cumulativeRewardsByPool[poolId][lastEpoch]; - if (_isCumulativeRewardSet(cumulativeReward)) { - return cumulativeReward; - } - - // Return the most recent CR, given it's less than `epoch`. - uint256 mostRecentEpoch = _cumulativeRewardsByPoolLastStored[poolId]; - if (mostRecentEpoch < epoch) { - cumulativeReward = _cumulativeRewardsByPool[poolId][mostRecentEpoch]; - if (_isCumulativeRewardSet(cumulativeReward)) { - return cumulativeReward; - } - } - - // Otherwise return an empty CR. - return IStructs.Fraction(0, 1); - } - /// @dev Overridden to use testWethAddress; function getWethContract() public