Refactor integrations directory structure; move core.ts, balance stores, and FillOrderWrapper to integrations

This commit is contained in:
Michael Zhu
2019-11-12 17:10:08 -08:00
parent bdca84fe72
commit 7f4cbba076
50 changed files with 387 additions and 446 deletions

View File

@@ -0,0 +1,107 @@
import { DummyERC20TokenContract, WETH9Contract } from '@0x/contracts-erc20';
import { DummyERC721TokenContract } from '@0x/contracts-erc721';
import { constants, getRandomInteger, TransactionFactory } from '@0x/contracts-test-utils';
import { SignatureType, SignedZeroExTransaction, ZeroExTransaction } from '@0x/types';
import { BigNumber } from '@0x/utils';
import * as _ from 'lodash';
import { AssertionResult } from '../assertions/function_assertion';
import { DeploymentManager } from '../deployment_manager';
import { SimulationEnvironment } from '../simulation';
export type Constructor<T = {}> = new (...args: any[]) => T;
export interface ActorConfig {
name?: string;
deployment: DeploymentManager;
simulationEnvironment?: SimulationEnvironment;
[mixinProperty: string]: any;
}
export class Actor {
public static count: number = 0;
public readonly address: string;
public readonly name: string;
public readonly privateKey: Buffer;
public readonly deployment: DeploymentManager;
public readonly simulationEnvironment?: SimulationEnvironment;
public simulationActions: {
[action: string]: AsyncIterableIterator<AssertionResult | void>;
} = {};
protected readonly _transactionFactory: TransactionFactory;
constructor(config: ActorConfig) {
Actor.count++;
this.address = config.deployment.accounts[Actor.count];
this.name = config.name || this.address;
this.deployment = config.deployment;
this.privateKey = constants.TESTRPC_PRIVATE_KEYS[config.deployment.accounts.indexOf(this.address)];
this.simulationEnvironment = config.simulationEnvironment;
this._transactionFactory = new TransactionFactory(
this.privateKey,
config.deployment.exchange.address,
config.deployment.chainId,
);
}
/**
* Sets a balance for an ERC20 token and approves a spender (defaults to the ERC20 asset proxy)
* to transfer the token.
*/
public async configureERC20TokenAsync(
token: DummyERC20TokenContract | WETH9Contract,
spender?: string,
amount?: BigNumber,
): Promise<void> {
if (token instanceof DummyERC20TokenContract) {
await token
.setBalance(this.address, amount || constants.INITIAL_ERC20_BALANCE)
.awaitTransactionSuccessAsync();
} else {
await token.deposit().awaitTransactionSuccessAsync({
from: this.address,
value: amount || constants.ONE_ETHER,
});
}
await token
.approve(spender || this.deployment.assetProxies.erc20Proxy.address, constants.MAX_UINT256)
.awaitTransactionSuccessAsync({ from: this.address });
}
/**
* Mints some number of ERC721 NFTs and approves a spender (defaults to the ERC721 asset proxy)
* to transfer the token.
*/
public async configureERC721TokenAsync(
token: DummyERC721TokenContract,
spender?: string,
numToMint: number = 1,
): Promise<BigNumber[]> {
const tokenIds: BigNumber[] = [];
_.times(numToMint, async () => {
const tokenId = getRandomInteger(constants.ZERO_AMOUNT, constants.MAX_UINT256);
await token.mint(this.address, tokenId).awaitTransactionSuccessAsync({
from: this.address,
});
tokenIds.push(tokenId);
});
await token
.setApprovalForAll(spender || this.deployment.assetProxies.erc721Proxy.address, true)
.awaitTransactionSuccessAsync({
from: this.address,
});
return tokenIds;
}
/**
* Signs a transaction.
*/
public async signTransactionAsync(
customTransactionParams: Partial<ZeroExTransaction>,
signatureType: SignatureType = SignatureType.EthSign,
): Promise<SignedZeroExTransaction> {
return this._transactionFactory.newSignedTransactionAsync(customTransactionParams, signatureType);
}
}

View File

@@ -0,0 +1,61 @@
import { BaseContract } from '@0x/base-contract';
import { ApprovalFactory, SignedCoordinatorApproval } from '@0x/contracts-coordinator';
import { SignatureType, SignedZeroExTransaction } from '@0x/types';
import { Actor, ActorConfig, Constructor } from './base';
interface FeeRecipientConfig extends ActorConfig {
verifyingContract?: BaseContract;
}
export interface FeeRecipientInterface {
approvalFactory?: ApprovalFactory;
signCoordinatorApprovalAsync: (
transaction: SignedZeroExTransaction,
txOrigin: string,
signatureType?: SignatureType,
) => Promise<SignedCoordinatorApproval>;
}
/**
* This mixin encapsulates functionaltiy associated with fee recipients within the 0x ecosystem.
* As of writing, the only extra functionality provided is signing Coordinator approvals.
*/
export function FeeRecipientMixin<TBase extends Constructor>(Base: TBase): TBase & Constructor<FeeRecipientInterface> {
return class extends Base {
public readonly actor: Actor;
public readonly approvalFactory?: ApprovalFactory;
/**
* The mixin pattern requires that this constructor uses `...args: any[]`, but this class
* really expects a single `FeeRecipientConfig` parameter (assuming `Actor` is used as the
* base class).
*/
constructor(...args: any[]) {
// tslint:disable-next-line:no-inferred-empty-object-type
super(...args);
this.actor = (this as any) as Actor;
const { verifyingContract } = args[0] as FeeRecipientConfig;
if (verifyingContract !== undefined) {
this.approvalFactory = new ApprovalFactory(this.actor.privateKey, verifyingContract.address);
}
}
/**
* Signs an coordinator transaction.
*/
public async signCoordinatorApprovalAsync(
transaction: SignedZeroExTransaction,
txOrigin: string,
signatureType: SignatureType = SignatureType.EthSign,
): Promise<SignedCoordinatorApproval> {
if (this.approvalFactory === undefined) {
throw new Error('No verifying contract provided in FeeRecipient constructor');
}
return this.approvalFactory.newSignedApprovalAsync(transaction, txOrigin, signatureType);
}
};
}
export class FeeRecipient extends FeeRecipientMixin(Actor) {}

View File

@@ -0,0 +1,11 @@
import { Actor } from './base';
import { KeeperMixin } from './keeper';
import { MakerMixin } from './maker';
import { PoolOperatorMixin } from './pool_operator';
import { StakerMixin } from './staker';
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

@@ -0,0 +1,81 @@
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 interface KeeperInterface {
endEpochAsync: (shouldFastForward?: boolean) => Promise<TransactionReceiptWithDecodedLogs>;
finalizePoolsAsync: (poolIds?: string[]) => Promise<TransactionReceiptWithDecodedLogs[]>;
}
/**
* This mixin encapsulates functionaltiy associated with keepers within the 0x ecosystem.
* This includes ending epochs sand finalizing pools in the staking system.
*/
export function KeeperMixin<TBase extends Constructor>(Base: TBase): TBase & Constructor<KeeperInterface> {
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[]) {
// tslint:disable-next-line:no-inferred-empty-object-type
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 =>
stakingWrapper.finalizePool(poolId).awaitTransactionSuccessAsync({
from: this.actor.address,
}),
),
);
}
};
}
export class Keeper extends KeeperMixin(Actor) {}

View File

@@ -0,0 +1,80 @@
import { constants, OrderFactory, orderUtils } from '@0x/contracts-test-utils';
import { Order, SignedOrder } from '@0x/types';
import { TransactionReceiptWithDecodedLogs } from 'ethereum-types';
import { Actor, ActorConfig, Constructor } from './base';
interface MakerConfig extends ActorConfig {
orderConfig: Partial<Order>;
}
export interface MakerInterface {
makerPoolId?: string;
orderFactory: OrderFactory;
signOrderAsync: (customOrderParams?: Partial<Order>) => Promise<SignedOrder>;
cancelOrderAsync: (order: SignedOrder) => Promise<TransactionReceiptWithDecodedLogs>;
joinStakingPoolAsync: (poolId: string) => Promise<TransactionReceiptWithDecodedLogs>;
}
/**
* This mixin encapsulates functionaltiy associated with makers within the 0x ecosystem.
* This includes signing and canceling orders, as well as joining a staking pool as a maker.
*/
export function MakerMixin<TBase extends Constructor>(Base: TBase): TBase & Constructor<MakerInterface> {
return class extends Base {
public makerPoolId?: string;
public readonly actor: Actor;
public readonly orderFactory: OrderFactory;
/**
* The mixin pattern requires that this constructor uses `...args: any[]`, but this class
* really expects a single `MakerConfig` parameter (assuming `Actor` is used as the base
* class).
*/
constructor(...args: any[]) {
// tslint:disable-next-line:no-inferred-empty-object-type
super(...args);
this.actor = (this as any) as Actor;
const { orderConfig } = args[0] as MakerConfig;
const defaultOrderParams = {
...constants.STATIC_ORDER_PARAMS,
makerAddress: this.actor.address,
exchangeAddress: this.actor.deployment.exchange.address,
chainId: this.actor.deployment.chainId,
...orderConfig,
};
this.orderFactory = new OrderFactory(this.actor.privateKey, defaultOrderParams);
}
/**
* Signs an order (optionally, with custom parameters) as the maker.
*/
public async signOrderAsync(customOrderParams: Partial<Order> = {}): Promise<SignedOrder> {
return this.orderFactory.newSignedOrderAsync(customOrderParams);
}
/**
* Cancels one of the maker's orders.
*/
public async cancelOrderAsync(order: SignedOrder): Promise<TransactionReceiptWithDecodedLogs> {
const params = orderUtils.createCancel(order);
return this.actor.deployment.exchange.cancelOrder(params.order).awaitTransactionSuccessAsync({
from: this.actor.address,
});
}
/**
* Joins the staking pool specified by the given ID.
*/
public async joinStakingPoolAsync(poolId: string): Promise<TransactionReceiptWithDecodedLogs> {
const stakingContract = this.actor.deployment.staking.stakingWrapper;
this.makerPoolId = poolId;
return stakingContract.joinStakingPoolAsMaker(poolId).awaitTransactionSuccessAsync({
from: this.actor.address,
});
}
};
}
export class Maker extends MakerMixin(Actor) {}

View File

