From c54d69e5aee0da4870fd50e62f3da69177e19431 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Wed, 31 Jul 2019 13:15:09 -0400 Subject: [PATCH] `@0x/contracts-exchange`: Create reference functions test util. `@0x/contracts-exchange`: Use reference functions to assert fill results in `isolated_fill_order` tests. --- contracts/exchange/test/internal.ts | 2 +- .../exchange/test/isolated_fill_order.ts | 118 +++++++++++++----- .../test/utils/isolated_exchange_wrapper.ts | 17 ++- .../test/utils/reference_functions.ts | 107 ++++++++++++++++ 4 files changed, 207 insertions(+), 37 deletions(-) create mode 100644 contracts/exchange/test/utils/reference_functions.ts diff --git a/contracts/exchange/test/internal.ts b/contracts/exchange/test/internal.ts index 48aec23621..6a544a2caa 100644 --- a/contracts/exchange/test/internal.ts +++ b/contracts/exchange/test/internal.ts @@ -23,7 +23,7 @@ const expect = chai.expect; const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); -const MAX_UINT256 = new BigNumber(2).pow(256).minus(1); +const { MAX_UINT256 } = constants; const emptyOrder: Order = { senderAddress: constants.NULL_ADDRESS, diff --git a/contracts/exchange/test/isolated_fill_order.ts b/contracts/exchange/test/isolated_fill_order.ts index 9c0079d2b5..9ee94f613c 100644 --- a/contracts/exchange/test/isolated_fill_order.ts +++ b/contracts/exchange/test/isolated_fill_order.ts @@ -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 * 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 => { + const { ZERO_AMOUNT } = constants; const TOMORROW = Math.floor(_.now() / 1000) + 60 * 60 * 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, makerAddress: randomAddress(), takerAddress: constants.NULL_ADDRESS, - makerFee: constants.ZERO_AMOUNT, - takerFee: constants.ZERO_AMOUNT, - makerAssetAmount: constants.ZERO_AMOUNT, - takerAssetAmount: constants.ZERO_AMOUNT, - salt: constants.ZERO_AMOUNT, + makerFee: ZERO_AMOUNT, + takerFee: ZERO_AMOUNT, + makerAssetAmount: ZERO_AMOUNT, + takerAssetAmount: ZERO_AMOUNT, + salt: ZERO_AMOUNT, feeRecipientAddress: constants.NULL_ADDRESS, - expirationTimeSeconds: toBN(TOMORROW), + expirationTimeSeconds: new BigNumber(TOMORROW), makerAssetData: hexRandom(ERC20_ASSET_DATA_LENGTH), takerAssetData: hexRandom(ERC20_ASSET_DATA_LENGTH), makerFeeAssetData: hexRandom(ERC20_ASSET_DATA_LENGTH), takerFeeAssetData: hexRandom(ERC20_ASSET_DATA_LENGTH), }; let takerAddress: string; - let testExchange: IsolatedExchangeWrapper; + let exchange: IsolatedExchangeWrapper; let nextSaltValue = 1; before(async () => { - [takerAddress] = await env.getAccountAddressesAsync(); - testExchange = await IsolatedExchangeWrapper.deployAsync( + [ takerAddress ] = await env.getAccountAddressesAsync(); + exchange = await IsolatedExchangeWrapper.deployAsync( env.web3Wrapper, _.assign(env.txDefaults, { from: takerAddress }), ); }); - function createOrder(details: Partial = {}): Order { - return _.assign({}, DEFAULT_ORDER, { salt: toBN(nextSaltValue++) }, details); + function createOrder(details: Partial = {}): Orderish { + return _.assign({}, DEFAULT_ORDER, { salt: new BigNumber(nextSaltValue++) }, details); } - for (const i of _.times(100)) { - it('works', async () => { - const order = createOrder({ - makerAssetAmount: toBN(1), - takerAssetAmount: toBN(2), - }); - const results = await testExchange.fillOrderAsync(order, 2); + async function fillOrderAndAssertResultsAsync( + order: Orderish, + takerAssetFillAmount: BigNumber, + ): Promise { + const efr = await calculateExpectedFillResultsAsync(order, takerAssetFillAmount); + const efb = calculateExpectedFillBalances(order, efr); + 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 { + 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); -} diff --git a/contracts/exchange/test/utils/isolated_exchange_wrapper.ts b/contracts/exchange/test/utils/isolated_exchange_wrapper.ts index 1572dcb807..64580a6e27 100644 --- a/contracts/exchange/test/utils/isolated_exchange_wrapper.ts +++ b/contracts/exchange/test/utils/isolated_exchange_wrapper.ts @@ -22,7 +22,8 @@ export interface IsolatedExchangeEvents { 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_BAD_SIGNATURE = createBadSignature(); @@ -65,9 +66,13 @@ export class IsolatedExchangeWrapper { this.logDecoder = new LogDecoder(web3Wrapper, artifacts); } + public async getTakerAssetFilledAmountAsync(order: Orderish): Promise { + return this.instance.filled.callAsync(this.getOrderHash(order)); + } + public async fillOrderAsync( - order: Order, - takerAssetFillAmount: BigNumber | number, + order: Orderish, + takerAssetFillAmount: Numberish, signature: string = DEFAULT_GOOD_SIGNATURE, txOpts?: TxData, ): Promise { @@ -80,7 +85,7 @@ export class IsolatedExchangeWrapper { ); } - public getOrderHash(order: Order): string { + public getOrderHash(order: Orderish): string { const domain = { verifyingContractAddress: this.instance.address, chainId: IsolatedExchangeWrapper.CHAIN_ID, @@ -125,14 +130,14 @@ interface TransactionContractFunction { } /** - * @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 { 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 { return `0x00${Buffer.from([type]).toString('hex')}`; diff --git a/contracts/exchange/test/utils/reference_functions.ts b/contracts/exchange/test/utils/reference_functions.ts new file mode 100644 index 0000000000..5092215113 --- /dev/null +++ b/contracts/exchange/test/utils/reference_functions.ts @@ -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, + }; +}