Merge pull request #2059 from 0xProject/feature/contracts/3.0/fillOrderAssertionWrapper

Fill Order Assertion Wrapper
This commit is contained in:
Greg Hysz 2019-08-15 00:12:40 +02:00 committed by GitHub
commit 64bc99101c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 558 additions and 775 deletions

View File

@ -0,0 +1,242 @@
import {
artifacts as proxyArtifacts,
ERC1155ProxyWrapper,
ERC20Wrapper,
ERC721Wrapper,
} from '@0x/contracts-asset-proxy';
import { artifacts as erc20Artifacts } from '@0x/contracts-erc20';
import { artifacts as erc721Artifacts } from '@0x/contracts-erc721';
import {
expect,
FillEventArgs,
filterLogsToArguments,
LogDecoder,
OrderStatus,
orderUtils,
Web3ProviderEngine,
} from '@0x/contracts-test-utils';
import { orderHashUtils } from '@0x/order-utils';
import { FillResults, SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils';
import { Web3Wrapper } from '@0x/web3-wrapper';
import { TransactionReceiptWithDecodedLogs, ZeroExProvider } from 'ethereum-types';
import * as _ from 'lodash';
import { artifacts, ExchangeContract } from '../../src';
import { calculateFillResults } from '../../src/reference_functions';
import { BalanceStore } from '../balance_stores/balance_store';
import { BlockchainBalanceStore } from '../balance_stores/blockchain_balance_store';
import { LocalBalanceStore } from '../balance_stores/local_balance_store';
export class FillOrderWrapper {
private readonly _exchange: ExchangeContract;
private readonly _blockchainBalanceStore: BlockchainBalanceStore;
private readonly _web3Wrapper: Web3Wrapper;
/**
* Simulates matching two orders by transferring amounts defined in
* `transferAmounts` and returns the results.
* @param orders The orders being matched and their filled states.
* @param takerAddress Address of taker (the address who matched the two orders)
* @param tokenBalances Current token balances.
* @param transferAmounts Amounts to transfer during the simulation.
* @return The new account balances and fill events that occurred during the match.
*/
public static simulateFillOrder(
signedOrder: SignedOrder,
takerAddress: string,
opts: { takerAssetFillAmount?: BigNumber } = {},
initBalanceStore: BalanceStore,
): [FillResults, FillEventArgs, BalanceStore] {
const balanceStore = LocalBalanceStore.create(initBalanceStore);
const takerAssetFillAmount =
opts.takerAssetFillAmount !== undefined ? opts.takerAssetFillAmount : signedOrder.takerAssetAmount;
const fillResults = calculateFillResults(signedOrder, takerAssetFillAmount);
const fillEvent = FillOrderWrapper.simulateFillEvent(signedOrder, takerAddress, fillResults);
// Taker -> Maker
balanceStore.transferAsset(
takerAddress,
signedOrder.makerAddress,
fillResults.takerAssetFilledAmount,
signedOrder.takerAssetData,
);
// Maker -> Taker
balanceStore.transferAsset(
signedOrder.makerAddress,
takerAddress,
fillResults.makerAssetFilledAmount,
signedOrder.makerAssetData,
);
// Taker -> Fee Recipient
balanceStore.transferAsset(
takerAddress,
signedOrder.feeRecipientAddress,
fillResults.takerFeePaid,
signedOrder.takerFeeAssetData,
);
// Maker -> Fee Recipient
balanceStore.transferAsset(
signedOrder.makerAddress,
signedOrder.feeRecipientAddress,
fillResults.makerFeePaid,
signedOrder.makerFeeAssetData,
);
return [fillResults, fillEvent, balanceStore];
}
/**
* Simulates the event emitted by the exchange contract when an order is filled.
*/
public static simulateFillEvent(order: SignedOrder, takerAddress: string, fillResults: FillResults): FillEventArgs {
// prettier-ignore
return {
orderHash: orderHashUtils.getOrderHashHex(order),
makerAddress: order.makerAddress,
takerAddress,
makerAssetFilledAmount: fillResults.makerAssetFilledAmount,
takerAssetFilledAmount: fillResults.takerAssetFilledAmount,
makerFeePaid: fillResults.makerFeePaid,
takerFeePaid: fillResults.takerFeePaid,
};
}
/**
* Extract the exchanges `Fill` event from a transaction receipt.
*/
private static _extractFillEventsfromReceipt(receipt: TransactionReceiptWithDecodedLogs): FillEventArgs[] {
const events = filterLogsToArguments<FillEventArgs>(receipt.logs, 'Fill');
const fieldsOfInterest = [
'orderHash',
'makerAddress',
'takerAddress',
'makerAssetFilledAmount',
'takerAssetFilledAmount',
'makerFeePaid',
'takerFeePaid',
];
return events.map(event => _.pick(event, fieldsOfInterest)) as FillEventArgs[];
}
/**
* Constructor.
* @param exchangeContract Insstance of the deployed exchange contract
* @param erc20Wrapper The ERC20 Wrapper used to interface with deployed erc20 tokens.
* @param erc721Wrapper The ERC721 Wrapper used to interface with deployed erc20 tokens.
* @param erc1155ProxyWrapper The ERC1155 Proxy Wrapper used to interface with deployed erc20 tokens.
* @param provider Web3 provider to be used by a `Web3Wrapper` instance
*/
public constructor(
exchangeContract: ExchangeContract,
erc20Wrapper: ERC20Wrapper,
erc721Wrapper: ERC721Wrapper,
erc1155ProxyWrapper: ERC1155ProxyWrapper,
provider: Web3ProviderEngine | ZeroExProvider,
) {
this._exchange = exchangeContract;
this._blockchainBalanceStore = new BlockchainBalanceStore(erc20Wrapper, erc721Wrapper, erc1155ProxyWrapper);
this._web3Wrapper = new Web3Wrapper(provider);
}
/**
* Returns the balance store used by this wrapper.
*/
public getBlockchainBalanceStore(): BlockchainBalanceStore {
return this._blockchainBalanceStore;
}
/**
* Fills an order and asserts the effects. This includes
* 1. The order info (via `getOrderInfo`)
* 2. The fill results returned by making an `eth_call` to `exchange.fillOrder`
* 3. The events emitted by the exchange when the order is filled.
* 4. The balance changes as a result of filling the order.
*/
public async fillOrderAndAssertEffectsAsync(
signedOrder: SignedOrder,
from: string,
opts: { takerAssetFillAmount?: BigNumber } = {},
): Promise<void> {
// Get init state
await this._blockchainBalanceStore.updateBalancesAsync();
const initTakerAssetFilledAmount = await this._exchange.filled.callAsync(
orderHashUtils.getOrderHashHex(signedOrder),
);
// Assert init state of exchange
await this._assertOrderStateAsync(signedOrder, initTakerAssetFilledAmount);
// Simulate and execute fill then assert outputs
const [
simulatedFillResults,
simulatedFillEvent,
simulatedFinalBalanceStore,
] = FillOrderWrapper.simulateFillOrder(signedOrder, from, opts, this._blockchainBalanceStore);
const [fillResults, fillEvent] = await this._fillOrderAsync(signedOrder, from, opts);
// Assert state transition
expect(simulatedFillResults, 'Fill Results').to.be.deep.equal(fillResults);
expect(simulatedFillEvent, 'Fill Events').to.be.deep.equal(fillEvent);
const areBalancesEqual = BalanceStore.isEqual(simulatedFinalBalanceStore, this._blockchainBalanceStore);
expect(areBalancesEqual, 'Balances After Fill').to.be.true();
// Assert end state of exchange
const finalTakerAssetFilledAmount = initTakerAssetFilledAmount.plus(fillResults.takerAssetFilledAmount);
await this._assertOrderStateAsync(signedOrder, finalTakerAssetFilledAmount);
}
/**
* Fills an order on-chain. As an optimization this function auto-updates the blockchain balance store
* used by this contract.
*/
protected async _fillOrderAsync(
signedOrder: SignedOrder,
from: string,
opts: { takerAssetFillAmount?: BigNumber } = {},
): Promise<[FillResults, FillEventArgs]> {
const params = orderUtils.createFill(signedOrder, opts.takerAssetFillAmount);
const fillResults = await this._exchange.fillOrder.callAsync(
params.order,
params.takerAssetFillAmount,
params.signature,
{ from },
);
// @TODO: Replace with `awaitTransactionAsync` once `development` is merged into `3.0` branch
const txHash = await this._exchange.fillOrder.sendTransactionAsync(
params.order,
params.takerAssetFillAmount,
params.signature,
{ from },
);
const logDecoder = new LogDecoder(this._web3Wrapper, {
...artifacts,
...proxyArtifacts,
...erc20Artifacts,
...erc721Artifacts,
});
const txReceipt = await logDecoder.getTxWithDecodedLogsAsync(txHash);
const fillEvent = FillOrderWrapper._extractFillEventsfromReceipt(txReceipt)[0];
await this._blockchainBalanceStore.updateBalancesAsync();
return [fillResults, fillEvent];
}
/**
* Asserts that the provided order's fill amount and order status
* are the expected values.
* @param order The order to verify for a correct state.
* @param expectedFilledAmount The amount that the order should
* have been filled.
* @param side The side that the provided order should be matched on.
* @param exchangeWrapper The ExchangeWrapper instance.
*/
private async _assertOrderStateAsync(
order: SignedOrder,
expectedFilledAmount: BigNumber = new BigNumber(0),
): Promise<void> {
const orderInfo = await this._exchange.getOrderInfo.callAsync(order);
// Check filled amount of order.
const actualFilledAmount = orderInfo.orderTakerAssetFilledAmount;
expect(actualFilledAmount, 'order filled amount').to.be.bignumber.equal(expectedFilledAmount);
// Check status of order.
const expectedStatus = expectedFilledAmount.isGreaterThanOrEqualTo(order.takerAssetAmount)
? OrderStatus.FullyFilled
: OrderStatus.Fillable;
const actualStatus = orderInfo.orderStatus;
expect(actualStatus, 'order status').to.equal(expectedStatus);
}
}

View File

@ -0,0 +1,92 @@
import { ERC1155Holdings, ERC1155HoldingsByOwner, TokenBalances } from '@0x/contracts-test-utils';
import { BigNumber } from '@0x/utils';
import * as _ from 'lodash';
/**
* Note - this should live in `@0x/contracts-test-utils` but that would create a circular
* dependency in `BlockchainBlanceStore`. We should be able to mvoe this once we can rely
* solely on auto-generated wrappers opposed to the existing ERC20Wrapper, ERC721Wrapper,
* and ERC1155Wrapper.
*/
export class BalanceStore {
protected _balances: TokenBalances;
/**
* Returns true iff balance stores do have the same entries.
* @param lhs First balance store to compare
* @param rhs Second balance store to compare
*/
public static isEqual(lhs: BalanceStore, rhs: BalanceStore): boolean {
return _.isEqual(lhs.getRawBalances(), rhs.getRawBalances());
}
/**
* Throws iff balance stores do not have the same entries.
* @param lhs First balance store to compare
* @param rhs Second balance store to compare
*/
public static assertEqual(lhs: BalanceStore, rhs: BalanceStore): void {
if (!BalanceStore.isEqual(lhs, rhs)) {
throw new Error(`Balance stores are not equal:\n\nLeft:\n${lhs}\n\nRight:\n${rhs}`);
}
}
/**
* Restructures `ERC1155HoldingsByOwner` to be compatible with `TokenBalances.erc1155`.
* @param erc1155HoldingsByOwner Holdings returned by `ERC1155ProxyWrapper.getBalancesAsync()`.
*/
protected static _transformERC1155Holdings(erc1155HoldingsByOwner: ERC1155HoldingsByOwner): ERC1155Holdings {
const result = {};
for (const owner of _.keys(erc1155HoldingsByOwner.fungible)) {
for (const contract of _.keys(erc1155HoldingsByOwner.fungible[owner])) {
_.set(result as any, [owner, contract, 'fungible'], erc1155HoldingsByOwner.fungible[owner][contract]);
}
}
for (const owner of _.keys(erc1155HoldingsByOwner.nonFungible)) {
for (const contract of _.keys(erc1155HoldingsByOwner.nonFungible[owner])) {
const tokenIds = _.flatten(_.values(erc1155HoldingsByOwner.nonFungible[owner][contract]));
_.set(result as any, [owner, contract, 'nonFungible'], _.uniqBy(tokenIds, v => v.toString(10)));
}
}
return result;
}
/**
* Encodes token balances in a way that they can be compared by lodash.
*/
protected static _encodeTokenBalances(obj: any): any {
if (!_.isPlainObject(obj)) {
if (BigNumber.isBigNumber(obj)) {
return obj.toString(10);
}
if (_.isArray(obj)) {
return _.sortBy(obj, v => BalanceStore._encodeTokenBalances(v));
}
return obj;
}
const keys = _.keys(obj).sort();
return _.zip(keys, keys.map(k => BalanceStore._encodeTokenBalances(obj[k])));
}
/**
* Constructor.
*/
public constructor() {
this._balances = { erc20: {}, erc721: {}, erc1155: {} };
}
/**
* Copies the balance from an existing balance store.
* @param balanceStore to copy balances from.
*/
public copyBalancesFrom(balanceStore: BalanceStore): void {
this._balances = _.cloneDeep(balanceStore._balances);
}
/**
* Returns the raw `TokenBalances` that this class encapsulates.
*/
public getRawBalances(): TokenBalances {
return _.cloneDeep(this._balances);
}
}

View File

@ -0,0 +1,40 @@
import { ERC1155ProxyWrapper, ERC20Wrapper, ERC721Wrapper } from '@0x/contracts-asset-proxy';
import { BalanceStore } from './balance_store';
export class BlockchainBalanceStore extends BalanceStore {
private readonly _erc20Wrapper: ERC20Wrapper;
private readonly _erc721Wrapper: ERC721Wrapper;
private readonly _erc1155ProxyWrapper: ERC1155ProxyWrapper;
/**
* Constructor.
* @param erc20Wrapper The ERC20 Wrapper used to interface with deployed erc20 tokens.
* @param erc721Wrapper The ERC721 Wrapper used to interface with deployed erc20 tokens.
* @param erc1155ProxyWrapper The ERC1155 Proxy Wrapper used to interface with deployed erc20 tokens.
*/
public constructor(
erc20Wrapper: ERC20Wrapper,
erc721Wrapper: ERC721Wrapper,
erc1155ProxyWrapper: ERC1155ProxyWrapper,
) {
super();
this._erc20Wrapper = erc20Wrapper;
this._erc721Wrapper = erc721Wrapper;
this._erc1155ProxyWrapper = erc1155ProxyWrapper;
}
/**
* Updates balances by querying on-chain values managed by the erc20, erc721, and erc1155 wrappers.
*/
public async updateBalancesAsync(): Promise<void> {
const [erc20, erc721, erc1155] = await Promise.all([
this._erc20Wrapper.getBalancesAsync(),
this._erc721Wrapper.getBalancesAsync(),
this._erc1155ProxyWrapper.getBalancesAsync(),
]);
this._balances.erc20 = erc20;
this._balances.erc721 = erc721;
this._balances.erc1155 = BalanceStore._transformERC1155Holdings(erc1155);
}
}

View File

@ -0,0 +1,108 @@
import { assetDataUtils } from '@0x/order-utils';
import { AssetProxyId } from '@0x/types';
import { BigNumber } from '@0x/utils';
import * as _ from 'lodash';
import { BalanceStore } from './balance_store';
export class LocalBalanceStore extends BalanceStore {
/**
* Creates a new balance store based on an existing one.
* @param balanceStore Existing balance store whose values should be copied.
*/
public static create(sourceBalanceStore?: BalanceStore): LocalBalanceStore {
const localBalanceStore = new LocalBalanceStore();
if (sourceBalanceStore !== undefined) {
localBalanceStore.copyBalancesFrom(sourceBalanceStore);
}
return localBalanceStore;
}
/**
* Constructor.
*/
public constructor() {
super();
}
/**
* 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 transferAsset(fromAddress: string, toAddress: string, amount: BigNumber, assetData: string): void {
if (fromAddress === toAddress) {
return;
}
const assetProxyId = assetDataUtils.decodeAssetProxyId(assetData);
switch (assetProxyId) {
case AssetProxyId.ERC20: {
const erc20AssetData = assetDataUtils.decodeERC20AssetData(assetData);
const assetAddress = erc20AssetData.tokenAddress;
const fromBalances = this._balances.erc20[fromAddress];
const toBalances = this._balances.erc20[toAddress];
fromBalances[assetAddress] = fromBalances[assetAddress].minus(amount);
toBalances[assetAddress] = toBalances[assetAddress].plus(amount);
break;
}
case AssetProxyId.ERC721: {
const erc721AssetData = assetDataUtils.decodeERC721AssetData(assetData);
const assetAddress = erc721AssetData.tokenAddress;
const tokenId = erc721AssetData.tokenId;
const fromTokens = this._balances.erc721[fromAddress][assetAddress];
const toTokens = this._balances.erc721[toAddress][assetAddress];
if (amount.gte(1)) {
const tokenIndex = _.findIndex(fromTokens, t => t.eq(tokenId));
if (tokenIndex !== -1) {
fromTokens.splice(tokenIndex, 1);
toTokens.push(tokenId);
}
}
break;
}
case AssetProxyId.ERC1155: {
const erc1155AssetData = assetDataUtils.decodeERC1155AssetData(assetData);
const assetAddress = erc1155AssetData.tokenAddress;
const fromBalances = this._balances.erc1155[fromAddress][assetAddress];
const toBalances = this._balances.erc1155[toAddress][assetAddress];
for (const i of _.times(erc1155AssetData.tokenIds.length)) {
const tokenId = erc1155AssetData.tokenIds[i];
const tokenValue = erc1155AssetData.tokenValues[i];
const tokenAmount = amount.times(tokenValue);
if (tokenAmount.gt(0)) {
const tokenIndex = _.findIndex(fromBalances.nonFungible, t => t.eq(tokenId));
if (tokenIndex !== -1) {
// Transfer a non-fungible.
fromBalances.nonFungible.splice(tokenIndex, 1);
toBalances.nonFungible.push(tokenId);
} else {
// Transfer a fungible.
const _tokenId = tokenId.toString(10);
fromBalances.fungible[_tokenId] = fromBalances.fungible[_tokenId].minus(tokenAmount);
toBalances.fungible[_tokenId] = toBalances.fungible[_tokenId].plus(tokenAmount);
}
}
}
// sort NFT's by name
toBalances.nonFungible.sort();
break;
}
case AssetProxyId.MultiAsset: {
const multiAssetData = assetDataUtils.decodeMultiAssetData(assetData);
for (const i of _.times(multiAssetData.amounts.length)) {
const nestedAmount = amount.times(multiAssetData.amounts[i]);
const nestedAssetData = multiAssetData.nestedAssetData[i];
this.transferAsset(fromAddress, toAddress, nestedAmount, nestedAssetData);
}
break;
}
case AssetProxyId.StaticCall:
// Do nothing
break;
default:
throw new Error(`Unhandled asset proxy ID: ${assetProxyId}`);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -35,12 +35,15 @@ export {
ERC20BalancesByOwner,
ERC1155FungibleHoldingsByOwner,
ERC1155HoldingsByOwner,
ERC1155Holdings,
ERC1155NonFungibleHoldingsByOwner,
ERC721TokenIdsByOwner,
FillEventArgs,
MarketBuyOrders,
MarketSellOrders,
OrderStatus,
Token,
TokenBalances,
TransactionDataParams,
} from './types';
export { blockchainTests, BlockchainTestsEnvironment, describe } from './mocha_blockchain';

View File

@ -137,3 +137,38 @@ export interface MatchOrder {
leftSignature: string;
rightSignature: string;
}
export interface ERC1155Holdings {
[owner: string]: {
[contract: string]: {
fungible: {
[tokenId: string]: BigNumber;
};
nonFungible: BigNumber[];
};
};
}
export interface TokenBalances {
erc20: {
[owner: string]: {
[contract: string]: BigNumber;
};
};
erc721: {
[owner: string]: {
[contract: string]: BigNumber[];
};
};
erc1155: ERC1155Holdings;
}
export interface FillEventArgs {
orderHash: string;
makerAddress: string;
takerAddress: string;
makerAssetFilledAmount: BigNumber;
takerAssetFilledAmount: BigNumber;
makerFeePaid: BigNumber;
takerFeePaid: BigNumber;
}