@@ -0,0 +1,107 @@
import { constants, StakingPoolById } from '@0x/contracts-staking';
import { getRandomInteger } from '@0x/contracts-test-utils';
import '@azure/core-asynciterator-polyfill';
import { TransactionReceiptWithDecodedLogs } from 'ethereum-types';
import * as _ from 'lodash';
import { validCreateStakingPoolAssertion } from '../assertions/createStakingPool';
import { validDecreaseStakingPoolOperatorShareAssertion } from '../assertions/decreaseStakingPoolOperatorShare';
import { AssertionResult } from '../assertions/function_assertion';
import { Actor, Constructor } from './base';
export interface PoolOperatorInterface {
createStakingPoolAsync: (operatorShare: number, addOperatorAsMaker?: boolean) => Promise<string>;
decreaseOperatorShareAsync: (
poolId: string,
newOperatorShare: number,
) => Promise<TransactionReceiptWithDecodedLogs>;
}
/**
* This mixin encapsulates functionaltiy associated with pool operators within the 0x ecosystem.
* This includes creating staking pools and decreasing the operator share of a pool.
*/
export function PoolOperatorMixin<TBase extends Constructor>(Base: TBase): TBase & Constructor<PoolOperatorInterface> {
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[]) {
// tslint:disable-next-line:no-inferred-empty-object-type
super(...args);
this.actor = (this as any) as Actor;
// Register this mixin's assertion generators
this.actor.simulationActions = {
...this.actor.simulationActions,
validCreateStakingPool: this._validCreateStakingPool(),
validDecreaseStakingPoolOperatorShare: this._validDecreaseStakingPoolOperatorShare(),
};
}
/**
* Creates a staking pool and returns the ID of the new pool.
*/
public async createStakingPoolAsync(
operatorShare: number,
addOperatorAsMaker: boolean = false,
): Promise<string> {
const stakingContract = this.actor.deployment.staking.stakingWrapper;
const txReceipt = await stakingContract
.createStakingPool(operatorShare, addOperatorAsMaker)
.awaitTransactionSuccessAsync({ from: this.actor.address });
const createStakingPoolLog = txReceipt.logs[0];
const poolId = (createStakingPoolLog as any).args.poolId;
return poolId;
}
/**
* Decreases the operator share of a specified staking pool.
*/
public async decreaseOperatorShareAsync(
poolId: string,
newOperatorShare: number,
): Promise<TransactionReceiptWithDecodedLogs> {
const stakingContract = this.actor.deployment.staking.stakingWrapper;
return stakingContract
.decreaseStakingPoolOperatorShare(poolId, newOperatorShare)
.awaitTransactionSuccessAsync({ from: this.actor.address });
}
private _getOperatorPoolIds(stakingPools: StakingPoolById): string[] {
const operatorPools = _.pickBy(stakingPools, pool => pool.operator === this.actor.address);
return Object.keys(operatorPools);
}
private async *_validCreateStakingPool(): AsyncIterableIterator<AssertionResult> {
const { stakingPools } = this.actor.simulationEnvironment!;
const assertion = validCreateStakingPoolAssertion(this.actor.deployment, stakingPools);
while (true) {
const operatorShare = getRandomInteger(0, constants.PPM);
yield assertion.executeAsync(operatorShare, false, { from: this.actor.address });
}
}
private async *_validDecreaseStakingPoolOperatorShare(): AsyncIterableIterator<AssertionResult | void> {
const { stakingPools } = this.actor.simulationEnvironment!;
const assertion = validDecreaseStakingPoolOperatorShareAssertion(this.actor.deployment, stakingPools);
while (true) {
const poolId = _.sample(this._getOperatorPoolIds(stakingPools));
if (poolId === undefined) {
yield undefined;
} else {
const operatorShare = getRandomInteger(0, stakingPools[poolId].operatorShare);
yield assertion.executeAsync(poolId, operatorShare, { from: this.actor.address });
}
}
}
};
}
export class PoolOperator extends PoolOperatorMixin(Actor) {}

View File

@@ -0,0 +1,133 @@
import { OwnerStakeByStatus, StakeInfo, StakeStatus, StoredBalance } from '@0x/contracts-staking';
import { getRandomInteger } from '@0x/contracts-test-utils';
import { BigNumber } from '@0x/utils';
import '@azure/core-asynciterator-polyfill';
import * as _ from 'lodash';
import { AssertionResult } from '../assertions/function_assertion';
import { validMoveStakeAssertion } from '../assertions/moveStake';
import { validStakeAssertion } from '../assertions/stake';
import { validUnstakeAssertion } from '../assertions/unstake';
import { Actor, Constructor } from './base';
export interface StakerInterface {
stakeAsync: (amount: BigNumber, poolId?: string) => Promise<void>;
}
/**
* This mixin encapsulates functionaltiy associated with stakers within the 0x ecosystem.
* This includes staking ZRX (and optionally delegating it to a specific pool).
*/
export function StakerMixin<TBase extends Constructor>(Base: TBase): TBase & Constructor<StakerInterface> {
return class extends Base {
public stake: OwnerStakeByStatus;
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[]) {
// tslint:disable-next-line:no-inferred-empty-object-type
super(...args);
this.actor = (this as any) as Actor;
this.stake = {
[StakeStatus.Undelegated]: new StoredBalance(),
[StakeStatus.Delegated]: { total: new StoredBalance() },
};
// Register this mixin's assertion generators
this.actor.simulationActions = {
...this.actor.simulationActions,
validStake: this._validStake(),
validUnstake: this._validUnstake(),
validMoveStake: this._validMoveStake(),
};
}
/**
* 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(amount).awaitTransactionSuccessAsync({
from: this.actor.address,
});
if (poolId !== undefined) {
await stakingWrapper
.moveStake(
new StakeInfo(StakeStatus.Undelegated),
new StakeInfo(StakeStatus.Delegated, poolId),
amount,
)
.awaitTransactionSuccessAsync({ from: this.actor.address });
}
}
private async *_validStake(): AsyncIterableIterator<AssertionResult> {
const { zrx } = this.actor.deployment.tokens;
const { deployment, balanceStore, globalStake } = this.actor.simulationEnvironment!;
const assertion = validStakeAssertion(deployment, balanceStore, globalStake, this.stake);
while (true) {
await balanceStore.updateErc20BalancesAsync();
const zrxBalance = balanceStore.balances.erc20[this.actor.address][zrx.address];
const amount = getRandomInteger(0, zrxBalance);
yield assertion.executeAsync(amount, { from: this.actor.address });
}
}
private async *_validUnstake(): AsyncIterableIterator<AssertionResult> {
const { stakingWrapper } = this.actor.deployment.staking;
const { deployment, balanceStore, globalStake } = this.actor.simulationEnvironment!;
const assertion = validUnstakeAssertion(deployment, balanceStore, globalStake, this.stake);
while (true) {
await balanceStore.updateErc20BalancesAsync();
const undelegatedStake = await stakingWrapper
.getOwnerStakeByStatus(this.actor.address, StakeStatus.Undelegated)
.callAsync();
const withdrawableStake = BigNumber.min(
undelegatedStake.currentEpochBalance,
undelegatedStake.nextEpochBalance,
);
const amount = getRandomInteger(0, withdrawableStake);
yield assertion.executeAsync(amount, { from: this.actor.address });
}
}
private async *_validMoveStake(): AsyncIterableIterator<AssertionResult> {
const { deployment, globalStake, stakingPools } = this.actor.simulationEnvironment!;
const assertion = validMoveStakeAssertion(deployment, globalStake, this.stake, stakingPools);
while (true) {
const fromPoolId = _.sample(Object.keys(_.omit(this.stake[StakeStatus.Delegated], ['total'])));
const fromStatus =
fromPoolId === undefined
? StakeStatus.Undelegated
: (_.sample([StakeStatus.Undelegated, StakeStatus.Delegated]) as StakeStatus);
const from = new StakeInfo(fromStatus, fromPoolId);
const toPoolId = _.sample(Object.keys(stakingPools));
const toStatus =
toPoolId === undefined
? StakeStatus.Undelegated
: (_.sample([StakeStatus.Undelegated, StakeStatus.Delegated]) as StakeStatus);
const to = new StakeInfo(toStatus, toPoolId);
const moveableStake =
from.status === StakeStatus.Undelegated
? this.stake[StakeStatus.Undelegated].nextEpochBalance
: this.stake[StakeStatus.Delegated][from.poolId].nextEpochBalance;
const amount = getRandomInteger(0, moveableStake);
yield assertion.executeAsync(from, to, amount, { from: this.actor.address });
}
}
};
}
export class Staker extends StakerMixin(Actor) {}

View File

@@ -0,0 +1,56 @@
import { SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils';
import { TransactionReceiptWithDecodedLogs, TxData } from 'ethereum-types';
import { DeploymentManager } from '../deployment_manager';
import { Actor, Constructor } from './base';
export interface TakerInterface {
fillOrderAsync: (
order: SignedOrder,
fillAmount: BigNumber,
txData?: Partial<TxData>,
) => Promise<TransactionReceiptWithDecodedLogs>;
}
/**
* This mixin encapsulates functionaltiy associated with takers within the 0x ecosystem.
* As of writing, the only extra functionality provided is a utility wrapper around `fillOrder`,
*/
export function TakerMixin<TBase extends Constructor>(Base: TBase): TBase & Constructor<TakerInterface> {
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[]) {
// tslint:disable-next-line:no-inferred-empty-object-type
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(order, fillAmount, order.signature)
.awaitTransactionSuccessAsync({
from: this.actor.address,
gasPrice: DeploymentManager.gasPrice,
value: DeploymentManager.protocolFee,
...txData,
});
}
};
}
export class Taker extends TakerMixin(Actor) {}

View File

@@ -0,0 +1,7 @@
{
"extends": ["@0x/tslint-config"],
"rules": {
"max-classes-per-file": false,
"no-non-null-assertion": false
}
}

View File

@@ -0,0 +1,12 @@
import { ObjectMap } from '@0x/types';
import * as _ from 'lodash';
import { Actor } from './base';
/**
* Utility function to convert Actors into an object mapping readable names to addresses.
* Useful for BalanceStore.
*/
export function actorAddressesByName(actors: Actor[]): ObjectMap<string> {
return _.zipObject(actors.map(actor => actor.name), actors.map(actor => actor.address));
}

View File

@@ -0,0 +1,54 @@
import { StakingPoolById, StoredBalance } from '@0x/contracts-staking';
import { expect } from '@0x/contracts-test-utils';
import { BigNumber, logUtils } from '@0x/utils';
import { TxData } from 'ethereum-types';
import { DeploymentManager } from '../deployment_manager';
import { FunctionAssertion, FunctionResult } from './function_assertion';
// tslint:disable:no-unnecessary-type-assertion
/**
* Returns a FunctionAssertion for `createStakingPool` which assumes valid input is provided. The
* FunctionAssertion checks that the new poolId is one more than the last poolId.
*/
export function validCreateStakingPoolAssertion(
deployment: DeploymentManager,
pools: StakingPoolById,
): FunctionAssertion<string, string> {
const { stakingWrapper } = deployment.staking;
return new FunctionAssertion(stakingWrapper.createStakingPool, {
// Returns the expected ID of th created pool
before: async () => {
const lastPoolId = await stakingWrapper.lastPoolId().callAsync();
// Effectively the last poolId + 1, but as a bytestring
return `0x${new BigNumber(lastPoolId)
.plus(1)
.toString(16)
.padStart(64, '0')}`;
},
after: async (
expectedPoolId: string,
result: FunctionResult,
operatorShare: number,
addOperatorAsMaker: boolean,
txData: Partial<TxData>,
) => {
logUtils.log(`createStakingPool(${operatorShare}, ${addOperatorAsMaker}) => ${expectedPoolId}`);
// Checks the logs for the new poolId, verifies that it is as expected
const log = result.receipt!.logs[0]; // tslint:disable-line:no-non-null-assertion
const actualPoolId = (log as any).args.poolId;
expect(actualPoolId).to.equal(expectedPoolId);
// Adds the new pool to local state
pools[actualPoolId] = {
operator: txData.from as string,
operatorShare,
delegatedStake: new StoredBalance(),
};
},
});
}

