diff --git a/contracts/exchange/contracts/src/MixinMatchOrders.sol b/contracts/exchange/contracts/src/MixinMatchOrders.sol index e5b6c39c67..27689e3892 100644 --- a/contracts/exchange/contracts/src/MixinMatchOrders.sol +++ b/contracts/exchange/contracts/src/MixinMatchOrders.sol @@ -57,20 +57,23 @@ contract MixinMatchOrders is require(leftOrders.length == leftSignatures.length, "Incompatible leftOrders and leftSignatures"); require(rightOrders.length == rightSignatures.length, "Incompatible rightOrders and rightSignatures"); - uint256 minLength = _min256(leftOrders.length, rightOrders.length); - - batchMatchedFillResults.left = new LibFillResults.FillResults[](minLength); - batchMatchedFillResults.right = new LibFillResults.FillResults[](minLength); + // Without simulating all of the order matching, this program cannot know how many + // matches there will be. To ensure that batchMatchedFillResults has enough memory + // allocated for the left and the right side, we will allocate enough space for the + // maximum amount of matches (the maximum of the left and the right sides). + uint256 maxLength = _max256(leftOrders.length, rightOrders.length); + batchMatchedFillResults.left = new LibFillResults.FillResults[](maxLength); + batchMatchedFillResults.right = new LibFillResults.FillResults[](maxLength); // Initialize initial variables - uint256 matchCount; + uint matchCount; uint256 leftIdx = 0; uint256 rightIdx = 0; - LibOrder.Order memory leftOrder = leftOrders[0]; LibOrder.Order memory rightOrder = rightOrders[0]; bytes memory leftSignature = leftSignatures[0]; - bytes memory rightSignature = leftSignatures[0]; + bytes memory rightSignature = rightSignatures[0]; + // Loop infinitely (until broken inside of the loop), but keep a counter of how // many orders have been matched. @@ -93,6 +96,7 @@ contract MixinMatchOrders is ); // batchMatchedFillResults.profitInRightMakerAsset += 0; // Placeholder for ZEIP 40 + // If the leftOrder is filled, update the leftIdx, leftOrder, and leftSignature, // or break out of the loop if there are no more leftOrders to match. if (_isFilled(leftOrder, matchResults.left)) { diff --git a/contracts/exchange/test/match_orders.ts b/contracts/exchange/test/match_orders.ts index 78a35f9bcb..156f25c377 100644 --- a/contracts/exchange/test/match_orders.ts +++ b/contracts/exchange/test/match_orders.ts @@ -14,7 +14,7 @@ import { DummyERC721TokenContract } from '@0x/contracts-erc721'; import { chaiSetup, constants, OrderFactory, provider, txDefaults, web3Wrapper } from '@0x/contracts-test-utils'; import { BlockchainLifecycle } from '@0x/dev-utils'; import { assetDataUtils, ExchangeRevertErrors, orderHashUtils } from '@0x/order-utils'; -import { OrderStatus } from '@0x/types'; +import { OrderStatus, SignedOrder } from '@0x/types'; import { BigNumber, providerUtils, ReentrancyGuardRevertErrors } from '@0x/utils'; import { Web3Wrapper } from '@0x/web3-wrapper'; import * as chai from 'chai'; @@ -31,6 +31,7 @@ import { import { MatchOrderTester, TokenBalances } from './utils/match_order_tester'; +const ZERO = new BigNumber(0); const ONE = new BigNumber(1); const TWO = new BigNumber(2); @@ -1850,5 +1851,193 @@ describe('matchOrders', () => { } }); }); + describe.only('batchMatchOrders', () => { + it('should fail if there are zero leftOrders', async () => { + const leftOrders: SignedOrder[] = []; + const rightOrders = [ + await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(1, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(2, 0), + feeRecipientAddress: feeRecipientAddressRight, + }), + ]; + const tx = exchangeWrapper.batchMatchOrdersAsync(leftOrders, rightOrders, takerAddress); + return expect(tx).to.be.rejected(); + }); + it('should fail if there are zero rightOrders', async () => { + const leftOrders = [ + await orderFactoryLeft.newSignedOrderAsync({ + makerAddress: makerAddressLeft, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(1, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(2, 0), + feeRecipientAddress: feeRecipientAddressLeft, + }), + ]; + const rightOrders: SignedOrder[] = []; + const tx = exchangeWrapper.batchMatchOrdersAsync(leftOrders, rightOrders, takerAddress); + return expect(tx).to.be.rejected(); + }); + it('should correctly match two opposite orders', async () => { + const leftOrders = [ + await orderFactoryLeft.newSignedOrderAsync({ + makerAddress: makerAddressLeft, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(2, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(1, 0), + feeRecipientAddress: feeRecipientAddressLeft, + }), + ]; + const rightOrders = [ + await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(1, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(2, 0), + feeRecipientAddress: feeRecipientAddressRight, + }), + ]; + const expectedTransferAmounts = [ + { + // Left Maker + leftMakerAssetSoldByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(2, 0), + leftMakerFeeAssetPaidByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + // Right Maker + rightMakerAssetSoldByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(1, 0), + rightMakerFeeAssetPaidByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + // Taker + leftTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + rightTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + }, + ]; + await matchOrderTester.batchMatchOrdersAndAssertEffectsAsync( + { + leftOrders, + rightOrders, + leftOrdersTakerAssetFilledAmounts: [ZERO], + rightOrdersTakerAssetFilledAmounts: [ZERO], + }, + takerAddress, + [[0, 0]], + expectedTransferAmounts, + ); + }); + it('should correctly match two left orders to one complementary right order', async () => { + const leftOrders = [ + await orderFactoryLeft.newSignedOrderAsync({ + makerAddress: makerAddressLeft, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(2, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(1, 0), + feeRecipientAddress: feeRecipientAddressLeft, + }), + await orderFactoryLeft.newSignedOrderAsync({ + makerAddress: makerAddressLeft, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(2, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(1, 0), + feeRecipientAddress: feeRecipientAddressLeft, + }), + ]; + const rightOrders = [ + await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(2, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(4, 0), + feeRecipientAddress: feeRecipientAddressRight, + }), + ]; + const expectedTransferAmounts = [ + { + // Left Maker + leftMakerAssetSoldByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(2, 0), + leftMakerFeeAssetPaidByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + // Right Maker + rightMakerAssetSoldByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(1, 0), + rightMakerFeeAssetPaidByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(50, 16), // 50% + // Taker + leftTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 50% + rightTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(50, 16), // 50% + }, + { + // Left Maker + leftMakerAssetSoldByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(2, 0), + leftMakerFeeAssetPaidByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 50% + // Right Maker + rightMakerAssetSoldByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(1, 0), + rightMakerFeeAssetPaidByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(50, 16), // 50% + // Taker + leftTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 50% + rightTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(50, 16), // 50% + }, + ]; + await matchOrderTester.batchMatchOrdersAndAssertEffectsAsync( + { + leftOrders, + rightOrders, + leftOrdersTakerAssetFilledAmounts: [ZERO, ZERO], + rightOrdersTakerAssetFilledAmounts: [ZERO], + }, + takerAddress, + [[0, 0], [1, 0]], + expectedTransferAmounts, + ); + }); + it('should correctly match one left order to two complementary right orders', async () => { + const leftOrders = [ + await orderFactoryLeft.newSignedOrderAsync({ + makerAddress: makerAddressLeft, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(4, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(2, 0), + feeRecipientAddress: feeRecipientAddressLeft, + }), + ]; + const rightOrders = [ + await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(1, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(2, 0), + feeRecipientAddress: feeRecipientAddressRight, + }), + await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(1, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(2, 0), + feeRecipientAddress: feeRecipientAddressRight, + }), + ]; + const expectedTransferAmounts = [ + { + // Left Maker + leftMakerAssetSoldByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(2, 0), + leftMakerFeeAssetPaidByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(50, 16), // 50% + // Right Maker + rightMakerAssetSoldByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(1, 0), + rightMakerFeeAssetPaidByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + // Taker + leftTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(50, 16), // 50% + rightTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + }, + { + // Left Maker + leftMakerAssetSoldByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(2, 0), + leftMakerFeeAssetPaidByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(50, 16), // 50% + // Right Maker + rightMakerAssetSoldByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(1, 0), + rightMakerFeeAssetPaidByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + // Taker + leftTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(50, 16), // 50% + rightTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + }, + ]; + await matchOrderTester.batchMatchOrdersAndAssertEffectsAsync( + { + leftOrders, + rightOrders, + leftOrdersTakerAssetFilledAmounts: [ZERO], + rightOrdersTakerAssetFilledAmounts: [ZERO, ZERO], + }, + takerAddress, + [[0, 0], [0, 1]], + expectedTransferAmounts, + ); + }); + }); }); // tslint:disable-line:max-file-line-count diff --git a/contracts/exchange/test/utils/exchange_wrapper.ts b/contracts/exchange/test/utils/exchange_wrapper.ts index 75cd042d86..38850e56de 100644 --- a/contracts/exchange/test/utils/exchange_wrapper.ts +++ b/contracts/exchange/test/utils/exchange_wrapper.ts @@ -266,6 +266,22 @@ export class ExchangeWrapper { const ordersInfo = (await this._exchange.getOrdersInfo.callAsync(signedOrders)) as OrderInfo[]; return ordersInfo; } + public async batchMatchOrdersAsync( + signedOrdersLeft: SignedOrder[], + signedOrdersRight: SignedOrder[], + from: string, + ): Promise { + const params = orderUtils.createBatchMatchOrders(signedOrdersLeft, signedOrdersRight); + const txHash = await this._exchange.batchMatchOrders.sendTransactionAsync( + params.leftOrders, + params.rightOrders, + params.leftSignatures, + params.rightSignatures, + { from }, + ); + const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); + return tx; + } public async matchOrdersAsync( signedOrderLeft: SignedOrder, signedOrderRight: SignedOrder, diff --git a/contracts/exchange/test/utils/match_order_tester.ts b/contracts/exchange/test/utils/match_order_tester.ts index 8a569aed44..1b8f42d159 100644 --- a/contracts/exchange/test/utils/match_order_tester.ts +++ b/contracts/exchange/test/utils/match_order_tester.ts @@ -56,6 +56,11 @@ export interface MatchResults { balances: TokenBalances; } +export interface BatchMatchResults { + matches: MatchResults[]; + filledAmounts: Array<[SignedOrder, BigNumber, string]>; +} + export interface ERC1155Holdings { [owner: string]: { [contract: string]: { @@ -81,6 +86,13 @@ export interface TokenBalances { erc1155: ERC1155Holdings; } +export interface BatchMatchedOrders { + leftOrders: SignedOrder[]; + rightOrders: SignedOrder[]; + leftOrdersTakerAssetFilledAmounts: BigNumber[]; + rightOrdersTakerAssetFilledAmounts: BigNumber[]; +} + export interface MatchedOrders { leftOrder: SignedOrder; rightOrder: SignedOrder; @@ -88,6 +100,12 @@ export interface MatchedOrders { rightOrderTakerAssetFilledAmount?: BigNumber; } +export type BatchMatchOrdersAsyncCall = ( + leftOrders: SignedOrder[], + rightOrders: SignedOrder[], + takerAddress: string, +) => Promise; + export type MatchOrdersAsyncCall = ( leftOrder: SignedOrder, rightOrder: SignedOrder, @@ -99,6 +117,7 @@ export class MatchOrderTester { public erc20Wrapper: ERC20Wrapper; public erc721Wrapper: ERC721Wrapper; public erc1155ProxyWrapper: ERC1155ProxyWrapper; + public batchMatchOrdersCallAsync?: BatchMatchOrdersAsyncCall; public matchOrdersCallAsync?: MatchOrdersAsyncCall; private readonly _initialTokenBalancesPromise: Promise; @@ -108,6 +127,8 @@ export class MatchOrderTester { * @param erc20Wrapper Used to fetch ERC20 balances. * @param erc721Wrapper Used to fetch ERC721 token owners. * @param erc1155Wrapper Used to fetch ERC1155 token owners. + * @param batchMatchOrdersCallAsync Optional, custom caller for + * `ExchangeWrapper.batchMatchOrdersAsync()`. * @param matchOrdersCallAsync Optional, custom caller for * `ExchangeWrapper.matchOrdersAsync()`. */ @@ -117,6 +138,7 @@ export class MatchOrderTester { erc721Wrapper: ERC721Wrapper, erc1155ProxyWrapper: ERC1155ProxyWrapper, matchOrdersCallAsync?: MatchOrdersAsyncCall, + batchMatchOrdersCallAsync?: BatchMatchOrdersAsyncCall, ) { this.exchangeWrapper = exchangeWrapper; this.erc20Wrapper = erc20Wrapper; @@ -126,6 +148,59 @@ export class MatchOrderTester { this._initialTokenBalancesPromise = this.getBalancesAsync(); } + /** + * Performs batch order matching on a set of complementary orders and asserts results. + * @param orders The list of orders and filled states + * @param matchPairs An array of left and right indices that will be used to perform + * the expected simulation. + * @param takerAddress Address of taker (the address who matched the two orders) + * @param expectedTransferAmounts Expected amounts transferred as a result of each round of + * order matching. Omitted fields are either set to 0 or their + * complementary field. + * @return Results of `batchMatchOrders()`. + */ + public async batchMatchOrdersAndAssertEffectsAsync( + orders: BatchMatchedOrders, + takerAddress: string, + matchPairs: Array<[number, number]>, + expectedTransferAmounts: Array>, + initialTokenBalances?: TokenBalances, + ): Promise { + // Ensure that the provided input is valid. + expect(matchPairs.length).to.be.eq(expectedTransferAmounts.length); + expect(orders.leftOrders.length).to.be.eq(orders.leftOrdersTakerAssetFilledAmounts.length); + expect(orders.rightOrders.length).to.be.eq(orders.rightOrdersTakerAssetFilledAmounts.length); + // Ensure that the exchange is in the expected state. + await assertBatchOrderStatesAsync(orders, this.exchangeWrapper); + // Get the token balances before executing `batchMatchOrders()`. + const _initialTokenBalances = initialTokenBalances + ? initialTokenBalances + : await this._initialTokenBalancesPromise; + // Execute `batchMatchOrders()` + const transactionReceipt = await this._executeBatchMatchOrdersAsync( + orders.leftOrders, + orders.rightOrders, + takerAddress, + ); + // Simulate the batch order match. + const batchMatchResults = simulateBatchMatchOrders( + orders, + takerAddress, + _initialTokenBalances, + matchPairs, + expectedTransferAmounts, + ); + // Validate the simulation against reality. + await assertBatchMatchResultsAsync( + batchMatchResults, + transactionReceipt, + await this.getBalancesAsync(), + _initialTokenBalances, + this.exchangeWrapper, + ); + return batchMatchResults; + } + /** * Matches two complementary orders and asserts results. * @param orders The matched orders and filled states. @@ -159,7 +234,7 @@ export class MatchOrderTester { _initialTokenBalances, toFullMatchTransferAmounts(expectedTransferAmounts), ); - // Validate the simulation against realit. + // Validate the simulation against reality. await assertMatchResultsAsync( matchResults, transactionReceipt, @@ -176,6 +251,18 @@ export class MatchOrderTester { return getTokenBalancesAsync(this.erc20Wrapper, this.erc721Wrapper, this.erc1155ProxyWrapper); } + private async _executeBatchMatchOrdersAsync( + leftOrders: SignedOrder[], + rightOrders: SignedOrder[], + takerAddress: string, + ): Promise { + const caller = + this.batchMatchOrdersCallAsync || + (async (_leftOrders: SignedOrder[], _rightOrders: SignedOrder[], _takerAddress: string) => + this.exchangeWrapper.batchMatchOrdersAsync(_leftOrders, _rightOrders, _takerAddress)); + return caller(leftOrders, rightOrders, takerAddress); + } + private async _executeMatchOrdersAsync( leftOrder: SignedOrder, rightOrder: SignedOrder, @@ -227,6 +314,97 @@ function toFullMatchTransferAmounts(partial: Partial): Mat }; } +/** + * Simulates matching a batch of orders by transferring amounts defined in + * `transferAmounts` and returns the results. + * @param orders The orders being batch matched and their filled states. + * @param takerAddress Address of taker (the address who matched the two orders) + * @param tokenBalances Current token balances. + * @param transferAmounts Amounts to transfer during the simulation. + * @return The new account balances and fill events that occurred during the match. + */ +function simulateBatchMatchOrders( + orders: BatchMatchedOrders, + takerAddress: string, + tokenBalances: TokenBalances, + matchPairs: Array<[number, number]>, + transferAmounts: Array>, +): BatchMatchResults { + // Initialize variables + let lastLeftIdx = 0; + let lastRightIdx = 0; + let matchedOrders: MatchedOrders; + const batchMatchResults: BatchMatchResults = { + matches: [], + filledAmounts: [], + }; + // Loop over all of the matched pairs from the round + for (let i = 0; i < matchPairs.length; i++) { + const leftIdx = matchPairs[i][0]; + const rightIdx = matchPairs[i][1]; + // Construct a matched order out of the current left and right orders + matchedOrders = { + leftOrder: orders.leftOrders[leftIdx], + rightOrder: orders.rightOrders[rightIdx], + leftOrderTakerAssetFilledAmount: orders.leftOrdersTakerAssetFilledAmounts[leftIdx], + rightOrderTakerAssetFilledAmount: orders.rightOrdersTakerAssetFilledAmounts[rightIdx], + }; + // If there has been a match recorded and one or both of the side indices have not changed, + // replace the side's taker asset filled amount + if (batchMatchResults.matches.length > 0) { + if (lastLeftIdx === leftIdx) { + matchedOrders.leftOrderTakerAssetFilledAmount = getLastMatch( + batchMatchResults, + ).orders.leftOrderTakerAssetFilledAmount; + } else { + batchMatchResults.filledAmounts.push([ + orders.leftOrders[lastLeftIdx], + getLastMatch(batchMatchResults).orders.leftOrderTakerAssetFilledAmount || ZERO, + 'left', + ]); + } + if (lastRightIdx === rightIdx) { + matchedOrders.rightOrderTakerAssetFilledAmount = getLastMatch( + batchMatchResults, + ).orders.rightOrderTakerAssetFilledAmount; + } else { + batchMatchResults.filledAmounts.push([ + orders.rightOrders[lastRightIdx], + getLastMatch(batchMatchResults).orders.rightOrderTakerAssetFilledAmount || ZERO, + 'right', + ]); + } + } + lastLeftIdx = leftIdx; + lastRightIdx = rightIdx; + // Add the latest match to the batch match results + batchMatchResults.matches.push( + simulateMatchOrders( + matchedOrders, + takerAddress, + tokenBalances, + toFullMatchTransferAmounts(transferAmounts[i]), + ), + ); + } + // The two orders indexed by lastLeftIdx and lastRightIdx were potentially + // filled; however, the TakerAssetFilledAmounts that pertain to these orders + // will not have been added to batchMatchResults, so we need to write them + // here. + batchMatchResults.filledAmounts.push([ + orders.leftOrders[lastLeftIdx], + getLastMatch(batchMatchResults).orders.leftOrderTakerAssetFilledAmount || ZERO, + 'left', + ]); + batchMatchResults.filledAmounts.push([ + orders.rightOrders[lastRightIdx], + getLastMatch(batchMatchResults).orders.rightOrderTakerAssetFilledAmount || ZERO, + 'right', + ]); + // Return the batch match results + return batchMatchResults; +} + /** * Simulates matching two orders by transferring amounts defined in * `transferAmounts` and returns the results. @@ -406,6 +584,34 @@ function transferAsset( } } +/** + * Checks that the results of `simulateBatchMatchOrders()` agrees with reality. + * @param batchMatchResults The results of a `simulateBatchMatchOrders()`. + * @param transactionReceipt The transaction receipt of a call to `matchOrders()`. + * @param actualTokenBalances The actual, on-chain token balances of known addresses. + * @param exchangeWrapper The ExchangeWrapper instance. + */ +async function assertBatchMatchResultsAsync( + batchMatchResults: BatchMatchResults, + transactionReceipt: TransactionReceiptWithDecodedLogs, + actualTokenBalances: TokenBalances, + initialTokenBalances: TokenBalances, + exchangeWrapper: ExchangeWrapper, +): Promise { + // Ensure that the batchMatchResults contain at least one match + expect(batchMatchResults.matches.length).to.be.gt(0); + // Check the fill events. + assertFillEvents( + batchMatchResults.matches.map(match => match.fills).reduce((total, fills) => total.concat(fills)), + transactionReceipt, + ); + // Check the token balances. + const newBalances = getUpdatedBalances(batchMatchResults, initialTokenBalances); + assertBalances(newBalances, actualTokenBalances); + // Check the Exchange state. + await assertPostBatchExchangeStateAsync(batchMatchResults, exchangeWrapper); +} + /** * Checks that the results of `simulateMatchOrders()` agrees with reality. * @param matchResults The results of a `simulateMatchOrders()`. @@ -528,9 +734,36 @@ function assertBalances(expectedBalances: TokenBalances, actualBalances: TokenBa } /** - * Asserts initial exchange state for matched orders. + * Asserts the initial exchange state for batch matched orders. + * @param orders Batch matched orders with intial filled amounts. + * @param exchangeWrapper ExchangeWrapper instance. + */ +async function assertBatchOrderStatesAsync( + orders: BatchMatchedOrders, + exchangeWrapper: ExchangeWrapper, +): Promise { + for (let i = 0; i < orders.leftOrders.length; i++) { + await assertOrderFilledAmountAsync( + orders.leftOrders[i], + orders.leftOrdersTakerAssetFilledAmounts[i], + 'left', + exchangeWrapper, + ); + } + for (let i = 0; i < orders.rightOrders.length; i++) { + await assertOrderFilledAmountAsync( + orders.rightOrders[i], + orders.rightOrdersTakerAssetFilledAmounts[i], + 'right', + exchangeWrapper, + ); + } +} + +/** + * Asserts the initial exchange state for matched orders. * @param orders Matched orders with intial filled amounts. - * @param exchangeWrapper ExchangeWrapper isntance. + * @param exchangeWrapper ExchangeWrapper instance. */ async function assertInitialOrderStatesAsync(orders: MatchedOrders, exchangeWrapper: ExchangeWrapper): Promise { const pairs = [ @@ -540,13 +773,23 @@ async function assertInitialOrderStatesAsync(orders: MatchedOrders, exchangeWrap await Promise.all( pairs.map(async ([order, expectedFilledAmount]) => { const side = order === orders.leftOrder ? 'left' : 'right'; - const orderHash = orderHashUtils.getOrderHashHex(order); - const actualFilledAmount = await exchangeWrapper.getTakerAssetFilledAmountAsync(orderHash); - expect(actualFilledAmount, `${side} order initial filled amount`).to.bignumber.equal(expectedFilledAmount); + await assertOrderFilledAmountAsync(order, expectedFilledAmount, side, exchangeWrapper); }), ); } +/** + * Asserts the exchange state after a call to `batchMatchOrders()`. + * @param batchMatchResults Results from a call to `simulateBatchMatchOrders()`. + * @param exchangeWrapper The ExchangeWrapper instance. + */ +async function assertPostBatchExchangeStateAsync( + batchMatchResults: BatchMatchResults, + exchangeWrapper: ExchangeWrapper, +): Promise { + await assertTriplesExchangeStateAsync(batchMatchResults.filledAmounts, exchangeWrapper); +} + /** * Asserts the exchange state after a call to `matchOrders()`. * @param matchResults Results from a call to `simulateMatchOrders()`. @@ -556,29 +799,62 @@ async function assertPostExchangeStateAsync( matchResults: MatchResults, exchangeWrapper: ExchangeWrapper, ): Promise { - const pairs = [ - [matchResults.orders.leftOrder, matchResults.orders.leftOrderTakerAssetFilledAmount], - [matchResults.orders.rightOrder, matchResults.orders.rightOrderTakerAssetFilledAmount], - ] as Array<[SignedOrder, BigNumber]>; + const triples = [ + [matchResults.orders.leftOrder, matchResults.orders.leftOrderTakerAssetFilledAmount, 'left'], + [matchResults.orders.rightOrder, matchResults.orders.rightOrderTakerAssetFilledAmount, 'right'], + ] as Array<[SignedOrder, BigNumber, string]>; + await assertTriplesExchangeStateAsync(triples, exchangeWrapper); +} + +/** + * Asserts the exchange state represented by provided sequence of triples. + * @param triples The sequence of triples to verifiy. Each triple consists + * of an `order`, a `takerAssetFilledAmount`, and a `side`, + * which will be used to determine if the exchange's state + * is valid. + * @param exchangeWrapper The ExchangeWrapper instance. + */ +async function assertTriplesExchangeStateAsync( + triples: Array<[SignedOrder, BigNumber, string]>, + exchangeWrapper: ExchangeWrapper, +): Promise { await Promise.all( - pairs.map(async ([order, expectedFilledAmount]) => { - const side = order === matchResults.orders.leftOrder ? 'left' : 'right'; - const orderInfo = await exchangeWrapper.getOrderInfoAsync(order); - // Check filled amount of order. - const actualFilledAmount = orderInfo.orderTakerAssetFilledAmount; - expect(actualFilledAmount, `${side} order final filled amount`).to.be.bignumber.equal(expectedFilledAmount); - // Check status of order. - const expectedStatus = expectedFilledAmount.isGreaterThanOrEqualTo(order.takerAssetAmount) - ? OrderStatus.FullyFilled - : OrderStatus.Fillable; - const actualStatus = orderInfo.orderStatus; - expect(actualStatus, `${side} order final status`).to.equal(expectedStatus); + triples.map(async ([order, expectedFilledAmount, side]) => { + expect(['left', 'right']).to.include(side); + await assertOrderFilledAmountAsync(order, expectedFilledAmount, side, exchangeWrapper); }), ); } /** - * Retrive the current token balances of all known addresses. + * Asserts that the provided order's fill amount and order status + * are the expected values. + * @param order The order to verify for a correct state. + * @param expectedFilledAmount The amount that the order should + * have been filled. + * @param side The side that the provided order should be matched on. + * @param exchangeWrapper The ExchangeWrapper instance. + */ +async function assertOrderFilledAmountAsync( + order: SignedOrder, + expectedFilledAmount: BigNumber, + side: string, + exchangeWrapper: ExchangeWrapper, +): Promise { + const orderInfo = await exchangeWrapper.getOrderInfoAsync(order); + // Check filled amount of order. + const actualFilledAmount = orderInfo.orderTakerAssetFilledAmount; + expect(actualFilledAmount, `${side} order final filled amount`).to.be.bignumber.equal(expectedFilledAmount); + // Check status of order. + const expectedStatus = expectedFilledAmount.isGreaterThanOrEqualTo(order.takerAssetAmount) + ? OrderStatus.FullyFilled + : OrderStatus.Fillable; + const actualStatus = orderInfo.orderStatus; + expect(actualStatus, `${side} order final status`).to.equal(expectedStatus); +} + +/** + * Retrieve the current token balances of all known addresses. * @param erc20Wrapper The ERC20Wrapper instance. * @param erc721Wrapper The ERC721Wrapper instance. * @param erc1155Wrapper The ERC1155ProxyWrapper instance. @@ -634,4 +910,61 @@ function encodeTokenBalances(obj: any): any { const keys = _.keys(obj).sort(); return _.zip(keys, keys.map(k => encodeTokenBalances(obj[k]))); } + +/** + * Gets the last match in a BatchMatchResults object. + * @param batchMatchResults The BatchMatchResults object. + * @return The last match of the results. + */ +function getLastMatch(batchMatchResults: BatchMatchResults): MatchResults { + return batchMatchResults.matches[batchMatchResults.matches.length - 1]; +} + +/** + * Get the token balances + * @param batchMatchResults The results of a batch order match + * @return The token balances results from after the batch + */ +function getUpdatedBalances(batchMatchResults: BatchMatchResults, initialTokenBalances: TokenBalances): TokenBalances { + return batchMatchResults.matches + .map(match => match.balances) + .reduce((totalBalances, balances) => aggregateBalances(totalBalances, balances, initialTokenBalances)); +} + +/** + * Takes a `totalBalances`, a `balances`, and an `initialBalances`, subtracts the `initialBalances + * from the `balances`, and then adds the result to `totalBalances`. + * @param totalBalances A set of balances to be updated with new results. + * @param balances A new set of results that deviate from the `initialBalances` by one matched + * order. Subtracting away the `initialBalances` leaves behind a diff of the + * matched orders effect on the `initialBalances`. + * @param initialBalances The token balances from before the call to `batchMatchOrders()`. + * @return The updated total balances using the derived balance difference. + */ +function aggregateBalances( + totalBalances: TokenBalances, + balances: TokenBalances, + initialBalances: TokenBalances, +): TokenBalances { + // ERC20 + for (const owner of _.keys(totalBalances.erc20)) { + for (const contract of _.keys(totalBalances.erc20[owner])) { + const difference = balances.erc20[owner][contract].minus(initialBalances.erc20[owner][contract]); + totalBalances.erc20[owner][contract] = totalBalances.erc20[owner][contract].plus(difference); + } + } + // ERC721 + for (const owner of _.keys(totalBalances.erc721)) { + for (const contract of _.keys(totalBalances.erc721[owner])) { + totalBalances.erc721[owner][contract] = _.zipWith( + totalBalances.erc721[owner][contract], + balances.erc721[owner][contract], + initialBalances.erc721[owner][contract], + (a: BigNumber, b: BigNumber, c: BigNumber) => a.plus(b.minus(c)), + ); + } + } + // TODO(jalextowle): Implement the same as the above for ERC1155 + return totalBalances; +} // tslint:disable-line:max-file-line-count diff --git a/contracts/test-utils/src/order_utils.ts b/contracts/test-utils/src/order_utils.ts index 7272b6f2d0..7dac2711d4 100644 --- a/contracts/test-utils/src/order_utils.ts +++ b/contracts/test-utils/src/order_utils.ts @@ -5,7 +5,7 @@ import { BigNumber } from '@0x/utils'; import * as _ from 'lodash'; import { constants } from './constants'; -import { CancelOrder, MatchOrder } from './types'; +import { BatchMatchOrder, CancelOrder, MatchOrder } from './types'; export const orderUtils = { getPartialAmountFloor(numerator: BigNumber, denominator: BigNumber, target: BigNumber): BigNumber { @@ -33,6 +33,19 @@ export const orderUtils = { getOrderWithoutDomain(signedOrder: SignedOrder): OrderWithoutDomain { return _.omit(signedOrder, ['signature', 'domain']) as OrderWithoutDomain; }, + createBatchMatchOrders(signedOrdersLeft: SignedOrder[], signedOrdersRight: SignedOrder[]): BatchMatchOrder { + return { + leftOrders: signedOrdersLeft.map(order => orderUtils.getOrderWithoutDomain(order)), + rightOrders: signedOrdersRight.map(order => { + const right = orderUtils.getOrderWithoutDomain(order); + right.makerAssetData = constants.NULL_BYTES; + right.takerAssetData = constants.NULL_BYTES; + return right; + }), + leftSignatures: signedOrdersLeft.map(order => order.signature), + rightSignatures: signedOrdersRight.map(order => order.signature), + }; + }, createMatchOrders(signedOrderLeft: SignedOrder, signedOrderRight: SignedOrder): MatchOrder { const fill = { left: orderUtils.getOrderWithoutDomain(signedOrderLeft), diff --git a/contracts/test-utils/src/types.ts b/contracts/test-utils/src/types.ts index 8d20f6c07f..b85f06a8b5 100644 --- a/contracts/test-utils/src/types.ts +++ b/contracts/test-utils/src/types.ts @@ -130,6 +130,13 @@ export interface CancelOrder { takerAssetCancelAmount: BigNumber; } +export interface BatchMatchOrder { + leftOrders: OrderWithoutDomain[]; + rightOrders: OrderWithoutDomain[]; + leftSignatures: string[]; + rightSignatures: string[]; +} + export interface MatchOrder { left: OrderWithoutDomain; right: OrderWithoutDomain;