Flesh out fillOrder integration tests

This commit is contained in:
Michael Zhu 2019-10-28 15:37:42 -07:00
parent 8aa69233e0
commit 7aa88307f6
17 changed files with 538 additions and 264 deletions

View File

@ -19,7 +19,7 @@ export class BlockchainBalanceStore extends BalanceStore {
public constructor(
tokenOwnersByName: TokenOwnersByName,
tokenContractsByName: Partial<TokenContractsByName>,
tokenIds: Partial<TokenIds>,
tokenIds: Partial<TokenIds> = {},
) {
super(tokenOwnersByName, tokenContractsByName);
this._tokenContracts = {

View File

@ -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';

View File

@ -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)) {}

View File

@ -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';

View File

@ -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<TBase extends Constructor>(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<TransactionReceiptWithDecodedLogs> {
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<TransactionReceiptWithDecodedLogs[]> {
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<IStakingEventsStakingPoolEarnedRewardsInEpochEventArgs>(
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) {}

View File

@ -10,7 +10,7 @@ export interface MakerConfig extends ActorConfig {
export function MakerMixin<TBase extends Constructor>(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<TBase extends Constructor>(Base: TBase) {
*/
public async joinStakingPoolAsync(poolId: string): Promise<TransactionReceiptWithDecodedLogs> {
const stakingContract = this.actor.deployment.staking.stakingWrapper;
this.poolId = poolId;
this.makerPoolId = poolId;
return stakingContract.joinStakingPoolAsMaker.awaitTransactionSuccessAsync(poolId, {
from: this.actor.address,
});

View File

@ -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<TBase extends Constructor>(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<TBase extends Constructor>(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<TBase extends Constructor>(Base: TBase) {
newOperatorShare: number,
): Promise<TransactionReceiptWithDecodedLogs> {
const stakingContract = this.actor.deployment.staking.stakingWrapper;
this.operatorShare = newOperatorShare;
this.operatorShares[poolId] = newOperatorShare;
return stakingContract.decreaseStakingPoolOperatorShare.awaitTransactionSuccessAsync(
poolId,
newOperatorShare,

View File

@ -0,0 +1,41 @@
import { StakeInfo, StakeStatus } from '@0x/contracts-staking';
import { BigNumber } from '@0x/utils';
import { Actor, Constructor } from './base';
export function StakerMixin<TBase extends Constructor>(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<void> {
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) {}

View File

@ -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<TBase extends Constructor>(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<TxData> = {},
): Promise<TransactionReceiptWithDecodedLogs> {
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) {}

View File

@ -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<IExchangeFillEventArgs>(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<IERC20TokenTransferEventArgs>(
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,
},
]);
});
});
});

View File

@ -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<IExchangeFillEventArgs>(
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<IERC20TokenTransferEventArgs>(
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<IStakingEventsStakingPoolEarnedRewardsInEpochEventArgs>(
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<IStakingEventsEpochEndedEventArgs>(
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<IStakingEventsRewardsPaidEventArgs>(
finalizePoolReceipt,
[
{
epoch: newEpoch,
poolId,
operatorReward,
membersReward: delegatorReward,
},
],
IStakingEventsEvents.RewardsPaid,
);
verifyEvents<IStakingEventsEpochFinalizedEventArgs>(
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);
});
});

View File

@ -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<void> {
// 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<void> {
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<void> {
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 = [];
}
}

View File

@ -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 {

View File

@ -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_];

View File

@ -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",

View File

@ -1,3 +1,4 @@
export * from './wrappers';
export * from './artifacts';
export { constants } from '../test/utils/constants';
export * from '../test/utils/types';

View File

@ -38,8 +38,8 @@ export function verifyEventsFromLogs<TEventArgs>(
eventName: string,
): void {
const _logs = filterLogsToArguments<TEventArgs>(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]);
});
}