View File

@@ -0,0 +1,30 @@
import { StakingPoolById } from '@0x/contracts-staking';
import { expect } from '@0x/contracts-test-utils';
import { logUtils } from '@0x/utils';
import { DeploymentManager } from '../deployment_manager';
import { FunctionAssertion, FunctionResult } from './function_assertion';
/**
* Returns a FunctionAssertion for `decreaseStakingPoolOperatorShare` which assumes valid input is
* provided. The FunctionAssertion checks that the operator share actually gets updated.
*/
export function validDecreaseStakingPoolOperatorShareAssertion(
deployment: DeploymentManager,
pools: StakingPoolById,
): FunctionAssertion<{}, void> {
const { stakingWrapper } = deployment.staking;
return new FunctionAssertion<{}, void>(stakingWrapper.decreaseStakingPoolOperatorShare, {
after: async (_beforeInfo, _result: FunctionResult, poolId: string, expectedOperatorShare: number) => {
logUtils.log(`decreaseStakingPoolOperatorShare(${poolId}, ${expectedOperatorShare})`);
// Checks that the on-chain pool's operator share has been updated.
const { operatorShare } = await stakingWrapper.getStakingPool(poolId).callAsync();
expect(operatorShare).to.bignumber.equal(expectedOperatorShare);
// Updates the pool in local state.
pools[poolId].operatorShare = operatorShare;
},
});
}

View File

@@ -0,0 +1,108 @@
import { ContractFunctionObj, ContractTxFunctionObj } from '@0x/base-contract';
import { TransactionReceiptWithDecodedLogs } from 'ethereum-types';
import * as _ from 'lodash';
// tslint:disable:max-classes-per-file
export type GenericContractFunction<T> = (...args: any[]) => ContractFunctionObj<T>;
export interface FunctionResult {
data?: any;
success: boolean;
receipt?: TransactionReceiptWithDecodedLogs;
}
/**
* This interface represents a condition that can be placed on a contract function.
* This can be used to represent the pre- and post-conditions of a "Hoare Triple" on a
* given contract function. The "Hoare Triple" is a way to represent the way that a
* function changes state.
* @param before A function that will be run before a call to the contract wrapper
* function. Ideally, this will be a "precondition."
* @param after A function that will be run after a call to the contract wrapper
* function.
*/
export interface Condition<TBefore> {
before: (...args: any[]) => Promise<TBefore>;
after: (beforeInfo: TBefore, result: FunctionResult, ...args: any[]) => Promise<any>;
}
/**
* The basic unit of abstraction for testing. This just consists of a command that
* can be run. For example, this can represent a simple command that can be run, or
* it can represent a command that executes a "Hoare Triple" (this is what most of
* our `Assertion` implementations will do in practice).
* @param runAsync The function to execute for the assertion.
*/
export interface Assertion {
executeAsync: (...args: any[]) => Promise<any>;
}
export interface AssertionResult<TBefore = unknown> {
beforeInfo: TBefore;
afterInfo: any;
}
/**
* This class implements `Assertion` and represents a "Hoare Triple" that can be
* executed.
*/
export class FunctionAssertion<TBefore, ReturnDataType> implements Assertion {
// A condition that will be applied to `wrapperFunction`.
public condition: Condition<TBefore>;
// The wrapper function that will be wrapped in assertions.
public wrapperFunction: (
...args: any[] // tslint:disable-line:trailing-comma
) => ContractTxFunctionObj<ReturnDataType> | ContractFunctionObj<ReturnDataType>;
constructor(
wrapperFunction: (
...args: any[] // tslint:disable-line:trailing-comma
) => ContractTxFunctionObj<ReturnDataType> | ContractFunctionObj<ReturnDataType>,
condition: Partial<Condition<TBefore>> = {},
) {
this.condition = {
before: _.noop.bind(this),
after: _.noop.bind(this),
...condition,
};
this.wrapperFunction = wrapperFunction;
}
/**
* Runs the wrapped function and fails if the before or after assertions fail.
* @param ...args The args to the contract wrapper function.
*/
public async executeAsync(...args: any[]): Promise<AssertionResult<TBefore>> {
// Call the before condition.
const beforeInfo = await this.condition.before(...args);
// Initialize the callResult so that the default success value is true.
const callResult: FunctionResult = { success: true };
// 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.wrapperFunction(...args) as ContractTxFunctionObj<ReturnDataType>;
callResult.data = await functionWithArgs.callAsync();
callResult.receipt =
functionWithArgs.awaitTransactionSuccessAsync !== undefined
? await functionWithArgs.awaitTransactionSuccessAsync() // tslint:disable-line:await-promise
: undefined;
// tslint:enable:await-promise
} catch (error) {
callResult.data = error;
callResult.success = false;
callResult.receipt = undefined;
}
// Call the after condition.
const afterInfo = await this.condition.after(beforeInfo, callResult, ...args);
return {
beforeInfo,
afterInfo,
};
}
}

View File

@@ -0,0 +1,139 @@
import {
GlobalStakeByStatus,
OwnerStakeByStatus,
StakeInfo,
StakeStatus,
StakingPoolById,
StoredBalance,
} from '@0x/contracts-staking';
import { constants, expect } from '@0x/contracts-test-utils';
import { BigNumber, logUtils } from '@0x/utils';
import { TxData } from 'ethereum-types';
import * as _ from 'lodash';
import { DeploymentManager } from '../deployment_manager';
import { FunctionAssertion } from './function_assertion';
function incrementNextEpochBalance(stakeBalance: StoredBalance, amount: BigNumber): void {
_.update(stakeBalance, ['nextEpochBalance'], balance => (balance || constants.ZERO_AMOUNT).plus(amount));
}
function decrementNextEpochBalance(stakeBalance: StoredBalance, amount: BigNumber): void {
_.update(stakeBalance, ['nextEpochBalance'], balance => (balance || constants.ZERO_AMOUNT).minus(amount));
}
function updateNextEpochBalances(
globalStake: GlobalStakeByStatus,
ownerStake: OwnerStakeByStatus,
pools: StakingPoolById,
from: StakeInfo,
to: StakeInfo,
amount: BigNumber,
): string[] {
// 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);
// Decrement global undelegated stake
decrementNextEpochBalance(globalStake[StakeStatus.Undelegated], amount);
} else if (from.status === StakeStatus.Delegated) {
// Decrement owner's delegated stake to this pool
decrementNextEpochBalance(ownerStake[StakeStatus.Delegated][from.poolId], amount);
// Decrement owner's total delegated stake
decrementNextEpochBalance(ownerStake[StakeStatus.Delegated].total, amount);
// Decrement global delegated stake
decrementNextEpochBalance(globalStake[StakeStatus.Delegated], amount);
// Decrement pool's delegated stake
decrementNextEpochBalance(pools[from.poolId].delegatedStake, amount);
updatedPools.push(from.poolId);
}
// Increment next epoch balances associated with the `to` stake
if (to.status === StakeStatus.Undelegated) {
incrementNextEpochBalance(ownerStake[StakeStatus.Undelegated], amount);
incrementNextEpochBalance(globalStake[StakeStatus.Undelegated], amount);
} 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);
// Increment owner's total delegated stake
incrementNextEpochBalance(ownerStake[StakeStatus.Delegated].total, amount);
// Increment global delegated stake
incrementNextEpochBalance(globalStake[StakeStatus.Delegated], amount);
// Increment pool's delegated stake
incrementNextEpochBalance(pools[to.poolId].delegatedStake, amount);
updatedPools.push(to.poolId);
}
return updatedPools;
}
/**
* Returns a FunctionAssertion for `moveStake` which assumes valid input is provided. The
* FunctionAssertion checks that the staker's
*/
export function validMoveStakeAssertion(
deployment: DeploymentManager,
globalStake: GlobalStakeByStatus,
ownerStake: OwnerStakeByStatus,
pools: StakingPoolById,
): FunctionAssertion<{}, void> {
const { stakingWrapper } = deployment.staking;
return new FunctionAssertion<{}, void>(stakingWrapper.moveStake, {
after: async (
_beforeInfo,
_result,
from: StakeInfo,
to: StakeInfo,
amount: BigNumber,
txData: Partial<TxData>,
) => {
logUtils.log(
`moveStake({status: ${StakeStatus[from.status]}, poolId: ${from.poolId} }, { status: ${
StakeStatus[to.status]
}, poolId: ${to.poolId} }, ${amount})`,
);
const owner = txData.from as string;
// Update local balances to match the expected result of this `moveStake` operation
const updatedPools = updateNextEpochBalances(globalStake, ownerStake, pools, from, to, amount);
// Fetches on-chain owner stake balances and checks against local balances
const ownerUndelegatedStake = {
...new StoredBalance(),
...(await stakingWrapper.getOwnerStakeByStatus(owner, StakeStatus.Undelegated).callAsync()),
};
const ownerDelegatedStake = {
...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);
// Fetches on-chain global stake balances and checks against local balances
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]);
// Fetches on-chain pool stake balances and checks against local balances
for (const poolId of updatedPools) {
const stakeDelegatedByOwner = await stakingWrapper
.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);
}
},
});
}

View File

