@0x/contracts-exchange: Create reference functions test util.

`@0x/contracts-exchange`: Use reference functions to assert fill results
in `isolated_fill_order` tests.
This commit is contained in:
Lawrence Forman 2019-07-31 13:15:09 -04:00
parent 38a1f08413
commit c54d69e5ae
4 changed files with 207 additions and 37 deletions

View File

@ -23,7 +23,7 @@ const expect = chai.expect;
const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
const MAX_UINT256 = new BigNumber(2).pow(256).minus(1); const { MAX_UINT256 } = constants;
const emptyOrder: Order = { const emptyOrder: Order = {
senderAddress: constants.NULL_ADDRESS, senderAddress: constants.NULL_ADDRESS,

View File

@ -1,59 +1,117 @@
import { blockchainTests, constants, expect, hexRandom } from '@0x/contracts-test-utils'; import {
blockchainTests,
constants,
expect,
FillResults,
hexRandom,
} from '@0x/contracts-test-utils';
import { BigNumber } from '@0x/utils'; import { BigNumber } from '@0x/utils';
import * as _ from 'lodash'; import * as _ from 'lodash';
import { IsolatedExchangeWrapper, Order } from './utils/isolated_exchange_wrapper'; import { AssetBalances, IsolatedExchangeWrapper, Orderish } from './utils/isolated_exchange_wrapper';
import { calculateFillResults } from './utils/reference_functions';
blockchainTests.resets.only('Isolated fillOrder() tests', env => { blockchainTests.resets.only('Isolated fillOrder() tests', env => {
const { ZERO_AMOUNT } = constants;
const TOMORROW = Math.floor(_.now() / 1000) + 60 * 60 * 24; const TOMORROW = Math.floor(_.now() / 1000) + 60 * 60 * 24;
const ERC20_ASSET_DATA_LENGTH = 24; const ERC20_ASSET_DATA_LENGTH = 24;
const DEFAULT_ORDER: Order = { const randomAddress = () => hexRandom(constants.ADDRESS_LENGTH);
const DEFAULT_ORDER: Orderish = {
senderAddress: constants.NULL_ADDRESS, senderAddress: constants.NULL_ADDRESS,
makerAddress: randomAddress(), makerAddress: randomAddress(),
takerAddress: constants.NULL_ADDRESS, takerAddress: constants.NULL_ADDRESS,
makerFee: constants.ZERO_AMOUNT, makerFee: ZERO_AMOUNT,
takerFee: constants.ZERO_AMOUNT, takerFee: ZERO_AMOUNT,
makerAssetAmount: constants.ZERO_AMOUNT, makerAssetAmount: ZERO_AMOUNT,
takerAssetAmount: constants.ZERO_AMOUNT, takerAssetAmount: ZERO_AMOUNT,
salt: constants.ZERO_AMOUNT, salt: ZERO_AMOUNT,
feeRecipientAddress: constants.NULL_ADDRESS, feeRecipientAddress: constants.NULL_ADDRESS,
expirationTimeSeconds: toBN(TOMORROW), expirationTimeSeconds: new BigNumber(TOMORROW),
makerAssetData: hexRandom(ERC20_ASSET_DATA_LENGTH), makerAssetData: hexRandom(ERC20_ASSET_DATA_LENGTH),
takerAssetData: hexRandom(ERC20_ASSET_DATA_LENGTH), takerAssetData: hexRandom(ERC20_ASSET_DATA_LENGTH),
makerFeeAssetData: hexRandom(ERC20_ASSET_DATA_LENGTH), makerFeeAssetData: hexRandom(ERC20_ASSET_DATA_LENGTH),
takerFeeAssetData: hexRandom(ERC20_ASSET_DATA_LENGTH), takerFeeAssetData: hexRandom(ERC20_ASSET_DATA_LENGTH),
}; };
let takerAddress: string; let takerAddress: string;
let testExchange: IsolatedExchangeWrapper; let exchange: IsolatedExchangeWrapper;
let nextSaltValue = 1; let nextSaltValue = 1;
before(async () => { before(async () => {
[takerAddress] = await env.getAccountAddressesAsync(); [ takerAddress ] = await env.getAccountAddressesAsync();
testExchange = await IsolatedExchangeWrapper.deployAsync( exchange = await IsolatedExchangeWrapper.deployAsync(
env.web3Wrapper, env.web3Wrapper,
_.assign(env.txDefaults, { from: takerAddress }), _.assign(env.txDefaults, { from: takerAddress }),
); );
}); });
function createOrder(details: Partial<Order> = {}): Order { function createOrder(details: Partial<Orderish> = {}): Orderish {
return _.assign({}, DEFAULT_ORDER, { salt: toBN(nextSaltValue++) }, details); return _.assign({}, DEFAULT_ORDER, { salt: new BigNumber(nextSaltValue++) }, details);
} }
for (const i of _.times(100)) { async function fillOrderAndAssertResultsAsync(
it('works', async () => { order: Orderish,
const order = createOrder({ takerAssetFillAmount: BigNumber,
makerAssetAmount: toBN(1), ): Promise<FillResults> {
takerAssetAmount: toBN(2), const efr = await calculateExpectedFillResultsAsync(order, takerAssetFillAmount);
}); const efb = calculateExpectedFillBalances(order, efr);
const results = await testExchange.fillOrderAsync(order, 2); const fillResults = await exchange.fillOrderAsync(order, takerAssetFillAmount);
// Check returned fillResults.
expect(fillResults.makerAssetFilledAmount)
.to.bignumber.eq(efr.makerAssetFilledAmount);
expect(fillResults.takerAssetFilledAmount)
.to.bignumber.eq(efr.takerAssetFilledAmount);
expect(fillResults.makerFeePaid)
.to.bignumber.eq(efr.makerFeePaid);
expect(fillResults.takerFeePaid)
.to.bignumber.eq(efr.takerFeePaid);
// Check balances.
for (const assetData of Object.keys(efb)) {
for (const address of Object.keys(efb[assetData])) {
expect(exchange.getBalanceChange(assetData, address))
.to.bignumber.eq(efb[assetData][address], `assetData: ${assetData}, address: ${address}`);
}
}
return fillResults;
}
async function calculateExpectedFillResultsAsync(
order: Orderish,
takerAssetFillAmount: BigNumber,
): Promise<FillResults> {
const takerAssetFilledAmount = await exchange.getTakerAssetFilledAmountAsync(order);
const remainingTakerAssetAmount = order.takerAssetAmount.minus(takerAssetFilledAmount);
return calculateFillResults(
order,
BigNumber.min(takerAssetFillAmount, remainingTakerAssetAmount),
);
}
function calculateExpectedFillBalances(
order: Orderish,
fillResults: FillResults,
): AssetBalances {
const balances: AssetBalances = {};
const addBalance = (assetData: string, address: string, amount: BigNumber) => {
balances[assetData] = balances[assetData] || {};
const balance = balances[assetData][address] || ZERO_AMOUNT;
balances[assetData][address] = balance.plus(amount);
};
addBalance(order.makerAssetData, order.makerAddress, fillResults.makerAssetFilledAmount.negated());
addBalance(order.makerAssetData, takerAddress, fillResults.makerAssetFilledAmount);
addBalance(order.takerAssetData, order.makerAddress, fillResults.takerAssetFilledAmount);
addBalance(order.takerAssetData, takerAddress, fillResults.takerAssetFilledAmount.negated());
addBalance(order.makerFeeAssetData, order.makerAddress, fillResults.makerFeePaid.negated());
addBalance(order.makerFeeAssetData, order.feeRecipientAddress, fillResults.makerFeePaid);
addBalance(order.takerFeeAssetData, takerAddress, fillResults.takerFeePaid.negated());
addBalance(order.takerFeeAssetData, order.feeRecipientAddress, fillResults.takerFeePaid);
return balances;
}
it('can fully fill an order', async () => {
const order = createOrder({
makerAssetAmount: new BigNumber(1),
takerAssetAmount: new BigNumber(2),
}); });
} return fillOrderAndAssertResultsAsync(order, order.takerAssetAmount);
});
}); });
function toBN(num: BigNumber | string | number): BigNumber {
return new BigNumber(num);
}
function randomAddress(): string {
return hexRandom(constants.ADDRESS_LENGTH);
}

View File

@ -22,7 +22,8 @@ export interface IsolatedExchangeEvents {
transferFromCalls: DispatchTransferFromCallArgs[]; transferFromCalls: DispatchTransferFromCallArgs[];
} }
export type Order = OrderWithoutDomain; export type Orderish = OrderWithoutDomain;
export type Numberish = string | number | BigNumber;
export const DEFAULT_GOOD_SIGNATURE = createGoodSignature(); export const DEFAULT_GOOD_SIGNATURE = createGoodSignature();
export const DEFAULT_BAD_SIGNATURE = createBadSignature(); export const DEFAULT_BAD_SIGNATURE = createBadSignature();
@ -65,9 +66,13 @@ export class IsolatedExchangeWrapper {
this.logDecoder = new LogDecoder(web3Wrapper, artifacts); this.logDecoder = new LogDecoder(web3Wrapper, artifacts);
} }
public async getTakerAssetFilledAmountAsync(order: Orderish): Promise<BigNumber> {
return this.instance.filled.callAsync(this.getOrderHash(order));
}
public async fillOrderAsync( public async fillOrderAsync(
order: Order, order: Orderish,
takerAssetFillAmount: BigNumber | number, takerAssetFillAmount: Numberish,
signature: string = DEFAULT_GOOD_SIGNATURE, signature: string = DEFAULT_GOOD_SIGNATURE,
txOpts?: TxData, txOpts?: TxData,
): Promise<FillResults> { ): Promise<FillResults> {
@ -80,7 +85,7 @@ export class IsolatedExchangeWrapper {
); );
} }
public getOrderHash(order: Order): string { public getOrderHash(order: Orderish): string {
const domain = { const domain = {
verifyingContractAddress: this.instance.address, verifyingContractAddress: this.instance.address,
chainId: IsolatedExchangeWrapper.CHAIN_ID, chainId: IsolatedExchangeWrapper.CHAIN_ID,
@ -125,14 +130,14 @@ interface TransactionContractFunction<TResult> {
} }
/** /**
* @dev Create a signature for the `TestIsolatedExchange` contract that will pass. * Create a signature for the `TestIsolatedExchange` contract that will pass.
*/ */
export function createGoodSignature(type: SignatureType = SignatureType.EIP712): string { export function createGoodSignature(type: SignatureType = SignatureType.EIP712): string {
return `0x01${Buffer.from([type]).toString('hex')}`; return `0x01${Buffer.from([type]).toString('hex')}`;
} }
/** /**
* @dev Create a signature for the `TestIsolatedExchange` contract that will fail. * Create a signature for the `TestIsolatedExchange` contract that will fail.
*/ */
export function createBadSignature(type: SignatureType = SignatureType.EIP712): string { export function createBadSignature(type: SignatureType = SignatureType.EIP712): string {
return `0x00${Buffer.from([type]).toString('hex')}`; return `0x00${Buffer.from([type]).toString('hex')}`;

View File

@ -0,0 +1,107 @@
import { constants, FillResults } from '@0x/contracts-test-utils';
import { LibMathRevertErrors } from '@0x/order-utils';
import { OrderWithoutDomain } from '@0x/types';
import { AnyRevertError, BigNumber, SafeMathRevertErrors } from '@0x/utils';
const { MAX_UINT256 } = constants;
export function isRoundingErrorFloor(
numerator: BigNumber,
denominator: BigNumber,
target: BigNumber,
): boolean {
if (denominator.eq(0)) {
throw new LibMathRevertErrors.DivisionByZeroError();
}
if (numerator.eq(0)) {
return false;
}
if (target.eq(0)) {
return false;
}
const product = numerator.multipliedBy(target);
const remainder = product.mod(denominator);
const remainderTimes1000 = remainder.multipliedBy('1000');
const isError = remainderTimes1000.gte(product);
if (remainderTimes1000.isGreaterThan(MAX_UINT256)) {
// Solidity implementation won't actually throw.
throw new AnyRevertError();
}
return isError;
}
export function IsRoundingErrorCeil(
numerator: BigNumber,
denominator: BigNumber,
target: BigNumber,
): boolean {
if (denominator.eq(0)) {
throw new LibMathRevertErrors.DivisionByZeroError();
}
if (numerator.eq(0)) {
return false;
}
if (target.eq(0)) {
return false;
}
const product = numerator.multipliedBy(target);
const remainder = product.mod(denominator);
const error = denominator.minus(remainder).mod(denominator);
const errorTimes1000 = error.multipliedBy('1000');
const isError = errorTimes1000.gte(product);
if (errorTimes1000.isGreaterThan(MAX_UINT256)) {
// Solidity implementation won't actually throw.
throw new AnyRevertError();
}
return isError;
}
export function safeGetPartialAmountFloor(
numerator: BigNumber,
denominator: BigNumber,
target: BigNumber,
): BigNumber {
if (denominator.eq(0)) {
throw new LibMathRevertErrors.DivisionByZeroError();
}
const isRoundingError = isRoundingErrorFloor(numerator, denominator, target);
if (isRoundingError) {
throw new LibMathRevertErrors.RoundingError(numerator, denominator, target);
}
const product = numerator.multipliedBy(target);
if (product.isGreaterThan(MAX_UINT256)) {
throw new SafeMathRevertErrors.SafeMathError(
SafeMathRevertErrors.SafeMathErrorCodes.Uint256MultiplicationOverflow,
numerator,
denominator,
);
}
return product.dividedToIntegerBy(denominator);
}
export function calculateFillResults(
order: OrderWithoutDomain,
takerAssetFilledAmount: BigNumber,
): FillResults {
const makerAssetFilledAmount = safeGetPartialAmountFloor(
takerAssetFilledAmount,
order.takerAssetAmount,
order.makerAssetAmount,
);
const makerFeePaid = safeGetPartialAmountFloor(
makerAssetFilledAmount,
order.makerAssetAmount,
order.makerFee,
);
const takerFeePaid = safeGetPartialAmountFloor(
takerAssetFilledAmount,
order.takerAssetAmount,
order.takerFee,
);
return {
makerAssetFilledAmount,
takerAssetFilledAmount,
makerFeePaid,
takerFeePaid,
};
}