diff --git a/contracts/exchange/test/balance_stores/blockchain_balance_store.ts b/contracts/exchange/test/balance_stores/blockchain_balance_store.ts index 64952296b2..73d4fff643 100644 --- a/contracts/exchange/test/balance_stores/blockchain_balance_store.ts +++ b/contracts/exchange/test/balance_stores/blockchain_balance_store.ts @@ -19,7 +19,7 @@ export class BlockchainBalanceStore extends BalanceStore { public constructor( tokenOwnersByName: TokenOwnersByName, tokenContractsByName: Partial, - tokenIds: Partial, + tokenIds: Partial = {}, ) { super(tokenOwnersByName, tokenContractsByName); this._tokenContracts = { diff --git a/contracts/integrations/src/index.ts b/contracts/integrations/src/index.ts index be79d53fa1..97179831da 100644 --- a/contracts/integrations/src/index.ts +++ b/contracts/integrations/src/index.ts @@ -2,4 +2,3 @@ export * from './artifacts'; export * from './wrappers'; export * from '../test/utils/function_assertions'; export * from '../test/utils/deployment_manager'; -export * from '../test/utils/address_manager'; diff --git a/contracts/integrations/test/actors/hybrids.ts b/contracts/integrations/test/actors/hybrids.ts index bd55763de8..c45660a785 100644 --- a/contracts/integrations/test/actors/hybrids.ts +++ b/contracts/integrations/test/actors/hybrids.ts @@ -1,5 +1,11 @@ import { Actor } from './base'; import { MakerMixin } from './maker'; import { PoolOperatorMixin } from './pool_operator'; +import { StakerMixin } from './staker'; +import { KeeperMixin } from './keeper'; export class OperatorMaker extends PoolOperatorMixin(MakerMixin(Actor)) {} +export class StakerMaker extends StakerMixin(MakerMixin(Actor)) {} +export class StakerOperator extends StakerMixin(PoolOperatorMixin(Actor)) {} +export class OperatorStakerMaker extends PoolOperatorMixin(StakerMixin(MakerMixin(Actor))) {} +export class StakerKeeper extends StakerMixin(KeeperMixin(Actor)) {} diff --git a/contracts/integrations/test/actors/index.ts b/contracts/integrations/test/actors/index.ts index 14934ac89a..d712eb0efd 100644 --- a/contracts/integrations/test/actors/index.ts +++ b/contracts/integrations/test/actors/index.ts @@ -2,5 +2,8 @@ export { Actor } from './base'; export { Maker } from './maker'; export { PoolOperator } from './pool_operator'; export { FeeRecipient } from './fee_recipient'; +export { Staker } from './staker'; +export { Keeper } from './keeper'; +export { Taker } from './taker'; export * from './hybrids'; export * from './utils'; diff --git a/contracts/integrations/test/actors/keeper.ts b/contracts/integrations/test/actors/keeper.ts new file mode 100644 index 0000000000..11bb03078d --- /dev/null +++ b/contracts/integrations/test/actors/keeper.ts @@ -0,0 +1,72 @@ +import { IStakingEventsStakingPoolEarnedRewardsInEpochEventArgs, TestStakingEvents } from '@0x/contracts-staking'; +import { filterLogsToArguments, web3Wrapper } from '@0x/contracts-test-utils'; +import { BigNumber } from '@0x/utils'; +import { BlockParamLiteral, TransactionReceiptWithDecodedLogs } from 'ethereum-types'; + +import { Actor, Constructor } from './base'; + +export function KeeperMixin(Base: TBase) { + return class extends Base { + public readonly actor: Actor; + + /** + * The mixin pattern requires that this constructor uses `...args: any[]`, but this class + * really expects a single `Actor` parameter (assuming `Actor` is used as the base + * class). + */ + constructor(...args: any[]) { + super(...args); + this.actor = (this as any) as Actor; + } + + /** + * Ends the current epoch, fast-forwarding to the end of the epoch by default. + */ + public async endEpochAsync(shouldFastForward: boolean = true): Promise { + const { stakingWrapper } = this.actor.deployment.staking; + if (shouldFastForward) { + // increase timestamp of next block by how many seconds we need to + // get to the next epoch. + const epochEndTime = await stakingWrapper.getCurrentEpochEarliestEndTimeInSeconds.callAsync(); + const lastBlockTime = await web3Wrapper.getBlockTimestampAsync('latest'); + const dt = Math.max(0, epochEndTime.minus(lastBlockTime).toNumber()); + await web3Wrapper.increaseTimeAsync(dt); + // mine next block + await web3Wrapper.mineBlockAsync(); + } + return stakingWrapper.endEpoch.awaitTransactionSuccessAsync({ from: this.actor.address }); + } + + /** + * Finalizes staking pools corresponding to the given `poolIds`. If none are provided, + * finalizes all pools that earned rewards in the previous epoch. + */ + public async finalizePoolsAsync(poolIds: string[] = []): Promise { + const { stakingWrapper } = this.actor.deployment.staking; + // If no poolIds provided, finalize all active pools from the previous epoch + if (poolIds.length === 0) { + const previousEpoch = (await stakingWrapper.currentEpoch.callAsync()).minus(1); + const events = filterLogsToArguments( + await stakingWrapper.getLogsAsync( + TestStakingEvents.StakingPoolEarnedRewardsInEpoch, + { fromBlock: BlockParamLiteral.Earliest, toBlock: BlockParamLiteral.Latest }, + { epoch: new BigNumber(previousEpoch) }, + ), + TestStakingEvents.StakingPoolEarnedRewardsInEpoch, + ); + poolIds.concat(events.map(event => event.poolId)); + } + + return Promise.all( + poolIds.map( + async poolId => + await stakingWrapper.finalizePool.awaitTransactionSuccessAsync(poolId, { + from: this.actor.address, + }), + ), + ); + } + }; +} + +export class Keeper extends KeeperMixin(Actor) {} diff --git a/contracts/integrations/test/actors/maker.ts b/contracts/integrations/test/actors/maker.ts index 3cbefe4039..3106614f70 100644 --- a/contracts/integrations/test/actors/maker.ts +++ b/contracts/integrations/test/actors/maker.ts @@ -10,7 +10,7 @@ export interface MakerConfig extends ActorConfig { export function MakerMixin(Base: TBase) { return class extends Base { - public poolId?: string; + public makerPoolId?: string; public readonly actor: Actor; public readonly orderFactory: OrderFactory; @@ -56,7 +56,7 @@ export function MakerMixin(Base: TBase) { */ public async joinStakingPoolAsync(poolId: string): Promise { const stakingContract = this.actor.deployment.staking.stakingWrapper; - this.poolId = poolId; + this.makerPoolId = poolId; return stakingContract.joinStakingPoolAsMaker.awaitTransactionSuccessAsync(poolId, { from: this.actor.address, }); diff --git a/contracts/integrations/test/actors/pool_operator.ts b/contracts/integrations/test/actors/pool_operator.ts index 3f15b8a002..37aa400cc6 100644 --- a/contracts/integrations/test/actors/pool_operator.ts +++ b/contracts/integrations/test/actors/pool_operator.ts @@ -1,28 +1,24 @@ import { TransactionReceiptWithDecodedLogs } from 'ethereum-types'; -import { Actor, ActorConfig, Constructor } from './base'; +import { Actor, Constructor } from './base'; -export interface PoolOperatorConfig extends ActorConfig { - operatorShare: number; +export interface OperatorShareByPoolId { + [poolId: string]: number; } export function PoolOperatorMixin(Base: TBase) { return class extends Base { - public operatorShare: number; - public readonly poolIds: string[] = []; + public readonly operatorShares: OperatorShareByPoolId = {}; public readonly actor: Actor; /** * The mixin pattern requires that this constructor uses `...args: any[]`, but this class - * really expects a single `PoolOperatorConfig` parameter (assuming `Actor` is used as the + * really expects a single `ActorConfig` parameter (assuming `Actor` is used as the * base class). */ constructor(...args: any[]) { super(...args); this.actor = (this as any) as Actor; - - const { operatorShare } = args[0] as PoolOperatorConfig; - this.operatorShare = operatorShare; } /** @@ -41,7 +37,7 @@ export function PoolOperatorMixin(Base: TBase) { const createStakingPoolLog = txReceipt.logs[0]; const poolId = (createStakingPoolLog as any).args.poolId; - this.poolIds.push(poolId); + this.operatorShares[poolId] = operatorShare; return poolId; } @@ -53,7 +49,7 @@ export function PoolOperatorMixin(Base: TBase) { newOperatorShare: number, ): Promise { const stakingContract = this.actor.deployment.staking.stakingWrapper; - this.operatorShare = newOperatorShare; + this.operatorShares[poolId] = newOperatorShare; return stakingContract.decreaseStakingPoolOperatorShare.awaitTransactionSuccessAsync( poolId, newOperatorShare, diff --git a/contracts/integrations/test/actors/staker.ts b/contracts/integrations/test/actors/staker.ts new file mode 100644 index 0000000000..60d80cf34c --- /dev/null +++ b/contracts/integrations/test/actors/staker.ts @@ -0,0 +1,41 @@ +import { StakeInfo, StakeStatus } from '@0x/contracts-staking'; +import { BigNumber } from '@0x/utils'; + +import { Actor, Constructor } from './base'; + +export function StakerMixin(Base: TBase) { + return class extends Base { + public readonly actor: Actor; + + /** + * The mixin pattern requires that this constructor uses `...args: any[]`, but this class + * really expects a single `ActorConfig` parameter (assuming `Actor` is used as the base + * class). + */ + constructor(...args: any[]) { + super(...args); + this.actor = (this as any) as Actor; + } + + /** + * Stakes the given amount of ZRX. If `poolId` is provided, subsequently delegates the newly + * staked ZRX with that pool. + */ + public async stakeAsync(amount: BigNumber, poolId?: string): Promise { + const { stakingWrapper } = this.actor.deployment.staking; + await stakingWrapper.stake.awaitTransactionSuccessAsync(amount, { + from: this.actor.address, + }); + if (poolId !== undefined) { + await stakingWrapper.moveStake.awaitTransactionSuccessAsync( + new StakeInfo(StakeStatus.Undelegated), + new StakeInfo(StakeStatus.Delegated, poolId), + amount, + { from: this.actor.address }, + ); + } + } + }; +} + +export class Staker extends StakerMixin(Actor) {} diff --git a/contracts/integrations/test/actors/taker.ts b/contracts/integrations/test/actors/taker.ts new file mode 100644 index 0000000000..6a9e13ec26 --- /dev/null +++ b/contracts/integrations/test/actors/taker.ts @@ -0,0 +1,45 @@ +import { SignedOrder } from '@0x/types'; +import { BigNumber } from '@0x/utils'; +import { TransactionReceiptWithDecodedLogs, TxData } from 'ethereum-types'; + +import { Actor, Constructor } from './base'; +import { DeploymentManager } from '../utils/deployment_manager'; + +export function TakerMixin(Base: TBase) { + return class extends Base { + public readonly actor: Actor; + + /** + * The mixin pattern requires that this constructor uses `...args: any[]`, but this class + * really expects a single `ActorConfig` parameter (assuming `Actor` is used as the base + * class). + */ + constructor(...args: any[]) { + super(...args); + this.actor = (this as any) as Actor; + } + + /** + * Fills an order by the given `fillAmount`. Defaults to paying the protocol fee in ETH. + */ + public async fillOrderAsync( + order: SignedOrder, + fillAmount: BigNumber, + txData: Partial = {}, + ): Promise { + return this.actor.deployment.exchange.fillOrder.awaitTransactionSuccessAsync( + order, + fillAmount, + order.signature, + { + from: this.actor.address, + gasPrice: DeploymentManager.gasPrice, + value: DeploymentManager.protocolFee, + ...txData, + }, + ); + } + }; +} + +export class Taker extends TakerMixin(Actor) {} diff --git a/contracts/integrations/test/internal-integration-tests/exchange_integration_test.ts b/contracts/integrations/test/internal-integration-tests/exchange_integration_test.ts deleted file mode 100644 index 33269335c8..0000000000 --- a/contracts/integrations/test/internal-integration-tests/exchange_integration_test.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { blockchainTests, constants, expect, filterLogsToArguments, OrderFactory } from '@0x/contracts-test-utils'; -import { DummyERC20TokenContract, IERC20TokenEvents, IERC20TokenTransferEventArgs } from '@0x/contracts-erc20'; -import { IExchangeEvents, IExchangeFillEventArgs } from '@0x/contracts-exchange'; -import { IStakingEventsEvents } from '@0x/contracts-staking'; -import { assetDataUtils, orderHashUtils } from '@0x/order-utils'; -import { BigNumber } from '@0x/utils'; - -import { AddressManager } from '../utils/address_manager'; -import { DeploymentManager } from '../utils/deployment_manager'; - -blockchainTests('Exchange & Staking', env => { - let accounts: string[]; - let makerAddress: string; - let takers: string[] = []; - let delegators: string[] = []; - let feeRecipientAddress: string; - let addressManager: AddressManager; - let deploymentManager: DeploymentManager; - let orderFactory: OrderFactory; - let makerAsset: DummyERC20TokenContract; - let takerAsset: DummyERC20TokenContract; - let feeAsset: DummyERC20TokenContract; - - const GAS_PRICE = 1e9; - - before(async () => { - const chainId = await env.getChainIdAsync(); - accounts = await env.getAccountAddressesAsync(); - [makerAddress, feeRecipientAddress, takers[0], takers[1], ...delegators] = accounts.slice(1); - deploymentManager = await DeploymentManager.deployAsync(env); - - // Create a staking pool with the operator as a maker address. - await deploymentManager.staking.stakingWrapper.createStakingPool.awaitTransactionSuccessAsync( - constants.ZERO_AMOUNT, - true, - { from: makerAddress }, - ); - - // Set up an address for market making. - addressManager = new AddressManager(); - await addressManager.addMakerAsync( - deploymentManager, - { - address: makerAddress, - mainToken: deploymentManager.tokens.erc20[0], - feeToken: deploymentManager.tokens.erc20[2], - }, - env, - deploymentManager.tokens.erc20[1], - feeRecipientAddress, - chainId, - ); - - // Set up two addresses for taking orders. - await Promise.all( - takers.map(taker => - addressManager.addTakerAsync(deploymentManager, { - address: taker, - mainToken: deploymentManager.tokens.erc20[1], - feeToken: deploymentManager.tokens.erc20[2], - }), - ), - ); - }); - - describe('fillOrder', () => { - it('should be able to fill an order', async () => { - const order = await addressManager.makers[0].orderFactory.newSignedOrderAsync({ - makerAddress, - makerAssetAmount: new BigNumber(1), - takerAssetAmount: new BigNumber(1), - makerFee: constants.ZERO_AMOUNT, - takerFee: constants.ZERO_AMOUNT, - feeRecipientAddress, - }); - - const receipt = await deploymentManager.exchange.fillOrder.awaitTransactionSuccessAsync( - order, - new BigNumber(1), - order.signature, - { - from: takers[0], - gasPrice: GAS_PRICE, - value: DeploymentManager.protocolFeeMultiplier.times(GAS_PRICE), - }, - ); - - // Ensure that the number of emitted logs is equal to 3. There should have been a fill event - // and two transfer events. A 'StakingPoolActivated' event should not be expected because - // the only staking pool that was created does not have enough stake. - expect(receipt.logs.length).to.be.eq(3); - - // Ensure that the fill event was correct. - const fillArgs = filterLogsToArguments(receipt.logs, IExchangeEvents.Fill); - expect(fillArgs.length).to.be.eq(1); - expect(fillArgs).to.be.deep.eq([ - { - makerAddress, - feeRecipientAddress, - makerAssetData: order.makerAssetData, - takerAssetData: order.takerAssetData, - makerFeeAssetData: order.makerFeeAssetData, - takerFeeAssetData: order.takerFeeAssetData, - orderHash: orderHashUtils.getOrderHashHex(order), - takerAddress: takers[0], - senderAddress: takers[0], - makerAssetFilledAmount: order.makerAssetAmount, - takerAssetFilledAmount: order.takerAssetAmount, - makerFeePaid: constants.ZERO_AMOUNT, - takerFeePaid: constants.ZERO_AMOUNT, - protocolFeePaid: DeploymentManager.protocolFeeMultiplier.times(GAS_PRICE), - }, - ]); - - // Ensure that the transfer events were correctly emitted. - const transferArgs = filterLogsToArguments( - receipt.logs, - IERC20TokenEvents.Transfer, - ); - expect(transferArgs.length).to.be.eq(2); - expect(transferArgs).to.be.deep.eq([ - { - _from: takers[0], - _to: makerAddress, - _value: order.takerAssetAmount, - }, - { - _from: makerAddress, - _to: takers[0], - _value: order.makerAssetAmount, - }, - ]); - }); - }); -}); diff --git a/contracts/integrations/test/internal-integration-tests/fillorder_test.ts b/contracts/integrations/test/internal-integration-tests/fillorder_test.ts new file mode 100644 index 0000000000..14552d3e18 --- /dev/null +++ b/contracts/integrations/test/internal-integration-tests/fillorder_test.ts @@ -0,0 +1,331 @@ +import { blockchainTests, constants, expect } from '@0x/contracts-test-utils'; +import { IERC20TokenEvents, IERC20TokenTransferEventArgs } from '@0x/contracts-erc20'; +import { + BlockchainBalanceStore, + IExchangeEvents, + IExchangeFillEventArgs, + LocalBalanceStore, +} from '@0x/contracts-exchange'; +import { + constants as stakingConstants, + IStakingEventsEpochEndedEventArgs, + IStakingEventsEpochFinalizedEventArgs, + IStakingEventsEvents, + IStakingEventsRewardsPaidEventArgs, + IStakingEventsStakingPoolEarnedRewardsInEpochEventArgs, +} from '@0x/contracts-staking'; +import { SignedOrder } from '@0x/types'; +import { assetDataUtils, orderHashUtils } from '@0x/order-utils'; +import { toBaseUnitAmount, verifyEvents } from '@0x/contracts-test-utils'; +import { BigNumber } from '@0x/utils'; +import { TransactionReceiptWithDecodedLogs } from 'ethereum-types'; + +import { Taker, actorAddressesByName, FeeRecipient, Maker, OperatorStakerMaker, StakerKeeper } from '../actors'; +import { DeploymentManager } from '../utils/deployment_manager'; + +blockchainTests.resets('fillOrder integration tests', env => { + let deployment: DeploymentManager; + let balanceStore: BlockchainBalanceStore; + + let feeRecipient: FeeRecipient; + let operator: OperatorStakerMaker; + let maker: Maker; + let taker: Taker; + let delegator: StakerKeeper; + + let poolId: string; + + before(async () => { + deployment = await DeploymentManager.deployAsync(env, { + numErc20TokensToDeploy: 2, + numErc721TokensToDeploy: 0, + numErc1155TokensToDeploy: 0, + }); + const [makerToken, takerToken] = deployment.tokens.erc20; + + feeRecipient = new FeeRecipient({ + name: 'Fee recipient', + deployment, + }); + const orderConfig = { + feeRecipientAddress: feeRecipient.address, + makerAssetData: assetDataUtils.encodeERC20AssetData(makerToken.address), + takerAssetData: assetDataUtils.encodeERC20AssetData(takerToken.address), + makerFeeAssetData: assetDataUtils.encodeERC20AssetData(makerToken.address), + takerFeeAssetData: assetDataUtils.encodeERC20AssetData(takerToken.address), + makerFee: constants.ZERO_AMOUNT, + takerFee: constants.ZERO_AMOUNT, + }; + operator = new OperatorStakerMaker({ + name: 'Pool operator', + deployment, + orderConfig, + }); + maker = new Maker({ + name: 'Maker', + deployment, + orderConfig, + }); + taker = new Taker({ name: 'Taker', deployment }); + delegator = new StakerKeeper({ name: 'Delegator', deployment }); + + await operator.configureERC20TokenAsync(makerToken); + await maker.configureERC20TokenAsync(makerToken); + await taker.configureERC20TokenAsync(takerToken); + await taker.configureERC20TokenAsync(deployment.tokens.weth, deployment.staking.stakingProxy.address); + + await operator.configureERC20TokenAsync(deployment.tokens.zrx); + await delegator.configureERC20TokenAsync(deployment.tokens.zrx); + + // Create a staking pool with the operator as a maker. + poolId = await operator.createStakingPoolAsync(0.95 * stakingConstants.PPM, true); + // A vanilla maker joins the pool as well. + await maker.joinStakingPoolAsync(poolId); + + const tokenOwners = { + ...actorAddressesByName([feeRecipient, operator, maker, taker, delegator]), + StakingProxy: deployment.staking.stakingProxy.address, + ZrxVault: deployment.staking.zrxVault.address, + }; + console.log(tokenOwners); + const tokenContracts = { + erc20: { makerToken, takerToken, ZRX: deployment.tokens.zrx, WETH: deployment.tokens.weth }, + }; + balanceStore = new BlockchainBalanceStore(tokenOwners, tokenContracts); + await balanceStore.updateBalancesAsync(); + }); + + function simulateFill( + order: SignedOrder, + txReceipt: TransactionReceiptWithDecodedLogs, + msgValue: BigNumber = DeploymentManager.protocolFee, + ): LocalBalanceStore { + const localBalanceStore = LocalBalanceStore.create(balanceStore); + // Transaction gas cost + localBalanceStore.burnGas(txReceipt.from, DeploymentManager.gasPrice.times(txReceipt.gasUsed)); + + // Taker -> Maker + localBalanceStore.transferAsset(taker.address, maker.address, order.takerAssetAmount, order.takerAssetData); + // Maker -> Taker + localBalanceStore.transferAsset(maker.address, taker.address, order.makerAssetAmount, order.makerAssetData); + + // Protocol fee + if (msgValue.isGreaterThanOrEqualTo(DeploymentManager.protocolFee)) { + localBalanceStore.sendEth( + txReceipt.from, + deployment.staking.stakingProxy.address, + DeploymentManager.protocolFee, + ); + msgValue = msgValue.minus(DeploymentManager.protocolFee); + } else { + localBalanceStore.transferAsset( + taker.address, + deployment.staking.stakingProxy.address, + DeploymentManager.protocolFee, + assetDataUtils.encodeERC20AssetData(deployment.tokens.weth.address), + ); + } + + return localBalanceStore; + } + + function verifyFillEvents(order: SignedOrder, receipt: TransactionReceiptWithDecodedLogs): void { + // Ensure that the fill event was correct. + verifyEvents( + receipt, + [ + { + makerAddress: maker.address, + feeRecipientAddress: feeRecipient.address, + makerAssetData: order.makerAssetData, + takerAssetData: order.takerAssetData, + makerFeeAssetData: order.makerFeeAssetData, + takerFeeAssetData: order.takerFeeAssetData, + orderHash: orderHashUtils.getOrderHashHex(order), + takerAddress: taker.address, + senderAddress: taker.address, + makerAssetFilledAmount: order.makerAssetAmount, + takerAssetFilledAmount: order.takerAssetAmount, + makerFeePaid: constants.ZERO_AMOUNT, + takerFeePaid: constants.ZERO_AMOUNT, + protocolFeePaid: DeploymentManager.protocolFee, + }, + ], + IExchangeEvents.Fill, + ); + + // Ensure that the transfer events were correctly emitted. + verifyEvents( + receipt, + [ + { + _from: taker.address, + _to: maker.address, + _value: order.takerAssetAmount, + }, + { + _from: maker.address, + _to: taker.address, + _value: order.makerAssetAmount, + }, + ], + IERC20TokenEvents.Transfer, + ); + } + + it('should fill an order', async () => { + // Create and fill the order + const order = await maker.signOrderAsync(); + const receipt = await taker.fillOrderAsync(order, order.takerAssetAmount); + + // Check balances + const expectedBalances = simulateFill(order, receipt); + await balanceStore.updateBalancesAsync(); + balanceStore.assertEquals(expectedBalances); + + // There should have been a fill event and two transfer events. A + // 'StakingPoolEarnedRewardsInEpoch' event should not be expected because the only staking + // pool that was created does not have enough stake. + verifyFillEvents(order, receipt); + }); + it('should activate a staking pool if it has sufficient stake', async () => { + // Stake just enough to qualify the pool for rewards. + await delegator.stakeAsync(toBaseUnitAmount(100), poolId); + + // The delegator, functioning as a keeper, ends the epoch so that delegated stake (theirs + // and the operator's) becomes active. This puts the staking pool above the minimumPoolStake + // threshold, so it should be able to earn rewards in the new epoch. + // Finalizing the pool shouldn't settle rewards because it didn't earn rewards last epoch. + await delegator.endEpochAsync(); + await delegator.finalizePoolsAsync([poolId]); + await balanceStore.updateBalancesAsync(); + + // Create and fill the order + const order = await maker.signOrderAsync(); + const receipt = await taker.fillOrderAsync(order, order.takerAssetAmount); + + // Check balances + const expectedBalances = simulateFill(order, receipt); + await balanceStore.updateBalancesAsync(); + balanceStore.assertEquals(expectedBalances); + + // In addition to the fill event and two transfer events emitted in the previous test, we + // now expect a `StakingPoolEarnedRewardsInEpoch` event to be emitted because the staking + // pool now has enough stake in the current epoch to earn rewards. + verifyFillEvents(order, receipt); + const currentEpoch = await deployment.staking.stakingWrapper.currentEpoch.callAsync(); + verifyEvents( + receipt, + [ + { + epoch: currentEpoch, + poolId, + }, + ], + IStakingEventsEvents.StakingPoolEarnedRewardsInEpoch, + ); + }); + it('should pay out rewards to operator and delegator', async () => { + // Operator and delegator each stake some ZRX; wait an epoch so that the stake is active. + await operator.stakeAsync(toBaseUnitAmount(100), poolId); + await delegator.stakeAsync(toBaseUnitAmount(50), poolId); + await delegator.endEpochAsync(); + + // Create and fill the order. One order's worth of protocol fees are now available as rewards. + const order = await maker.signOrderAsync(); + await taker.fillOrderAsync(order, order.takerAssetAmount); + const rewardsAvailable = DeploymentManager.protocolFee; + + // Fetch the current balances + await balanceStore.updateBalancesAsync(); + const expectedBalances = LocalBalanceStore.create(balanceStore); + + // End the epoch. This should wrap the staking proxy's ETH balance. + const endEpochReceipt = await delegator.endEpochAsync(); + const newEpoch = await deployment.staking.stakingWrapper.currentEpoch.callAsync(); + + // Check balances + expectedBalances.wrapEth( + deployment.staking.stakingProxy.address, + deployment.tokens.weth.address, + DeploymentManager.protocolFee, + ); + expectedBalances.burnGas(delegator.address, DeploymentManager.gasPrice.times(endEpochReceipt.gasUsed)); + await balanceStore.updateBalancesAsync(); + balanceStore.assertEquals(expectedBalances); + + // Check the EpochEnded event + const weightedDelegatorStake = toBaseUnitAmount(50).times(0.9); + verifyEvents( + endEpochReceipt, + [ + { + epoch: newEpoch.minus(1), + numPoolsToFinalize: new BigNumber(1), + rewardsAvailable, + totalFeesCollected: DeploymentManager.protocolFee, + totalWeightedStake: toBaseUnitAmount(100).plus(weightedDelegatorStake), + }, + ], + IStakingEventsEvents.EpochEnded, + ); + + // The rewards are split between the operator and delegator based on the pool's operatorShare + const operatorReward = rewardsAvailable + .times(operator.operatorShares[poolId]) + .dividedToIntegerBy(constants.PPM_DENOMINATOR); + const delegatorReward = rewardsAvailable.minus(operatorReward); + + // Finalize the pool. This should automatically pay the operator in WETH. + const [finalizePoolReceipt] = await delegator.finalizePoolsAsync([poolId]); + + // Check balances + expectedBalances.transferAsset( + deployment.staking.stakingProxy.address, + operator.address, + operatorReward, + assetDataUtils.encodeERC20AssetData(deployment.tokens.weth.address), + ); + expectedBalances.burnGas(delegator.address, DeploymentManager.gasPrice.times(finalizePoolReceipt.gasUsed)); + await balanceStore.updateBalancesAsync(); + balanceStore.assertEquals(expectedBalances); + + // Check finalization events + verifyEvents( + finalizePoolReceipt, + [ + { + epoch: newEpoch, + poolId, + operatorReward, + membersReward: delegatorReward, + }, + ], + IStakingEventsEvents.RewardsPaid, + ); + verifyEvents( + finalizePoolReceipt, + [ + { + epoch: newEpoch.minus(1), + rewardsPaid: rewardsAvailable, + rewardsRemaining: constants.ZERO_AMOUNT, + }, + ], + IStakingEventsEvents.EpochFinalized, + ); + }); + it('should credit rewards from orders made by the operator to their pool', async () => { + // Stake just enough to qualify the pool for rewards. + await delegator.stakeAsync(toBaseUnitAmount(100), poolId); + await delegator.endEpochAsync(); + + // Create and fill the order + const order = await operator.signOrderAsync(); + await taker.fillOrderAsync(order, order.takerAssetAmount); + + // Check that the pool has collected fees from the above fill. + const poolStats = await deployment.staking.stakingWrapper.getStakingPoolStatsThisEpoch.callAsync(poolId); + expect(poolStats.feesCollected).to.bignumber.equal(DeploymentManager.protocolFee); + }); +}); diff --git a/contracts/integrations/test/utils/address_manager.ts b/contracts/integrations/test/utils/address_manager.ts deleted file mode 100644 index 1aa01daedf..0000000000 --- a/contracts/integrations/test/utils/address_manager.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { DummyERC20TokenContract } from '@0x/contracts-erc20'; -import { constants, OrderFactory, BlockchainTestsEnvironment } from '@0x/contracts-test-utils'; -import { assetDataUtils, Order, SignatureType, SignedOrder } from '@0x/order-utils'; - -import { DeploymentManager } from '../../src'; - -interface MarketMaker { - address: string; - orderFactory: OrderFactory; -} - -interface ConfigurationArgs { - address: string; - mainToken: DummyERC20TokenContract; - feeToken: DummyERC20TokenContract; -} - -export class AddressManager { - // A set of addresses that have been configured for market making. - public makers: MarketMaker[]; - - // A set of addresses that have been configured to take orders. - public takers: string[]; - - /** - * Sets up an address to take orders. - */ - public async addTakerAsync(deploymentManager: DeploymentManager, configArgs: ConfigurationArgs): Promise { - // Configure the taker address with the taker and fee tokens. - await this._configureTokenForAddressAsync(deploymentManager, configArgs.address, configArgs.mainToken); - await this._configureTokenForAddressAsync(deploymentManager, configArgs.address, configArgs.feeToken); - - // Add the taker to the list of configured taker addresses. - this.takers.push(configArgs.address); - } - - /** - * Sets up an address for market making. - */ - public async addMakerAsync( - deploymentManager: DeploymentManager, - configArgs: ConfigurationArgs, - environment: BlockchainTestsEnvironment, - takerToken: DummyERC20TokenContract, - feeRecipientAddress: string, - chainId: number, - ): Promise { - const accounts = await environment.getAccountAddressesAsync(); - - // Set up order signing for the maker address. - const defaultOrderParams = { - ...constants.STATIC_ORDER_PARAMS, - makerAddress: configArgs.address, - makerAssetData: assetDataUtils.encodeERC20AssetData(configArgs.mainToken.address), - takerAssetData: assetDataUtils.encodeERC20AssetData(takerToken.address), - makerFeeAssetData: assetDataUtils.encodeERC20AssetData(configArgs.feeToken.address), - takerFeeAssetData: assetDataUtils.encodeERC20AssetData(configArgs.feeToken.address), - feeRecipientAddress, - exchangeAddress: deploymentManager.exchange.address, - chainId, - }; - const privateKey = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(configArgs.address)]; - const orderFactory = new OrderFactory(privateKey, defaultOrderParams); - - // Configure the maker address with the maker and fee tokens. - await this._configureTokenForAddressAsync(deploymentManager, configArgs.address, configArgs.mainToken); - await this._configureTokenForAddressAsync(deploymentManager, configArgs.address, configArgs.feeToken); - - // Add the maker to the list of configured maker addresses. - this.makers.push({ - address: configArgs.address, - orderFactory, - }); - } - - /** - * Sets up initial account balances for a token and approves the ERC20 asset proxy - * to transfer the token. - */ - protected async _configureTokenForAddressAsync( - deploymentManager: DeploymentManager, - address: string, - token: DummyERC20TokenContract, - ): Promise { - await token.setBalance.awaitTransactionSuccessAsync(address, constants.INITIAL_ERC20_BALANCE); - await token.approve.awaitTransactionSuccessAsync( - deploymentManager.assetProxies.erc20Proxy.address, - constants.MAX_UINT256, - { from: address }, - ); - } - - constructor() { - this.makers = []; - this.takers = []; - } -} diff --git a/contracts/integrations/test/utils/deployment_manager.ts b/contracts/integrations/test/utils/deployment_manager.ts index 6d3107b8e2..9cae5de926 100644 --- a/contracts/integrations/test/utils/deployment_manager.ts +++ b/contracts/integrations/test/utils/deployment_manager.ts @@ -7,12 +7,7 @@ import { StaticCallProxyContract, } from '@0x/contracts-asset-proxy'; import { artifacts as ERC1155Artifacts, ERC1155MintableContract } from '@0x/contracts-erc1155'; -import { - DummyERC20TokenContract, - artifacts as ERC20Artifacts, - ZRXTokenContract, - WETH9Contract, -} from '@0x/contracts-erc20'; +import { DummyERC20TokenContract, artifacts as ERC20Artifacts, WETH9Contract } from '@0x/contracts-erc20'; import { artifacts as ERC721Artifacts, DummyERC721TokenContract } from '@0x/contracts-erc721'; import { artifacts as exchangeArtifacts, @@ -108,7 +103,7 @@ interface TokenContracts { erc721: DummyERC721TokenContract[]; erc1155: ERC1155MintableContract[]; weth: WETH9Contract; - zrx: ZRXTokenContract; + zrx: DummyERC20TokenContract; } // Options to be passed to `deployAsync` @@ -150,7 +145,7 @@ export class DeploymentManager { exchangeArtifacts.Exchange, environment.provider, txDefaults, - { ...ERC20Artifacts, ...exchangeArtifacts }, + { ...ERC20Artifacts, ...exchangeArtifacts, ...stakingArtifacts }, new BigNumber(chainId), ); const governor = await ZeroExGovernorContract.deployFrom0xArtifactAsync( @@ -357,7 +352,7 @@ export class DeploymentManager { txDefaults, stakingArtifacts, tokens.weth.address, - tokens.zrx.address, + zrxVault.address, ); const stakingProxy = await StakingProxyContract.deployFrom0xArtifactAsync( stakingArtifacts.StakingProxy, @@ -366,7 +361,17 @@ export class DeploymentManager { stakingArtifacts, stakingLogic.address, ); - const stakingWrapper = new TestStakingContract(stakingProxy.address, environment.provider); + + const logDecoderDependencies = _.mapValues( + { ...stakingArtifacts, ...ERC20Artifacts, ...exchangeArtifacts }, + 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.awaitTransactionSuccessAsync(tokens.weth.address, { from: owner }); @@ -376,9 +381,13 @@ export class DeploymentManager { await stakingProxy.addAuthorizedAddress.awaitTransactionSuccessAsync(owner, { from: owner }); await zrxVault.addAuthorizedAddress.awaitTransactionSuccessAsync(owner, { from: owner }); + // Authorize the zrx vault in the erc20 proxy + await assetProxies.erc20Proxy.addAuthorizedAddress.awaitTransactionSuccessAsync(zrxVault.address, { + from: owner, + }); + // Configure the zrx vault and the staking contract. await zrxVault.setStakingProxy.awaitTransactionSuccessAsync(stakingProxy.address, { from: owner }); - await zrxVault.setStakingProxy.awaitTransactionSuccessAsync(stakingProxy.address, { from: owner }); return { stakingLogic, @@ -461,11 +470,15 @@ export class DeploymentManager { txDefaults, ERC20Artifacts, ); - const zrx = await ZRXTokenContract.deployFrom0xArtifactAsync( - ERC20Artifacts.ZRXToken, + const zrx = await DummyERC20TokenContract.deployFrom0xArtifactAsync( + ERC20Artifacts.DummyERC20Token, environment.provider, txDefaults, ERC20Artifacts, + constants.DUMMY_TOKEN_NAME, + constants.DUMMY_TOKEN_SYMBOL, + constants.DUMMY_TOKEN_DECIMALS, + constants.DUMMY_TOKEN_TOTAL_SUPPLY, ); return { diff --git a/contracts/staking/contracts/src/fees/MixinExchangeFees.sol b/contracts/staking/contracts/src/fees/MixinExchangeFees.sol index 6d908881eb..63f3aacf82 100644 --- a/contracts/staking/contracts/src/fees/MixinExchangeFees.sol +++ b/contracts/staking/contracts/src/fees/MixinExchangeFees.sol @@ -82,7 +82,6 @@ contract MixinExchangeFees is } // Look up the pool stats and aggregated stats for this epoch. - uint256 currentEpoch_ = currentEpoch; IStructs.PoolStats storage poolStatsPtr = poolStatsByEpoch[poolId][currentEpoch_]; IStructs.AggregatedStats storage aggregatedStatsPtr = aggregatedStatsByEpoch[currentEpoch_]; diff --git a/contracts/staking/package.json b/contracts/staking/package.json index 0980cac786..61a3e1cd3f 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|TestMixinStakeStorage|TestMixinStakingPool|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|TestProtocolFees|TestStaking|TestStakingNoWETH|TestStakingProxy|TestStorageLayoutAndConstants|ZrxVault).json" }, "repository": { "type": "git", diff --git a/contracts/staking/src/index.ts b/contracts/staking/src/index.ts index 4cb7af88e1..2f65d0dcec 100644 --- a/contracts/staking/src/index.ts +++ b/contracts/staking/src/index.ts @@ -1,3 +1,4 @@ export * from './wrappers'; export * from './artifacts'; export { constants } from '../test/utils/constants'; +export * from '../test/utils/types'; diff --git a/contracts/test-utils/src/log_utils.ts b/contracts/test-utils/src/log_utils.ts index 07086eca5c..313b7e07d6 100644 --- a/contracts/test-utils/src/log_utils.ts +++ b/contracts/test-utils/src/log_utils.ts @@ -38,8 +38,8 @@ export function verifyEventsFromLogs( eventName: string, ): void { const _logs = filterLogsToArguments(logs, eventName); - expect(_logs.length).to.eq(expectedEvents.length); + expect(_logs.length, `Number of ${eventName} events emitted`).to.eq(expectedEvents.length); _logs.forEach((log, index) => { - expect(log).to.deep.equal(expectedEvents[index]); + expect(log, `${eventName} event ${index}`).to.deep.equal(expectedEvents[index]); }); }