From 554d5f97df55779beed35ef73214e80b386d7927 Mon Sep 17 00:00:00 2001 From: Alex Browne Date: Thu, 26 Jul 2018 22:09:55 -0700 Subject: Add combinatorial tests for internal Exchange functions (#807) * WIP add combinatorial tests for internal Exchange functions * Change combinitorial testing strategy based on feedback * Check value of filled[orderHash] in updateFilledState tests * Add combinatorial tests for addFillResults * Add combinatorial tests for getPartialAmount * Implement generic `testWithReferenceFuncAsync` * Implement generic `testCombinatoriallyWithReferenceFuncAsync` * Add combinatorial tests for isRoundingError * Add combinatorial tests for calculateFillResults * Add support for Geth in internal contract tests * Fix contract artifacts * Change DECIMAL_PLACES to 78 and add a note. * Document new functions in utils * Optimize tests by only reseting state when needed * Rename/move some files * Print parameter names on failure in testWithReferenceFuncAsync * Add to changelog for utils package * Appease various linters * Rename some more things related to FillOrderCombinatorialUtils * Remove .only from test/exchange/internal.ts * Remove old test for isRoundingError and getPartialAmount * Appease linters again * Remove old todos * Fix typos, add comments, rename some things * Re-add some LibMath tests * Update contract internal tests to use new SafeMath revert reasons * Apply PR feedback from Amir * Apply PR feedback from Remco * Re-add networks to ZRXToken artifact * Remove duplicate Whitelist in compiler.json --- packages/contracts/compiler.json | 3 +- packages/contracts/package.json | 4 +- .../2.0.0/protocol/Exchange/MixinExchangeCore.sol | 1 - .../TestExchangeInternals.sol | 120 +++ packages/contracts/test/exchange/fill_order.ts | 51 +- packages/contracts/test/exchange/internal.ts | 305 +++++++ packages/contracts/test/exchange/libs.ts | 121 +-- packages/contracts/test/utils/artifacts.ts | 2 + packages/contracts/test/utils/assertions.ts | 27 + .../contracts/test/utils/combinatorial_utils.ts | 113 +++ .../test/utils/core_combinatorial_utils.ts | 878 --------------------- .../test/utils/fill_order_combinatorial_utils.ts | 878 +++++++++++++++++++++ .../contracts/test/utils/test_with_reference.ts | 119 +++ 13 files changed, 1625 insertions(+), 997 deletions(-) create mode 100644 packages/contracts/src/2.0.0/test/TestExchangeInternals/TestExchangeInternals.sol create mode 100644 packages/contracts/test/exchange/internal.ts create mode 100644 packages/contracts/test/utils/combinatorial_utils.ts delete mode 100644 packages/contracts/test/utils/core_combinatorial_utils.ts create mode 100644 packages/contracts/test/utils/fill_order_combinatorial_utils.ts create mode 100644 packages/contracts/test/utils/test_with_reference.ts (limited to 'packages/contracts') diff --git a/packages/contracts/compiler.json b/packages/contracts/compiler.json index dba836bde..ad35fc5b3 100644 --- a/packages/contracts/compiler.json +++ b/packages/contracts/compiler.json @@ -42,12 +42,13 @@ "TestConstants", "TestLibBytes", "TestLibs", + "TestExchangeInternals", "TestSignatureValidator", "TokenRegistry", "Validator", "Wallet", - "Whitelist", "WETH9", + "Whitelist", "ZRXToken" ] } diff --git a/packages/contracts/package.json b/packages/contracts/package.json index 9fe8046b1..014210d33 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -33,7 +33,7 @@ "lint-contracts": "solhint src/2.0.0/**/**/**/**/*.sol" }, "config": { - "abis": "../migrations/artifacts/2.0.0/@(AssetProxyOwner|DummyERC20Token|DummyERC721Receiver|DummyERC721Token|ERC20Proxy|ERC721Proxy|Forwarder|Exchange|ExchangeWrapper|IAssetData|IAssetProxy|MixinAuthorizable|MultiSigWallet|MultiSigWalletWithTimeLock|TestAssetProxyOwner|TestAssetProxyDispatcher|TestConstants|TestLibBytes|TestLibs|TestSignatureValidator|Validator|Wallet|TokenRegistry|Whitelist|WETH9|ZRXToken).json" + "abis": "../migrations/artifacts/2.0.0/@(AssetProxyOwner|DummyERC20Token|DummyERC721Receiver|DummyERC721Token|ERC20Proxy|ERC721Proxy|Forwarder|Exchange|ExchangeWrapper|IAssetData|IAssetProxy|MixinAuthorizable|MultiSigWallet|MultiSigWalletWithTimeLock|TestAssetProxyOwner|TestAssetProxyDispatcher|TestConstants|TestExchangeInternals|TestLibBytes|TestLibs|TestSignatureValidator|Validator|Wallet|TokenRegistry|Whitelist|WETH9|ZRXToken).json" }, "repository": { "type": "git", @@ -79,11 +79,13 @@ "@0xproject/typescript-typings": "^1.0.3", "@0xproject/utils": "^1.0.4", "@0xproject/web3-wrapper": "^1.1.2", + "@types/js-combinatorics": "^0.5.29", "bn.js": "^4.11.8", "ethereum-types": "^1.0.3", "ethereumjs-abi": "0.6.5", "ethereumjs-util": "^5.1.1", "ethers": "3.0.22", + "js-combinatorics": "^0.5.3", "lodash": "^4.17.4" } } diff --git a/packages/contracts/src/2.0.0/protocol/Exchange/MixinExchangeCore.sol b/packages/contracts/src/2.0.0/protocol/Exchange/MixinExchangeCore.sol index 354d0e9c3..ab5c6e507 100644 --- a/packages/contracts/src/2.0.0/protocol/Exchange/MixinExchangeCore.sol +++ b/packages/contracts/src/2.0.0/protocol/Exchange/MixinExchangeCore.sol @@ -207,7 +207,6 @@ contract MixinExchangeCore is /// @param order that was filled. /// @param takerAddress Address of taker who filled the order. /// @param orderTakerAssetFilledAmount Amount of order already filled. - /// @return fillResults Amounts filled and fees paid by maker and taker. function updateFilledState( Order memory order, address takerAddress, diff --git a/packages/contracts/src/2.0.0/test/TestExchangeInternals/TestExchangeInternals.sol b/packages/contracts/src/2.0.0/test/TestExchangeInternals/TestExchangeInternals.sol new file mode 100644 index 000000000..923bac97d --- /dev/null +++ b/packages/contracts/src/2.0.0/test/TestExchangeInternals/TestExchangeInternals.sol @@ -0,0 +1,120 @@ +/* + + Copyright 2018 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity 0.4.24; +pragma experimental ABIEncoderV2; + +import "../../protocol/Exchange/Exchange.sol"; + + +contract TestExchangeInternals is + Exchange +{ + constructor () + public + Exchange("") + {} + + /// @dev Adds properties of both FillResults instances. + /// Modifies the first FillResults instance specified. + /// Note that this function has been modified from the original + // internal version to return the FillResults. + /// @param totalFillResults Fill results instance that will be added onto. + /// @param singleFillResults Fill results instance that will be added to totalFillResults. + /// @return newTotalFillResults The result of adding singleFillResults to totalFilResults. + function publicAddFillResults(FillResults memory totalFillResults, FillResults memory singleFillResults) + public + pure + returns (FillResults memory) + { + addFillResults(totalFillResults, singleFillResults); + return totalFillResults; + } + + /// @dev Calculates amounts filled and fees paid by maker and taker. + /// @param order to be filled. + /// @param takerAssetFilledAmount Amount of takerAsset that will be filled. + /// @return fillResults Amounts filled and fees paid by maker and taker. + function publicCalculateFillResults( + Order memory order, + uint256 takerAssetFilledAmount + ) + public + pure + returns (FillResults memory fillResults) + { + return calculateFillResults(order, takerAssetFilledAmount); + } + + /// @dev Calculates partial value given a numerator and denominator. + /// @param numerator Numerator. + /// @param denominator Denominator. + /// @param target Value to calculate partial of. + /// @return Partial value of target. + function publicGetPartialAmount( + uint256 numerator, + uint256 denominator, + uint256 target + ) + public + pure + returns (uint256 partialAmount) + { + return getPartialAmount(numerator, denominator, target); + } + + /// @dev Checks if rounding error > 0.1%. + /// @param numerator Numerator. + /// @param denominator Denominator. + /// @param target Value to multiply with numerator/denominator. + /// @return Rounding error is present. + function publicIsRoundingError( + uint256 numerator, + uint256 denominator, + uint256 target + ) + public + pure + returns (bool isError) + { + return isRoundingError(numerator, denominator, target); + } + + /// @dev Updates state with results of a fill order. + /// @param order that was filled. + /// @param takerAddress Address of taker who filled the order. + /// @param orderTakerAssetFilledAmount Amount of order already filled. + /// @return fillResults Amounts filled and fees paid by maker and taker. + function publicUpdateFilledState( + Order memory order, + address takerAddress, + bytes32 orderHash, + uint256 orderTakerAssetFilledAmount, + FillResults memory fillResults + ) + public + { + updateFilledState( + order, + takerAddress, + orderHash, + orderTakerAssetFilledAmount, + fillResults + ); + } +} diff --git a/packages/contracts/test/exchange/fill_order.ts b/packages/contracts/test/exchange/fill_order.ts index 029bd66e2..1494fe093 100644 --- a/packages/contracts/test/exchange/fill_order.ts +++ b/packages/contracts/test/exchange/fill_order.ts @@ -2,7 +2,10 @@ import { BlockchainLifecycle } from '@0xproject/dev-utils'; import * as _ from 'lodash'; import { chaiSetup } from '../utils/chai_setup'; -import { CoreCombinatorialUtils, coreCombinatorialUtilsFactoryAsync } from '../utils/core_combinatorial_utils'; +import { + FillOrderCombinatorialUtils, + fillOrderCombinatorialUtilsFactoryAsync, +} from '../utils/fill_order_combinatorial_utils'; import { AllowanceAmountScenario, AssetDataScenario, @@ -47,11 +50,11 @@ const defaultFillScenario = { }; describe('FillOrder Tests', () => { - let coreCombinatorialUtils: CoreCombinatorialUtils; + let fillOrderCombinatorialUtils: FillOrderCombinatorialUtils; before(async () => { await blockchainLifecycle.startAsync(); - coreCombinatorialUtils = await coreCombinatorialUtilsFactoryAsync(web3Wrapper, txDefaults); + fillOrderCombinatorialUtils = await fillOrderCombinatorialUtilsFactoryAsync(web3Wrapper, txDefaults); }); after(async () => { await blockchainLifecycle.revertAsync(); @@ -67,19 +70,19 @@ describe('FillOrder Tests', () => { _.forEach(fillScenarios, fillScenario => { const description = `Combinatorial OrderFill: ${JSON.stringify(fillScenario)}`; it(description, async () => { - await coreCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario); + await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario); }); }); }; - const allFillScenarios = CoreCombinatorialUtils.generateFillOrderCombinations(); + const allFillScenarios = FillOrderCombinatorialUtils.generateFillOrderCombinations(); describe('Combinatorially generated fills orders', () => test(allFillScenarios)); it('should transfer the correct amounts when makerAssetAmount === takerAssetAmount', async () => { const fillScenario = { ...defaultFillScenario, }; - await coreCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario); + await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario); }); it('should transfer the correct amounts when makerAssetAmount > takerAssetAmount', async () => { const fillScenario = { @@ -89,7 +92,7 @@ describe('FillOrder Tests', () => { takerAssetAmountScenario: OrderAssetAmountScenario.Small, }, }; - await coreCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario); + await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario); }); it('should transfer the correct amounts when makerAssetAmount < takerAssetAmount', async () => { const fillScenario = { @@ -99,7 +102,7 @@ describe('FillOrder Tests', () => { makerAssetAmountScenario: OrderAssetAmountScenario.Small, }, }; - await coreCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario); + await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario); }); it('should transfer the correct amounts when taker is specified and order is claimed by taker', async () => { const fillScenario = { @@ -109,14 +112,14 @@ describe('FillOrder Tests', () => { takerScenario: TakerScenario.CorrectlySpecified, }, }; - await coreCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario); + await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario); }); it('should fill remaining value if takerAssetFillAmount > remaining takerAssetAmount', async () => { const fillScenario = { ...defaultFillScenario, takerAssetFillAmountScenario: TakerAssetFillAmountScenario.GreaterThanRemainingFillableTakerAssetAmount, }; - await coreCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario); + await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario); }); it('should throw when taker is specified and order is claimed by other', async () => { const fillScenario = { @@ -126,7 +129,7 @@ describe('FillOrder Tests', () => { takerScenario: TakerScenario.IncorrectlySpecified, }, }; - await coreCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario); + await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario); }); it('should throw if makerAssetAmount is 0', async () => { @@ -138,7 +141,7 @@ describe('FillOrder Tests', () => { }, takerAssetFillAmountScenario: TakerAssetFillAmountScenario.GreaterThanRemainingFillableTakerAssetAmount, }; - await coreCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario); + await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario); }); it('should throw if takerAssetAmount is 0', async () => { @@ -150,7 +153,7 @@ describe('FillOrder Tests', () => { }, takerAssetFillAmountScenario: TakerAssetFillAmountScenario.GreaterThanRemainingFillableTakerAssetAmount, }; - await coreCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario); + await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario); }); it('should throw if takerAssetFillAmount is 0', async () => { @@ -158,7 +161,7 @@ describe('FillOrder Tests', () => { ...defaultFillScenario, takerAssetFillAmountScenario: TakerAssetFillAmountScenario.Zero, }; - await coreCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario); + await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario); }); it('should throw if an order is expired', async () => { @@ -169,7 +172,7 @@ describe('FillOrder Tests', () => { expirationTimeSecondsScenario: ExpirationTimeSecondsScenario.InPast, }, }; - await coreCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario); + await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario); }); it('should throw if maker erc20Balances are too low to fill order', async () => { @@ -180,7 +183,7 @@ describe('FillOrder Tests', () => { traderAssetBalance: BalanceAmountScenario.TooLow, }, }; - await coreCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario); + await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario); }); it('should throw if taker erc20Balances are too low to fill order', async () => { @@ -191,7 +194,7 @@ describe('FillOrder Tests', () => { traderAssetBalance: BalanceAmountScenario.TooLow, }, }; - await coreCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario); + await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario); }); it('should throw if maker allowances are too low to fill order', async () => { @@ -202,7 +205,7 @@ describe('FillOrder Tests', () => { traderAssetAllowance: AllowanceAmountScenario.TooLow, }, }; - await coreCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario); + await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario); }); it('should throw if taker allowances are too low to fill order', async () => { @@ -213,7 +216,7 @@ describe('FillOrder Tests', () => { traderAssetAllowance: AllowanceAmountScenario.TooLow, }, }; - await coreCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario); + await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario); }); }); @@ -228,7 +231,7 @@ describe('FillOrder Tests', () => { }, takerAssetFillAmountScenario: TakerAssetFillAmountScenario.ExactlyRemainingFillableTakerAssetAmount, }; - await coreCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario); + await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario); }); it('should successfully fill order when makerAsset is ERC721 and takerAsset is ERC20', async () => { @@ -241,7 +244,7 @@ describe('FillOrder Tests', () => { }, takerAssetFillAmountScenario: TakerAssetFillAmountScenario.ExactlyRemainingFillableTakerAssetAmount, }; - await coreCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario, true); + await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario, true); }); it('should successfully fill order when makerAsset is ERC20 and takerAsset is ERC721', async () => { @@ -254,7 +257,7 @@ describe('FillOrder Tests', () => { }, takerAssetFillAmountScenario: TakerAssetFillAmountScenario.ExactlyRemainingFillableTakerAssetAmount, }; - await coreCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario); + await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario); }); it('should successfully fill order when makerAsset is ERC721 and approveAll is set for it', async () => { @@ -271,7 +274,7 @@ describe('FillOrder Tests', () => { traderAssetAllowance: AllowanceAmountScenario.Unlimited, }, }; - await coreCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario); + await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario); }); it('should successfully fill order when makerAsset and takerAsset are ERC721 and approveAll is set for them', async () => { @@ -292,7 +295,7 @@ describe('FillOrder Tests', () => { traderAssetAllowance: AllowanceAmountScenario.Unlimited, }, }; - await coreCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario); + await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario); }); }); }); diff --git a/packages/contracts/test/exchange/internal.ts b/packages/contracts/test/exchange/internal.ts new file mode 100644 index 000000000..67d1d2d2c --- /dev/null +++ b/packages/contracts/test/exchange/internal.ts @@ -0,0 +1,305 @@ +import { BlockchainLifecycle } from '@0xproject/dev-utils'; +import { Order, RevertReason, SignedOrder } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import * as _ from 'lodash'; + +import { TestExchangeInternalsContract } from '../../generated_contract_wrappers/test_exchange_internals'; +import { artifacts } from '../utils/artifacts'; +import { + getInvalidOpcodeErrorMessageForCallAsync, + getRevertReasonOrErrorMessageForSendTransactionAsync, +} from '../utils/assertions'; +import { chaiSetup } from '../utils/chai_setup'; +import { bytes32Values, testCombinatoriallyWithReferenceFuncAsync, uint256Values } from '../utils/combinatorial_utils'; +import { constants } from '../utils/constants'; +import { FillResults } from '../utils/types'; +import { provider, txDefaults, web3Wrapper } from '../utils/web3_wrapper'; + +chaiSetup.configure(); +const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); + +const MAX_UINT256 = new BigNumber(2).pow(256).minus(1); + +const emptyOrder: Order = { + senderAddress: constants.NULL_ADDRESS, + makerAddress: constants.NULL_ADDRESS, + takerAddress: constants.NULL_ADDRESS, + makerFee: new BigNumber(0), + takerFee: new BigNumber(0), + makerAssetAmount: new BigNumber(0), + takerAssetAmount: new BigNumber(0), + makerAssetData: '0x', + takerAssetData: '0x', + salt: new BigNumber(0), + exchangeAddress: constants.NULL_ADDRESS, + feeRecipientAddress: constants.NULL_ADDRESS, + expirationTimeSeconds: new BigNumber(0), +}; + +const emptySignedOrder: SignedOrder = { + ...emptyOrder, + signature: '', +}; + +const overflowErrorForCall = new Error(RevertReason.Uint256Overflow); + +async function referenceGetPartialAmountAsync( + numerator: BigNumber, + denominator: BigNumber, + target: BigNumber, +): Promise { + const invalidOpcodeErrorForCall = new Error(await getInvalidOpcodeErrorMessageForCallAsync()); + const product = numerator.mul(target); + if (product.greaterThan(MAX_UINT256)) { + throw overflowErrorForCall; + } + if (denominator.eq(0)) { + throw invalidOpcodeErrorForCall; + } + return product.dividedToIntegerBy(denominator); +} + +describe('Exchange core internal functions', () => { + let testExchange: TestExchangeInternalsContract; + let invalidOpcodeErrorForCall: Error | undefined; + let overflowErrorForSendTransaction: Error | undefined; + + before(async () => { + await blockchainLifecycle.startAsync(); + }); + after(async () => { + await blockchainLifecycle.revertAsync(); + }); + before(async () => { + testExchange = await TestExchangeInternalsContract.deployFrom0xArtifactAsync( + artifacts.TestExchangeInternals, + provider, + txDefaults, + ); + overflowErrorForSendTransaction = new Error( + await getRevertReasonOrErrorMessageForSendTransactionAsync(RevertReason.Uint256Overflow), + ); + invalidOpcodeErrorForCall = new Error(await getInvalidOpcodeErrorMessageForCallAsync()); + }); + // Note(albrow): Don't forget to add beforeEach and afterEach calls to reset + // the blockchain state for any tests which modify it! + + describe('addFillResults', async () => { + function makeFillResults(value: BigNumber): FillResults { + return { + makerAssetFilledAmount: value, + takerAssetFilledAmount: value, + makerFeePaid: value, + takerFeePaid: value, + }; + } + async function referenceAddFillResultsAsync( + totalValue: BigNumber, + singleValue: BigNumber, + ): Promise { + // Note(albrow): Here, each of totalFillResults and + // singleFillResults will consist of fields with the same values. + // This should be safe because none of the fields in a given + // FillResults are ever used together in a mathemetical operation. + // They are only used with the corresponding field from *the other* + // FillResults, which are different. + const totalFillResults = makeFillResults(totalValue); + const singleFillResults = makeFillResults(singleValue); + // HACK(albrow): _.mergeWith mutates the first argument! To + // workaround this we use _.cloneDeep. + return _.mergeWith( + _.cloneDeep(totalFillResults), + singleFillResults, + (totalVal: BigNumber, singleVal: BigNumber) => { + const newTotal = totalVal.add(singleVal); + if (newTotal.greaterThan(MAX_UINT256)) { + throw overflowErrorForCall; + } + return newTotal; + }, + ); + } + async function testAddFillResultsAsync(totalValue: BigNumber, singleValue: BigNumber): Promise { + const totalFillResults = makeFillResults(totalValue); + const singleFillResults = makeFillResults(singleValue); + return testExchange.publicAddFillResults.callAsync(totalFillResults, singleFillResults); + } + await testCombinatoriallyWithReferenceFuncAsync( + 'addFillResults', + referenceAddFillResultsAsync, + testAddFillResultsAsync, + [uint256Values, uint256Values], + ); + }); + + describe('calculateFillResults', async () => { + function makeOrder( + makerAssetAmount: BigNumber, + takerAssetAmount: BigNumber, + makerFee: BigNumber, + takerFee: BigNumber, + ): Order { + return { + ...emptyOrder, + makerAssetAmount, + takerAssetAmount, + makerFee, + takerFee, + }; + } + async function referenceCalculateFillResultsAsync( + orderTakerAssetAmount: BigNumber, + takerAssetFilledAmount: BigNumber, + otherAmount: BigNumber, + ): Promise { + // Note(albrow): Here we are re-using the same value (otherAmount) + // for order.makerAssetAmount, order.makerFee, and order.takerFee. + // This should be safe because they are never used with each other + // in any mathematical operation in either the reference TypeScript + // implementation or the Solidity implementation of + // calculateFillResults. + return { + makerAssetFilledAmount: await referenceGetPartialAmountAsync( + takerAssetFilledAmount, + orderTakerAssetAmount, + otherAmount, + ), + takerAssetFilledAmount, + makerFeePaid: await referenceGetPartialAmountAsync( + takerAssetFilledAmount, + orderTakerAssetAmount, + otherAmount, + ), + takerFeePaid: await referenceGetPartialAmountAsync( + takerAssetFilledAmount, + orderTakerAssetAmount, + otherAmount, + ), + }; + } + async function testCalculateFillResultsAsync( + orderTakerAssetAmount: BigNumber, + takerAssetFilledAmount: BigNumber, + otherAmount: BigNumber, + ): Promise { + const order = makeOrder(otherAmount, orderTakerAssetAmount, otherAmount, otherAmount); + return testExchange.publicCalculateFillResults.callAsync(order, takerAssetFilledAmount); + } + await testCombinatoriallyWithReferenceFuncAsync( + 'calculateFillResults', + referenceCalculateFillResultsAsync, + testCalculateFillResultsAsync, + [uint256Values, uint256Values, uint256Values], + ); + }); + + describe('getPartialAmount', async () => { + async function testGetPartialAmountAsync( + numerator: BigNumber, + denominator: BigNumber, + target: BigNumber, + ): Promise { + return testExchange.publicGetPartialAmount.callAsync(numerator, denominator, target); + } + await testCombinatoriallyWithReferenceFuncAsync( + 'getPartialAmount', + referenceGetPartialAmountAsync, + testGetPartialAmountAsync, + [uint256Values, uint256Values, uint256Values], + ); + }); + + describe('isRoundingError', async () => { + async function referenceIsRoundingErrorAsync( + numerator: BigNumber, + denominator: BigNumber, + target: BigNumber, + ): Promise { + const product = numerator.mul(target); + if (denominator.eq(0)) { + throw invalidOpcodeErrorForCall; + } + const remainder = product.mod(denominator); + if (remainder.eq(0)) { + return false; + } + if (product.greaterThan(MAX_UINT256)) { + throw overflowErrorForCall; + } + if (product.eq(0)) { + throw invalidOpcodeErrorForCall; + } + const remainderTimes1000000 = remainder.mul('1000000'); + if (remainderTimes1000000.greaterThan(MAX_UINT256)) { + throw overflowErrorForCall; + } + const errPercentageTimes1000000 = remainderTimes1000000.dividedToIntegerBy(product); + return errPercentageTimes1000000.greaterThan('1000'); + } + async function testIsRoundingErrorAsync( + numerator: BigNumber, + denominator: BigNumber, + target: BigNumber, + ): Promise { + return testExchange.publicIsRoundingError.callAsync(numerator, denominator, target); + } + await testCombinatoriallyWithReferenceFuncAsync( + 'isRoundingError', + referenceIsRoundingErrorAsync, + testIsRoundingErrorAsync, + [uint256Values, uint256Values, uint256Values], + ); + }); + + describe('updateFilledState', async () => { + // Note(albrow): Since updateFilledState modifies the state by calling + // sendTransaction, we must reset the state after each test. + beforeEach(async () => { + await blockchainLifecycle.startAsync(); + }); + afterEach(async () => { + await blockchainLifecycle.revertAsync(); + }); + async function referenceUpdateFilledStateAsync( + takerAssetFilledAmount: BigNumber, + orderTakerAssetFilledAmount: BigNumber, + // tslint:disable-next-line:no-unused-variable + orderHash: string, + ): Promise { + const totalFilledAmount = takerAssetFilledAmount.add(orderTakerAssetFilledAmount); + if (totalFilledAmount.greaterThan(MAX_UINT256)) { + throw overflowErrorForSendTransaction; + } + return totalFilledAmount; + } + async function testUpdateFilledStateAsync( + takerAssetFilledAmount: BigNumber, + orderTakerAssetFilledAmount: BigNumber, + orderHash: string, + ): Promise { + const fillResults = { + makerAssetFilledAmount: new BigNumber(0), + takerAssetFilledAmount, + makerFeePaid: new BigNumber(0), + takerFeePaid: new BigNumber(0), + }; + await web3Wrapper.awaitTransactionSuccessAsync( + await testExchange.publicUpdateFilledState.sendTransactionAsync( + emptySignedOrder, + constants.NULL_ADDRESS, + orderHash, + orderTakerAssetFilledAmount, + fillResults, + ), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + return testExchange.filled.callAsync(orderHash); + } + await testCombinatoriallyWithReferenceFuncAsync( + 'updateFilledState', + referenceUpdateFilledStateAsync, + testUpdateFilledStateAsync, + [uint256Values, uint256Values, bytes32Values], + ); + }); +}); diff --git a/packages/contracts/test/exchange/libs.ts b/packages/contracts/test/exchange/libs.ts index 51794d8a3..5c9f9aac7 100644 --- a/packages/contracts/test/exchange/libs.ts +++ b/packages/contracts/test/exchange/libs.ts @@ -67,6 +67,35 @@ describe('Exchange libs', () => { }); }); }); + // Note(albrow): These tests are designed to be supplemental to the + // combinatorial tests in test/exchange/internal. They test specific edge + // cases that are not covered by the combinatorial tests. + describe('LibMath', () => { + it('should return false if there is a rounding error of 0.1%', async () => { + const numerator = new BigNumber(20); + const denominator = new BigNumber(999); + const target = new BigNumber(50); + // rounding error = ((20*50/999) - floor(20*50/999)) / (20*50/999) = 0.1% + const isRoundingError = await libs.publicIsRoundingError.callAsync(numerator, denominator, target); + expect(isRoundingError).to.be.false(); + }); + it('should return false if there is a rounding of 0.09%', async () => { + const numerator = new BigNumber(20); + const denominator = new BigNumber(9991); + const target = new BigNumber(500); + // rounding error = ((20*500/9991) - floor(20*500/9991)) / (20*500/9991) = 0.09% + const isRoundingError = await libs.publicIsRoundingError.callAsync(numerator, denominator, target); + expect(isRoundingError).to.be.false(); + }); + it('should return true if there is a rounding error of 0.11%', async () => { + const numerator = new BigNumber(20); + const denominator = new BigNumber(9989); + const target = new BigNumber(500); + // rounding error = ((20*500/9989) - floor(20*500/9989)) / (20*500/9989) = 0.011% + const isRoundingError = await libs.publicIsRoundingError.callAsync(numerator, denominator, target); + expect(isRoundingError).to.be.true(); + }); + }); describe('LibOrder', () => { describe('getOrderSchema', () => { @@ -93,96 +122,4 @@ describe('Exchange libs', () => { }); }); }); - - describe('LibMath', () => { - describe('isRoundingError', () => { - it('should return false if there is a rounding error of 0.1%', async () => { - const numerator = new BigNumber(20); - const denominator = new BigNumber(999); - const target = new BigNumber(50); - // rounding error = ((20*50/999) - floor(20*50/999)) / (20*50/999) = 0.1% - const isRoundingError = await libs.publicIsRoundingError.callAsync(numerator, denominator, target); - expect(isRoundingError).to.be.false(); - }); - - it('should return false if there is a rounding of 0.09%', async () => { - const numerator = new BigNumber(20); - const denominator = new BigNumber(9991); - const target = new BigNumber(500); - // rounding error = ((20*500/9991) - floor(20*500/9991)) / (20*500/9991) = 0.09% - const isRoundingError = await libs.publicIsRoundingError.callAsync(numerator, denominator, target); - expect(isRoundingError).to.be.false(); - }); - - it('should return true if there is a rounding error of 0.11%', async () => { - const numerator = new BigNumber(20); - const denominator = new BigNumber(9989); - const target = new BigNumber(500); - // rounding error = ((20*500/9989) - floor(20*500/9989)) / (20*500/9989) = 0.011% - const isRoundingError = await libs.publicIsRoundingError.callAsync(numerator, denominator, target); - expect(isRoundingError).to.be.true(); - }); - - it('should return true if there is a rounding error > 0.1%', async () => { - const numerator = new BigNumber(3); - const denominator = new BigNumber(7); - const target = new BigNumber(10); - // rounding error = ((3*10/7) - floor(3*10/7)) / (3*10/7) = 6.67% - const isRoundingError = await libs.publicIsRoundingError.callAsync(numerator, denominator, target); - expect(isRoundingError).to.be.true(); - }); - - it('should return false when there is no rounding error', async () => { - const numerator = new BigNumber(1); - const denominator = new BigNumber(2); - const target = new BigNumber(10); - - const isRoundingError = await libs.publicIsRoundingError.callAsync(numerator, denominator, target); - expect(isRoundingError).to.be.false(); - }); - - it('should return false when there is rounding error <= 0.1%', async () => { - // randomly generated numbers - const numerator = new BigNumber(76564); - const denominator = new BigNumber(676373677); - const target = new BigNumber(105762562); - // rounding error = ((76564*105762562/676373677) - floor(76564*105762562/676373677)) / - // (76564*105762562/676373677) = 0.0007% - const isRoundingError = await libs.publicIsRoundingError.callAsync(numerator, denominator, target); - expect(isRoundingError).to.be.false(); - }); - }); - - describe('getPartialAmount', () => { - it('should return the numerator/denominator*target', async () => { - const numerator = new BigNumber(1); - const denominator = new BigNumber(2); - const target = new BigNumber(10); - - const partialAmount = await libs.publicGetPartialAmount.callAsync(numerator, denominator, target); - const expectedPartialAmount = 5; - expect(partialAmount).to.be.bignumber.equal(expectedPartialAmount); - }); - - it('should round down', async () => { - const numerator = new BigNumber(2); - const denominator = new BigNumber(3); - const target = new BigNumber(10); - - const partialAmount = await libs.publicGetPartialAmount.callAsync(numerator, denominator, target); - const expectedPartialAmount = 6; - expect(partialAmount).to.be.bignumber.equal(expectedPartialAmount); - }); - - it('should round .5 down', async () => { - const numerator = new BigNumber(1); - const denominator = new BigNumber(20); - const target = new BigNumber(10); - - const partialAmount = await libs.publicGetPartialAmount.callAsync(numerator, denominator, target); - const expectedPartialAmount = 0; - expect(partialAmount).to.be.bignumber.equal(expectedPartialAmount); - }); - }); - }); }); diff --git a/packages/contracts/test/utils/artifacts.ts b/packages/contracts/test/utils/artifacts.ts index 63bd555a4..e608ee174 100644 --- a/packages/contracts/test/utils/artifacts.ts +++ b/packages/contracts/test/utils/artifacts.ts @@ -16,6 +16,7 @@ import * as MultiSigWalletWithTimeLock from '../../artifacts/MultiSigWalletWithT import * as TestAssetProxyDispatcher from '../../artifacts/TestAssetProxyDispatcher.json'; import * as TestAssetProxyOwner from '../../artifacts/TestAssetProxyOwner.json'; import * as TestConstants from '../../artifacts/TestConstants.json'; +import * as TestExchangeInternals from '../../artifacts/TestExchangeInternals.json'; import * as TestLibBytes from '../../artifacts/TestLibBytes.json'; import * as TestLibs from '../../artifacts/TestLibs.json'; import * as TestSignatureValidator from '../../artifacts/TestSignatureValidator.json'; @@ -46,6 +47,7 @@ export const artifacts = { TestConstants: (TestConstants as any) as ContractArtifact, TestLibBytes: (TestLibBytes as any) as ContractArtifact, TestLibs: (TestLibs as any) as ContractArtifact, + TestExchangeInternals: (TestExchangeInternals as any) as ContractArtifact, TestSignatureValidator: (TestSignatureValidator as any) as ContractArtifact, Validator: (Validator as any) as ContractArtifact, Wallet: (Wallet as any) as ContractArtifact, diff --git a/packages/contracts/test/utils/assertions.ts b/packages/contracts/test/utils/assertions.ts index 112a470f6..61df800c8 100644 --- a/packages/contracts/test/utils/assertions.ts +++ b/packages/contracts/test/utils/assertions.ts @@ -15,6 +15,14 @@ let nodeType: NodeType | undefined; // resolve with either a transaction receipt or a transaction hash. export type sendTransactionResult = Promise; +/** + * Returns ganacheError if the backing Ethereum node is Ganache and gethError + * if it is Geth. + * @param ganacheError the error to be returned if the backing node is Ganache. + * @param gethError the error to be returned if the backing node is Geth. + * @returns either the given ganacheError or gethError depending on the backing + * node. + */ async function _getGanacheOrGethError(ganacheError: string, gethError: string): Promise { if (_.isUndefined(nodeType)) { nodeType = await web3Wrapper.getNodeTypeAsync(); @@ -41,6 +49,25 @@ async function _getContractCallFailedErrorMessageAsync(): Promise { return _getGanacheOrGethError('revert', 'Contract call failed'); } +/** + * Returns the expected error message for an 'invalid opcode' resulting from a + * contract call. The exact error message depends on the backing Ethereum node. + */ +export async function getInvalidOpcodeErrorMessageForCallAsync(): Promise { + return _getGanacheOrGethError('invalid opcode', 'Contract call failed'); +} + +/** + * Returns the expected error message for the given revert reason resulting from + * a sendTransaction call. The exact error message depends on the backing + * Ethereum node and whether it supports revert reasons. + * @param reason a specific revert reason. + * @returns the expected error message. + */ +export async function getRevertReasonOrErrorMessageForSendTransactionAsync(reason: RevertReason): Promise { + return _getGanacheOrGethError(reason, 'always failing transaction'); +} + /** * Rejects if the given Promise does not reject with an error indicating * insufficient funds. diff --git a/packages/contracts/test/utils/combinatorial_utils.ts b/packages/contracts/test/utils/combinatorial_utils.ts new file mode 100644 index 000000000..d72b41f8a --- /dev/null +++ b/packages/contracts/test/utils/combinatorial_utils.ts @@ -0,0 +1,113 @@ +import { BigNumber } from '@0xproject/utils'; +import * as combinatorics from 'js-combinatorics'; + +import { testWithReferenceFuncAsync } from './test_with_reference'; + +// A set of values corresponding to the uint256 type in Solidity. This set +// contains some notable edge cases, including some values which will overflow +// the uint256 type when used in different mathematical operations. +export const uint256Values = [ + new BigNumber(0), + new BigNumber(1), + new BigNumber(2), + // Non-trivial big number. + new BigNumber(2).pow(64), + // Max that does not overflow when squared. + new BigNumber(2).pow(128).minus(1), + // Min that does overflow when squared. + new BigNumber(2).pow(128), + // Max that does not overflow when doubled. + new BigNumber(2).pow(255).minus(1), + // Min that does overflow when doubled. + new BigNumber(2).pow(255), + // Max that does not overflow. + new BigNumber(2).pow(256).minus(1), +]; + +// A set of values corresponding to the bytes32 type in Solidity. +export const bytes32Values = [ + // Min + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000001', + '0x0000000000000000000000000000000000000000000000000000000000000002', + // Non-trivial big number. + '0x000000000000f000000000000000000000000000000000000000000000000000', + // Max + '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', +]; + +export async function testCombinatoriallyWithReferenceFuncAsync( + name: string, + referenceFunc: (p0: P0, p1: P1) => Promise, + testFunc: (p0: P0, p1: P1) => Promise, + allValues: [P0[], P1[]], +): Promise; +export async function testCombinatoriallyWithReferenceFuncAsync( + name: string, + referenceFunc: (p0: P0, p1: P1, p2: P2) => Promise, + testFunc: (p0: P0, p1: P1, p2: P2) => Promise, + allValues: [P0[], P1[], P2[]], +): Promise; +export async function testCombinatoriallyWithReferenceFuncAsync( + name: string, + referenceFunc: (p0: P0, p1: P1, p2: P2, p3: P3) => Promise, + testFunc: (p0: P0, p1: P1, p2: P2, p3: P3) => Promise, + allValues: [P0[], P1[], P2[], P3[]], +): Promise; +export async function testCombinatoriallyWithReferenceFuncAsync( + name: string, + referenceFunc: (p0: P0, p1: P1, p2: P2, p3: P3, p4: P4) => Promise, + testFunc: (p0: P0, p1: P1, p2: P2, p3: P3, p4: P4) => Promise, + allValues: [P0[], P1[], P2[], P3[], P4[]], +): Promise; + +/** + * Uses combinatorics to test the behavior of a test function by comparing it to + * the expected behavior (defined by a reference function) for a large number of + * possible input values. + * + * First generates test cases by taking the cartesian product of the given + * values. Each test case is a set of N values corresponding to the N arguments + * for the test func and the reference func. For each test case, first the + * reference function will be called to obtain an "expected result", or if the + * reference function throws/rejects, an "expected error". Next, the test + * function will be called to obtain an "actual result", or if the test function + * throws/rejects, an "actual error". Each test case passes if at least one of + * the following conditions is met: + * + * 1) Neither the reference function or the test function throw and the + * "expected result" equals the "actual result". + * + * 2) Both the reference function and the test function throw and the "actual + * error" message *contains* the "expected error" message. + * + * The first test case which does not meet one of these conditions will cause + * the entire test to fail and this function will throw/reject. + * + * @param referenceFuncAsync a reference function implemented in pure + * JavaScript/TypeScript which accepts N arguments and returns the "expected + * result" or "expected error" for a given test case. + * @param testFuncAsync a test function which, e.g., makes a call or sends a + * transaction to a contract. It accepts the same N arguments returns the + * "actual result" or "actual error" for a given test case. + * @param values an array of N arrays. Each inner array is a set of possible + * values which are passed into both the reference function and the test + * function. + * @return A Promise that resolves if the test passes and rejects if the test + * fails, according to the rules described above. + */ +export async function testCombinatoriallyWithReferenceFuncAsync( + name: string, + referenceFuncAsync: (...args: any[]) => Promise, + testFuncAsync: (...args: any[]) => Promise, + allValues: any[], +): Promise { + const testCases = combinatorics.cartesianProduct(...allValues); + let counter = 0; + testCases.forEach(async testCase => { + counter += 1; + it(`${name} ${counter}/${testCases.length}`, async () => { + await testWithReferenceFuncAsync(referenceFuncAsync, testFuncAsync, testCase as any); + }); + }); +} diff --git a/packages/contracts/test/utils/core_combinatorial_utils.ts b/packages/contracts/test/utils/core_combinatorial_utils.ts deleted file mode 100644 index 44a5199c0..000000000 --- a/packages/contracts/test/utils/core_combinatorial_utils.ts +++ /dev/null @@ -1,878 +0,0 @@ -import { - assetDataUtils, - BalanceAndProxyAllowanceLazyStore, - ExchangeTransferSimulator, - orderHashUtils, - OrderStateUtils, - OrderValidationUtils, -} from '@0xproject/order-utils'; -import { AssetProxyId, RevertReason, SignatureType, SignedOrder } from '@0xproject/types'; -import { BigNumber, errorUtils, logUtils } from '@0xproject/utils'; -import { Web3Wrapper } from '@0xproject/web3-wrapper'; -import * as chai from 'chai'; -import { LogWithDecodedArgs, Provider, TxData } from 'ethereum-types'; -import * as _ from 'lodash'; -import 'make-promises-safe'; - -import { ExchangeContract, ExchangeFillEventArgs } from '../../generated_contract_wrappers/exchange'; - -import { artifacts } from './artifacts'; -import { expectTransactionFailedAsync } from './assertions'; -import { AssetWrapper } from './asset_wrapper'; -import { chaiSetup } from './chai_setup'; -import { constants } from './constants'; -import { ERC20Wrapper } from './erc20_wrapper'; -import { ERC721Wrapper } from './erc721_wrapper'; -import { ExchangeWrapper } from './exchange_wrapper'; -import { OrderFactoryFromScenario } from './order_factory_from_scenario'; -import { orderUtils } from './order_utils'; -import { signingUtils } from './signing_utils'; -import { SimpleAssetBalanceAndProxyAllowanceFetcher } from './simple_asset_balance_and_proxy_allowance_fetcher'; -import { SimpleOrderFilledCancelledFetcher } from './simple_order_filled_cancelled_fetcher'; -import { - AllowanceAmountScenario, - AssetDataScenario, - BalanceAmountScenario, - ExpirationTimeSecondsScenario, - FeeRecipientAddressScenario, - FillScenario, - OrderAssetAmountScenario, - TakerAssetFillAmountScenario, - TakerScenario, - TraderStateScenario, -} from './types'; - -chaiSetup.configure(); -const expect = chai.expect; - -/** - * Instantiates a new instance of CoreCombinatorialUtils. Since this method has some - * required async setup, a factory method is required. - * @param web3Wrapper Web3Wrapper instance - * @param txDefaults Default Ethereum tx options - * @return CoreCombinatorialUtils instance - */ -export async function coreCombinatorialUtilsFactoryAsync( - web3Wrapper: Web3Wrapper, - txDefaults: Partial, -): Promise { - const accounts = await web3Wrapper.getAvailableAddressesAsync(); - const userAddresses = _.slice(accounts, 0, 5); - const [ownerAddress, makerAddress, takerAddress] = userAddresses; - const makerPrivateKey = constants.TESTRPC_PRIVATE_KEYS[userAddresses.indexOf(makerAddress)]; - - const provider = web3Wrapper.getProvider(); - const erc20Wrapper = new ERC20Wrapper(provider, userAddresses, ownerAddress); - const erc721Wrapper = new ERC721Wrapper(provider, userAddresses, ownerAddress); - - const erc20EighteenDecimalTokenCount = 3; - const eighteenDecimals = new BigNumber(18); - const [ - erc20EighteenDecimalTokenA, - erc20EighteenDecimalTokenB, - zrxToken, - ] = await erc20Wrapper.deployDummyTokensAsync(erc20EighteenDecimalTokenCount, eighteenDecimals); - const zrxAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address); - - const erc20FiveDecimalTokenCount = 2; - const fiveDecimals = new BigNumber(5); - const [erc20FiveDecimalTokenA, erc20FiveDecimalTokenB] = await erc20Wrapper.deployDummyTokensAsync( - erc20FiveDecimalTokenCount, - fiveDecimals, - ); - const erc20Proxy = await erc20Wrapper.deployProxyAsync(); - await erc20Wrapper.setBalancesAndAllowancesAsync(); - - const [erc721Token] = await erc721Wrapper.deployDummyTokensAsync(); - const erc721Proxy = await erc721Wrapper.deployProxyAsync(); - await erc721Wrapper.setBalancesAndAllowancesAsync(); - const erc721Balances = await erc721Wrapper.getBalancesAsync(); - - const assetWrapper = new AssetWrapper([erc20Wrapper, erc721Wrapper]); - - const exchangeContract = await ExchangeContract.deployFrom0xArtifactAsync( - artifacts.Exchange, - provider, - txDefaults, - zrxAssetData, - ); - const exchangeWrapper = new ExchangeWrapper(exchangeContract, provider); - await exchangeWrapper.registerAssetProxyAsync(erc20Proxy.address, ownerAddress); - await exchangeWrapper.registerAssetProxyAsync(erc721Proxy.address, ownerAddress); - - await web3Wrapper.awaitTransactionSuccessAsync( - await erc20Proxy.addAuthorizedAddress.sendTransactionAsync(exchangeContract.address, { - from: ownerAddress, - }), - constants.AWAIT_TRANSACTION_MINED_MS, - ); - await web3Wrapper.awaitTransactionSuccessAsync( - await erc721Proxy.addAuthorizedAddress.sendTransactionAsync(exchangeContract.address, { - from: ownerAddress, - }), - constants.AWAIT_TRANSACTION_MINED_MS, - ); - - const orderFactory = new OrderFactoryFromScenario( - userAddresses, - zrxToken.address, - [erc20EighteenDecimalTokenA.address, erc20EighteenDecimalTokenB.address], - [erc20FiveDecimalTokenA.address, erc20FiveDecimalTokenB.address], - erc721Token, - erc721Balances, - exchangeContract.address, - ); - - const coreCombinatorialUtils = new CoreCombinatorialUtils( - orderFactory, - ownerAddress, - makerAddress, - makerPrivateKey, - takerAddress, - zrxAssetData, - exchangeWrapper, - assetWrapper, - ); - return coreCombinatorialUtils; -} - -export class CoreCombinatorialUtils { - public orderFactory: OrderFactoryFromScenario; - public ownerAddress: string; - public makerAddress: string; - public makerPrivateKey: Buffer; - public takerAddress: string; - public zrxAssetData: string; - public exchangeWrapper: ExchangeWrapper; - public assetWrapper: AssetWrapper; - public static generateFillOrderCombinations(): FillScenario[] { - const takerScenarios = [ - TakerScenario.Unspecified, - // TakerScenario.CorrectlySpecified, - // TakerScenario.IncorrectlySpecified, - ]; - const feeRecipientScenarios = [ - FeeRecipientAddressScenario.EthUserAddress, - // FeeRecipientAddressScenario.BurnAddress, - ]; - const makerAssetAmountScenario = [ - OrderAssetAmountScenario.Large, - // OrderAssetAmountScenario.Zero, - // OrderAssetAmountScenario.Small, - ]; - const takerAssetAmountScenario = [ - OrderAssetAmountScenario.Large, - // OrderAssetAmountScenario.Zero, - // OrderAssetAmountScenario.Small, - ]; - const makerFeeScenario = [ - OrderAssetAmountScenario.Large, - // OrderAssetAmountScenario.Small, - // OrderAssetAmountScenario.Zero, - ]; - const takerFeeScenario = [ - OrderAssetAmountScenario.Large, - // OrderAssetAmountScenario.Small, - // OrderAssetAmountScenario.Zero, - ]; - const expirationTimeSecondsScenario = [ - ExpirationTimeSecondsScenario.InFuture, - ExpirationTimeSecondsScenario.InPast, - ]; - const makerAssetDataScenario = [ - AssetDataScenario.ERC20FiveDecimals, - AssetDataScenario.ERC20NonZRXEighteenDecimals, - AssetDataScenario.ERC721, - AssetDataScenario.ZRXFeeToken, - ]; - const takerAssetDataScenario = [ - AssetDataScenario.ERC20FiveDecimals, - AssetDataScenario.ERC20NonZRXEighteenDecimals, - AssetDataScenario.ERC721, - AssetDataScenario.ZRXFeeToken, - ]; - const takerAssetFillAmountScenario = [ - TakerAssetFillAmountScenario.ExactlyRemainingFillableTakerAssetAmount, - // TakerAssetFillAmountScenario.GreaterThanRemainingFillableTakerAssetAmount, - // TakerAssetFillAmountScenario.LessThanRemainingFillableTakerAssetAmount, - ]; - const makerAssetBalanceScenario = [ - BalanceAmountScenario.Higher, - // BalanceAmountScenario.Exact, - // BalanceAmountScenario.TooLow, - ]; - const makerAssetAllowanceScenario = [ - AllowanceAmountScenario.Higher, - // AllowanceAmountScenario.Exact, - // AllowanceAmountScenario.TooLow, - // AllowanceAmountScenario.Unlimited, - ]; - const makerZRXBalanceScenario = [ - BalanceAmountScenario.Higher, - // BalanceAmountScenario.Exact, - // BalanceAmountScenario.TooLow, - ]; - const makerZRXAllowanceScenario = [ - AllowanceAmountScenario.Higher, - // AllowanceAmountScenario.Exact, - // AllowanceAmountScenario.TooLow, - // AllowanceAmountScenario.Unlimited, - ]; - const takerAssetBalanceScenario = [ - BalanceAmountScenario.Higher, - // BalanceAmountScenario.Exact, - // BalanceAmountScenario.TooLow, - ]; - const takerAssetAllowanceScenario = [ - AllowanceAmountScenario.Higher, - // AllowanceAmountScenario.Exact, - // AllowanceAmountScenario.TooLow, - // AllowanceAmountScenario.Unlimited, - ]; - const takerZRXBalanceScenario = [ - BalanceAmountScenario.Higher, - // BalanceAmountScenario.Exact, - // BalanceAmountScenario.TooLow, - ]; - const takerZRXAllowanceScenario = [ - AllowanceAmountScenario.Higher, - // AllowanceAmountScenario.Exact, - // AllowanceAmountScenario.TooLow, - // AllowanceAmountScenario.Unlimited, - ]; - const fillScenarioArrays = CoreCombinatorialUtils._getAllCombinations([ - takerScenarios, - feeRecipientScenarios, - makerAssetAmountScenario, - takerAssetAmountScenario, - makerFeeScenario, - takerFeeScenario, - expirationTimeSecondsScenario, - makerAssetDataScenario, - takerAssetDataScenario, - takerAssetFillAmountScenario, - makerAssetBalanceScenario, - makerAssetAllowanceScenario, - makerZRXBalanceScenario, - makerZRXAllowanceScenario, - takerAssetBalanceScenario, - takerAssetAllowanceScenario, - takerZRXBalanceScenario, - takerZRXAllowanceScenario, - ]); - - const fillScenarios = _.map(fillScenarioArrays, fillScenarioArray => { - // tslint:disable:custom-no-magic-numbers - const fillScenario: FillScenario = { - orderScenario: { - takerScenario: fillScenarioArray[0] as TakerScenario, - feeRecipientScenario: fillScenarioArray[1] as FeeRecipientAddressScenario, - makerAssetAmountScenario: fillScenarioArray[2] as OrderAssetAmountScenario, - takerAssetAmountScenario: fillScenarioArray[3] as OrderAssetAmountScenario, - makerFeeScenario: fillScenarioArray[4] as OrderAssetAmountScenario, - takerFeeScenario: fillScenarioArray[5] as OrderAssetAmountScenario, - expirationTimeSecondsScenario: fillScenarioArray[6] as ExpirationTimeSecondsScenario, - makerAssetDataScenario: fillScenarioArray[7] as AssetDataScenario, - takerAssetDataScenario: fillScenarioArray[8] as AssetDataScenario, - }, - takerAssetFillAmountScenario: fillScenarioArray[9] as TakerAssetFillAmountScenario, - makerStateScenario: { - traderAssetBalance: fillScenarioArray[10] as BalanceAmountScenario, - traderAssetAllowance: fillScenarioArray[11] as AllowanceAmountScenario, - zrxFeeBalance: fillScenarioArray[12] as BalanceAmountScenario, - zrxFeeAllowance: fillScenarioArray[13] as AllowanceAmountScenario, - }, - takerStateScenario: { - traderAssetBalance: fillScenarioArray[14] as BalanceAmountScenario, - traderAssetAllowance: fillScenarioArray[15] as AllowanceAmountScenario, - zrxFeeBalance: fillScenarioArray[16] as BalanceAmountScenario, - zrxFeeAllowance: fillScenarioArray[17] as AllowanceAmountScenario, - }, - }; - // tslint:enable:custom-no-magic-numbers - return fillScenario; - }); - - return fillScenarios; - } - /** - * Recursive implementation of generating all combinations of the supplied - * string-containing arrays. - */ - private static _getAllCombinations(arrays: string[][]): string[][] { - // Base case - if (arrays.length === 1) { - const remainingValues = _.map(arrays[0], val => { - return [val]; - }); - return remainingValues; - } else { - const result = []; - const restOfArrays = arrays.slice(1); - const allCombinationsOfRemaining = CoreCombinatorialUtils._getAllCombinations(restOfArrays); // recur with the rest of array - // tslint:disable:prefer-for-of - for (let i = 0; i < allCombinationsOfRemaining.length; i++) { - for (let j = 0; j < arrays[0].length; j++) { - result.push([arrays[0][j], ...allCombinationsOfRemaining[i]]); - } - } - // tslint:enable:prefer-for-of - return result; - } - } - constructor( - orderFactory: OrderFactoryFromScenario, - ownerAddress: string, - makerAddress: string, - makerPrivateKey: Buffer, - takerAddress: string, - zrxAssetData: string, - exchangeWrapper: ExchangeWrapper, - assetWrapper: AssetWrapper, - ) { - this.orderFactory = orderFactory; - this.ownerAddress = ownerAddress; - this.makerAddress = makerAddress; - this.makerPrivateKey = makerPrivateKey; - this.takerAddress = takerAddress; - this.zrxAssetData = zrxAssetData; - this.exchangeWrapper = exchangeWrapper; - this.assetWrapper = assetWrapper; - } - public async testFillOrderScenarioAsync( - provider: Provider, - fillScenario: FillScenario, - isVerbose: boolean = false, - ): Promise { - // 1. Generate order - const order = this.orderFactory.generateOrder(fillScenario.orderScenario); - - // 2. Sign order - const orderHashBuff = orderHashUtils.getOrderHashBuffer(order); - const signature = signingUtils.signMessage(orderHashBuff, this.makerPrivateKey, SignatureType.EthSign); - const signedOrder = { - ...order, - signature: `0x${signature.toString('hex')}`, - }; - - const balanceAndProxyAllowanceFetcher = new SimpleAssetBalanceAndProxyAllowanceFetcher(this.assetWrapper); - const orderFilledCancelledFetcher = new SimpleOrderFilledCancelledFetcher( - this.exchangeWrapper, - this.zrxAssetData, - ); - - // 3. Figure out fill amount - const takerAssetFillAmount = await this._getTakerAssetFillAmountAsync( - signedOrder, - fillScenario.takerAssetFillAmountScenario, - balanceAndProxyAllowanceFetcher, - orderFilledCancelledFetcher, - ); - - // 4. Permutate the maker and taker balance/allowance scenarios - await this._modifyTraderStateAsync( - fillScenario.makerStateScenario, - fillScenario.takerStateScenario, - signedOrder, - takerAssetFillAmount, - ); - - // 5. If I fill it by X, what are the resulting balances/allowances/filled amounts expected? - const orderValidationUtils = new OrderValidationUtils(orderFilledCancelledFetcher); - const lazyStore = new BalanceAndProxyAllowanceLazyStore(balanceAndProxyAllowanceFetcher); - const exchangeTransferSimulator = new ExchangeTransferSimulator(lazyStore); - - let fillRevertReasonIfExists; - try { - await orderValidationUtils.validateFillOrderThrowIfInvalidAsync( - exchangeTransferSimulator, - provider, - signedOrder, - takerAssetFillAmount, - this.takerAddress, - this.zrxAssetData, - ); - if (isVerbose) { - logUtils.log(`Expecting fillOrder to succeed.`); - } - } catch (err) { - fillRevertReasonIfExists = err.message; - if (isVerbose) { - logUtils.log(`Expecting fillOrder to fail with:`); - logUtils.log(err); - } - } - - // 6. Fill the order - await this._fillOrderAndAssertOutcomeAsync( - signedOrder, - takerAssetFillAmount, - lazyStore, - fillRevertReasonIfExists, - ); - } - private async _fillOrderAndAssertOutcomeAsync( - signedOrder: SignedOrder, - takerAssetFillAmount: BigNumber, - lazyStore: BalanceAndProxyAllowanceLazyStore, - fillRevertReasonIfExists: RevertReason | undefined, - ): Promise { - if (!_.isUndefined(fillRevertReasonIfExists)) { - return expectTransactionFailedAsync( - this.exchangeWrapper.fillOrderAsync(signedOrder, this.takerAddress, { takerAssetFillAmount }), - fillRevertReasonIfExists, - ); - } - - const makerAddress = signedOrder.makerAddress; - const makerAssetData = signedOrder.makerAssetData; - const takerAssetData = signedOrder.takerAssetData; - const feeRecipient = signedOrder.feeRecipientAddress; - - const expMakerAssetBalanceOfMaker = await lazyStore.getBalanceAsync(makerAssetData, makerAddress); - const expMakerAssetAllowanceOfMaker = await lazyStore.getProxyAllowanceAsync(makerAssetData, makerAddress); - const expTakerAssetBalanceOfMaker = await lazyStore.getBalanceAsync(takerAssetData, makerAddress); - const expZRXAssetBalanceOfMaker = await lazyStore.getBalanceAsync(this.zrxAssetData, makerAddress); - const expZRXAssetAllowanceOfMaker = await lazyStore.getProxyAllowanceAsync(this.zrxAssetData, makerAddress); - const expTakerAssetBalanceOfTaker = await lazyStore.getBalanceAsync(takerAssetData, this.takerAddress); - const expTakerAssetAllowanceOfTaker = await lazyStore.getProxyAllowanceAsync(takerAssetData, this.takerAddress); - const expMakerAssetBalanceOfTaker = await lazyStore.getBalanceAsync(makerAssetData, this.takerAddress); - const expZRXAssetBalanceOfTaker = await lazyStore.getBalanceAsync(this.zrxAssetData, this.takerAddress); - const expZRXAssetAllowanceOfTaker = await lazyStore.getProxyAllowanceAsync( - this.zrxAssetData, - this.takerAddress, - ); - const expZRXAssetBalanceOfFeeRecipient = await lazyStore.getBalanceAsync(this.zrxAssetData, feeRecipient); - - const orderHash = orderHashUtils.getOrderHashHex(signedOrder); - const alreadyFilledTakerAmount = await this.exchangeWrapper.getTakerAssetFilledAmountAsync(orderHash); - const remainingTakerAmountToFill = signedOrder.takerAssetAmount.minus(alreadyFilledTakerAmount); - const expFilledTakerAmount = takerAssetFillAmount.gt(remainingTakerAmountToFill) - ? remainingTakerAmountToFill - : alreadyFilledTakerAmount.add(takerAssetFillAmount); - - const expFilledMakerAmount = orderUtils.getPartialAmount( - expFilledTakerAmount, - signedOrder.takerAssetAmount, - signedOrder.makerAssetAmount, - ); - - // - Let's fill the order! - const txReceipt = await this.exchangeWrapper.fillOrderAsync(signedOrder, this.takerAddress, { - takerAssetFillAmount, - }); - - const actFilledTakerAmount = await this.exchangeWrapper.getTakerAssetFilledAmountAsync(orderHash); - expect(actFilledTakerAmount).to.be.bignumber.equal(expFilledTakerAmount, 'filledTakerAmount'); - - expect(txReceipt.logs.length).to.be.equal(1, 'logs length'); - // tslint:disable-next-line:no-unnecessary-type-assertion - const log = txReceipt.logs[0] as LogWithDecodedArgs; - expect(log.args.makerAddress).to.be.equal(makerAddress, 'log.args.makerAddress'); - expect(log.args.takerAddress).to.be.equal(this.takerAddress, 'log.args.this.takerAddress'); - expect(log.args.feeRecipientAddress).to.be.equal(feeRecipient, 'log.args.feeRecipientAddress'); - expect(log.args.makerAssetFilledAmount).to.be.bignumber.equal( - expFilledMakerAmount, - 'log.args.makerAssetFilledAmount', - ); - expect(log.args.takerAssetFilledAmount).to.be.bignumber.equal( - expFilledTakerAmount, - 'log.args.takerAssetFilledAmount', - ); - const expMakerFeePaid = orderUtils.getPartialAmount( - expFilledTakerAmount, - signedOrder.takerAssetAmount, - signedOrder.makerFee, - ); - expect(log.args.makerFeePaid).to.be.bignumber.equal(expMakerFeePaid, 'log.args.makerFeePaid'); - const expTakerFeePaid = orderUtils.getPartialAmount( - expFilledTakerAmount, - signedOrder.takerAssetAmount, - signedOrder.takerFee, - ); - expect(log.args.takerFeePaid).to.be.bignumber.equal(expTakerFeePaid, 'logs.args.takerFeePaid'); - expect(log.args.orderHash).to.be.equal(orderHash, 'log.args.orderHash'); - expect(log.args.makerAssetData).to.be.equal(makerAssetData, 'log.args.makerAssetData'); - expect(log.args.takerAssetData).to.be.equal(takerAssetData, 'log.args.takerAssetData'); - - const actMakerAssetBalanceOfMaker = await this.assetWrapper.getBalanceAsync(makerAddress, makerAssetData); - expect(actMakerAssetBalanceOfMaker).to.be.bignumber.equal( - expMakerAssetBalanceOfMaker, - 'makerAssetBalanceOfMaker', - ); - - const actMakerAssetAllowanceOfMaker = await this.assetWrapper.getProxyAllowanceAsync( - makerAddress, - makerAssetData, - ); - expect(actMakerAssetAllowanceOfMaker).to.be.bignumber.equal( - expMakerAssetAllowanceOfMaker, - 'makerAssetAllowanceOfMaker', - ); - - const actTakerAssetBalanceOfMaker = await this.assetWrapper.getBalanceAsync(makerAddress, takerAssetData); - expect(actTakerAssetBalanceOfMaker).to.be.bignumber.equal( - expTakerAssetBalanceOfMaker, - 'takerAssetBalanceOfMaker', - ); - - const actZRXAssetBalanceOfMaker = await this.assetWrapper.getBalanceAsync(makerAddress, this.zrxAssetData); - expect(actZRXAssetBalanceOfMaker).to.be.bignumber.equal(expZRXAssetBalanceOfMaker, 'ZRXAssetBalanceOfMaker'); - - const actZRXAssetAllowanceOfMaker = await this.assetWrapper.getProxyAllowanceAsync( - makerAddress, - this.zrxAssetData, - ); - expect(actZRXAssetAllowanceOfMaker).to.be.bignumber.equal( - expZRXAssetAllowanceOfMaker, - 'ZRXAssetAllowanceOfMaker', - ); - - const actTakerAssetBalanceOfTaker = await this.assetWrapper.getBalanceAsync(this.takerAddress, takerAssetData); - expect(actTakerAssetBalanceOfTaker).to.be.bignumber.equal( - expTakerAssetBalanceOfTaker, - 'TakerAssetBalanceOfTaker', - ); - - const actTakerAssetAllowanceOfTaker = await this.assetWrapper.getProxyAllowanceAsync( - this.takerAddress, - takerAssetData, - ); - - expect(actTakerAssetAllowanceOfTaker).to.be.bignumber.equal( - expTakerAssetAllowanceOfTaker, - 'TakerAssetAllowanceOfTaker', - ); - - const actMakerAssetBalanceOfTaker = await this.assetWrapper.getBalanceAsync(this.takerAddress, makerAssetData); - expect(actMakerAssetBalanceOfTaker).to.be.bignumber.equal( - expMakerAssetBalanceOfTaker, - 'MakerAssetBalanceOfTaker', - ); - - const actZRXAssetBalanceOfTaker = await this.assetWrapper.getBalanceAsync(this.takerAddress, this.zrxAssetData); - expect(actZRXAssetBalanceOfTaker).to.be.bignumber.equal(expZRXAssetBalanceOfTaker, 'ZRXAssetBalanceOfTaker'); - - const actZRXAssetAllowanceOfTaker = await this.assetWrapper.getProxyAllowanceAsync( - this.takerAddress, - this.zrxAssetData, - ); - expect(actZRXAssetAllowanceOfTaker).to.be.bignumber.equal( - expZRXAssetAllowanceOfTaker, - 'ZRXAssetAllowanceOfTaker', - ); - - const actZRXAssetBalanceOfFeeRecipient = await this.assetWrapper.getBalanceAsync( - feeRecipient, - this.zrxAssetData, - ); - expect(actZRXAssetBalanceOfFeeRecipient).to.be.bignumber.equal( - expZRXAssetBalanceOfFeeRecipient, - 'ZRXAssetBalanceOfFeeRecipient', - ); - } - private async _getTakerAssetFillAmountAsync( - signedOrder: SignedOrder, - takerAssetFillAmountScenario: TakerAssetFillAmountScenario, - balanceAndProxyAllowanceFetcher: SimpleAssetBalanceAndProxyAllowanceFetcher, - orderFilledCancelledFetcher: SimpleOrderFilledCancelledFetcher, - ): Promise { - const orderStateUtils = new OrderStateUtils(balanceAndProxyAllowanceFetcher, orderFilledCancelledFetcher); - const fillableTakerAssetAmount = await orderStateUtils.getMaxFillableTakerAssetAmountAsync( - signedOrder, - this.takerAddress, - ); - - let takerAssetFillAmount; - switch (takerAssetFillAmountScenario) { - case TakerAssetFillAmountScenario.Zero: - takerAssetFillAmount = new BigNumber(0); - break; - - case TakerAssetFillAmountScenario.ExactlyRemainingFillableTakerAssetAmount: - takerAssetFillAmount = fillableTakerAssetAmount; - break; - - case TakerAssetFillAmountScenario.GreaterThanRemainingFillableTakerAssetAmount: - takerAssetFillAmount = fillableTakerAssetAmount.add(1); - break; - - case TakerAssetFillAmountScenario.LessThanRemainingFillableTakerAssetAmount: - const takerAssetProxyId = assetDataUtils.decodeAssetProxyId(signedOrder.takerAssetData); - const makerAssetProxyId = assetDataUtils.decodeAssetProxyId(signedOrder.makerAssetData); - const isEitherAssetERC721 = - takerAssetProxyId === AssetProxyId.ERC721 || makerAssetProxyId === AssetProxyId.ERC721; - if (isEitherAssetERC721) { - throw new Error( - 'Cannot test `TakerAssetFillAmountScenario.LessThanRemainingFillableTakerAssetAmount` together with ERC721 assets since orders involving ERC721 must always be filled exactly.', - ); - } - takerAssetFillAmount = fillableTakerAssetAmount.div(2).floor(); - break; - - default: - throw errorUtils.spawnSwitchErr('TakerAssetFillAmountScenario', takerAssetFillAmountScenario); - } - - return takerAssetFillAmount; - } - private async _modifyTraderStateAsync( - makerStateScenario: TraderStateScenario, - takerStateScenario: TraderStateScenario, - signedOrder: SignedOrder, - takerAssetFillAmount: BigNumber, - ): Promise { - const makerAssetFillAmount = orderUtils.getPartialAmount( - takerAssetFillAmount, - signedOrder.takerAssetAmount, - signedOrder.makerAssetAmount, - ); - switch (makerStateScenario.traderAssetBalance) { - case BalanceAmountScenario.Higher: - break; // Noop since this is already the default - - case BalanceAmountScenario.TooLow: - if (makerAssetFillAmount.eq(0)) { - throw new Error(`Cannot set makerAssetBalanceOfMaker TooLow if makerAssetFillAmount is 0`); - } - const tooLowBalance = makerAssetFillAmount.minus(1); - await this.assetWrapper.setBalanceAsync( - signedOrder.makerAddress, - signedOrder.makerAssetData, - tooLowBalance, - ); - break; - - case BalanceAmountScenario.Exact: - const exactBalance = makerAssetFillAmount; - await this.assetWrapper.setBalanceAsync( - signedOrder.makerAddress, - signedOrder.makerAssetData, - exactBalance, - ); - break; - - default: - throw errorUtils.spawnSwitchErr( - 'makerStateScenario.traderAssetBalance', - makerStateScenario.traderAssetBalance, - ); - } - - const makerFee = orderUtils.getPartialAmount( - takerAssetFillAmount, - signedOrder.takerAssetAmount, - signedOrder.makerFee, - ); - switch (makerStateScenario.zrxFeeBalance) { - case BalanceAmountScenario.Higher: - break; // Noop since this is already the default - - case BalanceAmountScenario.TooLow: - if (makerFee.eq(0)) { - throw new Error(`Cannot set zrxAsserBalanceOfMaker TooLow if makerFee is 0`); - } - const tooLowBalance = makerFee.minus(1); - await this.assetWrapper.setBalanceAsync(signedOrder.makerAddress, this.zrxAssetData, tooLowBalance); - break; - - case BalanceAmountScenario.Exact: - const exactBalance = makerFee; - await this.assetWrapper.setBalanceAsync(signedOrder.makerAddress, this.zrxAssetData, exactBalance); - break; - - default: - throw errorUtils.spawnSwitchErr('makerStateScenario.zrxFeeBalance', makerStateScenario.zrxFeeBalance); - } - - switch (makerStateScenario.traderAssetAllowance) { - case AllowanceAmountScenario.Higher: - break; // Noop since this is already the default - - case AllowanceAmountScenario.TooLow: - const tooLowAllowance = makerAssetFillAmount.minus(1); - await this.assetWrapper.setProxyAllowanceAsync( - signedOrder.makerAddress, - signedOrder.makerAssetData, - tooLowAllowance, - ); - break; - - case AllowanceAmountScenario.Exact: - const exactAllowance = makerAssetFillAmount; - await this.assetWrapper.setProxyAllowanceAsync( - signedOrder.makerAddress, - signedOrder.makerAssetData, - exactAllowance, - ); - break; - - case AllowanceAmountScenario.Unlimited: - await this.assetWrapper.setProxyAllowanceAsync( - signedOrder.makerAddress, - signedOrder.makerAssetData, - constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS, - ); - break; - - default: - throw errorUtils.spawnSwitchErr( - 'makerStateScenario.traderAssetAllowance', - makerStateScenario.traderAssetAllowance, - ); - } - - switch (makerStateScenario.zrxFeeAllowance) { - case AllowanceAmountScenario.Higher: - break; // Noop since this is already the default - - case AllowanceAmountScenario.TooLow: - const tooLowAllowance = makerFee.minus(1); - await this.assetWrapper.setProxyAllowanceAsync( - signedOrder.makerAddress, - this.zrxAssetData, - tooLowAllowance, - ); - break; - - case AllowanceAmountScenario.Exact: - const exactAllowance = makerFee; - await this.assetWrapper.setProxyAllowanceAsync( - signedOrder.makerAddress, - this.zrxAssetData, - exactAllowance, - ); - break; - - case AllowanceAmountScenario.Unlimited: - await this.assetWrapper.setProxyAllowanceAsync( - signedOrder.makerAddress, - this.zrxAssetData, - constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS, - ); - break; - - default: - throw errorUtils.spawnSwitchErr( - 'makerStateScenario.zrxFeeAllowance', - makerStateScenario.zrxFeeAllowance, - ); - } - - switch (takerStateScenario.traderAssetBalance) { - case BalanceAmountScenario.Higher: - break; // Noop since this is already the default - - case BalanceAmountScenario.TooLow: - if (takerAssetFillAmount.eq(0)) { - throw new Error(`Cannot set takerAssetBalanceOfTaker TooLow if takerAssetFillAmount is 0`); - } - const tooLowBalance = takerAssetFillAmount.minus(1); - await this.assetWrapper.setBalanceAsync(this.takerAddress, signedOrder.takerAssetData, tooLowBalance); - break; - - case BalanceAmountScenario.Exact: - const exactBalance = takerAssetFillAmount; - await this.assetWrapper.setBalanceAsync(this.takerAddress, signedOrder.takerAssetData, exactBalance); - break; - - default: - throw errorUtils.spawnSwitchErr( - 'takerStateScenario.traderAssetBalance', - takerStateScenario.traderAssetBalance, - ); - } - - const takerFee = orderUtils.getPartialAmount( - takerAssetFillAmount, - signedOrder.takerAssetAmount, - signedOrder.takerFee, - ); - switch (takerStateScenario.zrxFeeBalance) { - case BalanceAmountScenario.Higher: - break; // Noop since this is already the default - - case BalanceAmountScenario.TooLow: - if (takerFee.eq(0)) { - throw new Error(`Cannot set zrxAssetBalanceOfTaker TooLow if takerFee is 0`); - } - const tooLowBalance = takerFee.minus(1); - await this.assetWrapper.setBalanceAsync(this.takerAddress, this.zrxAssetData, tooLowBalance); - break; - - case BalanceAmountScenario.Exact: - const exactBalance = takerFee; - await this.assetWrapper.setBalanceAsync(this.takerAddress, this.zrxAssetData, exactBalance); - break; - - default: - throw errorUtils.spawnSwitchErr('takerStateScenario.zrxFeeBalance', takerStateScenario.zrxFeeBalance); - } - - switch (takerStateScenario.traderAssetAllowance) { - case AllowanceAmountScenario.Higher: - break; // Noop since this is already the default - - case AllowanceAmountScenario.TooLow: - const tooLowAllowance = takerAssetFillAmount.minus(1); - await this.assetWrapper.setProxyAllowanceAsync( - this.takerAddress, - signedOrder.takerAssetData, - tooLowAllowance, - ); - break; - - case AllowanceAmountScenario.Exact: - const exactAllowance = takerAssetFillAmount; - await this.assetWrapper.setProxyAllowanceAsync( - this.takerAddress, - signedOrder.takerAssetData, - exactAllowance, - ); - break; - - case AllowanceAmountScenario.Unlimited: - await this.assetWrapper.setProxyAllowanceAsync( - this.takerAddress, - signedOrder.takerAssetData, - constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS, - ); - break; - - default: - throw errorUtils.spawnSwitchErr( - 'takerStateScenario.traderAssetAllowance', - takerStateScenario.traderAssetAllowance, - ); - } - - switch (takerStateScenario.zrxFeeAllowance) { - case AllowanceAmountScenario.Higher: - break; // Noop since this is already the default - - case AllowanceAmountScenario.TooLow: - const tooLowAllowance = takerFee.minus(1); - await this.assetWrapper.setProxyAllowanceAsync(this.takerAddress, this.zrxAssetData, tooLowAllowance); - break; - - case AllowanceAmountScenario.Exact: - const exactAllowance = takerFee; - await this.assetWrapper.setProxyAllowanceAsync(this.takerAddress, this.zrxAssetData, exactAllowance); - break; - - case AllowanceAmountScenario.Unlimited: - await this.assetWrapper.setProxyAllowanceAsync( - this.takerAddress, - this.zrxAssetData, - constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS, - ); - break; - - default: - throw errorUtils.spawnSwitchErr( - 'takerStateScenario.zrxFeeAllowance', - takerStateScenario.zrxFeeAllowance, - ); - } - } -} // tslint:disable:max-file-line-count diff --git a/packages/contracts/test/utils/fill_order_combinatorial_utils.ts b/packages/contracts/test/utils/fill_order_combinatorial_utils.ts new file mode 100644 index 000000000..bdd1e62a2 --- /dev/null +++ b/packages/contracts/test/utils/fill_order_combinatorial_utils.ts @@ -0,0 +1,878 @@ +import { + assetDataUtils, + BalanceAndProxyAllowanceLazyStore, + ExchangeTransferSimulator, + orderHashUtils, + OrderStateUtils, + OrderValidationUtils, +} from '@0xproject/order-utils'; +import { AssetProxyId, RevertReason, SignatureType, SignedOrder } from '@0xproject/types'; +import { BigNumber, errorUtils, logUtils } from '@0xproject/utils'; +import { Web3Wrapper } from '@0xproject/web3-wrapper'; +import * as chai from 'chai'; +import { LogWithDecodedArgs, Provider, TxData } from 'ethereum-types'; +import * as _ from 'lodash'; +import 'make-promises-safe'; + +import { ExchangeContract, ExchangeFillEventArgs } from '../../generated_contract_wrappers/exchange'; + +import { artifacts } from './artifacts'; +import { expectTransactionFailedAsync } from './assertions'; +import { AssetWrapper } from './asset_wrapper'; +import { chaiSetup } from './chai_setup'; +import { constants } from './constants'; +import { ERC20Wrapper } from './erc20_wrapper'; +import { ERC721Wrapper } from './erc721_wrapper'; +import { ExchangeWrapper } from './exchange_wrapper'; +import { OrderFactoryFromScenario } from './order_factory_from_scenario'; +import { orderUtils } from './order_utils'; +import { signingUtils } from './signing_utils'; +import { SimpleAssetBalanceAndProxyAllowanceFetcher } from './simple_asset_balance_and_proxy_allowance_fetcher'; +import { SimpleOrderFilledCancelledFetcher } from './simple_order_filled_cancelled_fetcher'; +import { + AllowanceAmountScenario, + AssetDataScenario, + BalanceAmountScenario, + ExpirationTimeSecondsScenario, + FeeRecipientAddressScenario, + FillScenario, + OrderAssetAmountScenario, + TakerAssetFillAmountScenario, + TakerScenario, + TraderStateScenario, +} from './types'; + +chaiSetup.configure(); +const expect = chai.expect; + +/** + * Instantiates a new instance of FillOrderCombinatorialUtils. Since this method has some + * required async setup, a factory method is required. + * @param web3Wrapper Web3Wrapper instance + * @param txDefaults Default Ethereum tx options + * @return FillOrderCombinatorialUtils instance + */ +export async function fillOrderCombinatorialUtilsFactoryAsync( + web3Wrapper: Web3Wrapper, + txDefaults: Partial, +): Promise { + const accounts = await web3Wrapper.getAvailableAddressesAsync(); + const userAddresses = _.slice(accounts, 0, 5); + const [ownerAddress, makerAddress, takerAddress] = userAddresses; + const makerPrivateKey = constants.TESTRPC_PRIVATE_KEYS[userAddresses.indexOf(makerAddress)]; + + const provider = web3Wrapper.getProvider(); + const erc20Wrapper = new ERC20Wrapper(provider, userAddresses, ownerAddress); + const erc721Wrapper = new ERC721Wrapper(provider, userAddresses, ownerAddress); + + const erc20EighteenDecimalTokenCount = 3; + const eighteenDecimals = new BigNumber(18); + const [ + erc20EighteenDecimalTokenA, + erc20EighteenDecimalTokenB, + zrxToken, + ] = await erc20Wrapper.deployDummyTokensAsync(erc20EighteenDecimalTokenCount, eighteenDecimals); + const zrxAssetData = assetDataUtils.encodeERC20AssetData(zrxToken.address); + + const erc20FiveDecimalTokenCount = 2; + const fiveDecimals = new BigNumber(5); + const [erc20FiveDecimalTokenA, erc20FiveDecimalTokenB] = await erc20Wrapper.deployDummyTokensAsync( + erc20FiveDecimalTokenCount, + fiveDecimals, + ); + const erc20Proxy = await erc20Wrapper.deployProxyAsync(); + await erc20Wrapper.setBalancesAndAllowancesAsync(); + + const [erc721Token] = await erc721Wrapper.deployDummyTokensAsync(); + const erc721Proxy = await erc721Wrapper.deployProxyAsync(); + await erc721Wrapper.setBalancesAndAllowancesAsync(); + const erc721Balances = await erc721Wrapper.getBalancesAsync(); + + const assetWrapper = new AssetWrapper([erc20Wrapper, erc721Wrapper]); + + const exchangeContract = await ExchangeContract.deployFrom0xArtifactAsync( + artifacts.Exchange, + provider, + txDefaults, + zrxAssetData, + ); + const exchangeWrapper = new ExchangeWrapper(exchangeContract, provider); + await exchangeWrapper.registerAssetProxyAsync(erc20Proxy.address, ownerAddress); + await exchangeWrapper.registerAssetProxyAsync(erc721Proxy.address, ownerAddress); + + await web3Wrapper.awaitTransactionSuccessAsync( + await erc20Proxy.addAuthorizedAddress.sendTransactionAsync(exchangeContract.address, { + from: ownerAddress, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await erc721Proxy.addAuthorizedAddress.sendTransactionAsync(exchangeContract.address, { + from: ownerAddress, + }), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + + const orderFactory = new OrderFactoryFromScenario( + userAddresses, + zrxToken.address, + [erc20EighteenDecimalTokenA.address, erc20EighteenDecimalTokenB.address], + [erc20FiveDecimalTokenA.address, erc20FiveDecimalTokenB.address], + erc721Token, + erc721Balances, + exchangeContract.address, + ); + + const fillOrderCombinatorialUtils = new FillOrderCombinatorialUtils( + orderFactory, + ownerAddress, + makerAddress, + makerPrivateKey, + takerAddress, + zrxAssetData, + exchangeWrapper, + assetWrapper, + ); + return fillOrderCombinatorialUtils; +} + +export class FillOrderCombinatorialUtils { + public orderFactory: OrderFactoryFromScenario; + public ownerAddress: string; + public makerAddress: string; + public makerPrivateKey: Buffer; + public takerAddress: string; + public zrxAssetData: string; + public exchangeWrapper: ExchangeWrapper; + public assetWrapper: AssetWrapper; + public static generateFillOrderCombinations(): FillScenario[] { + const takerScenarios = [ + TakerScenario.Unspecified, + // TakerScenario.CorrectlySpecified, + // TakerScenario.IncorrectlySpecified, + ]; + const feeRecipientScenarios = [ + FeeRecipientAddressScenario.EthUserAddress, + // FeeRecipientAddressScenario.BurnAddress, + ]; + const makerAssetAmountScenario = [ + OrderAssetAmountScenario.Large, + // OrderAssetAmountScenario.Zero, + // OrderAssetAmountScenario.Small, + ]; + const takerAssetAmountScenario = [ + OrderAssetAmountScenario.Large, + // OrderAssetAmountScenario.Zero, + // OrderAssetAmountScenario.Small, + ]; + const makerFeeScenario = [ + OrderAssetAmountScenario.Large, + // OrderAssetAmountScenario.Small, + // OrderAssetAmountScenario.Zero, + ]; + const takerFeeScenario = [ + OrderAssetAmountScenario.Large, + // OrderAssetAmountScenario.Small, + // OrderAssetAmountScenario.Zero, + ]; + const expirationTimeSecondsScenario = [ + ExpirationTimeSecondsScenario.InFuture, + ExpirationTimeSecondsScenario.InPast, + ]; + const makerAssetDataScenario = [ + AssetDataScenario.ERC20FiveDecimals, + AssetDataScenario.ERC20NonZRXEighteenDecimals, + AssetDataScenario.ERC721, + AssetDataScenario.ZRXFeeToken, + ]; + const takerAssetDataScenario = [ + AssetDataScenario.ERC20FiveDecimals, + AssetDataScenario.ERC20NonZRXEighteenDecimals, + AssetDataScenario.ERC721, + AssetDataScenario.ZRXFeeToken, + ]; + const takerAssetFillAmountScenario = [ + TakerAssetFillAmountScenario.ExactlyRemainingFillableTakerAssetAmount, + // TakerAssetFillAmountScenario.GreaterThanRemainingFillableTakerAssetAmount, + // TakerAssetFillAmountScenario.LessThanRemainingFillableTakerAssetAmount, + ]; + const makerAssetBalanceScenario = [ + BalanceAmountScenario.Higher, + // BalanceAmountScenario.Exact, + // BalanceAmountScenario.TooLow, + ]; + const makerAssetAllowanceScenario = [ + AllowanceAmountScenario.Higher, + // AllowanceAmountScenario.Exact, + // AllowanceAmountScenario.TooLow, + // AllowanceAmountScenario.Unlimited, + ]; + const makerZRXBalanceScenario = [ + BalanceAmountScenario.Higher, + // BalanceAmountScenario.Exact, + // BalanceAmountScenario.TooLow, + ]; + const makerZRXAllowanceScenario = [ + AllowanceAmountScenario.Higher, + // AllowanceAmountScenario.Exact, + // AllowanceAmountScenario.TooLow, + // AllowanceAmountScenario.Unlimited, + ]; + const takerAssetBalanceScenario = [ + BalanceAmountScenario.Higher, + // BalanceAmountScenario.Exact, + // BalanceAmountScenario.TooLow, + ]; + const takerAssetAllowanceScenario = [ + AllowanceAmountScenario.Higher, + // AllowanceAmountScenario.Exact, + // AllowanceAmountScenario.TooLow, + // AllowanceAmountScenario.Unlimited, + ]; + const takerZRXBalanceScenario = [ + BalanceAmountScenario.Higher, + // BalanceAmountScenario.Exact, + // BalanceAmountScenario.TooLow, + ]; + const takerZRXAllowanceScenario = [ + AllowanceAmountScenario.Higher, + // AllowanceAmountScenario.Exact, + // AllowanceAmountScenario.TooLow, + // AllowanceAmountScenario.Unlimited, + ]; + const fillScenarioArrays = FillOrderCombinatorialUtils._getAllCombinations([ + takerScenarios, + feeRecipientScenarios, + makerAssetAmountScenario, + takerAssetAmountScenario, + makerFeeScenario, + takerFeeScenario, + expirationTimeSecondsScenario, + makerAssetDataScenario, + takerAssetDataScenario, + takerAssetFillAmountScenario, + makerAssetBalanceScenario, + makerAssetAllowanceScenario, + makerZRXBalanceScenario, + makerZRXAllowanceScenario, + takerAssetBalanceScenario, + takerAssetAllowanceScenario, + takerZRXBalanceScenario, + takerZRXAllowanceScenario, + ]); + + const fillScenarios = _.map(fillScenarioArrays, fillScenarioArray => { + // tslint:disable:custom-no-magic-numbers + const fillScenario: FillScenario = { + orderScenario: { + takerScenario: fillScenarioArray[0] as TakerScenario, + feeRecipientScenario: fillScenarioArray[1] as FeeRecipientAddressScenario, + makerAssetAmountScenario: fillScenarioArray[2] as OrderAssetAmountScenario, + takerAssetAmountScenario: fillScenarioArray[3] as OrderAssetAmountScenario, + makerFeeScenario: fillScenarioArray[4] as OrderAssetAmountScenario, + takerFeeScenario: fillScenarioArray[5] as OrderAssetAmountScenario, + expirationTimeSecondsScenario: fillScenarioArray[6] as ExpirationTimeSecondsScenario, + makerAssetDataScenario: fillScenarioArray[7] as AssetDataScenario, + takerAssetDataScenario: fillScenarioArray[8] as AssetDataScenario, + }, + takerAssetFillAmountScenario: fillScenarioArray[9] as TakerAssetFillAmountScenario, + makerStateScenario: { + traderAssetBalance: fillScenarioArray[10] as BalanceAmountScenario, + traderAssetAllowance: fillScenarioArray[11] as AllowanceAmountScenario, + zrxFeeBalance: fillScenarioArray[12] as BalanceAmountScenario, + zrxFeeAllowance: fillScenarioArray[13] as AllowanceAmountScenario, + }, + takerStateScenario: { + traderAssetBalance: fillScenarioArray[14] as BalanceAmountScenario, + traderAssetAllowance: fillScenarioArray[15] as AllowanceAmountScenario, + zrxFeeBalance: fillScenarioArray[16] as BalanceAmountScenario, + zrxFeeAllowance: fillScenarioArray[17] as AllowanceAmountScenario, + }, + }; + // tslint:enable:custom-no-magic-numbers + return fillScenario; + }); + + return fillScenarios; + } + /** + * Recursive implementation of generating all combinations of the supplied + * string-containing arrays. + */ + private static _getAllCombinations(arrays: string[][]): string[][] { + // Base case + if (arrays.length === 1) { + const remainingValues = _.map(arrays[0], val => { + return [val]; + }); + return remainingValues; + } else { + const result = []; + const restOfArrays = arrays.slice(1); + const allCombinationsOfRemaining = FillOrderCombinatorialUtils._getAllCombinations(restOfArrays); // recur with the rest of array + // tslint:disable:prefer-for-of + for (let i = 0; i < allCombinationsOfRemaining.length; i++) { + for (let j = 0; j < arrays[0].length; j++) { + result.push([arrays[0][j], ...allCombinationsOfRemaining[i]]); + } + } + // tslint:enable:prefer-for-of + return result; + } + } + constructor( + orderFactory: OrderFactoryFromScenario, + ownerAddress: string, + makerAddress: string, + makerPrivateKey: Buffer, + takerAddress: string, + zrxAssetData: string, + exchangeWrapper: ExchangeWrapper, + assetWrapper: AssetWrapper, + ) { + this.orderFactory = orderFactory; + this.ownerAddress = ownerAddress; + this.makerAddress = makerAddress; + this.makerPrivateKey = makerPrivateKey; + this.takerAddress = takerAddress; + this.zrxAssetData = zrxAssetData; + this.exchangeWrapper = exchangeWrapper; + this.assetWrapper = assetWrapper; + } + public async testFillOrderScenarioAsync( + provider: Provider, + fillScenario: FillScenario, + isVerbose: boolean = false, + ): Promise { + // 1. Generate order + const order = this.orderFactory.generateOrder(fillScenario.orderScenario); + + // 2. Sign order + const orderHashBuff = orderHashUtils.getOrderHashBuffer(order); + const signature = signingUtils.signMessage(orderHashBuff, this.makerPrivateKey, SignatureType.EthSign); + const signedOrder = { + ...order, + signature: `0x${signature.toString('hex')}`, + }; + + const balanceAndProxyAllowanceFetcher = new SimpleAssetBalanceAndProxyAllowanceFetcher(this.assetWrapper); + const orderFilledCancelledFetcher = new SimpleOrderFilledCancelledFetcher( + this.exchangeWrapper, + this.zrxAssetData, + ); + + // 3. Figure out fill amount + const takerAssetFillAmount = await this._getTakerAssetFillAmountAsync( + signedOrder, + fillScenario.takerAssetFillAmountScenario, + balanceAndProxyAllowanceFetcher, + orderFilledCancelledFetcher, + ); + + // 4. Permutate the maker and taker balance/allowance scenarios + await this._modifyTraderStateAsync( + fillScenario.makerStateScenario, + fillScenario.takerStateScenario, + signedOrder, + takerAssetFillAmount, + ); + + // 5. If I fill it by X, what are the resulting balances/allowances/filled amounts expected? + const orderValidationUtils = new OrderValidationUtils(orderFilledCancelledFetcher); + const lazyStore = new BalanceAndProxyAllowanceLazyStore(balanceAndProxyAllowanceFetcher); + const exchangeTransferSimulator = new ExchangeTransferSimulator(lazyStore); + + let fillRevertReasonIfExists; + try { + await orderValidationUtils.validateFillOrderThrowIfInvalidAsync( + exchangeTransferSimulator, + provider, + signedOrder, + takerAssetFillAmount, + this.takerAddress, + this.zrxAssetData, + ); + if (isVerbose) { + logUtils.log(`Expecting fillOrder to succeed.`); + } + } catch (err) { + fillRevertReasonIfExists = err.message; + if (isVerbose) { + logUtils.log(`Expecting fillOrder to fail with:`); + logUtils.log(err); + } + } + + // 6. Fill the order + await this._fillOrderAndAssertOutcomeAsync( + signedOrder, + takerAssetFillAmount, + lazyStore, + fillRevertReasonIfExists, + ); + } + private async _fillOrderAndAssertOutcomeAsync( + signedOrder: SignedOrder, + takerAssetFillAmount: BigNumber, + lazyStore: BalanceAndProxyAllowanceLazyStore, + fillRevertReasonIfExists: RevertReason | undefined, + ): Promise { + if (!_.isUndefined(fillRevertReasonIfExists)) { + return expectTransactionFailedAsync( + this.exchangeWrapper.fillOrderAsync(signedOrder, this.takerAddress, { takerAssetFillAmount }), + fillRevertReasonIfExists, + ); + } + + const makerAddress = signedOrder.makerAddress; + const makerAssetData = signedOrder.makerAssetData; + const takerAssetData = signedOrder.takerAssetData; + const feeRecipient = signedOrder.feeRecipientAddress; + + const expMakerAssetBalanceOfMaker = await lazyStore.getBalanceAsync(makerAssetData, makerAddress); + const expMakerAssetAllowanceOfMaker = await lazyStore.getProxyAllowanceAsync(makerAssetData, makerAddress); + const expTakerAssetBalanceOfMaker = await lazyStore.getBalanceAsync(takerAssetData, makerAddress); + const expZRXAssetBalanceOfMaker = await lazyStore.getBalanceAsync(this.zrxAssetData, makerAddress); + const expZRXAssetAllowanceOfMaker = await lazyStore.getProxyAllowanceAsync(this.zrxAssetData, makerAddress); + const expTakerAssetBalanceOfTaker = await lazyStore.getBalanceAsync(takerAssetData, this.takerAddress); + const expTakerAssetAllowanceOfTaker = await lazyStore.getProxyAllowanceAsync(takerAssetData, this.takerAddress); + const expMakerAssetBalanceOfTaker = await lazyStore.getBalanceAsync(makerAssetData, this.takerAddress); + const expZRXAssetBalanceOfTaker = await lazyStore.getBalanceAsync(this.zrxAssetData, this.takerAddress); + const expZRXAssetAllowanceOfTaker = await lazyStore.getProxyAllowanceAsync( + this.zrxAssetData, + this.takerAddress, + ); + const expZRXAssetBalanceOfFeeRecipient = await lazyStore.getBalanceAsync(this.zrxAssetData, feeRecipient); + + const orderHash = orderHashUtils.getOrderHashHex(signedOrder); + const alreadyFilledTakerAmount = await this.exchangeWrapper.getTakerAssetFilledAmountAsync(orderHash); + const remainingTakerAmountToFill = signedOrder.takerAssetAmount.minus(alreadyFilledTakerAmount); + const expFilledTakerAmount = takerAssetFillAmount.gt(remainingTakerAmountToFill) + ? remainingTakerAmountToFill + : alreadyFilledTakerAmount.add(takerAssetFillAmount); + + const expFilledMakerAmount = orderUtils.getPartialAmount( + expFilledTakerAmount, + signedOrder.takerAssetAmount, + signedOrder.makerAssetAmount, + ); + + // - Let's fill the order! + const txReceipt = await this.exchangeWrapper.fillOrderAsync(signedOrder, this.takerAddress, { + takerAssetFillAmount, + }); + + const actFilledTakerAmount = await this.exchangeWrapper.getTakerAssetFilledAmountAsync(orderHash); + expect(actFilledTakerAmount).to.be.bignumber.equal(expFilledTakerAmount, 'filledTakerAmount'); + + expect(txReceipt.logs.length).to.be.equal(1, 'logs length'); + // tslint:disable-next-line:no-unnecessary-type-assertion + const log = txReceipt.logs[0] as LogWithDecodedArgs; + expect(log.args.makerAddress).to.be.equal(makerAddress, 'log.args.makerAddress'); + expect(log.args.takerAddress).to.be.equal(this.takerAddress, 'log.args.this.takerAddress'); + expect(log.args.feeRecipientAddress).to.be.equal(feeRecipient, 'log.args.feeRecipientAddress'); + expect(log.args.makerAssetFilledAmount).to.be.bignumber.equal( + expFilledMakerAmount, + 'log.args.makerAssetFilledAmount', + ); + expect(log.args.takerAssetFilledAmount).to.be.bignumber.equal( + expFilledTakerAmount, + 'log.args.takerAssetFilledAmount', + ); + const expMakerFeePaid = orderUtils.getPartialAmount( + expFilledTakerAmount, + signedOrder.takerAssetAmount, + signedOrder.makerFee, + ); + expect(log.args.makerFeePaid).to.be.bignumber.equal(expMakerFeePaid, 'log.args.makerFeePaid'); + const expTakerFeePaid = orderUtils.getPartialAmount( + expFilledTakerAmount, + signedOrder.takerAssetAmount, + signedOrder.takerFee, + ); + expect(log.args.takerFeePaid).to.be.bignumber.equal(expTakerFeePaid, 'logs.args.takerFeePaid'); + expect(log.args.orderHash).to.be.equal(orderHash, 'log.args.orderHash'); + expect(log.args.makerAssetData).to.be.equal(makerAssetData, 'log.args.makerAssetData'); + expect(log.args.takerAssetData).to.be.equal(takerAssetData, 'log.args.takerAssetData'); + + const actMakerAssetBalanceOfMaker = await this.assetWrapper.getBalanceAsync(makerAddress, makerAssetData); + expect(actMakerAssetBalanceOfMaker).to.be.bignumber.equal( + expMakerAssetBalanceOfMaker, + 'makerAssetBalanceOfMaker', + ); + + const actMakerAssetAllowanceOfMaker = await this.assetWrapper.getProxyAllowanceAsync( + makerAddress, + makerAssetData, + ); + expect(actMakerAssetAllowanceOfMaker).to.be.bignumber.equal( + expMakerAssetAllowanceOfMaker, + 'makerAssetAllowanceOfMaker', + ); + + const actTakerAssetBalanceOfMaker = await this.assetWrapper.getBalanceAsync(makerAddress, takerAssetData); + expect(actTakerAssetBalanceOfMaker).to.be.bignumber.equal( + expTakerAssetBalanceOfMaker, + 'takerAssetBalanceOfMaker', + ); + + const actZRXAssetBalanceOfMaker = await this.assetWrapper.getBalanceAsync(makerAddress, this.zrxAssetData); + expect(actZRXAssetBalanceOfMaker).to.be.bignumber.equal(expZRXAssetBalanceOfMaker, 'ZRXAssetBalanceOfMaker'); + + const actZRXAssetAllowanceOfMaker = await this.assetWrapper.getProxyAllowanceAsync( + makerAddress, + this.zrxAssetData, + ); + expect(actZRXAssetAllowanceOfMaker).to.be.bignumber.equal( + expZRXAssetAllowanceOfMaker, + 'ZRXAssetAllowanceOfMaker', + ); + + const actTakerAssetBalanceOfTaker = await this.assetWrapper.getBalanceAsync(this.takerAddress, takerAssetData); + expect(actTakerAssetBalanceOfTaker).to.be.bignumber.equal( + expTakerAssetBalanceOfTaker, + 'TakerAssetBalanceOfTaker', + ); + + const actTakerAssetAllowanceOfTaker = await this.assetWrapper.getProxyAllowanceAsync( + this.takerAddress, + takerAssetData, + ); + + expect(actTakerAssetAllowanceOfTaker).to.be.bignumber.equal( + expTakerAssetAllowanceOfTaker, + 'TakerAssetAllowanceOfTaker', + ); + + const actMakerAssetBalanceOfTaker = await this.assetWrapper.getBalanceAsync(this.takerAddress, makerAssetData); + expect(actMakerAssetBalanceOfTaker).to.be.bignumber.equal( + expMakerAssetBalanceOfTaker, + 'MakerAssetBalanceOfTaker', + ); + + const actZRXAssetBalanceOfTaker = await this.assetWrapper.getBalanceAsync(this.takerAddress, this.zrxAssetData); + expect(actZRXAssetBalanceOfTaker).to.be.bignumber.equal(expZRXAssetBalanceOfTaker, 'ZRXAssetBalanceOfTaker'); + + const actZRXAssetAllowanceOfTaker = await this.assetWrapper.getProxyAllowanceAsync( + this.takerAddress, + this.zrxAssetData, + ); + expect(actZRXAssetAllowanceOfTaker).to.be.bignumber.equal( + expZRXAssetAllowanceOfTaker, + 'ZRXAssetAllowanceOfTaker', + ); + + const actZRXAssetBalanceOfFeeRecipient = await this.assetWrapper.getBalanceAsync( + feeRecipient, + this.zrxAssetData, + ); + expect(actZRXAssetBalanceOfFeeRecipient).to.be.bignumber.equal( + expZRXAssetBalanceOfFeeRecipient, + 'ZRXAssetBalanceOfFeeRecipient', + ); + } + private async _getTakerAssetFillAmountAsync( + signedOrder: SignedOrder, + takerAssetFillAmountScenario: TakerAssetFillAmountScenario, + balanceAndProxyAllowanceFetcher: SimpleAssetBalanceAndProxyAllowanceFetcher, + orderFilledCancelledFetcher: SimpleOrderFilledCancelledFetcher, + ): Promise { + const orderStateUtils = new OrderStateUtils(balanceAndProxyAllowanceFetcher, orderFilledCancelledFetcher); + const fillableTakerAssetAmount = await orderStateUtils.getMaxFillableTakerAssetAmountAsync( + signedOrder, + this.takerAddress, + ); + + let takerAssetFillAmount; + switch (takerAssetFillAmountScenario) { + case TakerAssetFillAmountScenario.Zero: + takerAssetFillAmount = new BigNumber(0); + break; + + case TakerAssetFillAmountScenario.ExactlyRemainingFillableTakerAssetAmount: + takerAssetFillAmount = fillableTakerAssetAmount; + break; + + case TakerAssetFillAmountScenario.GreaterThanRemainingFillableTakerAssetAmount: + takerAssetFillAmount = fillableTakerAssetAmount.add(1); + break; + + case TakerAssetFillAmountScenario.LessThanRemainingFillableTakerAssetAmount: + const takerAssetProxyId = assetDataUtils.decodeAssetProxyId(signedOrder.takerAssetData); + const makerAssetProxyId = assetDataUtils.decodeAssetProxyId(signedOrder.makerAssetData); + const isEitherAssetERC721 = + takerAssetProxyId === AssetProxyId.ERC721 || makerAssetProxyId === AssetProxyId.ERC721; + if (isEitherAssetERC721) { + throw new Error( + 'Cannot test `TakerAssetFillAmountScenario.LessThanRemainingFillableTakerAssetAmount` together with ERC721 assets since orders involving ERC721 must always be filled exactly.', + ); + } + takerAssetFillAmount = fillableTakerAssetAmount.div(2).floor(); + break; + + default: + throw errorUtils.spawnSwitchErr('TakerAssetFillAmountScenario', takerAssetFillAmountScenario); + } + + return takerAssetFillAmount; + } + private async _modifyTraderStateAsync( + makerStateScenario: TraderStateScenario, + takerStateScenario: TraderStateScenario, + signedOrder: SignedOrder, + takerAssetFillAmount: BigNumber, + ): Promise { + const makerAssetFillAmount = orderUtils.getPartialAmount( + takerAssetFillAmount, + signedOrder.takerAssetAmount, + signedOrder.makerAssetAmount, + ); + switch (makerStateScenario.traderAssetBalance) { + case BalanceAmountScenario.Higher: + break; // Noop since this is already the default + + case BalanceAmountScenario.TooLow: + if (makerAssetFillAmount.eq(0)) { + throw new Error(`Cannot set makerAssetBalanceOfMaker TooLow if makerAssetFillAmount is 0`); + } + const tooLowBalance = makerAssetFillAmount.minus(1); + await this.assetWrapper.setBalanceAsync( + signedOrder.makerAddress, + signedOrder.makerAssetData, + tooLowBalance, + ); + break; + + case BalanceAmountScenario.Exact: + const exactBalance = makerAssetFillAmount; + await this.assetWrapper.setBalanceAsync( + signedOrder.makerAddress, + signedOrder.makerAssetData, + exactBalance, + ); + break; + + default: + throw errorUtils.spawnSwitchErr( + 'makerStateScenario.traderAssetBalance', + makerStateScenario.traderAssetBalance, + ); + } + + const makerFee = orderUtils.getPartialAmount( + takerAssetFillAmount, + signedOrder.takerAssetAmount, + signedOrder.makerFee, + ); + switch (makerStateScenario.zrxFeeBalance) { + case BalanceAmountScenario.Higher: + break; // Noop since this is already the default + + case BalanceAmountScenario.TooLow: + if (makerFee.eq(0)) { + throw new Error(`Cannot set zrxAsserBalanceOfMaker TooLow if makerFee is 0`); + } + const tooLowBalance = makerFee.minus(1); + await this.assetWrapper.setBalanceAsync(signedOrder.makerAddress, this.zrxAssetData, tooLowBalance); + break; + + case BalanceAmountScenario.Exact: + const exactBalance = makerFee; + await this.assetWrapper.setBalanceAsync(signedOrder.makerAddress, this.zrxAssetData, exactBalance); + break; + + default: + throw errorUtils.spawnSwitchErr('makerStateScenario.zrxFeeBalance', makerStateScenario.zrxFeeBalance); + } + + switch (makerStateScenario.traderAssetAllowance) { + case AllowanceAmountScenario.Higher: + break; // Noop since this is already the default + + case AllowanceAmountScenario.TooLow: + const tooLowAllowance = makerAssetFillAmount.minus(1); + await this.assetWrapper.setProxyAllowanceAsync( + signedOrder.makerAddress, + signedOrder.makerAssetData, + tooLowAllowance, + ); + break; + + case AllowanceAmountScenario.Exact: + const exactAllowance = makerAssetFillAmount; + await this.assetWrapper.setProxyAllowanceAsync( + signedOrder.makerAddress, + signedOrder.makerAssetData, + exactAllowance, + ); + break; + + case AllowanceAmountScenario.Unlimited: + await this.assetWrapper.setProxyAllowanceAsync( + signedOrder.makerAddress, + signedOrder.makerAssetData, + constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS, + ); + break; + + default: + throw errorUtils.spawnSwitchErr( + 'makerStateScenario.traderAssetAllowance', + makerStateScenario.traderAssetAllowance, + ); + } + + switch (makerStateScenario.zrxFeeAllowance) { + case AllowanceAmountScenario.Higher: + break; // Noop since this is already the default + + case AllowanceAmountScenario.TooLow: + const tooLowAllowance = makerFee.minus(1); + await this.assetWrapper.setProxyAllowanceAsync( + signedOrder.makerAddress, + this.zrxAssetData, + tooLowAllowance, + ); + break; + + case AllowanceAmountScenario.Exact: + const exactAllowance = makerFee; + await this.assetWrapper.setProxyAllowanceAsync( + signedOrder.makerAddress, + this.zrxAssetData, + exactAllowance, + ); + break; + + case AllowanceAmountScenario.Unlimited: + await this.assetWrapper.setProxyAllowanceAsync( + signedOrder.makerAddress, + this.zrxAssetData, + constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS, + ); + break; + + default: + throw errorUtils.spawnSwitchErr( + 'makerStateScenario.zrxFeeAllowance', + makerStateScenario.zrxFeeAllowance, + ); + } + + switch (takerStateScenario.traderAssetBalance) { + case BalanceAmountScenario.Higher: + break; // Noop since this is already the default + + case BalanceAmountScenario.TooLow: + if (takerAssetFillAmount.eq(0)) { + throw new Error(`Cannot set takerAssetBalanceOfTaker TooLow if takerAssetFillAmount is 0`); + } + const tooLowBalance = takerAssetFillAmount.minus(1); + await this.assetWrapper.setBalanceAsync(this.takerAddress, signedOrder.takerAssetData, tooLowBalance); + break; + + case BalanceAmountScenario.Exact: + const exactBalance = takerAssetFillAmount; + await this.assetWrapper.setBalanceAsync(this.takerAddress, signedOrder.takerAssetData, exactBalance); + break; + + default: + throw errorUtils.spawnSwitchErr( + 'takerStateScenario.traderAssetBalance', + takerStateScenario.traderAssetBalance, + ); + } + + const takerFee = orderUtils.getPartialAmount( + takerAssetFillAmount, + signedOrder.takerAssetAmount, + signedOrder.takerFee, + ); + switch (takerStateScenario.zrxFeeBalance) { + case BalanceAmountScenario.Higher: + break; // Noop since this is already the default + + case BalanceAmountScenario.TooLow: + if (takerFee.eq(0)) { + throw new Error(`Cannot set zrxAssetBalanceOfTaker TooLow if takerFee is 0`); + } + const tooLowBalance = takerFee.minus(1); + await this.assetWrapper.setBalanceAsync(this.takerAddress, this.zrxAssetData, tooLowBalance); + break; + + case BalanceAmountScenario.Exact: + const exactBalance = takerFee; + await this.assetWrapper.setBalanceAsync(this.takerAddress, this.zrxAssetData, exactBalance); + break; + + default: + throw errorUtils.spawnSwitchErr('takerStateScenario.zrxFeeBalance', takerStateScenario.zrxFeeBalance); + } + + switch (takerStateScenario.traderAssetAllowance) { + case AllowanceAmountScenario.Higher: + break; // Noop since this is already the default + + case AllowanceAmountScenario.TooLow: + const tooLowAllowance = takerAssetFillAmount.minus(1); + await this.assetWrapper.setProxyAllowanceAsync( + this.takerAddress, + signedOrder.takerAssetData, + tooLowAllowance, + ); + break; + + case AllowanceAmountScenario.Exact: + const exactAllowance = takerAssetFillAmount; + await this.assetWrapper.setProxyAllowanceAsync( + this.takerAddress, + signedOrder.takerAssetData, + exactAllowance, + ); + break; + + case AllowanceAmountScenario.Unlimited: + await this.assetWrapper.setProxyAllowanceAsync( + this.takerAddress, + signedOrder.takerAssetData, + constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS, + ); + break; + + default: + throw errorUtils.spawnSwitchErr( + 'takerStateScenario.traderAssetAllowance', + takerStateScenario.traderAssetAllowance, + ); + } + + switch (takerStateScenario.zrxFeeAllowance) { + case AllowanceAmountScenario.Higher: + break; // Noop since this is already the default + + case AllowanceAmountScenario.TooLow: + const tooLowAllowance = takerFee.minus(1); + await this.assetWrapper.setProxyAllowanceAsync(this.takerAddress, this.zrxAssetData, tooLowAllowance); + break; + + case AllowanceAmountScenario.Exact: + const exactAllowance = takerFee; + await this.assetWrapper.setProxyAllowanceAsync(this.takerAddress, this.zrxAssetData, exactAllowance); + break; + + case AllowanceAmountScenario.Unlimited: + await this.assetWrapper.setProxyAllowanceAsync( + this.takerAddress, + this.zrxAssetData, + constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS, + ); + break; + + default: + throw errorUtils.spawnSwitchErr( + 'takerStateScenario.zrxFeeAllowance', + takerStateScenario.zrxFeeAllowance, + ); + } + } +} // tslint:disable:max-file-line-count diff --git a/packages/contracts/test/utils/test_with_reference.ts b/packages/contracts/test/utils/test_with_reference.ts new file mode 100644 index 000000000..599b1eed4 --- /dev/null +++ b/packages/contracts/test/utils/test_with_reference.ts @@ -0,0 +1,119 @@ +import * as chai from 'chai'; +import * as _ from 'lodash'; + +import { chaiSetup } from './chai_setup'; + +chaiSetup.configure(); +const expect = chai.expect; + +export async function testWithReferenceFuncAsync( + referenceFunc: (p0: P0) => Promise, + testFunc: (p0: P0) => Promise, + values: [P0], +): Promise; +export async function testWithReferenceFuncAsync( + referenceFunc: (p0: P0, p1: P1) => Promise, + testFunc: (p0: P0, p1: P1) => Promise, + values: [P0, P1], +): Promise; +export async function testWithReferenceFuncAsync( + referenceFunc: (p0: P0, p1: P1, p2: P2) => Promise, + testFunc: (p0: P0, p1: P1, p2: P2) => Promise, + values: [P0, P1, P2], +): Promise; +export async function testWithReferenceFuncAsync( + referenceFunc: (p0: P0, p1: P1, p2: P2, p3: P3) => Promise, + testFunc: (p0: P0, p1: P1, p2: P2, p3: P3) => Promise, + values: [P0, P1, P2, P3], +): Promise; +export async function testWithReferenceFuncAsync( + referenceFunc: (p0: P0, p1: P1, p2: P2, p3: P3, p4: P4) => Promise, + testFunc: (p0: P0, p1: P1, p2: P2, p3: P3, p4: P4) => Promise, + values: [P0, P1, P2, P3, P4], +): Promise; + +/** + * Tests the behavior of a test function by comparing it to the expected + * behavior (defined by a reference function). + * + * First the reference function will be called to obtain an "expected result", + * or if the reference function throws/rejects, an "expected error". Next, the + * test function will be called to obtain an "actual result", or if the test + * function throws/rejects, an "actual error". The test passes if at least one + * of the following conditions is met: + * + * 1) Neither the reference function or the test function throw and the + * "expected result" equals the "actual result". + * + * 2) Both the reference function and the test function throw and the "actual + * error" message *contains* the "expected error" message. + * + * @param referenceFuncAsync a reference function implemented in pure + * JavaScript/TypeScript which accepts N arguments and returns the "expected + * result" or throws/rejects with the "expected error". + * @param testFuncAsync a test function which, e.g., makes a call or sends a + * transaction to a contract. It accepts the same N arguments returns the + * "actual result" or throws/rejects with the "actual error". + * @param values an array of N values, where each value corresponds in-order to + * an argument to both the test function and the reference function. + * @return A Promise that resolves if the test passes and rejects if the test + * fails, according to the rules described above. + */ +export async function testWithReferenceFuncAsync( + referenceFuncAsync: (...args: any[]) => Promise, + testFuncAsync: (...args: any[]) => Promise, + values: any[], +): Promise { + let expectedResult: any; + let expectedErr: string | undefined; + try { + expectedResult = await referenceFuncAsync(...values); + } catch (e) { + expectedErr = e.message; + } + let actualResult: any | undefined; + try { + actualResult = await testFuncAsync(...values); + if (!_.isUndefined(expectedErr)) { + throw new Error( + `Expected error containing ${expectedErr} but got no error\n\tTest case: ${_getTestCaseString( + referenceFuncAsync, + values, + )}`, + ); + } + } catch (e) { + if (_.isUndefined(expectedErr)) { + throw new Error(`${e.message}\n\tTest case: ${_getTestCaseString(referenceFuncAsync, values)}`); + } else { + expect(e.message).to.contain( + expectedErr, + `${e.message}\n\tTest case: ${_getTestCaseString(referenceFuncAsync, values)}`, + ); + } + } + if (!_.isUndefined(actualResult) && !_.isUndefined(expectedResult)) { + expect(actualResult).to.deep.equal( + expectedResult, + `Test case: ${_getTestCaseString(referenceFuncAsync, values)}`, + ); + } +} + +function _getTestCaseString(referenceFuncAsync: (...args: any[]) => Promise, values: any[]): string { + const paramNames = _getParameterNames(referenceFuncAsync); + return JSON.stringify(_.zipObject(paramNames, values)); +} + +// Source: https://stackoverflow.com/questions/1007981/how-to-get-function-parameter-names-values-dynamically +function _getParameterNames(func: (...args: any[]) => any): string[] { + return _.toString(func) + .replace(/[/][/].*$/gm, '') // strip single-line comments + .replace(/\s+/g, '') // strip white space + .replace(/[/][*][^/*]*[*][/]/g, '') // strip multi-line comments + .split('){', 1)[0] + .replace(/^[^(]*[(]/, '') // extract the parameters + .replace(/=[^,]+/g, '') // strip any ES6 defaults + .split(',') + .filter(Boolean); // split & filter [""] +} -- cgit v1.2.3