From e9d49d96a61c10ec4513b03035f7607915a268c0 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Wed, 7 Aug 2019 16:37:33 -0700 Subject: [PATCH] Changed the testing style to be more assertion based rather than reference based to leverage the fixtures from the integration tests --- .../contracts/src/MixinMatchOrders.sol | 2 +- .../contracts/test/TestExchangeInternals.sol | 67 + contracts/exchange/src/reference_functions.ts | 45 +- contracts/exchange/test/dispatcher.ts | 2 +- contracts/exchange/test/internal.ts | 1339 ++++++++++++++++- .../exchange/test/match_orders_unit_tests.ts | 480 ------ 6 files changed, 1433 insertions(+), 502 deletions(-) delete mode 100644 contracts/exchange/test/match_orders_unit_tests.ts diff --git a/contracts/exchange/contracts/src/MixinMatchOrders.sol b/contracts/exchange/contracts/src/MixinMatchOrders.sol index d91e47f127..981228bc9a 100644 --- a/contracts/exchange/contracts/src/MixinMatchOrders.sol +++ b/contracts/exchange/contracts/src/MixinMatchOrders.sol @@ -724,7 +724,7 @@ contract MixinMatchOrders is address takerAddress, LibFillResults.MatchedFillResults memory matchedFillResults ) - private + internal { address leftFeeRecipientAddress = leftOrder.feeRecipientAddress; address rightFeeRecipientAddress = rightOrder.feeRecipientAddress; diff --git a/contracts/exchange/contracts/test/TestExchangeInternals.sol b/contracts/exchange/contracts/test/TestExchangeInternals.sol index 151a2858de..b233404272 100644 --- a/contracts/exchange/contracts/test/TestExchangeInternals.sol +++ b/contracts/exchange/contracts/test/TestExchangeInternals.sol @@ -39,6 +39,16 @@ contract TestExchangeInternals is Exchange(chainId) {} + function assertValidMatch( + LibOrder.Order memory leftOrder, + LibOrder.Order memory rightOrder + ) + public + view + { + _assertValidMatch(leftOrder, rightOrder); + } + function calculateFillResults( Order memory order, uint256 takerAssetFilledAmount @@ -50,6 +60,43 @@ contract TestExchangeInternals is return _calculateFillResults(order, takerAssetFilledAmount); } + function calculateCompleteFillBoth( + uint256 leftMakerAssetAmountRemaining, + uint256 leftTakerAssetAmountRemaining, + uint256 rightMakerAssetAmountRemaining, + uint256 rightTakerAssetAmountRemaining + ) + public + pure + returns (MatchedFillResults memory fillResults) + { + _calculateCompleteFillBoth( + fillResults, + leftMakerAssetAmountRemaining, + leftTakerAssetAmountRemaining, + rightMakerAssetAmountRemaining, + rightTakerAssetAmountRemaining + ); + return fillResults; + } + + function calculateCompleteRightFill( + LibOrder.Order memory leftOrder, + uint256 rightMakerAssetAmountRemaining, + uint256 rightTakerAssetAmountRemaining + ) + public + pure + returns (MatchedFillResults memory fillResults) + { + _calculateCompleteRightFill( + fillResults, + leftOrder, + rightMakerAssetAmountRemaining, + rightTakerAssetAmountRemaining + ); + } + /// @dev Call `_updateFilledState()` but first set `filled[order]` to /// `orderTakerAssetFilledAmount`. function testUpdateFilledState( @@ -82,6 +129,26 @@ contract TestExchangeInternals is _settleOrder(orderHash, order, takerAddress, fillResults); } + function settleMatchOrders( + bytes32 leftOrderHash, + bytes32 rightOrderHash, + LibOrder.Order memory leftOrder, + LibOrder.Order memory rightOrder, + address takerAddress, + LibFillResults.MatchedFillResults memory matchedFillResults + ) + public + { + _settleMatchedOrders( + leftOrderHash, + rightOrderHash, + leftOrder, + rightOrder, + takerAddress, + matchedFillResults + ); + } + /// @dev Overidden to only log arguments so we can test `_settleOrder()`. function _dispatchTransferFrom( bytes32 orderHash, diff --git a/contracts/exchange/src/reference_functions.ts b/contracts/exchange/src/reference_functions.ts index dcb0e10883..0ee4cc6d89 100644 --- a/contracts/exchange/src/reference_functions.ts +++ b/contracts/exchange/src/reference_functions.ts @@ -1,11 +1,23 @@ import { ReferenceFunctions as ExchangeLibsReferenceFunctions } from '@0x/contracts-exchange-libs'; import { constants } from '@0x/contracts-test-utils'; import { ReferenceFunctions as UtilsReferenceFunctions } from '@0x/contracts-utils'; -import { FillResults, MatchedFillResults, OrderWithoutDomain } from '@0x/types'; +import { ExchangeRevertErrors, orderHashUtils } from '@0x/order-utils'; +import { FillResults, MatchedFillResults, Order, OrderWithoutDomain } from '@0x/types'; import { BigNumber } from '@0x/utils'; const { safeGetPartialAmountCeil, safeGetPartialAmountFloor } = ExchangeLibsReferenceFunctions; -const { safeSub } = UtilsReferenceFunctions; +const { safeMul, safeSub } = UtilsReferenceFunctions; + +/** + * Ensure that there is a profitable spread. + */ +export function assertValidMatch(leftOrder: Order, rightOrder: Order): void { + if (safeMul(leftOrder.makerAssetAmount, rightOrder.makerAssetAmount) < safeMul(leftOrder.takerAssetAmount, rightOrder.takerAssetAmount)) { + const orderHashHexLeft = orderHashUtils.getOrderHashHex(leftOrder); + const orderHashHexRight = orderHashUtils.getOrderHashHex(rightOrder); + throw new ExchangeRevertErrors.NegativeSpreadError(orderHashHexLeft, orderHashHexRight); + } +} /** * Calculates amounts filled and fees paid by maker and taker. @@ -51,14 +63,8 @@ export function calculateMatchedFillResults( rightOrderTakerAssetFilledAmount, ); - // Calculate the profit from the matching - matchedFillResults.profitInLeftMakerAsset = safeSub( - matchedFillResults.left.makerAssetFilledAmount, - matchedFillResults.right.takerAssetFilledAmount, - ); - if (leftTakerAssetAmountRemaining.isGreaterThan(rightMakerAssetAmountRemaining)) { - // Case 1: Right order is fully filled + // Case 1 calculateCompleteRightFill( leftOrder, rightMakerAssetAmountRemaining, @@ -66,7 +72,7 @@ export function calculateMatchedFillResults( matchedFillResults, ); } else if (leftTakerAssetAmountRemaining.isLessThan(rightMakerAssetAmountRemaining)) { - // Case 2: Left order is fully filled + // Case 2 matchedFillResults.left.makerAssetFilledAmount = leftMakerAssetAmountRemaining; matchedFillResults.left.takerAssetFilledAmount = leftTakerAssetAmountRemaining; matchedFillResults.right.makerAssetFilledAmount = leftTakerAssetAmountRemaining; @@ -76,8 +82,7 @@ export function calculateMatchedFillResults( leftTakerAssetAmountRemaining, ); } else { - // Case 3: Both orders are fully filled. Technically, this could be captured by the above cases, but - // this calculation will be more precise since it does not include rounding. + // Case 3 calculateCompleteFillBoth( leftMakerAssetAmountRemaining, leftTakerAssetAmountRemaining, @@ -94,6 +99,12 @@ export function calculateMatchedFillResults( matchedFillResults, ); + // Calculate the profit from the matching + matchedFillResults.profitInLeftMakerAsset = safeSub( + matchedFillResults.left.makerAssetFilledAmount, + matchedFillResults.right.takerAssetFilledAmount, + ); + return matchedFillResults; } @@ -206,18 +217,20 @@ function getRemainingFillAmounts( leftOrderTakerAssetFilledAmount: BigNumber, rightOrderTakerAssetFilledAmount: BigNumber, ): [ BigNumber, BigNumber, BigNumber, BigNumber ] { + const leftTakerAssetRemaining = safeSub(leftOrder.takerAssetAmount, leftOrderTakerAssetFilledAmount); + const rightTakerAssetRemaining = safeSub(rightOrder.takerAssetAmount, rightOrderTakerAssetFilledAmount); return [ safeGetPartialAmountFloor( leftOrder.makerAssetAmount, leftOrder.takerAssetAmount, - leftOrderTakerAssetFilledAmount + leftTakerAssetRemaining, ), - safeSub(leftOrder.takerAssetAmount, leftOrderTakerAssetFilledAmount), + leftTakerAssetRemaining, safeGetPartialAmountFloor( rightOrder.makerAssetAmount, rightOrder.takerAssetAmount, - rightOrderTakerAssetFilledAmount + rightTakerAssetRemaining, ), - safeSub(rightOrder.takerAssetAmount, rightOrderTakerAssetFilledAmount), + rightTakerAssetRemaining, ]; } diff --git a/contracts/exchange/test/dispatcher.ts b/contracts/exchange/test/dispatcher.ts index c547d44f17..71e8ab182e 100644 --- a/contracts/exchange/test/dispatcher.ts +++ b/contracts/exchange/test/dispatcher.ts @@ -271,7 +271,7 @@ describe('AssetProxyDispatcher', () => { return expect(tx).to.revertWith(expectedError); }); - it('should should revert with the correct error when assetData length < 4 bytes', async () => { + it('should revert with the correct error when assetData length < 4 bytes', async () => { await assetProxyDispatcher.registerAssetProxy.awaitTransactionSuccessAsync(erc20Proxy.address, { from: owner, }); diff --git a/contracts/exchange/test/internal.ts b/contracts/exchange/test/internal.ts index 4467beaf2b..71ebae0545 100644 --- a/contracts/exchange/test/internal.ts +++ b/contracts/exchange/test/internal.ts @@ -6,12 +6,15 @@ import { expect, hexRandom, LogDecoder, + OrderFactory, + orderUtils, testCombinatoriallyWithReferenceFunc, uint256Values, } from '@0x/contracts-test-utils'; -import { LibMathRevertErrors } from '@0x/order-utils'; -import { FillResults, OrderWithoutDomain as Order } from '@0x/types'; +import { ExchangeRevertErrors, LibMathRevertErrors, orderHashUtils } from '@0x/order-utils'; +import { FillResults, MatchedFillResults, OrderWithoutDomain as Order, SignedOrder } from '@0x/types'; import { BigNumber, SafeMathRevertErrors } from '@0x/utils'; +import { Web3Wrapper } from '@0x/web3-wrapper'; import { LogWithDecodedArgs } from 'ethereum-types'; import * as _ from 'lodash'; @@ -23,7 +26,7 @@ import { TestExchangeInternalsFillEventArgs, } from '../src'; -blockchainTests('Exchange core internal functions', env => { +blockchainTests.only('Exchange core internal functions', env => { const CHAIN_ID = 1337; const ONE_ETHER = constants.ONE_ETHER; const EMPTY_ORDER: Order = { @@ -48,9 +51,15 @@ blockchainTests('Exchange core internal functions', env => { let testExchange: TestExchangeInternalsContract; let logDecoder: LogDecoder; let senderAddress: string; + let makerAddressLeft: string; + let makerAddressRight: string; + let orderFactoryLeft: OrderFactory; + let orderFactoryRight: OrderFactory; before(async () => { - [senderAddress] = await env.getAccountAddressesAsync(); + const accounts = await env.getAccountAddressesAsync() + const addresses = ([senderAddress, makerAddressLeft, makerAddressRight] = accounts); + testExchange = await TestExchangeInternalsContract.deployFrom0xArtifactAsync( artifacts.TestExchangeInternals, env.provider, @@ -58,6 +67,340 @@ blockchainTests('Exchange core internal functions', env => { new BigNumber(CHAIN_ID), ); logDecoder = new LogDecoder(env.web3Wrapper, artifacts); + + const domain = { + verifyingContractAddress: testExchange.address, + chainId: 1337, // The chain id for the isolated exchange + }; + + const leftMakerAssetData = randomAssetData(); + const leftTakerAssetData = randomAssetData(); + + // Create default order parameters + const defaultOrderParamsLeft = { + ...constants.STATIC_ORDER_PARAMS, + makerAddress: makerAddressLeft, + makerAssetData: leftMakerAssetData, + takerAssetData: leftTakerAssetData, + makerFeeAssetData: randomAssetData(), + takerFeeAssetData: randomAssetData(), + feeRecipientAddress: randomAddress(), + domain, + }; + const defaultOrderParamsRight = { + ...constants.STATIC_ORDER_PARAMS, + makerAddress: makerAddressRight, + makerAssetData: leftTakerAssetData, + takerAssetData: leftMakerAssetData, + makerFeeAssetData: randomAssetData(), + takerFeeAssetData: randomAssetData(), + feeRecipientAddress: randomAddress(), + domain, + }; + const privateKeyLeft = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(makerAddressLeft)]; + orderFactoryLeft = new OrderFactory(privateKeyLeft, defaultOrderParamsLeft); + const privateKeyRight = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(makerAddressRight)]; + orderFactoryRight = new OrderFactory(privateKeyRight, defaultOrderParamsRight); + }); + + blockchainTests('assertValidMatch', () => { + it('should revert if the prices of the left order is less than the price of the right order', async () => { + const leftOrder = await orderFactoryLeft.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(49, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(100, 18), + }); + const rightOrder = await orderFactoryRight.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(100, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(50, 18), + }); + const orderHashHexLeft = orderHashUtils.getOrderHashHex(leftOrder); + const orderHashHexRight = orderHashUtils.getOrderHashHex(rightOrder); + const expectedError = new ExchangeRevertErrors.NegativeSpreadError(orderHashHexLeft, orderHashHexRight); + return expect(testExchange.assertValidMatch.callAsync(leftOrder, rightOrder)).to.revertWith(expectedError); + }); + + it('should succeed if the prices of the left and right orders are equal', async () => { + const leftOrder = await orderFactoryLeft.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(50, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(100, 18), + }); + const rightOrder = await orderFactoryRight.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(100, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(50, 18), + }); + return expect(testExchange.assertValidMatch.callAsync(leftOrder, rightOrder)).to.be.fulfilled(''); + }); + + it('should succeed if the price of the left order is higher than the price of the right', async () => { + const leftOrder = await orderFactoryLeft.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(50, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(100, 18), + }); + const rightOrder = await orderFactoryRight.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(100, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(50, 18), + }); + return expect(testExchange.assertValidMatch.callAsync(leftOrder, rightOrder)).to.be.fulfilled(''); + }); + }); + + blockchainTests('calculateCompleteFillBoth', () => { + /** + * Asserts that the actual result of calling `calculateCompleteFillBoth()` is as expected. + */ + async function assertCalculateCompleteFillBoth( + expectedMatchedFillResults: MatchedFillResults, + leftMakerAssetAmountRemaining: BigNumber, + leftTakerAssetAmountRemaining: BigNumber, + rightMakerAssetAmountRemaining: BigNumber, + rightTakerAssetAmountRemaining: BigNumber, + ): Promise { + const actualMatchedFillResults = await testExchange.calculateCompleteFillBoth.callAsync( + leftMakerAssetAmountRemaining, + leftTakerAssetAmountRemaining, + rightMakerAssetAmountRemaining, + rightTakerAssetAmountRemaining, + ); + expect(actualMatchedFillResults).to.be.deep.eq(expectedMatchedFillResults); + } + + it('should assign everything to zero if all inputs are zero', async () => { + const expectedMatchedFillResults = { + left: { + makerAssetFilledAmount: constants.ZERO_AMOUNT, + takerAssetFilledAmount: constants.ZERO_AMOUNT, + makerFeePaid: constants.ZERO_AMOUNT, + takerFeePaid: constants.ZERO_AMOUNT, + }, + right: { + makerAssetFilledAmount: constants.ZERO_AMOUNT, + takerAssetFilledAmount: constants.ZERO_AMOUNT, + makerFeePaid: constants.ZERO_AMOUNT, + takerFeePaid: constants.ZERO_AMOUNT, + }, + profitInLeftMakerAsset: constants.ZERO_AMOUNT, + profitInRightMakerAsset: constants.ZERO_AMOUNT, + }; + await assertCalculateCompleteFillBoth( + expectedMatchedFillResults, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + ); + }); + + // Note: This call would never be able to be made through our contracts, since these orders will not actually be + // fully filled. This is just a test case to make sure. + it('should correctly update the fillResults with nonzero input', async () => { + const expectedMatchedFillResults = { + left: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(17, 0), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(98, 0), + makerFeePaid: constants.ZERO_AMOUNT, + takerFeePaid: constants.ZERO_AMOUNT, + }, + right: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(75, 0), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(13, 0), + makerFeePaid: constants.ZERO_AMOUNT, + takerFeePaid: constants.ZERO_AMOUNT, + }, + profitInLeftMakerAsset: constants.ZERO_AMOUNT, + profitInRightMakerAsset: constants.ZERO_AMOUNT, + }; + await assertCalculateCompleteFillBoth( + expectedMatchedFillResults, + new BigNumber(17), + new BigNumber(98), + new BigNumber(75), + new BigNumber(13), + ); + }); + + it('should correctly update the fillResults with nonzero input', async () => { + const expectedMatchedFillResults = { + left: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(5, 0), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(10, 0), + makerFeePaid: constants.ZERO_AMOUNT, + takerFeePaid: constants.ZERO_AMOUNT, + }, + right: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(10, 0), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(5, 0), + makerFeePaid: constants.ZERO_AMOUNT, + takerFeePaid: constants.ZERO_AMOUNT, + }, + profitInLeftMakerAsset: constants.ZERO_AMOUNT, + profitInRightMakerAsset: constants.ZERO_AMOUNT, + }; + await assertCalculateCompleteFillBoth( + expectedMatchedFillResults, + new BigNumber(5), + new BigNumber(10), + new BigNumber(10), + new BigNumber(5), + ); + }); + + it('should correctly update the fillResults with nonzero input', async () => { + const expectedMatchedFillResults = { + left: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + makerFeePaid: constants.ZERO_AMOUNT, + takerFeePaid: constants.ZERO_AMOUNT, + }, + right: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + makerFeePaid: constants.ZERO_AMOUNT, + takerFeePaid: constants.ZERO_AMOUNT, + }, + profitInLeftMakerAsset: constants.ZERO_AMOUNT, + profitInRightMakerAsset: constants.ZERO_AMOUNT, + }; + await assertCalculateCompleteFillBoth( + expectedMatchedFillResults, + Web3Wrapper.toBaseUnitAmount(5, 18), + Web3Wrapper.toBaseUnitAmount(10, 18), + Web3Wrapper.toBaseUnitAmount(10, 18), + Web3Wrapper.toBaseUnitAmount(5, 18), + ); + }); + + it('should correctly update the fillResults with nonzero input', async () => { + const expectedMatchedFillResults = { + left: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + makerFeePaid: constants.ZERO_AMOUNT, + takerFeePaid: constants.ZERO_AMOUNT, + }, + right: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + makerFeePaid: constants.ZERO_AMOUNT, + takerFeePaid: constants.ZERO_AMOUNT, + }, + profitInLeftMakerAsset: constants.ZERO_AMOUNT, + profitInRightMakerAsset: constants.ZERO_AMOUNT, + }; + await assertCalculateCompleteFillBoth( + expectedMatchedFillResults, + Web3Wrapper.toBaseUnitAmount(5, 18), + Web3Wrapper.toBaseUnitAmount(10, 18), + Web3Wrapper.toBaseUnitAmount(10, 18), + Web3Wrapper.toBaseUnitAmount(2, 18), + ); + }); + }); + + blockchainTests('calculateCompleteRightFill', () => { + /** + * Asserts that the results of calling `calculateCompleteRightFill()` are consistent with the expected results. + */ + async function assertCalculateCompleteRightFill( + expectedMatchedFillResults: MatchedFillResults, + leftOrder: SignedOrder, + rightMakerAssetAmountRemaining: BigNumber, + rightTakerAssetAmountRemaining: BigNumber, + ): Promise { + const actualMatchedFillResults = await testExchange.calculateCompleteRightFill.callAsync( + leftOrder, + rightMakerAssetAmountRemaining, + rightTakerAssetAmountRemaining, + ); + expect(actualMatchedFillResults).to.be.deep.eq(expectedMatchedFillResults); + } + + it('should correctly calculate the complete right fill', async () => { + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(17, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(98, 0), + }); + const expectedMatchedFillResults = { + left: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(13, 0), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(75, 0), + makerFeePaid: constants.ZERO_AMOUNT, + takerFeePaid: constants.ZERO_AMOUNT, + }, + right: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(75, 0), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(13, 0), + makerFeePaid: constants.ZERO_AMOUNT, + takerFeePaid: constants.ZERO_AMOUNT, + }, + profitInLeftMakerAsset: constants.ZERO_AMOUNT, + profitInRightMakerAsset: constants.ZERO_AMOUNT, + }; + await assertCalculateCompleteRightFill( + expectedMatchedFillResults, + signedOrderLeft, + Web3Wrapper.toBaseUnitAmount(75, 0), + Web3Wrapper.toBaseUnitAmount(13, 0), + ); + }); + + it('should correctly calculate the complete right fill', async () => { + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(12, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(97, 0), + }); + const expectedMatchedFillResults = { + left: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(11, 0), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(89, 0), + makerFeePaid: constants.ZERO_AMOUNT, + takerFeePaid: constants.ZERO_AMOUNT, + }, + right: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(89, 0), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(1, 0), + makerFeePaid: constants.ZERO_AMOUNT, + takerFeePaid: constants.ZERO_AMOUNT, + }, + profitInLeftMakerAsset: constants.ZERO_AMOUNT, + profitInRightMakerAsset: constants.ZERO_AMOUNT, + }; + await assertCalculateCompleteRightFill( + expectedMatchedFillResults, + signedOrderLeft, + Web3Wrapper.toBaseUnitAmount(89, 0), + Web3Wrapper.toBaseUnitAmount(1, 0), + ); + }); + + it('should correctly calculate the complete right fill', async () => { + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(50, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(100, 18), + }); + const expectedMatchedFillResults = { + left: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + makerFeePaid: constants.ZERO_AMOUNT, + takerFeePaid: constants.ZERO_AMOUNT, + }, + right: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + makerFeePaid: constants.ZERO_AMOUNT, + takerFeePaid: constants.ZERO_AMOUNT, + }, + profitInLeftMakerAsset: constants.ZERO_AMOUNT, + profitInRightMakerAsset: constants.ZERO_AMOUNT, + }; + await assertCalculateCompleteRightFill( + expectedMatchedFillResults, + signedOrderLeft, + Web3Wrapper.toBaseUnitAmount(10, 18), + Web3Wrapper.toBaseUnitAmount(2, 18), + ); + }); }); blockchainTests('calculateFillResults', () => { @@ -274,6 +617,759 @@ blockchainTests('Exchange core internal functions', env => { }); }); + blockchainTests('calculateMatchedFillResults', async () => { + /** + * Asserts that the results of calling `calculateMatchedFillResults()` is consistent with the results that are expected. + */ + async function assertCalculateMatchedFillResultsAsync( + expectedMatchedFillResults: MatchedFillResults, + leftOrder: SignedOrder, + rightOrder: SignedOrder, + leftOrderTakerAssetFilledAmount: BigNumber, + rightOrderTakerAssetFilledAmount: BigNumber, + from?: string, + ): Promise { + const actualMatchedFillResults = await testExchange.calculateMatchedFillResults.callAsync( + leftOrder, + rightOrder, + leftOrderTakerAssetFilledAmount, + rightOrderTakerAssetFilledAmount, + false, + { from }, + ); + expect(actualMatchedFillResults).to.be.deep.eq(expectedMatchedFillResults); + } + + it('should correctly calculate the results when only the right order is fully filled', async () => { + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(17, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(98, 0), + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(75, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(13, 0), + }); + const expectedMatchedFillResults = { + left: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(13, 0), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(75, 0), + makerFeePaid: Web3Wrapper.toBaseUnitAmount( + new BigNumber('76.4705882352941176'), + 16, + ), + takerFeePaid: Web3Wrapper.toBaseUnitAmount( + new BigNumber('76.5306122448979591'), + 16, + ), + }, + right: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(75, 0), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(13, 0), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + }, + profitInLeftMakerAsset: constants.ZERO_AMOUNT, + profitInRightMakerAsset: constants.ZERO_AMOUNT, + }; + await assertCalculateMatchedFillResultsAsync( + expectedMatchedFillResults, + signedOrderLeft, + signedOrderRight, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + ); + }); + + it('should correctly calculate the results when only the left order is fully filled', async () => { + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(15, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(90, 0), + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(97, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(14, 0), + }); + const expectedMatchedFillResults = { + left: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(15, 0), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(90, 0), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + }, + right: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(90, 0), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(13, 0), + makerFeePaid: Web3Wrapper.toBaseUnitAmount( + new BigNumber('92.7835051546391752'), + 16, + ), // 92.85% + takerFeePaid: Web3Wrapper.toBaseUnitAmount( + new BigNumber('92.8571428571428571'), + 16, + ), // 92.85% + }, + profitInLeftMakerAsset: Web3Wrapper.toBaseUnitAmount(2, 0), + profitInRightMakerAsset: constants.ZERO_AMOUNT, + }; + await assertCalculateMatchedFillResultsAsync( + expectedMatchedFillResults, + signedOrderLeft, + signedOrderRight, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + ); + }); + + it('should give right maker a better price when rounding', async () => { + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAddress: makerAddressLeft, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(16, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(22, 0), + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(83, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(49, 0), + }); + const expectedMatchedFillResults = { + left: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(16, 0), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(22, 0), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + }, + right: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(22, 0), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(13, 0), + makerFeePaid: Web3Wrapper.toBaseUnitAmount( + new BigNumber('26.5060240963855421'), + 16, + ), // 26.506% + takerFeePaid: Web3Wrapper.toBaseUnitAmount( + new BigNumber('26.5306122448979591'), + 16, + ), // 26.531% + }, + profitInLeftMakerAsset: Web3Wrapper.toBaseUnitAmount(3, 0), + profitInRightMakerAsset: constants.ZERO_AMOUNT, + }; + await assertCalculateMatchedFillResultsAsync( + expectedMatchedFillResults, + signedOrderLeft, + signedOrderRight, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + ); + }); + + it('should give left maker a better sell price when rounding', async () => { + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAddress: makerAddressLeft, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(12, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(97, 0), + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(89, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(1, 0), + }); + const expectedMatchedFillResults = { + left: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(11, 0), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(89, 0), + makerFeePaid: Web3Wrapper.toBaseUnitAmount( + new BigNumber('91.6666666666666666'), + 16, + ), // 91.6% + takerFeePaid: Web3Wrapper.toBaseUnitAmount( + new BigNumber('91.7525773195876288'), + 16, + ), // 91.75% + }, + right: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(89, 0), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(1, 0), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + }, + profitInLeftMakerAsset: Web3Wrapper.toBaseUnitAmount(10, 0), + profitInRightMakerAsset: constants.ZERO_AMOUNT, + }; + await assertCalculateMatchedFillResultsAsync( + expectedMatchedFillResults, + signedOrderLeft, + signedOrderRight, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + ); + }) + + it('Should give right maker and right taker a favorable fee price when rounding', async () => { + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAddress: makerAddressLeft, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(16, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(22, 0), + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(83, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(49, 0), + makerFee: Web3Wrapper.toBaseUnitAmount(10000, 0), + takerFee: Web3Wrapper.toBaseUnitAmount(10000, 0), + }); + const expectedMatchedFillResults = { + left: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(16, 0), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(22, 0), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + }, + right: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(22, 0), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(13, 0), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(2650, 0), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(2653, 0), + }, + profitInLeftMakerAsset: Web3Wrapper.toBaseUnitAmount(3, 0), + profitInRightMakerAsset: constants.ZERO_AMOUNT, + }; + await assertCalculateMatchedFillResultsAsync( + expectedMatchedFillResults, + signedOrderLeft, + signedOrderRight, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + ); + }); + + it('Should give left maker and left taker a favorable fee price when rounding', async () => { + // Create orders to match + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAddress: makerAddressLeft, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(12, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(97, 0), + makerFee: Web3Wrapper.toBaseUnitAmount(10000, 0), + takerFee: Web3Wrapper.toBaseUnitAmount(10000, 0), + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(89, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(1, 0), + }); + const expectedMatchedFillResults = { + left: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(11, 0), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(89, 0), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(9166, 0), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(9175, 0), + }, + right: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(89, 0), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(1, 0), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + }, + profitInLeftMakerAsset: Web3Wrapper.toBaseUnitAmount(10, 0), + profitInRightMakerAsset: constants.ZERO_AMOUNT, + }; + await assertCalculateMatchedFillResultsAsync( + expectedMatchedFillResults, + signedOrderLeft, + signedOrderRight, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + ); + }); + + it('Should transfer correct amounts when right order fill amount deviates from amount derived by `Exchange.fillOrder`', async () => { + // Create orders to match + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAddress: makerAddressLeft, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(1000, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(1005, 0), + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(2126, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(1063, 0), + }); + const expectedMatchedFillResults = { + left: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(1000, 0), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(1005, 0), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + }, + right: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(1005, 0), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(503, 0), + makerFeePaid: Web3Wrapper.toBaseUnitAmount( + new BigNumber('47.2718720602069614'), + 16, + ), // 47.27% + takerFeePaid: Web3Wrapper.toBaseUnitAmount( + new BigNumber('47.3189087488240827'), + 16, + ), // 47.31% + }, + profitInLeftMakerAsset: Web3Wrapper.toBaseUnitAmount(497, 0), + profitInRightMakerAsset: constants.ZERO_AMOUNT, + }; + await assertCalculateMatchedFillResultsAsync( + expectedMatchedFillResults, + signedOrderLeft, + signedOrderRight, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + ); + }); + + it('should transfer the correct amounts when orders completely fill each other', async () => { + // Create orders to match + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + }); + const expectedMatchedFillResults = { + left: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + }, + right: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + }, + profitInLeftMakerAsset: Web3Wrapper.toBaseUnitAmount(3, 18), + profitInRightMakerAsset: constants.ZERO_AMOUNT, + }; + await assertCalculateMatchedFillResultsAsync( + expectedMatchedFillResults, + signedOrderLeft, + signedOrderRight, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + ); + }); + + it('should transfer the correct amounts when orders completely fill each other and taker doesnt take a profit', async () => { + // Create orders to match + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + }); + const expectedMatchedFillResults = { + left: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + }, + right: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + }, + profitInLeftMakerAsset: Web3Wrapper.toBaseUnitAmount(0, 0), + profitInRightMakerAsset: constants.ZERO_AMOUNT, + }; + await assertCalculateMatchedFillResultsAsync( + expectedMatchedFillResults, + signedOrderLeft, + signedOrderRight, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + ); + }); + + it('should transfer the correct amounts when left order is completely filled and right order is partially filled', async () => { + // Create orders to match + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(20, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(4, 18), + }); + const expectedMatchedFillResults = { + left: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + }, + right: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(50, 16), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(50, 16), + }, + profitInLeftMakerAsset: Web3Wrapper.toBaseUnitAmount(3, 18), + profitInRightMakerAsset: constants.ZERO_AMOUNT, + }; + await assertCalculateMatchedFillResultsAsync( + expectedMatchedFillResults, + signedOrderLeft, + signedOrderRight, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + ); + }); + + it('should transfer the correct amounts when right order is completely filled and left order is partially filled', async () => { + // Create orders to match + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(50, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(100, 18), + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + }); + const expectedMatchedFillResults = { + left: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(10, 16), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(10, 16), + }, + right: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + }, + profitInLeftMakerAsset: Web3Wrapper.toBaseUnitAmount(3, 18), + profitInRightMakerAsset: constants.ZERO_AMOUNT, + }; + await assertCalculateMatchedFillResultsAsync( + expectedMatchedFillResults, + signedOrderLeft, + signedOrderRight, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + ); + }); + it('should transfer the correct amounts if fee recipient is the same across both matched orders', async () => { + const feeRecipientAddress = randomAddress(); + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + feeRecipientAddress, + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + feeRecipientAddress, + }); + const expectedMatchedFillResults = { + left: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + }, + right: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + }, + profitInLeftMakerAsset: Web3Wrapper.toBaseUnitAmount(3, 18), + profitInRightMakerAsset: constants.ZERO_AMOUNT, + }; + await assertCalculateMatchedFillResultsAsync( + expectedMatchedFillResults, + signedOrderLeft, + signedOrderRight, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + ); + }); + + it('should transfer the correct amounts if taker == leftMaker', async () => { + // Create orders to match + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + }); + const expectedMatchedFillResults = { + left: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + }, + right: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + }, + profitInLeftMakerAsset: Web3Wrapper.toBaseUnitAmount(3, 18), + profitInRightMakerAsset: constants.ZERO_AMOUNT, + }; + await assertCalculateMatchedFillResultsAsync( + expectedMatchedFillResults, + signedOrderLeft, + signedOrderRight, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + signedOrderLeft.makerAddress, + ); + }); + + it('should transfer the correct amounts if taker == leftMaker', async () => { + // Create orders to match + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + }); + const expectedMatchedFillResults = { + left: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + }, + right: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + }, + profitInLeftMakerAsset: Web3Wrapper.toBaseUnitAmount(3, 18), + profitInRightMakerAsset: constants.ZERO_AMOUNT, + }; + await assertCalculateMatchedFillResultsAsync( + expectedMatchedFillResults, + signedOrderLeft, + signedOrderRight, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + signedOrderRight.makerAddress, + ); + }); + + it('should transfer the correct amounts if taker == leftFeeRecipient', async () => { + const feeRecipientAddressLeft = randomAddress(); + + // Create orders to match + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + }); + const expectedMatchedFillResults = { + left: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + }, + right: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + }, + profitInLeftMakerAsset: Web3Wrapper.toBaseUnitAmount(3, 18), + profitInRightMakerAsset: constants.ZERO_AMOUNT, + }; + await assertCalculateMatchedFillResultsAsync( + expectedMatchedFillResults, + signedOrderLeft, + signedOrderRight, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + feeRecipientAddressLeft, + ); + }); + + it('should transfer the correct amounts if taker == rightFeeRecipient', async () => { + const feeRecipientAddressRight = randomAddress(); + + // Create orders to match + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + const expectedMatchedFillResults = { + left: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + }, + right: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + }, + profitInLeftMakerAsset: Web3Wrapper.toBaseUnitAmount(3, 18), + profitInRightMakerAsset: constants.ZERO_AMOUNT, + }; + await assertCalculateMatchedFillResultsAsync( + expectedMatchedFillResults, + signedOrderLeft, + signedOrderRight, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + feeRecipientAddressRight, + ); + }); + + it('should transfer the correct amounts if leftMaker == leftFeeRecipient && rightMaker == rightFeeRecipient', async () => { + // Create orders to match + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + feeRecipientAddress: makerAddressLeft, + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + feeRecipientAddress: makerAddressRight, + }); + const expectedMatchedFillResults = { + left: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + }, + right: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + }, + profitInLeftMakerAsset: Web3Wrapper.toBaseUnitAmount(3, 18), + profitInRightMakerAsset: constants.ZERO_AMOUNT, + }; + await assertCalculateMatchedFillResultsAsync( + expectedMatchedFillResults, + signedOrderLeft, + signedOrderRight, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + ); + }); + + it('should transfer the correct amounts if leftMaker == leftFeeRecipient && leftMakerFeeAsset == leftTakerAsset', async () => { + // Create orders to match + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + }); + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + makerFeeAssetData: signedOrderRight.makerAssetData, + feeRecipientAddress: makerAddressLeft, + }); + const expectedMatchedFillResults = { + left: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + }, + right: { + makerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + takerAssetFilledAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + makerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + takerFeePaid: Web3Wrapper.toBaseUnitAmount(100, 16), + }, + profitInLeftMakerAsset: Web3Wrapper.toBaseUnitAmount(3, 18), + profitInRightMakerAsset: constants.ZERO_AMOUNT, + }; + await assertCalculateMatchedFillResultsAsync( + expectedMatchedFillResults, + signedOrderLeft, + signedOrderRight, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + ); + }); + + /* + it('should transfer the correct amounts if rightMaker == rightFeeRecipient && rightMakerFeeAsset == rightTakerAsset', async () => { + // Create orders to match + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + makerFeeAssetData: signedOrderLeft.makerAssetData, + feeRecipientAddress: makerAddressRight, + }); + await testCalculateMatchedFillResultsAsync( + matchOrders, + signedOrderLeft, + signedOrderRight, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + ); + }); + + it('should transfer the correct amounts if rightMaker == rightFeeRecipient && rightTakerAsset == rightMakerFeeAsset && leftMaker == leftFeeRecipient && leftTakerAsset == leftMakerFeeAsset', async () => { + // Create orders to match + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + feeRecipientAddress: makerAddressLeft, + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + makerFeeAssetData: signedOrderLeft.makerAssetData, + feeRecipientAddress: makerAddressRight, + }); + await testCalculateMatchedFillResultsAsync( + matchOrders, + signedOrderLeft, + signedOrderRight, + constants.ZERO_AMOUNT, + constants.ZERO_AMOUNT, + ); + }); + */ + }); + blockchainTests.resets('updateFilledState', async () => { const ORDER_DEFAULTS = { senderAddress: randomAddress(), @@ -439,5 +1535,240 @@ blockchainTests('Exchange core internal functions', env => { expect(logs[3].args.amount).to.bignumber.eq(fillResults.makerFeePaid); }); }); + + blockchainTests('settleMatchOrders', () => { + const getLeftOrder = () => { + return { + senderAddress: randomAddress(), + makerAddress: randomAddress(), + takerAddress: randomAddress(), + makerFee: ONE_ETHER.times(0.001), + takerFee: ONE_ETHER.times(0.003), + makerAssetAmount: ONE_ETHER, + takerAssetAmount: ONE_ETHER.times(0.5), + makerAssetData: randomAssetData(), + takerAssetData: randomAssetData(), + makerFeeAssetData: randomAssetData(), + takerFeeAssetData: randomAssetData(), + salt: new BigNumber(_.random(0, 1e8)), + feeRecipientAddress: randomAddress(), + expirationTimeSeconds: new BigNumber(_.random(0, 1e8)), + }; + }; + + const getRightOrder = () => { + return { + senderAddress: randomAddress(), + makerAddress: randomAddress(), + takerAddress: randomAddress(), + makerFee: ONE_ETHER.times(0.002), + takerFee: ONE_ETHER.times(0.004), + makerAssetAmount: ONE_ETHER, + takerAssetAmount: ONE_ETHER.times(0.6), + makerAssetData: randomAssetData(), + takerAssetData: randomAssetData(), + makerFeeAssetData: randomAssetData(), + takerFeeAssetData: randomAssetData(), + salt: new BigNumber(_.random(0, 1e8)), + feeRecipientAddress: randomAddress(), + expirationTimeSeconds: new BigNumber(_.random(0, 1e8)), + }; + }; + + it('calls `_dispatchTransferFrom()` from in the right order when the fee recipients and taker fee asset data are the same', async () => { + const leftOrder = getLeftOrder(); + const rightOrder = getRightOrder(); + const leftOrderHash = randomHash(); + const rightOrderHash = randomHash(); + const takerAddress = randomAddress(); + const matchedFillResults = { + left: { + makerAssetFilledAmount: ONE_ETHER.times(2), + takerAssetFilledAmount: ONE_ETHER.times(10), + makerFeePaid: ONE_ETHER.times(0.01), + takerFeePaid: ONE_ETHER.times(0.025), + }, + right: { + takerAssetFilledAmount: ONE_ETHER.times(20), + makerAssetFilledAmount: ONE_ETHER.times(4), + makerFeePaid: ONE_ETHER.times(0.02), + takerFeePaid: ONE_ETHER.times(0.05), + }, + profitInLeftMakerAsset: ONE_ETHER, + profitInRightMakerAsset: ONE_ETHER.times(2), + }; + + // Set the fee recipient addresses and the taker fee asset data fields to be the same + rightOrder.feeRecipientAddress = leftOrder.feeRecipientAddress; + rightOrder.takerFeeAssetData = leftOrder.takerFeeAssetData; + + // Call settleMatchOrders and collect the logs + const receipt = await logDecoder.getTxWithDecodedLogsAsync( + await testExchange.settleMatchOrders.sendTransactionAsync( + leftOrderHash, + rightOrderHash, + leftOrder, + rightOrder, + takerAddress, + matchedFillResults, + ), + ); + const logs = receipt.logs as Array< + LogWithDecodedArgs + >; + + // Ensure that the logs have the correct lengths and names + expect(logs.length).to.be.eq(7); + expect(_.every(logs, log => log.event === 'DispatchTransferFromCalled')).to.be.true(); + + // Right maker asset -> left maker + expect(logs[0].args.orderHash).to.be.eq(rightOrderHash); + expect(logs[0].args.assetData).to.be.eq(rightOrder.makerAssetData); + expect(logs[0].args.from).to.be.eq(rightOrder.makerAddress); + expect(logs[0].args.to).to.be.eq(leftOrder.makerAddress); + expect(logs[0].args.amount).bignumber.to.be.eq(matchedFillResults.left.takerAssetFilledAmount); + + // Left maker asset -> right maker + expect(logs[1].args.orderHash).to.be.eq(leftOrderHash); + expect(logs[1].args.assetData).to.be.eq(leftOrder.makerAssetData); + expect(logs[1].args.from).to.be.eq(leftOrder.makerAddress); + expect(logs[1].args.to).to.be.eq(rightOrder.makerAddress); + expect(logs[1].args.amount).bignumber.to.be.eq(matchedFillResults.right.takerAssetFilledAmount); + + // Right maker fee -> right fee recipient + expect(logs[2].args.orderHash).to.be.eq(rightOrderHash); + expect(logs[2].args.assetData).to.be.eq(rightOrder.makerFeeAssetData); + expect(logs[2].args.from).to.be.eq(rightOrder.makerAddress); + expect(logs[2].args.to).to.be.eq(rightOrder.feeRecipientAddress); + expect(logs[2].args.amount).bignumber.to.be.eq(matchedFillResults.right.makerFeePaid); + + // Left maker fee -> left fee recipient + expect(logs[3].args.orderHash).to.be.eq(leftOrderHash); + expect(logs[3].args.assetData).to.be.eq(leftOrder.makerFeeAssetData); + expect(logs[3].args.from).to.be.eq(leftOrder.makerAddress); + expect(logs[3].args.to).to.be.eq(leftOrder.feeRecipientAddress); + expect(logs[3].args.amount).bignumber.to.be.eq(matchedFillResults.left.makerFeePaid); + + // Left maker -> taker profit + expect(logs[4].args.orderHash).to.be.eq(leftOrderHash); + expect(logs[4].args.assetData).to.be.eq(leftOrder.makerAssetData); + expect(logs[4].args.from).to.be.eq(leftOrder.makerAddress); + expect(logs[4].args.to).to.be.eq(takerAddress); + expect(logs[4].args.amount).bignumber.to.be.eq(matchedFillResults.profitInLeftMakerAsset); + + // right maker -> taker profit + expect(logs[5].args.orderHash).to.be.eq(rightOrderHash); + expect(logs[5].args.assetData).to.be.eq(rightOrder.makerAssetData); + expect(logs[5].args.from).to.be.eq(rightOrder.makerAddress); + expect(logs[5].args.to).to.be.eq(takerAddress); + expect(logs[5].args.amount).bignumber.to.be.eq(matchedFillResults.profitInRightMakerAsset); + + // taker fees -> fee recipient + expect(logs[6].args.orderHash).to.be.eq(leftOrderHash); + expect(logs[6].args.assetData).to.be.eq(leftOrder.takerFeeAssetData); + expect(logs[6].args.from).to.be.eq(takerAddress); + expect(logs[6].args.to).to.be.eq(leftOrder.feeRecipientAddress); + expect(logs[6].args.amount).bignumber.to.be.eq(ONE_ETHER.times(0.075)); + }); + + it('calls `_dispatchTransferFrom()` from in the right order when the fee recipients and taker fee asset data are not the same', async () => { + const leftOrder = getLeftOrder(); + const rightOrder = getRightOrder(); + const leftOrderHash = randomHash(); + const rightOrderHash = randomHash(); + const takerAddress = randomAddress(); + const matchedFillResults = { + left: { + makerAssetFilledAmount: ONE_ETHER.times(2), + takerAssetFilledAmount: ONE_ETHER.times(10), + makerFeePaid: ONE_ETHER.times(0.01), + takerFeePaid: ONE_ETHER.times(0.025), + }, + right: { + takerAssetFilledAmount: ONE_ETHER.times(20), + makerAssetFilledAmount: ONE_ETHER.times(4), + makerFeePaid: ONE_ETHER.times(0.02), + takerFeePaid: ONE_ETHER.times(0.05), + }, + profitInLeftMakerAsset: ONE_ETHER, + profitInRightMakerAsset: ONE_ETHER.times(2), + }; + + // Call settleMatchOrders and collect the logs + const receipt = await logDecoder.getTxWithDecodedLogsAsync( + await testExchange.settleMatchOrders.sendTransactionAsync( + leftOrderHash, + rightOrderHash, + leftOrder, + rightOrder, + takerAddress, + matchedFillResults, + ), + ); + const logs = receipt.logs as Array< + LogWithDecodedArgs + >; + + // Ensure that the logs have the correct lengths and names + expect(logs.length).to.be.eq(8); + expect(_.every(logs, log => log.event === 'DispatchTransferFromCalled')).to.be.true(); + + // Right maker asset -> left maker + expect(logs[0].args.orderHash).to.be.eq(rightOrderHash); + expect(logs[0].args.assetData).to.be.eq(rightOrder.makerAssetData); + expect(logs[0].args.from).to.be.eq(rightOrder.makerAddress); + expect(logs[0].args.to).to.be.eq(leftOrder.makerAddress); + expect(logs[0].args.amount).bignumber.to.be.eq(matchedFillResults.left.takerAssetFilledAmount); + + // Left maker asset -> right maker + expect(logs[1].args.orderHash).to.be.eq(leftOrderHash); + expect(logs[1].args.assetData).to.be.eq(leftOrder.makerAssetData); + expect(logs[1].args.from).to.be.eq(leftOrder.makerAddress); + expect(logs[1].args.to).to.be.eq(rightOrder.makerAddress); + expect(logs[1].args.amount).bignumber.to.be.eq(matchedFillResults.right.takerAssetFilledAmount); + + // Right maker fee -> right fee recipient + expect(logs[2].args.orderHash).to.be.eq(rightOrderHash); + expect(logs[2].args.assetData).to.be.eq(rightOrder.makerFeeAssetData); + expect(logs[2].args.from).to.be.eq(rightOrder.makerAddress); + expect(logs[2].args.to).to.be.eq(rightOrder.feeRecipientAddress); + expect(logs[2].args.amount).bignumber.to.be.eq(matchedFillResults.right.makerFeePaid); + + // Left maker fee -> left fee recipient + expect(logs[3].args.orderHash).to.be.eq(leftOrderHash); + expect(logs[3].args.assetData).to.be.eq(leftOrder.makerFeeAssetData); + expect(logs[3].args.from).to.be.eq(leftOrder.makerAddress); + expect(logs[3].args.to).to.be.eq(leftOrder.feeRecipientAddress); + expect(logs[3].args.amount).bignumber.to.be.eq(matchedFillResults.left.makerFeePaid); + + // Left maker -> taker profit + expect(logs[4].args.orderHash).to.be.eq(leftOrderHash); + expect(logs[4].args.assetData).to.be.eq(leftOrder.makerAssetData); + expect(logs[4].args.from).to.be.eq(leftOrder.makerAddress); + expect(logs[4].args.to).to.be.eq(takerAddress); + expect(logs[4].args.amount).bignumber.to.be.eq(matchedFillResults.profitInLeftMakerAsset); + + // right maker -> taker profit + expect(logs[5].args.orderHash).to.be.eq(rightOrderHash); + expect(logs[5].args.assetData).to.be.eq(rightOrder.makerAssetData); + expect(logs[5].args.from).to.be.eq(rightOrder.makerAddress); + expect(logs[5].args.to).to.be.eq(takerAddress); + expect(logs[5].args.amount).bignumber.to.be.eq(matchedFillResults.profitInRightMakerAsset); + + // Right taker fee -> right fee recipient + expect(logs[6].args.orderHash).to.be.eq(rightOrderHash); + expect(logs[6].args.assetData).to.be.eq(rightOrder.takerFeeAssetData); + expect(logs[6].args.from).to.be.eq(takerAddress); + expect(logs[6].args.to).to.be.eq(rightOrder.feeRecipientAddress); + expect(logs[6].args.amount).bignumber.to.be.eq(matchedFillResults.right.takerFeePaid); + + // Right taker fee -> right fee recipient + expect(logs[7].args.orderHash).to.be.eq(leftOrderHash); + expect(logs[7].args.assetData).to.be.eq(leftOrder.takerFeeAssetData); + expect(logs[7].args.from).to.be.eq(takerAddress); + expect(logs[7].args.to).to.be.eq(leftOrder.feeRecipientAddress); + expect(logs[7].args.amount).bignumber.to.be.eq(matchedFillResults.left.takerFeePaid); + }); + }); }); // tslint:disable-line:max-file-line-count diff --git a/contracts/exchange/test/match_orders_unit_tests.ts b/contracts/exchange/test/match_orders_unit_tests.ts deleted file mode 100644 index 5607fa5db5..0000000000 --- a/contracts/exchange/test/match_orders_unit_tests.ts +++ /dev/null @@ -1,480 +0,0 @@ -import { - artifacts as assetProxyArtifacts, - ERC1155ProxyContract, - ERC1155ProxyWrapper, - ERC20ProxyContract, - ERC20Wrapper, - ERC721ProxyContract, - ERC721Wrapper, - MultiAssetProxyContract, -} from '@0x/contracts-asset-proxy'; -import { ERC1155Contract as ERC1155TokenContract, Erc1155Wrapper as ERC1155Wrapper } from '@0x/contracts-erc1155'; -import { DummyERC20TokenContract } from '@0x/contracts-erc20'; -import { DummyERC721TokenContract } from '@0x/contracts-erc721'; -import { ReferenceFunctions as ExchangeLibsReferenceFunctions } from '@0x/contracts-exchange-libs'; -import { - blockchainTests, - chaiSetup, - constants, - describe, - OrderFactory, - orderUtils, - provider, - txDefaults, - web3Wrapper, -} from '@0x/contracts-test-utils'; -import { BlockchainLifecycle } from '@0x/dev-utils'; -import { assetDataUtils, ExchangeRevertErrors, orderHashUtils } from '@0x/order-utils'; -import { OrderWithoutDomain, OrderStatus, SignedOrder } from '@0x/types'; -import { BigNumber, providerUtils } from '@0x/utils'; -import { Web3Wrapper } from '@0x/web3-wrapper'; -import * as chai from 'chai'; -import * as _ from 'lodash'; - -import { - artifacts, - constants as exchangeConstants, - ExchangeContract, - ExchangeWrapper, - ReentrantERC20TokenContract, - ReferenceFunctions, - TestMatchOrdersContract, -} from '../src'; -import { calculateCompleteFillBoth, calculateCompleteRightFill } from '../src/reference_functions'; - -import { MatchOrderTester, TokenBalances } from './utils/match_order_tester'; - -// Reduce the number of tokens to deploy to speed up tests, since we don't need -// so many. -constants.NUM_DUMMY_ERC721_TO_DEPLOY = 1; -constants.NUM_DUMMY_ERC1155_CONTRACTS_TO_DEPLOY = 1; - -/** - * Tests the _calculateCompleteFillBoth function with the provided inputs by making a call - * to the provided matchOrders contract's externalCalculateCompleteFillBoth function with the - * provided inputs and asserting that the resultant struct is correct. - * @param matchOrders The TestMatchOrders contract object that should be used to make the call to - * the smart contract. - * @param leftMakerAssetAmountRemaining The left maker asset remaining field for the function call. - * @param leftTakerAssetAmountRemaining The left taker asset remaining field for the function call. - * @param rightMakerAssetAmountRemaining The right maker asset remaining field for the function call. - * @param rightTakerAssetAmountRemaining The right taker asset remaining field for the function call. - */ -async function testCalculateCompleteFillBothAsync( - matchOrders: TestMatchOrdersContract, - args: BigNumber[], -): Promise { - // Ensure that the correct number of arguments were provided. - expect(args.length).to.be.eq(4); - - // Get the expected matched fill results from calling the reference function. - const expectedMatchedFillResults = calculateCompleteFillBoth( - args[0], - args[1], - args[2], - args[3], - ); - - // Get the resultant matched fill results from the call to _calculateCompleteFillBoth. - const actualMatchedFillResults = await matchOrders.externalCalculateCompleteFillBoth.callAsync( - args[0], - args[1], - args[2], - args[3], - ); - - expect(actualMatchedFillResults).to.be.deep.eq(expectedMatchedFillResults); -} - -/** - * Tests the _calculateCompleteRightFill function with the provided inputs by making a call - * to the provided matchOrders contract's externalCalculateCompleteRightFill function with the - * provided inputs and asserting that the resultant struct is correct. - * @param matchOrders The TestMatchOrders contract object that should be used to make the call to - * the smart contract. - */ -async function testCalculateCompleteRightFillAsync( - matchOrders: TestMatchOrdersContract, - leftOrder: OrderWithoutDomain, - args: BigNumber[], -): Promise { - // Ensure that the correct number of arguments were provided. - expect(args.length).to.be.eq(2); - - // Get the expected matched fill results from calling the reference function. - const expectedMatchedFillResults = calculateCompleteRightFill( - leftOrder, - args[0], - args[1], - ); - - // Get the resultant matched fill results from the call to _calculateCompleteRightFill. - const actualMatchedFillResults = await matchOrders.publicCalculateCompleteRightFill.callAsync( - leftOrder, - args[0], - args[1], - ); - - expect(actualMatchedFillResults).to.be.deep.eq(expectedMatchedFillResults); -} - -chaiSetup.configure(); -const expect = chai.expect; - -blockchainTests.resets.only('MatchOrders Tests', ({ web3Wrapper, txDefaults }) => { - let chainId: number; - let makerAddressLeft: string; - let makerAddressRight: string; - let owner: string; - let takerAddress: string; - let feeRecipientAddressLeft: string; - let feeRecipientAddressRight: string; - - let erc20Tokens: DummyERC20TokenContract[]; - let erc721Token: DummyERC721TokenContract; - let erc1155Token: ERC1155TokenContract; - let reentrantErc20Token: ReentrantERC20TokenContract; - let exchange: ExchangeContract; - let erc20Proxy: ERC20ProxyContract; - let erc721Proxy: ERC721ProxyContract; - let erc1155Proxy: ERC1155ProxyContract; - let erc1155ProxyWrapper: ERC1155ProxyWrapper; - - let exchangeWrapper: ExchangeWrapper; - let erc20Wrapper: ERC20Wrapper; - let erc721Wrapper: ERC721Wrapper; - let erc1155Wrapper: ERC1155Wrapper; - let orderFactoryLeft: OrderFactory; - let orderFactoryRight: OrderFactory; - - let tokenBalances: TokenBalances; - - let defaultERC20MakerAssetAddress: string; - let defaultERC20TakerAssetAddress: string; - let defaultERC721AssetAddress: string; - let defaultERC1155AssetAddress: string; - let defaultFeeTokenAddress: string; - - let matchOrders: TestMatchOrdersContract; - - before(async () => { - // Get the chain ID. - chainId = await providerUtils.getChainIdAsync(provider); - // Create accounts - const accounts = await web3Wrapper.getAvailableAddressesAsync(); - const usedAddresses = ([ - owner, - makerAddressLeft, - makerAddressRight, - takerAddress, - feeRecipientAddressLeft, - feeRecipientAddressRight, - ] = accounts); - const addressesWithBalances = usedAddresses.slice(1); - // Create wrappers - erc20Wrapper = new ERC20Wrapper(provider, addressesWithBalances, owner); - erc721Wrapper = new ERC721Wrapper(provider, addressesWithBalances, owner); - erc1155ProxyWrapper = new ERC1155ProxyWrapper(provider, addressesWithBalances, owner); - // Deploy ERC20 token & ERC20 proxy - const numDummyErc20ToDeploy = 4; - erc20Tokens = await erc20Wrapper.deployDummyTokensAsync(numDummyErc20ToDeploy, constants.DUMMY_TOKEN_DECIMALS); - erc20Proxy = await erc20Wrapper.deployProxyAsync(); - await erc20Wrapper.setBalancesAndAllowancesAsync(); - // Deploy ERC721 token and proxy - [erc721Token] = await erc721Wrapper.deployDummyTokensAsync(); - erc721Proxy = await erc721Wrapper.deployProxyAsync(); - await erc721Wrapper.setBalancesAndAllowancesAsync(); - // Deploy ERC1155 token and proxy - [erc1155Wrapper] = await erc1155ProxyWrapper.deployDummyContractsAsync(); - erc1155Token = erc1155Wrapper.getContract(); - erc1155Proxy = await erc1155ProxyWrapper.deployProxyAsync(); - await erc1155ProxyWrapper.setBalancesAndAllowancesAsync(); - // Deploy MultiAssetProxy. - const multiAssetProxyContract = await MultiAssetProxyContract.deployFrom0xArtifactAsync( - assetProxyArtifacts.MultiAssetProxy, - provider, - txDefaults, - ); - // Depoy exchange - exchange = await ExchangeContract.deployFrom0xArtifactAsync( - artifacts.Exchange, - provider, - txDefaults, - new BigNumber(chainId), - ); - exchangeWrapper = new ExchangeWrapper(exchange, provider); - await exchangeWrapper.registerAssetProxyAsync(erc20Proxy.address, owner); - await exchangeWrapper.registerAssetProxyAsync(erc721Proxy.address, owner); - await exchangeWrapper.registerAssetProxyAsync(erc1155Proxy.address, owner); - await exchangeWrapper.registerAssetProxyAsync(multiAssetProxyContract.address, owner); - // Authorize proxies. - await erc20Proxy.addAuthorizedAddress.awaitTransactionSuccessAsync( - exchange.address, - { from: owner }, - constants.AWAIT_TRANSACTION_MINED_MS, - ); - await erc721Proxy.addAuthorizedAddress.awaitTransactionSuccessAsync( - exchange.address, - { from: owner }, - constants.AWAIT_TRANSACTION_MINED_MS, - ); - await erc1155Proxy.addAuthorizedAddress.awaitTransactionSuccessAsync( - exchange.address, - { from: owner }, - constants.AWAIT_TRANSACTION_MINED_MS, - ); - await multiAssetProxyContract.addAuthorizedAddress.awaitTransactionSuccessAsync( - exchange.address, - { from: owner }, - constants.AWAIT_TRANSACTION_MINED_MS, - ); - await erc20Proxy.addAuthorizedAddress.awaitTransactionSuccessAsync( - multiAssetProxyContract.address, - { from: owner }, - constants.AWAIT_TRANSACTION_MINED_MS, - ); - await erc721Proxy.addAuthorizedAddress.awaitTransactionSuccessAsync( - multiAssetProxyContract.address, - { from: owner }, - constants.AWAIT_TRANSACTION_MINED_MS, - ); - await erc1155Proxy.addAuthorizedAddress.awaitTransactionSuccessAsync( - multiAssetProxyContract.address, - { from: owner }, - constants.AWAIT_TRANSACTION_MINED_MS, - ); - await multiAssetProxyContract.registerAssetProxy.awaitTransactionSuccessAsync( - erc20Proxy.address, - { from: owner }, - constants.AWAIT_TRANSACTION_MINED_MS, - ); - await multiAssetProxyContract.registerAssetProxy.awaitTransactionSuccessAsync( - erc721Proxy.address, - { from: owner }, - constants.AWAIT_TRANSACTION_MINED_MS, - ); - await multiAssetProxyContract.registerAssetProxy.awaitTransactionSuccessAsync( - erc1155Proxy.address, - { from: owner }, - constants.AWAIT_TRANSACTION_MINED_MS, - ); - - reentrantErc20Token = await ReentrantERC20TokenContract.deployFrom0xArtifactAsync( - artifacts.ReentrantERC20Token, - provider, - txDefaults, - exchange.address, - ); - - // Set default addresses - defaultERC20MakerAssetAddress = erc20Tokens[0].address; - defaultERC20TakerAssetAddress = erc20Tokens[1].address; - defaultFeeTokenAddress = erc20Tokens[2].address; - defaultERC721AssetAddress = erc721Token.address; - defaultERC1155AssetAddress = erc1155Token.address; - const domain = { - verifyingContractAddress: exchange.address, - chainId, - }; - // Create default order parameters - const defaultOrderParamsLeft = { - ...constants.STATIC_ORDER_PARAMS, - makerAddress: makerAddressLeft, - makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress), - takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress), - makerFeeAssetData: assetDataUtils.encodeERC20AssetData(defaultFeeTokenAddress), - takerFeeAssetData: assetDataUtils.encodeERC20AssetData(defaultFeeTokenAddress), - feeRecipientAddress: feeRecipientAddressLeft, - domain, - }; - const defaultOrderParamsRight = { - ...constants.STATIC_ORDER_PARAMS, - makerAddress: makerAddressRight, - makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress), - takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress), - makerFeeAssetData: assetDataUtils.encodeERC20AssetData(defaultFeeTokenAddress), - takerFeeAssetData: assetDataUtils.encodeERC20AssetData(defaultFeeTokenAddress), - feeRecipientAddress: feeRecipientAddressRight, - domain, - }; - const privateKeyLeft = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(makerAddressLeft)]; - orderFactoryLeft = new OrderFactory(privateKeyLeft, defaultOrderParamsLeft); - const privateKeyRight = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(makerAddressRight)]; - orderFactoryRight = new OrderFactory(privateKeyRight, defaultOrderParamsRight); - - // Deploy the TestMatchOrders contract - matchOrders = await TestMatchOrdersContract.deployFrom0xArtifactAsync( - artifacts.TestMatchOrders, - provider, - txDefaults, - ); - }); - - describe('_assertValidMatch', () => { - it('should revert if the prices of the left order is less than the price of the right order', async () => { - const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ - makerAssetAmount: Web3Wrapper.toBaseUnitAmount(49, 18), - takerAssetAmount: Web3Wrapper.toBaseUnitAmount(100, 18), - }); - const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ - makerAssetAmount: Web3Wrapper.toBaseUnitAmount(100, 18), - takerAssetAmount: Web3Wrapper.toBaseUnitAmount(50, 18), - }); - const orderHashHexLeft = orderHashUtils.getOrderHashHex(signedOrderLeft); - const orderHashHexRight = orderHashUtils.getOrderHashHex(signedOrderRight); - const expectedError = new ExchangeRevertErrors.NegativeSpreadError(orderHashHexLeft, orderHashHexRight); - const tx = matchOrders.publicAssertValidMatch.callAsync(signedOrderLeft, signedOrderRight); - expect(tx).to.revertWith(expectedError); - }); - - it('should revert if the prices of the left and right orders are equal', async () => { - const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ - makerAssetAmount: Web3Wrapper.toBaseUnitAmount(49, 18), - takerAssetAmount: Web3Wrapper.toBaseUnitAmount(100, 18), - }); - const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ - makerAssetAmount: Web3Wrapper.toBaseUnitAmount(100, 18), - takerAssetAmount: Web3Wrapper.toBaseUnitAmount(50, 18), - }); - const orderHashHexLeft = orderHashUtils.getOrderHashHex(signedOrderLeft); - const orderHashHexRight = orderHashUtils.getOrderHashHex(signedOrderRight); - const expectedError = new ExchangeRevertErrors.NegativeSpreadError(orderHashHexLeft, orderHashHexRight); - const tx = matchOrders.publicAssertValidMatch.callAsync(signedOrderLeft, signedOrderRight); - expect(tx).to.revertWith(expectedError); - }); - - it('should succeed if the price of the left order is higher than the price of the right', async () => { - const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ - makerAssetAmount: Web3Wrapper.toBaseUnitAmount(50, 18), - takerAssetAmount: Web3Wrapper.toBaseUnitAmount(100, 18), - }); - const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ - makerAssetAmount: Web3Wrapper.toBaseUnitAmount(100, 18), - takerAssetAmount: Web3Wrapper.toBaseUnitAmount(49, 18), - }); - await matchOrders.publicAssertValidMatch.callAsync(signedOrderLeft, signedOrderRight) - }); - }); - - describe('_calculateMatchedFillResults', () => { - // FIXME - Test case 2 and verify that it is correctly hitting case 1 and 3. - // FIXME - Test the profit calculations for all three cases - }); - - describe('_calculateCompleteFillBoth', () => { - it('should assign everything to zero if all inputs are zero', async () => { - await testCalculateCompleteFillBothAsync( - matchOrders, - [0, 0, 0, 0].map(value => new BigNumber(value)), - ); - }); - - it('should correctly update the fillResults with nonzero input', async () => { - await testCalculateCompleteFillBothAsync( - matchOrders, - [17, 98, 75, 13].map(value => new BigNumber(value)), - ); - }); - - it('should correctly update the fillResults with nonzero input', async () => { - await testCalculateCompleteFillBothAsync( - matchOrders, - [ - 5, - 10, - 10, - 5, - ].map(value => new BigNumber(value)), - ); - }); - - it('should correctly update the fillResults with nonzero input', async () => { - await testCalculateCompleteFillBothAsync( - matchOrders, - [ - Web3Wrapper.toBaseUnitAmount(5, 18), - Web3Wrapper.toBaseUnitAmount(10, 18), - Web3Wrapper.toBaseUnitAmount(10, 18), - Web3Wrapper.toBaseUnitAmount(5, 18), - ].map(value => new BigNumber(value)), - ); - }); - - it('should correctly update the fillResults with nonzero input', async () => { - await testCalculateCompleteFillBothAsync( - matchOrders, - [ - Web3Wrapper.toBaseUnitAmount(5, 18), - Web3Wrapper.toBaseUnitAmount(10, 18), - Web3Wrapper.toBaseUnitAmount(10, 18), - Web3Wrapper.toBaseUnitAmount(2, 18), - ].map(value => new BigNumber(value)), - ); - }); - }); - - describe('_calculateCompleteRightFill', () => { - /** - * NOTE(jalextowle): These test cases actually cover all code branches of _calculateCompleteRightFill (in - * fact any one of these test cases provide 100% coverage), but they do not verify that _safeGetPartialAmountCeil - * and _safeGetPartialAmountFloor revert appropriately. Keeping in the spirit of unit testing, these functions - * are unit tested to ensure that they exhibit the correct behavior in a more isolated setting. - */ - - it('should correctly calculate the complete right fill', async () => { - const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ - makerAddress: makerAddressLeft, - makerAssetAmount: Web3Wrapper.toBaseUnitAmount(17, 0), - takerAssetAmount: Web3Wrapper.toBaseUnitAmount(98, 0), - feeRecipientAddress: feeRecipientAddressLeft, - }); - await testCalculateCompleteRightFillAsync( - matchOrders, - signedOrderLeft, - [ - Web3Wrapper.toBaseUnitAmount(75, 0), - Web3Wrapper.toBaseUnitAmount(13, 0), - ], - ); - }); - - it('should correctly calculate the complete right fill', async () => { - const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ - makerAddress: makerAddressLeft, - makerAssetAmount: Web3Wrapper.toBaseUnitAmount(12, 0), - takerAssetAmount: Web3Wrapper.toBaseUnitAmount(97, 0), - feeRecipientAddress: feeRecipientAddressLeft, - }); - await testCalculateCompleteRightFillAsync( - matchOrders, - signedOrderLeft, - [ - Web3Wrapper.toBaseUnitAmount(89, 0), - Web3Wrapper.toBaseUnitAmount(1, 0), - ], - ); - }); - - it('should correctly calculate the complete right fill', async () => { - const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ - makerAddress: makerAddressLeft, - makerAssetAmount: Web3Wrapper.toBaseUnitAmount(50, 18), - takerAssetAmount: Web3Wrapper.toBaseUnitAmount(100, 18), - feeRecipientAddress: feeRecipientAddressLeft, - }); - await testCalculateCompleteRightFillAsync( - matchOrders, - signedOrderLeft, - [ - Web3Wrapper.toBaseUnitAmount(10, 18), - Web3Wrapper.toBaseUnitAmount(2, 18), - ], - ); - }); - }); - - describe('_settleMatchedOrders', () => { - // FIXME - - }); -}); -// tslint:disable-line:max-file-line-count