@@ -0,0 +1,79 @@
import { GlobalStakeByStatus, OwnerStakeByStatus, StakeStatus, StoredBalance } from '@0x/contracts-staking';
import { expect } from '@0x/contracts-test-utils';
import { BigNumber, logUtils } 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 { FunctionAssertion, FunctionResult } from './function_assertion';
function expectedUndelegatedStake(
initStake: OwnerStakeByStatus | GlobalStakeByStatus,
amount: BigNumber,
): StoredBalance {
return {
currentEpoch: initStake[StakeStatus.Undelegated].currentEpoch,
currentEpochBalance: initStake[StakeStatus.Undelegated].currentEpochBalance.plus(amount),
nextEpochBalance: initStake[StakeStatus.Undelegated].nextEpochBalance.plus(amount),
};
}
/**
* Returns a FunctionAssertion for `stake` which assumes valid input is provided. The
* FunctionAssertion checks that the staker and zrxVault's balances of ZRX decrease and increase,
* respectively, by the input amount.
*/
export function validStakeAssertion(
deployment: DeploymentManager,
balanceStore: BlockchainBalanceStore,
globalStake: GlobalStakeByStatus,
ownerStake: OwnerStakeByStatus,
): FunctionAssertion<LocalBalanceStore, void> {
const { stakingWrapper, zrxVault } = deployment.staking;
return new FunctionAssertion(stakingWrapper.stake, {
before: async (amount: BigNumber, txData: Partial<TxData>) => {
// Simulates the transfer of ZRX from staker to vault
const expectedBalances = LocalBalanceStore.create(deployment.devUtils, balanceStore);
await expectedBalances.transferAssetAsync(
txData.from as string,
zrxVault.address,
amount,
await deployment.devUtils.encodeERC20AssetData(deployment.tokens.zrx.address).callAsync(),
);
return expectedBalances;
},
after: async (
expectedBalances: LocalBalanceStore,
_result: FunctionResult,
amount: BigNumber,
txData: Partial<TxData>,
) => {
logUtils.log(`stake(${amount})`);
// Checks that the ZRX transfer updated balances as expected.
await balanceStore.updateErc20BalancesAsync();
balanceStore.assertEquals(expectedBalances);
// Checks that the owner's undelegated stake has increased by the stake amount
const ownerUndelegatedStake = await stakingWrapper
.getOwnerStakeByStatus(txData.from as string, StakeStatus.Undelegated)
.callAsync();
const expectedOwnerUndelegatedStake = expectedUndelegatedStake(ownerStake, amount);
expect(ownerUndelegatedStake, 'Owner undelegated stake').to.deep.equal(expectedOwnerUndelegatedStake);
// Updates local state accordingly
ownerStake[StakeStatus.Undelegated] = expectedOwnerUndelegatedStake;
// Checks that the global undelegated stake has also increased by the stake amount
const globalUndelegatedStake = await stakingWrapper
.getGlobalStakeByStatus(StakeStatus.Undelegated)
.callAsync();
const expectedGlobalUndelegatedStake = expectedUndelegatedStake(globalStake, amount);
expect(globalUndelegatedStake, 'Global undelegated stake').to.deep.equal(expectedGlobalUndelegatedStake);
// Updates local state accordingly
globalStake[StakeStatus.Undelegated] = expectedGlobalUndelegatedStake;
},
});
}

View File

@@ -0,0 +1,79 @@
import { GlobalStakeByStatus, OwnerStakeByStatus, StakeStatus, StoredBalance } from '@0x/contracts-staking';
import { expect } from '@0x/contracts-test-utils';
import { BigNumber, logUtils } 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 { FunctionAssertion, FunctionResult } from './function_assertion';
function expectedUndelegatedStake(
initStake: OwnerStakeByStatus | GlobalStakeByStatus,
amount: BigNumber,
): StoredBalance {
return {
currentEpoch: initStake[StakeStatus.Undelegated].currentEpoch,
currentEpochBalance: initStake[StakeStatus.Undelegated].currentEpochBalance.minus(amount),
nextEpochBalance: initStake[StakeStatus.Undelegated].nextEpochBalance.minus(amount),
};
}
/**
* Returns a FunctionAssertion for `unstake` which assumes valid input is provided. The
* FunctionAssertion checks that the staker and zrxVault's balances of ZRX increase and decrease,
* respectively, by the input amount.
*/
export function validUnstakeAssertion(
deployment: DeploymentManager,
balanceStore: BlockchainBalanceStore,
globalStake: GlobalStakeByStatus,
ownerStake: OwnerStakeByStatus,
): FunctionAssertion<LocalBalanceStore, void> {
const { stakingWrapper, zrxVault } = deployment.staking;
return new FunctionAssertion(stakingWrapper.unstake, {
before: async (amount: BigNumber, txData: Partial<TxData>) => {
// Simulates the transfer of ZRX from vault to staker
const expectedBalances = LocalBalanceStore.create(deployment.devUtils, balanceStore);
await expectedBalances.transferAssetAsync(
zrxVault.address,
txData.from as string,
amount,
await deployment.devUtils.encodeERC20AssetData(deployment.tokens.zrx.address).callAsync(),
);
return expectedBalances;
},
after: async (
expectedBalances: LocalBalanceStore,
_result: FunctionResult,
amount: BigNumber,
txData: Partial<TxData>,
) => {
logUtils.log(`unstake(${amount})`);
// Checks that the ZRX transfer updated balances as expected.
await balanceStore.updateErc20BalancesAsync();
balanceStore.assertEquals(expectedBalances);
// Checks that the owner's undelegated stake has decreased by the stake amount
const ownerUndelegatedStake = await stakingWrapper
.getOwnerStakeByStatus(txData.from as string, StakeStatus.Undelegated)
.callAsync();
const expectedOwnerUndelegatedStake = expectedUndelegatedStake(ownerStake, amount);
expect(ownerUndelegatedStake, 'Owner undelegated stake').to.deep.equal(expectedOwnerUndelegatedStake);
// Updates local state accordingly
ownerStake[StakeStatus.Undelegated] = expectedOwnerUndelegatedStake;
// Checks that the global undelegated stake has also decreased by the stake amount
const globalUndelegatedStake = await stakingWrapper
.getGlobalStakeByStatus(StakeStatus.Undelegated)
.callAsync();
const expectedGlobalUndelegatedStake = expectedUndelegatedStake(globalStake, amount);
expect(globalUndelegatedStake, 'Global undelegated stake').to.deep.equal(expectedGlobalUndelegatedStake);
// Updates local state accordingly
globalStake[StakeStatus.Undelegated] = expectedGlobalUndelegatedStake;
},
});
}

View File

@@ -0,0 +1,143 @@
import { BaseContract } from '@0x/base-contract';
import { constants, expect, TokenBalances } from '@0x/contracts-test-utils';
import * as _ from 'lodash';
import { TokenAddresses, TokenContractsByName, TokenOwnersByName } from './types';
export class BalanceStore {
public balances: TokenBalances;
protected _tokenAddresses: TokenAddresses;
protected _ownerAddresses: string[];
private _addressNames: {
[address: string]: string;
};
/**
* Constructor.
* @param tokenOwnersByName Addresses of token owners to track balances of.
* @param tokenContractsByName Contracts of tokens to track balances of.
*/
public constructor(tokenOwnersByName: TokenOwnersByName, tokenContractsByName: Partial<TokenContractsByName>) {
this.balances = { erc20: {}, erc721: {}, erc1155: {}, eth: {} };
this._ownerAddresses = Object.values(tokenOwnersByName);
_.defaults(tokenContractsByName, { erc20: {}, erc721: {}, erc1155: {} });
const tokenAddressesByName = _.mapValues(
{ ...tokenContractsByName.erc20, ...tokenContractsByName.erc721, ...tokenContractsByName.erc1155 },
contract => (contract as BaseContract).address,
);
this._addressNames = _.invert({ ...tokenOwnersByName, ...tokenAddressesByName });
this._tokenAddresses = {
erc20: Object.values(tokenContractsByName.erc20 || {}).map(contract => contract.address),
erc721: Object.values(tokenContractsByName.erc721 || {}).map(contract => contract.address),
erc1155: Object.values(tokenContractsByName.erc1155 || {}).map(contract => contract.address),
};
}
/**
* Registers the given token owner in this balance store. The token owner's balance will be
* tracked in subsequent operations.
* @param address Address of the token owner
* @param name Name of the token owner
*/
public registerTokenOwner(address: string, name: string): void {
this._ownerAddresses.push(address);
this._addressNames[address] = name;
}
/**
* Throws iff balance stores do not have the same entries.
* @param rhs Balance store to compare to
*/
public assertEquals(rhs: BalanceStore): void {
this._assertEthBalancesEqual(rhs);
this._assertErc20BalancesEqual(rhs);
this._assertErc721BalancesEqual(rhs);
this._assertErc1155BalancesEqual(rhs);
}
/**
* Copies from an existing balance store.
* @param balanceStore to copy from.
*/
public cloneFrom(balanceStore: BalanceStore): void {
this.balances = _.cloneDeep(balanceStore.balances);
this._tokenAddresses = _.cloneDeep(balanceStore._tokenAddresses);
this._ownerAddresses = _.cloneDeep(balanceStore._ownerAddresses);
this._addressNames = _.cloneDeep(balanceStore._addressNames);
}
/**
* Returns the human-readable name for the given address, if it exists.
* @param address The address to get the name for.
*/
private _readableAddressName(address: string): string {
return this._addressNames[address] || address;
}
/**
* Throws iff balance stores do not have the same ETH balances.
* @param rhs Balance store to compare to
*/
private _assertEthBalancesEqual(rhs: BalanceStore): void {
for (const ownerAddress of [...this._ownerAddresses, ...rhs._ownerAddresses]) {
const thisBalance = _.get(this.balances.eth, [ownerAddress], constants.ZERO_AMOUNT);
const rhsBalance = _.get(rhs.balances.eth, [ownerAddress], constants.ZERO_AMOUNT);
expect(thisBalance, `${this._readableAddressName(ownerAddress)} ETH balance`).to.bignumber.equal(
rhsBalance,
);
}
}
/**
* Throws iff balance stores do not have the same ERC20 balances.
* @param rhs Balance store to compare to
*/
private _assertErc20BalancesEqual(rhs: BalanceStore): void {
for (const ownerAddress of [...this._ownerAddresses, ...rhs._ownerAddresses]) {
for (const tokenAddress of [...this._tokenAddresses.erc20, ...rhs._tokenAddresses.erc20]) {
const thisBalance = _.get(this.balances.erc20, [ownerAddress, tokenAddress], constants.ZERO_AMOUNT);
const rhsBalance = _.get(rhs.balances.erc20, [ownerAddress, tokenAddress], constants.ZERO_AMOUNT);
expect(
thisBalance,
`${this._readableAddressName(ownerAddress)} ${this._readableAddressName(tokenAddress)} balance`,
).to.bignumber.equal(rhsBalance);
}
}
}
/**
* Throws iff balance stores do not have the same ERC721 balances.
* @param rhs Balance store to compare to
*/
private _assertErc721BalancesEqual(rhs: BalanceStore): void {
for (const ownerAddress of [...this._ownerAddresses, ...rhs._ownerAddresses]) {
for (const tokenAddress of [...this._tokenAddresses.erc721, ...rhs._tokenAddresses.erc721]) {
const thisBalance = _.get(this.balances.erc721, [ownerAddress, tokenAddress], []);
const rhsBalance = _.get(rhs.balances.erc721, [ownerAddress, tokenAddress], []);
expect(
thisBalance,
`${this._readableAddressName(ownerAddress)} ${this._readableAddressName(tokenAddress)} balance`,
).to.deep.equal(rhsBalance);
}
}
}
/**
* Throws iff balance stores do not have the same ERC1155 balances.
* @param rhs Balance store to compare to
*/
private _assertErc1155BalancesEqual(rhs: BalanceStore): void {
for (const ownerAddress of [...this._ownerAddresses, ...rhs._ownerAddresses]) {
for (const tokenAddress of [...this._tokenAddresses.erc1155, ...rhs._tokenAddresses.erc1155]) {
const thisBalance = _.get(this.balances.erc1155, [ownerAddress, tokenAddress], {});
const rhsBalance = _.get(rhs.balances.erc1155, [ownerAddress, tokenAddress], {});
expect(
thisBalance,
`${this._readableAddressName(ownerAddress)} ${this._readableAddressName(tokenAddress)} balance`,
).to.deep.equal(rhsBalance);
}
}
}
}

View File

@@ -0,0 +1,139 @@
import { web3Wrapper } from '@0x/contracts-test-utils';
import { BigNumber } from '@0x/utils';
import * as combinatorics from 'js-combinatorics';
import * as _ from 'lodash';
import { BalanceStore } from './balance_store';
import { TokenContracts, TokenContractsByName, TokenIds, TokenOwnersByName } from './types';
export class BlockchainBalanceStore extends BalanceStore {
private readonly _tokenContracts: TokenContracts;
private readonly _tokenIds: TokenIds;
/**
* Constructor.
* @param tokenOwnersByName The addresses of token owners whose balances will be tracked.
* @param tokenContractsByName The contracts of tokens to track.
* @param tokenIds The tokenIds of ERC721 and ERC1155 assets to track.
*/
public constructor(
tokenOwnersByName: TokenOwnersByName,
tokenContractsByName: Partial<TokenContractsByName>,
tokenIds: Partial<TokenIds> = {},
) {
super(tokenOwnersByName, tokenContractsByName);
this._tokenContracts = {
erc20: Object.values(tokenContractsByName.erc20 || {}),
erc721: Object.values(tokenContractsByName.erc721 || {}),
erc1155: Object.values(tokenContractsByName.erc1155 || {}),
};
this._tokenIds = {
erc721: tokenIds.erc721 || {},
erc1155: tokenIds.erc1155 || {},
};
}
/**
* Updates balances by querying on-chain values.
*/
public async updateBalancesAsync(): Promise<void> {
await Promise.all([
this.updateEthBalancesAsync(),
this.updateErc20BalancesAsync(),
this.updateErc721BalancesAsync(),
this.updateErc1155BalancesAsync(),
]);
}
/**
* Updates ETH balances.
*/
public async updateEthBalancesAsync(): Promise<void> {
const ethBalances = _.zipObject(
this._ownerAddresses,
await Promise.all(this._ownerAddresses.map(address => web3Wrapper.getBalanceInWeiAsync(address))),
);
this.balances.eth = ethBalances;
}
/**
* Updates ERC20 balances.
*/
public async updateErc20BalancesAsync(): Promise<void> {
const balances = await Promise.all(
this._ownerAddresses.map(async account =>
_.zipObject(
this._tokenContracts.erc20.map(token => token.address),
await Promise.all(this._tokenContracts.erc20.map(token => token.balanceOf(account).callAsync())),
),
),
);
this.balances.erc20 = _.zipObject(this._ownerAddresses, balances);
}
/**
* Updates ERC721 balances.
*/
public async updateErc721BalancesAsync(): Promise<void> {
const erc721ContractsByAddress = _.zipObject(
this._tokenContracts.erc721.map(contract => contract.address),
this._tokenContracts.erc721,
);
this.balances.erc721 = {};
for (const [tokenAddress, tokenIds] of Object.entries(this._tokenIds.erc721)) {
for (const tokenId of tokenIds) {
const tokenOwner = await erc721ContractsByAddress[tokenAddress].ownerOf(tokenId).callAsync();
_.update(this.balances.erc721, [tokenOwner, tokenAddress], nfts => _.union([tokenId], nfts).sort());
}
}
}
/**
* Updates ERC1155 balances.
*/
public async updateErc1155BalancesAsync(): Promise<void> {
const erc1155ContractsByAddress = _.zipObject(
this._tokenContracts.erc1155.map(contract => contract.address),
this._tokenContracts.erc1155,
);
for (const [tokenAddress, { fungible, nonFungible }] of Object.entries(this._tokenIds.erc1155)) {
const contract = erc1155ContractsByAddress[tokenAddress];
const tokenIds = [...fungible, ...nonFungible];
if (this._ownerAddresses.length === 0 || tokenIds.length === 0) {
continue;
}
const [_tokenIds, _tokenOwners] = _.unzip(
combinatorics.cartesianProduct(tokenIds, this._ownerAddresses).toArray(),
);
const balances = await contract
.balanceOfBatch(_tokenOwners as string[], _tokenIds as BigNumber[])
.callAsync();
let i = 0;
for (const tokenOwner of this._ownerAddresses) {
// Fungible tokens
_.set(this.balances.erc1155, [tokenOwner, tokenAddress, 'fungible'], {});
for (const tokenId of fungible) {
_.set(
this.balances.erc1155,
[tokenOwner, tokenAddress, 'fungible', tokenId.toString()],
balances[i++],
);
}
// Non-fungible tokens
_.set(this.balances.erc1155, [tokenOwner, tokenAddress, 'nonFungible'], []);
for (const tokenId of nonFungible) {
const isOwner = balances[i++];
if (isOwner.isEqualTo(1)) {
_.update(this.balances.erc1155, [tokenOwner, tokenAddress, 'nonFungible'], nfts =>
_.union([tokenId], nfts).sort(),
);
}
}
}
}
}
}

View File

@@ -0,0 +1,174 @@
import { DevUtilsContract } from '@0x/contracts-dev-utils';
import { constants, Numberish } from '@0x/contracts-test-utils';
import { AssetProxyId } from '@0x/types';
import { BigNumber } from '@0x/utils';
import * as _ from 'lodash';
import { BalanceStore } from './balance_store';
import { TokenContractsByName, TokenOwnersByName } from './types';
export class LocalBalanceStore extends BalanceStore {
/**
* Creates a new balance store based on an existing one.
* @param sourceBalanceStore Existing balance store whose values should be copied.
*/
public static create(devUtils: DevUtilsContract, sourceBalanceStore?: BalanceStore): LocalBalanceStore {
const localBalanceStore = new LocalBalanceStore(devUtils);
if (sourceBalanceStore !== undefined) {
localBalanceStore.cloneFrom(sourceBalanceStore);
}
return localBalanceStore;
}
/**
* Constructor.
* Note that parameters are given {} defaults because `LocalBalanceStore`s will typically
* be initialized via `create`.
*/
protected constructor(
private readonly _devUtils: DevUtilsContract,
tokenOwnersByName: TokenOwnersByName = {},
tokenContractsByName: Partial<TokenContractsByName> = {},
) {
super(tokenOwnersByName, tokenContractsByName);
}
/**
* Decreases the ETH balance of an address to simulate gas usage.
* @param senderAddress Address whose ETH balance to decrease.
* @param amount Amount to decrease the balance by.
*/
public burnGas(senderAddress: string, amount: Numberish): void {
this.balances.eth[senderAddress] = this.balances.eth[senderAddress].minus(amount);
}
/**
* Converts some amount of the ETH balance of an address to WETH balance to simulate wrapping ETH.
* @param senderAddress Address whose ETH to wrap.
* @param amount Amount to wrap.
*/
public wrapEth(senderAddress: string, wethAddress: string, amount: Numberish): void {
this.balances.eth[senderAddress] = this.balances.eth[senderAddress].minus(amount);
_.update(this.balances.erc20, [senderAddress, wethAddress], balance =>
(balance || constants.ZERO_AMOUNT).plus(amount),
);
}
/**
* Sends ETH from `fromAddress` to `toAddress`.
* @param fromAddress Sender of ETH.
* @param toAddress Receiver of ETH.
* @param amount Amount of ETH to transfer.
*/
public sendEth(fromAddress: string, toAddress: string, amount: Numberish): void {
this.balances.eth[fromAddress] = this.balances.eth[fromAddress].minus(amount);
this.balances.eth[toAddress] = this.balances.eth[toAddress].plus(amount);
}
/**
* Transfers assets from `fromAddress` to `toAddress`.
* @param fromAddress Sender of asset(s)
* @param toAddress Receiver of asset(s)
* @param amount Amount of asset(s) to transfer
* @param assetData Asset data of assets being transferred.
*/
public async transferAssetAsync(
fromAddress: string,
toAddress: string,
amount: BigNumber,
assetData: string,
): Promise<void> {
if (fromAddress === toAddress) {
return;
}
const assetProxyId = await this._devUtils.decodeAssetProxyId(assetData).callAsync();
switch (assetProxyId) {
case AssetProxyId.ERC20: {
// tslint:disable-next-line:no-unused-variable
const [_proxyId, tokenAddress] = await this._devUtils.decodeERC20AssetData(assetData).callAsync();
_.update(this.balances.erc20, [fromAddress, tokenAddress], balance => balance.minus(amount));
_.update(this.balances.erc20, [toAddress, tokenAddress], balance =>
(balance || constants.ZERO_AMOUNT).plus(amount),
);
break;
}
case AssetProxyId.ERC721: {
// tslint:disable-next-line:no-unused-variable
const [_proxyId, tokenAddress, tokenId] = await this._devUtils
.decodeERC721AssetData(assetData)
.callAsync();
const fromTokens = _.get(this.balances.erc721, [fromAddress, tokenAddress], []);
const toTokens = _.get(this.balances.erc721, [toAddress, tokenAddress], []);
if (amount.gte(1)) {
const tokenIndex = _.findIndex(fromTokens as BigNumber[], t => t.eq(tokenId));
if (tokenIndex !== -1) {
fromTokens.splice(tokenIndex, 1);
toTokens.push(tokenId);
toTokens.sort();
}
}
_.set(this.balances.erc721, [fromAddress, tokenAddress], fromTokens);
_.set(this.balances.erc721, [toAddress, tokenAddress], toTokens);
break;
}
case AssetProxyId.ERC1155: {
const [
_proxyId, // tslint:disable-line:no-unused-variable
tokenAddress,
tokenIds,
tokenValues,
] = await this._devUtils.decodeERC1155AssetData(assetData).callAsync();
const fromBalances = {
// tslint:disable-next-line:no-inferred-empty-object-type
fungible: _.get(this.balances.erc1155, [fromAddress, tokenAddress, 'fungible'], {}),
nonFungible: _.get(this.balances.erc1155, [fromAddress, tokenAddress, 'nonFungible'], []),
};
const toBalances = {
// tslint:disable-next-line:no-inferred-empty-object-type
fungible: _.get(this.balances.erc1155, [toAddress, tokenAddress, 'fungible'], {}),
nonFungible: _.get(this.balances.erc1155, [toAddress, tokenAddress, 'nonFungible'], []),
};
for (const [i, tokenId] of tokenIds.entries()) {
const tokenValue = tokenValues[i];
const tokenAmount = amount.times(tokenValue);
if (tokenAmount.gt(0)) {
const tokenIndex = _.findIndex(fromBalances.nonFungible as BigNumber[], t => t.eq(tokenId));
if (tokenIndex !== -1) {
// Transfer a non-fungible.
fromBalances.nonFungible.splice(tokenIndex, 1);
toBalances.nonFungible.push(tokenId);
// sort NFT's by name
toBalances.nonFungible.sort();
} else {
// Transfer a fungible.
const _tokenId = tokenId.toString();
_.update(fromBalances.fungible, [_tokenId], balance => balance.minus(tokenAmount));
_.update(toBalances.fungible, [_tokenId], balance =>
(balance || constants.ZERO_AMOUNT).plus(tokenAmount),
);
}
}
}
_.set(this.balances.erc1155, [fromAddress, tokenAddress], fromBalances);
_.set(this.balances.erc1155, [toAddress, tokenAddress], toBalances);
break;
}
case AssetProxyId.MultiAsset: {
// tslint:disable-next-line:no-unused-variable
const [_proxyId, amounts, nestedAssetData] = await this._devUtils
.decodeMultiAssetData(assetData)
.callAsync();
for (const [i, amt] of amounts.entries()) {
const nestedAmount = amount.times(amt);
await this.transferAssetAsync(fromAddress, toAddress, nestedAmount, nestedAssetData[i]);
}
break;
}
case AssetProxyId.StaticCall:
// Do nothing
break;
default:
throw new Error(`Unhandled asset proxy ID: ${assetProxyId}`);
}
}
}

View File

@@ -0,0 +1,51 @@
import { ERC1155MintableContract } from '@0x/contracts-erc1155';
import { DummyERC20TokenContract, DummyNoReturnERC20TokenContract, WETH9Contract } from '@0x/contracts-erc20';
import { DummyERC721TokenContract } from '@0x/contracts-erc721';
import { BigNumber } from '@0x/utils';
// alias for clarity
type address = string;
interface TokenData<TERC20, TERC721, TERC1155> {
erc20: TERC20;
erc721: TERC721;
erc1155: TERC1155;
}
export type TokenAddresses = TokenData<address[], address[], address[]>;
export type TokenContracts = TokenData<
Array<DummyERC20TokenContract | DummyNoReturnERC20TokenContract | WETH9Contract>,
DummyERC721TokenContract[],
ERC1155MintableContract[]
>;
interface Named<T> {
[readableName: string]: T;
}
export type TokenOwnersByName = Named<address>;
export type TokenAddressesByName = TokenData<Named<address>, Named<address>, Named<address>>;
export type TokenContractsByName = TokenData<
Named<DummyERC20TokenContract | DummyNoReturnERC20TokenContract | WETH9Contract>,
Named<DummyERC721TokenContract>,
Named<ERC1155MintableContract>
>;
interface ERC721TokenIds {
[tokenAddress: string]: BigNumber[];
}
interface ERC1155TokenIds {
[tokenAddress: string]: {
fungible: BigNumber[];
nonFungible: BigNumber[];
};
}
export interface TokenIds {
erc721: ERC721TokenIds;
erc1155: ERC1155TokenIds;
}

View File

@@ -0,0 +1,499 @@
import {
artifacts as assetProxyArtifacts,
ERC1155ProxyContract,
ERC20ProxyContract,
ERC721ProxyContract,
MultiAssetProxyContract,
StaticCallProxyContract,
} from '@0x/contracts-asset-proxy';
import { DevUtilsContract } from '@0x/contracts-dev-utils';
import { artifacts as ERC1155Artifacts, ERC1155MintableContract } from '@0x/contracts-erc1155';
import { artifacts as ERC20Artifacts, DummyERC20TokenContract, WETH9Contract } from '@0x/contracts-erc20';
import { artifacts as ERC721Artifacts, DummyERC721TokenContract } from '@0x/contracts-erc721';
import { artifacts as exchangeArtifacts, ExchangeContract } from '@0x/contracts-exchange';
import { artifacts as multisigArtifacts, ZeroExGovernorContract } from '@0x/contracts-multisig';
import {
artifacts as stakingArtifacts,
StakingProxyContract,
TestStakingContract,
ZrxVaultContract,
} from '@0x/contracts-staking';
import { BlockchainTestsEnvironment, constants } from '@0x/contracts-test-utils';
import { BigNumber } from '@0x/utils';
import { TxData } from 'ethereum-types';
import * as _ from 'lodash';
import { AssetProxyDispatcher, Authorizable, Ownable } from './wrapper_interfaces';
/**
* Adds a batch of authorities to a list of authorizable contracts.
* @param owner The owner of the authorizable contracts.
* @param authorizers The authorizable contracts.
* @param authorities A list of addresses to authorize in each authorizer contract.
*/
async function batchAddAuthorizedAddressAsync(
owner: string,
authorizers: Authorizable[],
authorities: string[],
): Promise<void> {
for (const authorizer of authorizers) {
for (const authority of authorities) {
await authorizer.addAuthorizedAddress(authority).awaitTransactionSuccessAsync({ from: owner });
}
}
}
/**
* Batch registers asset proxies in a list of registry contracts.
* @param owner The owner of the registry accounts.
* @param registries The registries that the asset proxies should be registered in.
* @param proxies A list of proxy contracts to register.
*/
async function batchRegisterAssetProxyAsync(
owner: string,
registries: AssetProxyDispatcher[],
proxies: string[],
): Promise<void> {
for (const registry of registries) {
for (const proxy of proxies) {
await registry.registerAssetProxy(proxy).awaitTransactionSuccessAsync({ from: owner });
}
}
}
/**
* Transfers ownership of several contracts from one address to another.
* @param owner The address that currently owns the contract instances.
* @param newOwner The address that will be given ownership of the contract instances.
* @param ownedContracts The contracts whose ownership will be transferred.
*/
async function batchTransferOwnershipAsync(
owner: string,
newOwner: ZeroExGovernorContract,
ownedContracts: Ownable[],
): Promise<void> {
for (const ownedContract of ownedContracts) {
await ownedContract.transferOwnership(newOwner.address).awaitTransactionSuccessAsync({ from: owner });
}
}
// Contract wrappers for all of the asset proxies
interface AssetProxyContracts {
erc20Proxy: ERC20ProxyContract;
erc721Proxy: ERC721ProxyContract;
erc1155Proxy: ERC1155ProxyContract;
multiAssetProxy: MultiAssetProxyContract;
staticCallProxy: StaticCallProxyContract;
}
// Contract wrappers for all of the staking contracts
interface StakingContracts {
stakingLogic: TestStakingContract;
stakingProxy: StakingProxyContract;
stakingWrapper: TestStakingContract;
zrxVault: ZrxVaultContract;
}
// Contract wrappers for tokens.
interface TokenContracts {
erc20: DummyERC20TokenContract[];
erc721: DummyERC721TokenContract[];
erc1155: ERC1155MintableContract[];
weth: WETH9Contract;
zrx: DummyERC20TokenContract;
}
// Options to be passed to `deployAsync`
export interface DeploymentOptions {
owner: string;
numErc1155TokensToDeploy: number;
numErc20TokensToDeploy: number;
numErc721TokensToDeploy: number;
}
export class DeploymentManager {
public static readonly protocolFeeMultiplier = new BigNumber(150000);
public static readonly gasPrice = new BigNumber(1e9); // 1 Gwei
public static readonly protocolFee = DeploymentManager.gasPrice.times(DeploymentManager.protocolFeeMultiplier);
/**
* Fully deploy the 0x exchange and staking contracts and configure the system with the
* asset proxy owner multisig.
* @param environment A blockchain test environment to use for contracts deployment.
* @param options Specifies the owner address and number of tokens to deploy.
*/
public static async deployAsync(
environment: BlockchainTestsEnvironment,
options: Partial<DeploymentOptions> = {},
): Promise<DeploymentManager> {
const chainId = await environment.getChainIdAsync();
const accounts = await environment.getAccountAddressesAsync();
const owner = options.owner || (await environment.getAccountAddressesAsync())[0];
const txDefaults = {
...environment.txDefaults,
from: owner,
gasPrice: DeploymentManager.gasPrice,
};
// Deploy the contracts using the same owner and environment.
const assetProxies = await DeploymentManager._deployAssetProxyContractsAsync(environment, txDefaults);
const exchange = await ExchangeContract.deployFrom0xArtifactAsync(
exchangeArtifacts.Exchange,
environment.provider,
txDefaults,
{ ...ERC20Artifacts, ...exchangeArtifacts, ...stakingArtifacts },
new BigNumber(chainId),
);
const governor = await ZeroExGovernorContract.deployFrom0xArtifactAsync(
multisigArtifacts.ZeroExGovernor,
environment.provider,
txDefaults,
multisigArtifacts,
[],
[],
[],
[owner],
new BigNumber(1),
constants.ZERO_AMOUNT,
);
const tokens = await DeploymentManager._deployTokenContractsAsync(environment, txDefaults, options);
const staking = await DeploymentManager._deployStakingContractsAsync(
environment,
owner,
txDefaults,
tokens,
assetProxies,
);
// Configure the asset proxies with the exchange and the exchange with the staking contracts.
await DeploymentManager._configureAssetProxiesWithExchangeAsync(assetProxies, exchange, owner);
await DeploymentManager._configureExchangeWithStakingAsync(exchange, staking, owner);
// Authorize the asset-proxy owner in the staking proxy and in the zrx vault.
await staking.stakingProxy.addAuthorizedAddress(governor.address).awaitTransactionSuccessAsync({
from: owner,
});
await staking.zrxVault.addAuthorizedAddress(governor.address).awaitTransactionSuccessAsync({
from: owner,
});
// Remove authorization for the original owner address.
await staking.stakingProxy.removeAuthorizedAddress(owner).awaitTransactionSuccessAsync({ from: owner });
await staking.zrxVault.removeAuthorizedAddress(owner).awaitTransactionSuccessAsync({ from: owner });
// Transfer complete ownership of the system to the asset proxy owner.
await batchTransferOwnershipAsync(owner, governor, [
assetProxies.erc20Proxy,
assetProxies.erc721Proxy,
assetProxies.erc1155Proxy,
assetProxies.multiAssetProxy,
exchange,
staking.stakingProxy,
]);
const devUtils = new DevUtilsContract(constants.NULL_ADDRESS, environment.provider);
return new DeploymentManager(
assetProxies,
governor,
exchange,
staking,
tokens,
chainId,
accounts,
txDefaults,
devUtils,
);
}
/**
* Configures a set of asset proxies with an exchange contract.
* @param assetProxies A set of asset proxies to be configured.
* @param exchange An exchange contract to configure with the asset proxies.
* @param owner An owner address to use when configuring the asset proxies.
*/
protected static async _configureAssetProxiesWithExchangeAsync(
assetProxies: AssetProxyContracts,
exchange: ExchangeContract,
owner: string,
): Promise<void> {
// Register the asset proxies in the exchange contract.
await batchRegisterAssetProxyAsync(
owner,
[exchange],
[
assetProxies.erc20Proxy.address,
assetProxies.erc721Proxy.address,
assetProxies.erc1155Proxy.address,
assetProxies.multiAssetProxy.address,
assetProxies.staticCallProxy.address,
],
);
// Register the asset proxies in the multi-asset proxy.
await batchRegisterAssetProxyAsync(
owner,
[assetProxies.multiAssetProxy],
[
assetProxies.erc20Proxy.address,
assetProxies.erc721Proxy.address,
assetProxies.erc1155Proxy.address,
assetProxies.staticCallProxy.address,
],
);
// Add the multi-asset proxy as an authorized address of the token proxies.
await batchAddAuthorizedAddressAsync(
owner,
[assetProxies.erc20Proxy, assetProxies.erc721Proxy, assetProxies.erc1155Proxy],
[assetProxies.multiAssetProxy.address],
);
// Add the exchange as an authorized address in all of the proxies.
await batchAddAuthorizedAddressAsync(
owner,
[
assetProxies.erc20Proxy,
assetProxies.erc721Proxy,
assetProxies.erc1155Proxy,
assetProxies.multiAssetProxy,
],
[exchange.address],
);
}
/**
* Configures an exchange contract with staking contracts
* @param exchange The Exchange contract.
* @param staking The Staking contracts.
* @param owner An owner address to use when configuring the asset proxies.
*/
protected static async _configureExchangeWithStakingAsync(
exchange: ExchangeContract,
staking: StakingContracts,
owner: string,
): Promise<void> {
// Configure the exchange for staking.
await exchange.setProtocolFeeCollectorAddress(staking.stakingProxy.address).awaitTransactionSuccessAsync({
from: owner,
});
await exchange.setProtocolFeeMultiplier(DeploymentManager.protocolFeeMultiplier).awaitTransactionSuccessAsync();
// Register the exchange contract in staking.
await staking.stakingWrapper.addExchangeAddress(exchange.address).awaitTransactionSuccessAsync({ from: owner });
}
/**
* Deploy a set of asset proxy contracts.
* @param environment The blockchain environment to use.
* @param txDefaults Defaults to use when deploying the asset proxies.
*/
protected static async _deployAssetProxyContractsAsync(
environment: BlockchainTestsEnvironment,
txDefaults: Partial<TxData>,
): Promise<AssetProxyContracts> {
const erc20Proxy = await ERC20ProxyContract.deployFrom0xArtifactAsync(
assetProxyArtifacts.ERC20Proxy,
environment.provider,
txDefaults,
assetProxyArtifacts,
);
const erc721Proxy = await ERC721ProxyContract.deployFrom0xArtifactAsync(
assetProxyArtifacts.ERC721Proxy,
environment.provider,
txDefaults,
assetProxyArtifacts,
);
const erc1155Proxy = await ERC1155ProxyContract.deployFrom0xArtifactAsync(
assetProxyArtifacts.ERC1155Proxy,
environment.provider,
txDefaults,
assetProxyArtifacts,
);
const multiAssetProxy = await MultiAssetProxyContract.deployFrom0xArtifactAsync(
assetProxyArtifacts.MultiAssetProxy,
environment.provider,
txDefaults,
assetProxyArtifacts,
);
const staticCallProxy = await StaticCallProxyContract.deployFrom0xArtifactAsync(
assetProxyArtifacts.StaticCallProxy,
environment.provider,
txDefaults,
assetProxyArtifacts,
);
return {
erc20Proxy,
erc721Proxy,
erc1155Proxy,
multiAssetProxy,
staticCallProxy,
};
}
/**
* Deploy a set of staking contracts.
* @param environment The blockchain environment to use.
* @param owner An owner address to use when configuring the asset proxies.
* @param txDefaults Defaults to use when deploying the asset proxies.
* @param tokens A set of token contracts to use during deployment of the staking contracts.
* @param assetProxies A set of asset proxies to use with the staking contracts.
*/
protected static async _deployStakingContractsAsync(
environment: BlockchainTestsEnvironment,
owner: string,
txDefaults: Partial<TxData>,
tokens: TokenContracts,
assetProxies: AssetProxyContracts,
): Promise<StakingContracts> {
const zrxVault = await ZrxVaultContract.deployFrom0xArtifactAsync(
stakingArtifacts.ZrxVault,
environment.provider,
txDefaults,
stakingArtifacts,
assetProxies.erc20Proxy.address,
tokens.zrx.address,
);
const stakingLogic = await TestStakingContract.deployFrom0xArtifactAsync(
stakingArtifacts.TestStaking,
environment.provider,
txDefaults,
stakingArtifacts,
tokens.weth.address,
zrxVault.address,
);
const stakingProxy = await StakingProxyContract.deployFrom0xArtifactAsync(
stakingArtifacts.StakingProxy,
environment.provider,
txDefaults,
stakingArtifacts,
stakingLogic.address,
);
const stakingWrapper = new TestStakingContract(stakingProxy.address, environment.provider, txDefaults);
// Add the zrx vault and the weth contract to the staking proxy.
await stakingWrapper.setWethContract(tokens.weth.address).awaitTransactionSuccessAsync({ from: owner });
await stakingWrapper.setZrxVault(zrxVault.address).awaitTransactionSuccessAsync({ from: owner });
// Authorize the owner address in the staking proxy and the zrx vault.
await stakingProxy.addAuthorizedAddress(owner).awaitTransactionSuccessAsync({ from: owner });
await zrxVault.addAuthorizedAddress(owner).awaitTransactionSuccessAsync({ from: owner });
// Authorize the zrx vault in the erc20 proxy
await assetProxies.erc20Proxy.addAuthorizedAddress(zrxVault.address).awaitTransactionSuccessAsync({
from: owner,
});
// Configure the zrx vault and the staking contract.
await zrxVault.setStakingProxy(stakingProxy.address).awaitTransactionSuccessAsync({ from: owner });
return {
stakingLogic,
stakingProxy,
stakingWrapper,
zrxVault,
};
}
/**
* Deploy a set of token contracts.
* @param environment The blockchain environment to use.
* @param txDefaults Defaults to use when deploying the asset proxies.
* @param options Specifies how many tokens of each standard to deploy.
*/
protected static async _deployTokenContractsAsync(
environment: BlockchainTestsEnvironment,
txDefaults: Partial<TxData>,
options: Partial<DeploymentOptions>,
): Promise<TokenContracts> {
const numErc20TokensToDeploy =
options.numErc20TokensToDeploy !== undefined
? options.numErc20TokensToDeploy
: constants.NUM_DUMMY_ERC20_TO_DEPLOY;
const numErc721TokensToDeploy =
options.numErc721TokensToDeploy !== undefined
? options.numErc721TokensToDeploy
: constants.NUM_DUMMY_ERC721_TO_DEPLOY;
const numErc1155TokensToDeploy =
options.numErc1155TokensToDeploy !== undefined
? options.numErc1155TokensToDeploy
: constants.NUM_DUMMY_ERC1155_CONTRACTS_TO_DEPLOY;
const erc20 = await Promise.all(
_.times(numErc20TokensToDeploy, async () =>
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,
),
),
);
const erc721 = await Promise.all(
_.times(numErc721TokensToDeploy, async () =>
DummyERC721TokenContract.deployFrom0xArtifactAsync(
ERC721Artifacts.DummyERC721Token,
environment.provider,
txDefaults,
ERC721Artifacts,
constants.DUMMY_TOKEN_NAME,
constants.DUMMY_TOKEN_SYMBOL,
),
),
);
const erc1155 = await Promise.all(
_.times(numErc1155TokensToDeploy, async () =>
ERC1155MintableContract.deployFrom0xArtifactAsync(
ERC1155Artifacts.ERC1155Mintable,
environment.provider,
txDefaults,
ERC1155Artifacts,
),
),
);
const weth = await WETH9Contract.deployFrom0xArtifactAsync(
ERC20Artifacts.WETH9,
environment.provider,
txDefaults,
ERC20Artifacts,
);
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 {
erc20,
erc721,
erc1155,
weth,
zrx,
};
}
protected constructor(
public assetProxies: AssetProxyContracts,
public governor: ZeroExGovernorContract,
public exchange: ExchangeContract,
public staking: StakingContracts,
public tokens: TokenContracts,
public chainId: number,
public accounts: string[],
public txDefaults: Partial<TxData>,
public devUtils: DevUtilsContract,
) {}
}
// tslint:disable:max-file-line-count

View File

@@ -0,0 +1,38 @@
import { GlobalStakeByStatus, StakeStatus, StakingPoolById, StoredBalance } from '@0x/contracts-staking';
import * as _ from 'lodash';
import { AssertionResult } from './assertions/function_assertion';
import { BlockchainBalanceStore } from './balances/blockchain_balance_store';
import { DeploymentManager } from './deployment_manager';
// tslint:disable:max-classes-per-file
export class SimulationEnvironment {
public globalStake: GlobalStakeByStatus = {
[StakeStatus.Undelegated]: new StoredBalance(),
[StakeStatus.Delegated]: new StoredBalance(),
};
public stakingPools: StakingPoolById = {};
public constructor(public readonly deployment: DeploymentManager, public balanceStore: BlockchainBalanceStore) {}
}
export abstract class Simulation {
public readonly generator = this._assertionGenerator();
constructor(public environment: SimulationEnvironment) {}
public async fuzzAsync(steps?: number): Promise<void> {
if (steps !== undefined) {
for (let i = 0; i < steps; i++) {
await this.generator.next();
}
} else {
while (true) {
await this.generator.next();
}
}
}
protected abstract _assertionGenerator(): AsyncIterableIterator<AssertionResult | void>;
}

View File

@@ -0,0 +1,181 @@
import { constants as stakingConstants } from '@0x/contracts-staking';
import { blockchainTests, expect } from '@0x/contracts-test-utils';
import { DeploymentManager } from '../deployment_manager';
import { Authorizable, Ownable } from '../wrapper_interfaces';
blockchainTests('Deployment Manager', env => {
let owner: string;
let deploymentManager: DeploymentManager;
before(async () => {
[owner] = await env.getAccountAddressesAsync();
deploymentManager = await DeploymentManager.deployAsync(env);
});
async function batchAssertAuthorizedAsync(
authorizedAddress: string,
authorizedContracts: Authorizable[],
): Promise<void> {
for (const authorized of authorizedContracts) {
expect(await authorized.authorized(authorizedAddress).callAsync()).to.be.true();
}
}
async function batchAssertOwnerAsync(ownerAddress: string, owners: Ownable[]): Promise<void> {
for (const ownerContract of owners) {
expect(await ownerContract.owner().callAsync()).to.be.eq(ownerAddress);
}
}
describe('asset proxy owner', () => {
it('should be owned by `owner`', async () => {
// Ensure that the owners of the asset proxy only contain the owner.
const owners = await deploymentManager.governor.getOwners().callAsync();
expect(owners).to.be.deep.eq([owner]);
});
});
describe('asset proxies', () => {
it('should be owned be the asset proxy owner', async () => {
await batchAssertOwnerAsync(deploymentManager.governor.address, [
deploymentManager.assetProxies.erc1155Proxy,
deploymentManager.assetProxies.erc20Proxy,
deploymentManager.assetProxies.erc721Proxy,
deploymentManager.assetProxies.multiAssetProxy,
]);
});
it('should have authorized the multi-asset proxy', async () => {
await batchAssertAuthorizedAsync(deploymentManager.assetProxies.multiAssetProxy.address, [
deploymentManager.assetProxies.erc1155Proxy,
deploymentManager.assetProxies.erc20Proxy,
deploymentManager.assetProxies.erc721Proxy,
]);
});
it('should have authorized the exchange', async () => {
await batchAssertAuthorizedAsync(deploymentManager.exchange.address, [
deploymentManager.assetProxies.erc1155Proxy,
deploymentManager.assetProxies.erc20Proxy,
deploymentManager.assetProxies.erc721Proxy,
deploymentManager.assetProxies.multiAssetProxy,
]);
});
it('should have the correct authorities list', async () => {
// The multi-asset proxy should only have the exchange in the authorities list.
const authorities = await deploymentManager.assetProxies.multiAssetProxy
.getAuthorizedAddresses()
.callAsync();
expect(authorities).to.be.deep.eq([deploymentManager.exchange.address]);
// The other asset proxies should have the exchange and the multi-asset proxy in their
// authorities list.
const erc20ProxyAuthorities = await deploymentManager.assetProxies.erc20Proxy
.getAuthorizedAddresses()
.callAsync();
expect(erc20ProxyAuthorities).to.deep.eq([
deploymentManager.staking.zrxVault.address,
deploymentManager.assetProxies.multiAssetProxy.address,
deploymentManager.exchange.address,
]);
const erc1155ProxyAuthorities = await deploymentManager.assetProxies.erc1155Proxy
.getAuthorizedAddresses()
.callAsync();
expect(erc1155ProxyAuthorities).to.deep.eq([
deploymentManager.assetProxies.multiAssetProxy.address,
deploymentManager.exchange.address,
]);
const erc721ProxyAuthorities = await deploymentManager.assetProxies.erc721Proxy
.getAuthorizedAddresses()
.callAsync();
expect(erc721ProxyAuthorities).to.deep.eq([
deploymentManager.assetProxies.multiAssetProxy.address,
deploymentManager.exchange.address,
]);
});
});
describe('exchange', () => {
it('should be owned by the asset proxy owner', async () => {
const exchangeOwner = await deploymentManager.exchange.owner().callAsync();
expect(exchangeOwner).to.be.eq(deploymentManager.governor.address);
});
/*
TODO(jalextowle): This test should be enabled once the Exchange is
made an Authorizable contract.
it('should have authorized the asset proxy owner', async () => {
const isAuthorized = await deploymentManager.exchange.owner(
deploymentManager.governor.address,
).callAsync();
expect(isAuthorized).to.be.true();
});
*/
it('should have registered the staking proxy', async () => {
const feeCollector = await deploymentManager.exchange.protocolFeeCollector().callAsync();
expect(feeCollector).to.be.eq(deploymentManager.staking.stakingProxy.address);
});
it('should have set the protocol fee multiplier', async () => {
const feeMultiplier = await deploymentManager.exchange.protocolFeeMultiplier().callAsync();
expect(feeMultiplier).bignumber.to.be.eq(DeploymentManager.protocolFeeMultiplier);
});
});
describe('staking', () => {
it('should be owned by the asset proxy owner', async () => {
const stakingOwner = await deploymentManager.staking.stakingProxy.owner().callAsync();
expect(stakingOwner).to.be.eq(deploymentManager.governor.address);
});
it('should have authorized the asset proxy owner in the staking proxy', async () => {
const isAuthorized = await deploymentManager.staking.stakingProxy
.authorized(deploymentManager.governor.address)
.callAsync();
expect(isAuthorized).to.be.true();
});
it('should have registered the exchange in the staking proxy', async () => {
const isValid = await deploymentManager.staking.stakingProxy
.validExchanges(deploymentManager.exchange.address)
.callAsync();
expect(isValid).to.be.true();
});
it('should have registered the staking contract in the staking proxy', async () => {
const stakingContract = await deploymentManager.staking.stakingProxy.stakingContract().callAsync();
expect(stakingContract).to.be.eq(deploymentManager.staking.stakingLogic.address);
});
it('should have registered the weth contract in the staking contract', async () => {
const weth = await deploymentManager.staking.stakingWrapper.testWethAddress().callAsync();
expect(weth).to.be.eq(deploymentManager.tokens.weth.address);
});
it('should have registered the zrx vault in the staking contract', async () => {
const zrxVault = await deploymentManager.staking.stakingWrapper.testZrxVaultAddress().callAsync();
expect(zrxVault).to.be.eq(deploymentManager.staking.zrxVault.address);
});
it('should have registered the staking proxy in the zrx vault', async () => {
const stakingProxy = await deploymentManager.staking.zrxVault.stakingProxyAddress().callAsync();
expect(stakingProxy).to.be.eq(deploymentManager.staking.stakingProxy.address);
});
it('should have correctly set the params', async () => {
const params = await deploymentManager.staking.stakingWrapper.getParams().callAsync();
expect(params).to.be.deep.eq([
stakingConstants.DEFAULT_PARAMS.epochDurationInSeconds,
stakingConstants.DEFAULT_PARAMS.rewardDelegatedStakeWeight,
stakingConstants.DEFAULT_PARAMS.minimumPoolStake,
stakingConstants.DEFAULT_PARAMS.cobbDouglasAlphaNumerator,
stakingConstants.DEFAULT_PARAMS.cobbDouglasAlphaDenominator,
]);
});
});
});

View File

@@ -0,0 +1,127 @@
import { blockchainTests, constants, expect, filterLogsToArguments, getRandomInteger } from '@0x/contracts-test-utils';
import { BigNumber, StringRevertError } from '@0x/utils';
import { TransactionReceiptWithDecodedLogs } from 'ethereum-types';
import { artifacts } from '../../artifacts';
import { TestFrameworkContract, TestFrameworkEventEventArgs, TestFrameworkEvents } from '../../wrappers';
import { FunctionAssertion, FunctionResult } from '../assertions/function_assertion';
const { ZERO_AMOUNT, MAX_UINT256 } = constants;
blockchainTests.resets('FunctionAssertion Unit Tests', env => {
let exampleContract: TestFrameworkContract;
before(async () => {
exampleContract = await TestFrameworkContract.deployFrom0xArtifactAsync(
artifacts.TestFramework,
env.provider,
env.txDefaults,
artifacts,
);
});
describe('executeAsync', () => {
it('should call the before function with the provided arguments', async () => {
let sideEffectTarget = ZERO_AMOUNT;
const assertion = new FunctionAssertion<void, BigNumber>(
exampleContract.returnInteger.bind(exampleContract),
{
before: async (_input: BigNumber) => {
sideEffectTarget = randomInput;
},
},
);
const randomInput = getRandomInteger(ZERO_AMOUNT, MAX_UINT256);
await assertion.executeAsync(randomInput);
expect(sideEffectTarget).bignumber.to.be.eq(randomInput);
});
it('should call the after function with the provided arguments', async () => {
let sideEffectTarget = ZERO_AMOUNT;
const assertion = new FunctionAssertion<void, BigNumber>(
exampleContract.returnInteger.bind(exampleContract),
{
after: async (_beforeInfo: any, _result: FunctionResult, input: BigNumber) => {
sideEffectTarget = input;
},
},
);
const randomInput = getRandomInteger(ZERO_AMOUNT, MAX_UINT256);
await assertion.executeAsync(randomInput);
expect(sideEffectTarget).bignumber.to.be.eq(randomInput);
});
it('should not fail immediately if the wrapped function fails', async () => {
const assertion = new FunctionAssertion<{}, void>(exampleContract.emptyRevert.bind(exampleContract));
await assertion.executeAsync();
});
it('should pass the return value of "before" to "after"', async () => {
const randomInput = getRandomInteger(ZERO_AMOUNT, MAX_UINT256);
let sideEffectTarget = ZERO_AMOUNT;
const assertion = new FunctionAssertion<BigNumber, BigNumber>(
exampleContract.returnInteger.bind(exampleContract),
{
before: async (_input: BigNumber) => {
return randomInput;
},
after: async (beforeInfo: any, _result: FunctionResult, _input: BigNumber) => {
sideEffectTarget = beforeInfo;
},
},
);
await assertion.executeAsync(randomInput);
expect(sideEffectTarget).bignumber.to.be.eq(randomInput);
});
it('should pass the result from the function call to "after"', async () => {
let sideEffectTarget = ZERO_AMOUNT;
const assertion = new FunctionAssertion<void, BigNumber>(
exampleContract.returnInteger.bind(exampleContract),
{
after: async (_beforeInfo: any, result: FunctionResult, _input: BigNumber) => {
sideEffectTarget = result.data;
},
},
);
const randomInput = getRandomInteger(ZERO_AMOUNT, MAX_UINT256);
await assertion.executeAsync(randomInput);
expect(sideEffectTarget).bignumber.to.be.eq(randomInput);
});
it('should pass the receipt from the function call to "after"', async () => {
let sideEffectTarget: TransactionReceiptWithDecodedLogs;
const assertion = new FunctionAssertion<void, void>(exampleContract.emitEvent.bind(exampleContract), {
after: async (_beforeInfo: any, result: FunctionResult, _input: string) => {
if (result.receipt) {
sideEffectTarget = result.receipt;
}
},
});
const input = 'emitted data';
await assertion.executeAsync(input);
// Ensure that the correct events were emitted.
const [event] = filterLogsToArguments<TestFrameworkEventEventArgs>(
sideEffectTarget!.logs, // tslint:disable-line:no-non-null-assertion
TestFrameworkEvents.Event,
);
expect(event).to.be.deep.eq({ input });
});
it('should pass the error to "after" if the function call fails', async () => {
let sideEffectTarget: Error;
const assertion = new FunctionAssertion<void, void>(exampleContract.stringRevert.bind(exampleContract), {
after: async (_beforeInfo: any, result: FunctionResult, _input: string) => {
sideEffectTarget = result.data;
},
});
const message = 'error message';
await assertion.executeAsync(message);
const expectedError = new StringRevertError(message);
return expect(Promise.reject(sideEffectTarget!)).to.revertWith(expectedError); // tslint:disable-line
});
});
});

View File

@@ -0,0 +1,21 @@
import { ContractFunctionObj, ContractTxFunctionObj } from '@0x/base-contract';
import { BlockParam, CallData } from 'ethereum-types';
// tslint:disable:max-classes-per-file
// Generated Wrapper Interfaces
export abstract class AssetProxyDispatcher {
public abstract registerAssetProxy(assetProxy: string): ContractTxFunctionObj<void>;
public abstract getAssetProxy(assetProxyId: string): ContractFunctionObj<string>;
}
export abstract class Ownable {
public abstract transferOwnership(newOwner: string): ContractTxFunctionObj<void>;
public abstract owner(callData?: Partial<CallData>, defaultBlock?: BlockParam): ContractFunctionObj<string>;
}
export abstract class Authorizable extends Ownable {
public abstract addAuthorizedAddress(target: string): ContractTxFunctionObj<void>;
public abstract removeAuthorizedAddress(target: string): ContractTxFunctionObj<void>;
public abstract authorized(authority: string): ContractFunctionObj<boolean>;
public abstract getAuthorizedAddresses(): ContractFunctionObj<string[]>;
}