diff --git a/contracts/dev-utils/compiler.json b/contracts/dev-utils/compiler.json index ed5b2a9f99..92530cb9b4 100644 --- a/contracts/dev-utils/compiler.json +++ b/contracts/dev-utils/compiler.json @@ -24,9 +24,9 @@ }, "contracts": [ "src/DevUtils.sol", + "src/EthBalanceChecker.sol", "src/LibAssetData.sol", "src/LibTransactionDecoder.sol", - "src/EthBalanceChecker.sol", "src/OrderTransferSimulationUtils.sol" ] } diff --git a/contracts/dev-utils/package.json b/contracts/dev-utils/package.json index 01a7bffed4..42b0611232 100644 --- a/contracts/dev-utils/package.json +++ b/contracts/dev-utils/package.json @@ -34,7 +34,7 @@ "lint-contracts": "solhint -c ../.solhint.json contracts/**/**/**/**/*.sol" }, "config": { - "abis": "./generated-artifacts/@(DevUtils|LibAssetData|LibTransactionDecoder|EthBalanceChecker|OrderTransferSimulationUtils).json", + "abis": "./generated-artifacts/@(DevUtils|EthBalanceChecker|LibAssetData|LibTransactionDecoder|OrderTransferSimulationUtils).json", "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually." }, "repository": { diff --git a/contracts/dev-utils/src/artifacts.ts b/contracts/dev-utils/src/artifacts.ts index 8356ce7563..6a8002013c 100644 --- a/contracts/dev-utils/src/artifacts.ts +++ b/contracts/dev-utils/src/artifacts.ts @@ -6,6 +6,7 @@ import { ContractArtifact } from 'ethereum-types'; import * as DevUtils from '../generated-artifacts/DevUtils.json'; +import * as EthBalanceChecker from '../generated-artifacts/EthBalanceChecker.json'; import * as LibAssetData from '../generated-artifacts/LibAssetData.json'; import * as LibTransactionDecoder from '../generated-artifacts/LibTransactionDecoder.json'; import * as OrderTransferSimulationUtils from '../generated-artifacts/OrderTransferSimulationUtils.json'; @@ -13,5 +14,6 @@ export const artifacts = { DevUtils: DevUtils as ContractArtifact, LibAssetData: LibAssetData as ContractArtifact, LibTransactionDecoder: LibTransactionDecoder as ContractArtifact, + EthBalanceChecker: EthBalanceChecker as ContractArtifact, OrderTransferSimulationUtils: OrderTransferSimulationUtils as ContractArtifact, }; diff --git a/contracts/dev-utils/src/wrappers.ts b/contracts/dev-utils/src/wrappers.ts index 13c6397a9d..53bf08c126 100644 --- a/contracts/dev-utils/src/wrappers.ts +++ b/contracts/dev-utils/src/wrappers.ts @@ -4,6 +4,7 @@ * ----------------------------------------------------------------------------- */ export * from '../generated-wrappers/dev_utils'; +export * from '../generated-wrappers/eth_balance_checker'; export * from '../generated-wrappers/lib_asset_data'; export * from '../generated-wrappers/lib_transaction_decoder'; export * from '../generated-wrappers/order_transfer_simulation_utils'; diff --git a/contracts/dev-utils/tsconfig.json b/contracts/dev-utils/tsconfig.json index 97fefca01e..9918b06d4a 100644 --- a/contracts/dev-utils/tsconfig.json +++ b/contracts/dev-utils/tsconfig.json @@ -4,9 +4,9 @@ "include": ["./src/**/*", "./test/**/*", "./generated-wrappers/**/*"], "files": [ "generated-artifacts/DevUtils.json", + "generated-artifacts/EthBalanceChecker.json", "generated-artifacts/LibAssetData.json", "generated-artifacts/LibTransactionDecoder.json", - "generated-artifacts/EthBalanceChecker.json", "generated-artifacts/OrderTransferSimulationUtils.json" ], "exclude": ["./deploy/solc/solc_bin"] diff --git a/contracts/exchange-libs/contracts/src/LibFillResults.sol b/contracts/exchange-libs/contracts/src/LibFillResults.sol index 14cc192daa..13dd37283f 100644 --- a/contracts/exchange-libs/contracts/src/LibFillResults.sol +++ b/contracts/exchange-libs/contracts/src/LibFillResults.sol @@ -1,6 +1,6 @@ /* - Copyright 2018 ZeroEx Intl. + Copyright 2019 ZeroEx Intl. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -24,6 +24,13 @@ import "@0x/contracts-utils/contracts/src/SafeMath.sol"; contract LibFillResults is SafeMath { + struct BatchMatchedFillResults { + FillResults[] left; // Fill results for left orders + FillResults[] right; // Fill results for right orders + uint256 profitInLeftMakerAsset; // Profit taken from left makers + uint256 profitInRightMakerAsset; // Profit taken from right makers + } + struct FillResults { uint256 makerAssetFilledAmount; // Total amount of makerAsset(s) filled. uint256 takerAssetFilledAmount; // Total amount of takerAsset(s) filled. @@ -32,9 +39,10 @@ contract LibFillResults is } struct MatchedFillResults { - FillResults left; // Amounts filled and fees paid of left order. - FillResults right; // Amounts filled and fees paid of right order. - uint256 leftMakerAssetSpreadAmount; // Spread between price of left and right order, denominated in the left order's makerAsset, paid to taker. + FillResults left; // Amounts filled and fees paid of left order. + FillResults right; // Amounts filled and fees paid of right order. + uint256 profitInLeftMakerAsset; // Profit taken from the left maker + uint256 profitInRightMakerAsset; // Profit taken from the right maker } /// @dev Adds properties of both FillResults instances. diff --git a/contracts/exchange/compiler.json b/contracts/exchange/compiler.json index 1bad898077..a47ba5d22c 100644 --- a/contracts/exchange/compiler.json +++ b/contracts/exchange/compiler.json @@ -38,6 +38,7 @@ "test/ReentrantERC20Token.sol", "test/TestAssetProxyDispatcher.sol", "test/TestExchangeInternals.sol", + "test/TestExchangeMath.sol", "test/TestLibExchangeRichErrorDecoder.sol", "test/TestSignatureValidator.sol", "test/TestValidatorWallet.sol" diff --git a/contracts/exchange/contracts/src/LibExchangeRichErrors.sol b/contracts/exchange/contracts/src/LibExchangeRichErrors.sol index ea3b28edd3..732ba24baf 100644 --- a/contracts/exchange/contracts/src/LibExchangeRichErrors.sol +++ b/contracts/exchange/contracts/src/LibExchangeRichErrors.sol @@ -97,6 +97,10 @@ library LibExchangeRichErrors { bytes4 internal constant INCOMPLETE_FILL_ERROR_SELECTOR = 0x152aa60e; + // bytes4(keccak256("BatchMatchOrdersError(uint8)")) + bytes4 internal constant BATCH_MATCH_ORDERS_ERROR_SELECTOR = + 0xd4092f4f; + // solhint-disable func-name-mixedcase function SignatureErrorSelector() internal @@ -242,6 +246,27 @@ library LibExchangeRichErrors { return INCOMPLETE_FILL_ERROR_SELECTOR; } + function BatchMatchOrdersErrorSelector() + internal + pure + returns (bytes4) + { + return BATCH_MATCH_ORDERS_ERROR_SELECTOR; + } + + function BatchMatchOrdersError( + IExchangeRichErrors.BatchMatchOrdersErrorCodes errorCode + ) + internal + pure + returns (bytes memory) + { + return abi.encodeWithSelector( + BATCH_MATCH_ORDERS_ERROR_SELECTOR, + errorCode + ); + } + function SignatureError( IExchangeRichErrors.SignatureErrorCodes errorCode, bytes32 hash, diff --git a/contracts/exchange/contracts/src/MixinExchangeCore.sol b/contracts/exchange/contracts/src/MixinExchangeCore.sol index 15a9514fa5..2bd897a76c 100644 --- a/contracts/exchange/contracts/src/MixinExchangeCore.sol +++ b/contracts/exchange/contracts/src/MixinExchangeCore.sol @@ -33,6 +33,7 @@ import "./MixinSignatureValidator.sol"; contract MixinExchangeCore is IExchangeCore, + IExchangeRichErrors, LibExchangeSelectors, LibMath, LibFillResults, @@ -69,7 +70,11 @@ contract MixinExchangeCore is // Ensure orderEpoch is monotonically increasing if (newOrderEpoch <= oldOrderEpoch) { - LibRichErrors._rrevert(LibExchangeRichErrors.OrderEpochError(makerAddress, orderSenderAddress, oldOrderEpoch)); + LibRichErrors._rrevert(LibExchangeRichErrors.OrderEpochError( + makerAddress, + orderSenderAddress, + oldOrderEpoch + )); } // Update orderEpoch @@ -340,14 +345,18 @@ contract MixinExchangeCore is // Validate sender is allowed to fill this order if (order.senderAddress != address(0)) { if (order.senderAddress != msg.sender) { - LibRichErrors._rrevert(LibExchangeRichErrors.InvalidSenderError(orderInfo.orderHash, msg.sender)); + LibRichErrors._rrevert(LibExchangeRichErrors.InvalidSenderError( + orderInfo.orderHash, msg.sender + )); } } // Validate taker is allowed to fill this order if (order.takerAddress != address(0)) { if (order.takerAddress != takerAddress) { - LibRichErrors._rrevert(LibExchangeRichErrors.InvalidTakerError(orderInfo.orderHash, takerAddress)); + LibRichErrors._rrevert(LibExchangeRichErrors.InvalidTakerError( + orderInfo.orderHash, takerAddress + )); } } @@ -366,7 +375,7 @@ contract MixinExchangeCore is makerAddress, signature)) { LibRichErrors._rrevert(LibExchangeRichErrors.SignatureError( - IExchangeRichErrors.SignatureErrorCodes.BAD_SIGNATURE, + SignatureErrorCodes.BAD_SIGNATURE, orderInfo.orderHash, makerAddress, signature @@ -395,7 +404,7 @@ contract MixinExchangeCore is // TODO: reconsider necessity for v2.1 if (takerAssetFillAmount == 0) { LibRichErrors._rrevert(LibExchangeRichErrors.FillError( - IExchangeRichErrors.FillErrorCodes.INVALID_TAKER_AMOUNT, + FillErrorCodes.INVALID_TAKER_AMOUNT, orderInfo.orderHash )); } @@ -405,7 +414,7 @@ contract MixinExchangeCore is // as an extra defence against potential bugs. if (takerAssetFilledAmount > takerAssetFillAmount) { LibRichErrors._rrevert(LibExchangeRichErrors.FillError( - IExchangeRichErrors.FillErrorCodes.TAKER_OVERPAY, + FillErrorCodes.TAKER_OVERPAY, orderInfo.orderHash )); } @@ -416,7 +425,7 @@ contract MixinExchangeCore is if (_safeAdd(orderInfo.orderTakerAssetFilledAmount, takerAssetFilledAmount) > order.takerAssetAmount) { LibRichErrors._rrevert(LibExchangeRichErrors.FillError( - IExchangeRichErrors.FillErrorCodes.OVERFILL, + FillErrorCodes.OVERFILL, orderInfo.orderHash )); } @@ -441,7 +450,7 @@ contract MixinExchangeCore is if (_safeMul(makerAssetFilledAmount, order.takerAssetAmount) > _safeMul(order.makerAssetAmount, takerAssetFilledAmount)) { LibRichErrors._rrevert(LibExchangeRichErrors.FillError( - IExchangeRichErrors.FillErrorCodes.INVALID_FILL_PRICE, + FillErrorCodes.INVALID_FILL_PRICE, orderInfo.orderHash )); } diff --git a/contracts/exchange/contracts/src/MixinMatchOrders.sol b/contracts/exchange/contracts/src/MixinMatchOrders.sol index 7be4a72e2c..c3954e62a3 100644 --- a/contracts/exchange/contracts/src/MixinMatchOrders.sol +++ b/contracts/exchange/contracts/src/MixinMatchOrders.sol @@ -1,5 +1,5 @@ /* - Copyright 2018 ZeroEx Intl. + Copyright 2019 ZeroEx Intl. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at @@ -34,104 +34,59 @@ contract MixinMatchOrders is { using LibBytes for bytes; - /// @dev Match two complementary orders that have a profitable spread. - /// Each order is filled at their respective price point. However, the calculations are - /// carried out as though the orders are both being filled at the right order's price point. - /// The profit made by the left order goes to the taker (who matched the two orders). - /// @param leftOrder First order to match. - /// @param rightOrder Second order to match. - /// @param leftSignature Proof that order was created by the left maker. - /// @param rightSignature Proof that order was created by the right maker. - /// @return matchedFillResults Amounts filled and fees paid by maker and taker of matched orders. - function matchOrders( - LibOrder.Order memory leftOrder, - LibOrder.Order memory rightOrder, - bytes memory leftSignature, - bytes memory rightSignature + /// @dev Match complementary orders that have a profitable spread. + /// Each order is filled at their respective price point, and + /// the matcher receives a profit denominated in the left maker asset. + /// @param leftOrders Set of orders with the same maker / taker asset. + /// @param rightOrders Set of orders to match against `leftOrders` + /// @param leftSignatures Proof that left orders were created by the left makers. + /// @param rightSignatures Proof that right orders were created by the right makers. + /// @return batchMatchedFillResults Amounts filled and profit generated. + function batchMatchOrders( + LibOrder.Order[] memory leftOrders, + LibOrder.Order[] memory rightOrders, + bytes[] memory leftSignatures, + bytes[] memory rightSignatures ) public nonReentrant - returns (LibFillResults.MatchedFillResults memory matchedFillResults) + returns (LibFillResults.BatchMatchedFillResults memory batchMatchedFillResults) { - // We assume that rightOrder.takerAssetData == leftOrder.makerAssetData and rightOrder.makerAssetData == leftOrder.takerAssetData - // by pointing these values to the same location in memory. This is cheaper than checking equality. - // If this assumption isn't true, the match will fail at signature validation. - rightOrder.makerAssetData = leftOrder.takerAssetData; - rightOrder.takerAssetData = leftOrder.makerAssetData; - - // Get left & right order info - LibOrder.OrderInfo memory leftOrderInfo = getOrderInfo(leftOrder); - LibOrder.OrderInfo memory rightOrderInfo = getOrderInfo(rightOrder); - - // Fetch taker address - address takerAddress = _getCurrentContextAddress(); - - // Either our context is valid or we revert - _assertFillableOrder( - leftOrder, - leftOrderInfo, - takerAddress, - leftSignature - ); - _assertFillableOrder( - rightOrder, - rightOrderInfo, - takerAddress, - rightSignature - ); - _assertValidMatch(leftOrder, rightOrder); - - // Compute proportional fill amounts - matchedFillResults = calculateMatchedFillResults( - leftOrder, - rightOrder, - leftOrderInfo.orderTakerAssetFilledAmount, - rightOrderInfo.orderTakerAssetFilledAmount - ); - - // Validate fill contexts - _assertValidFill( - leftOrder, - leftOrderInfo, - matchedFillResults.left.takerAssetFilledAmount, - matchedFillResults.left.takerAssetFilledAmount, - matchedFillResults.left.makerAssetFilledAmount - ); - _assertValidFill( - rightOrder, - rightOrderInfo, - matchedFillResults.right.takerAssetFilledAmount, - matchedFillResults.right.takerAssetFilledAmount, - matchedFillResults.right.makerAssetFilledAmount - ); - - // Update exchange state - _updateFilledState( - leftOrder, - takerAddress, - leftOrderInfo.orderHash, - leftOrderInfo.orderTakerAssetFilledAmount, - matchedFillResults.left - ); - _updateFilledState( - rightOrder, - takerAddress, - rightOrderInfo.orderHash, - rightOrderInfo.orderTakerAssetFilledAmount, - matchedFillResults.right + return _batchMatchOrders( + leftOrders, + rightOrders, + leftSignatures, + rightSignatures, + false ); + } - // Settle matched orders. Succeeds or throws. - _settleMatchedOrders( - leftOrderInfo.orderHash, - rightOrderInfo.orderHash, - leftOrder, - rightOrder, - takerAddress, - matchedFillResults + /// @dev Match complementary orders that have a profitable spread. + /// Each order is maximally filled at their respective price point, and + /// the matcher receives a profit denominated in either the left maker asset, + /// right maker asset, or a combination of both. + /// @param leftOrders Set of orders with the same maker / taker asset. + /// @param rightOrders Set of orders to match against `leftOrders` + /// @param leftSignatures Proof that left orders were created by the left makers. + /// @param rightSignatures Proof that right orders were created by the right makers. + /// @return batchMatchedFillResults Amounts filled and profit generated. + function batchMatchOrdersWithMaximalFill( + LibOrder.Order[] memory leftOrders, + LibOrder.Order[] memory rightOrders, + bytes[] memory leftSignatures, + bytes[] memory rightSignatures + ) + public + nonReentrant + returns (LibFillResults.BatchMatchedFillResults memory batchMatchedFillResults) + { + return _batchMatchOrders( + leftOrders, + rightOrders, + leftSignatures, + rightSignatures, + true ); - - return matchedFillResults; } /// @dev Calculates fill amounts for the matched orders. @@ -142,12 +97,15 @@ contract MixinMatchOrders is /// @param rightOrder Second order to match. /// @param leftOrderTakerAssetFilledAmount Amount of left order already filled. /// @param rightOrderTakerAssetFilledAmount Amount of right order already filled. + /// @param shouldMaximallyFillOrders A value that indicates whether or not this calculation should use + /// the maximal fill order matching strategy. /// @param matchedFillResults Amounts to fill and fees to pay by maker and taker of matched orders. function calculateMatchedFillResults( LibOrder.Order memory leftOrder, LibOrder.Order memory rightOrder, uint256 leftOrderTakerAssetFilledAmount, - uint256 rightOrderTakerAssetFilledAmount + uint256 rightOrderTakerAssetFilledAmount, + bool shouldMaximallyFillOrders ) public pure @@ -167,47 +125,29 @@ contract MixinMatchOrders is rightTakerAssetAmountRemaining ); - // Calculate fill results for maker and taker assets: at least one order will be fully filled. - // The maximum amount the left maker can buy is `leftTakerAssetAmountRemaining` - // The maximum amount the right maker can sell is `rightMakerAssetAmountRemaining` - // We have two distinct cases for calculating the fill results: - // Case 1. - // If the left maker can buy more than the right maker can sell, then only the right order is fully filled. - // If the left maker can buy exactly what the right maker can sell, then both orders are fully filled. - // Case 2. - // If the left maker cannot buy more than the right maker can sell, then only the left order is fully filled. - if (leftTakerAssetAmountRemaining >= rightMakerAssetAmountRemaining) { - // Case 1: Right order is fully filled - matchedFillResults.right.makerAssetFilledAmount = rightMakerAssetAmountRemaining; - matchedFillResults.right.takerAssetFilledAmount = rightTakerAssetAmountRemaining; - matchedFillResults.left.takerAssetFilledAmount = matchedFillResults.right.makerAssetFilledAmount; - // Round down to ensure the maker's exchange rate does not exceed the price specified by the order. - // We favor the maker when the exchange rate must be rounded. - matchedFillResults.left.makerAssetFilledAmount = _safeGetPartialAmountFloor( - leftOrder.makerAssetAmount, - leftOrder.takerAssetAmount, - matchedFillResults.left.takerAssetFilledAmount + // Maximally fill the orders and pay out profits to the matcher in one or both of the maker assets. + if (shouldMaximallyFillOrders) { + _calculateMatchedFillResultsWithMaximalFill( + matchedFillResults, + leftOrder, + rightOrder, + leftMakerAssetAmountRemaining, + leftTakerAssetAmountRemaining, + rightMakerAssetAmountRemaining, + rightTakerAssetAmountRemaining ); } else { - // Case 2: Left order is fully filled - matchedFillResults.left.makerAssetFilledAmount = leftMakerAssetAmountRemaining; - matchedFillResults.left.takerAssetFilledAmount = leftTakerAssetAmountRemaining; - matchedFillResults.right.makerAssetFilledAmount = matchedFillResults.left.takerAssetFilledAmount; - // Round up to ensure the maker's exchange rate does not exceed the price specified by the order. - // We favor the maker when the exchange rate must be rounded. - matchedFillResults.right.takerAssetFilledAmount = _safeGetPartialAmountCeil( - rightOrder.takerAssetAmount, - rightOrder.makerAssetAmount, - matchedFillResults.right.makerAssetFilledAmount + _calculateMatchedFillResults( + matchedFillResults, + leftOrder, + rightOrder, + leftMakerAssetAmountRemaining, + leftTakerAssetAmountRemaining, + rightMakerAssetAmountRemaining, + rightTakerAssetAmountRemaining ); } - // Calculate amount given to taker - matchedFillResults.leftMakerAssetSpreadAmount = _safeSub( - matchedFillResults.left.makerAssetFilledAmount, - matchedFillResults.right.takerAssetFilledAmount - ); - // Compute fees for left order matchedFillResults.left.makerFeePaid = _safeGetPartialAmountFloor( matchedFillResults.left.makerAssetFilledAmount, @@ -236,6 +176,62 @@ contract MixinMatchOrders is return matchedFillResults; } + /// @dev Match two complementary orders that have a profitable spread. + /// Each order is filled at their respective price point. However, the calculations are + /// carried out as though the orders are both being filled at the right order's price point. + /// The profit made by the left order goes to the taker (who matched the two orders). + /// @param leftOrder First order to match. + /// @param rightOrder Second order to match. + /// @param leftSignature Proof that order was created by the left maker. + /// @param rightSignature Proof that order was created by the right maker. + /// @return matchedFillResults Amounts filled and fees paid by maker and taker of matched orders. + function matchOrders( + LibOrder.Order memory leftOrder, + LibOrder.Order memory rightOrder, + bytes memory leftSignature, + bytes memory rightSignature + ) + public + nonReentrant + returns (LibFillResults.MatchedFillResults memory matchedFillResults) + { + return _matchOrders( + leftOrder, + rightOrder, + leftSignature, + rightSignature, + false + ); + } + + /// @dev Match two complementary orders that have a profitable spread. + /// Each order is maximally filled at their respective price point, and + /// the matcher receives a profit denominated in either the left maker asset, + /// right maker asset, or a combination of both. + /// @param leftOrder First order to match. + /// @param rightOrder Second order to match. + /// @param leftSignature Proof that order was created by the left maker. + /// @param rightSignature Proof that order was created by the right maker. + /// @return matchedFillResults Amounts filled by maker and taker of matched orders. + function matchOrdersWithMaximalFill( + LibOrder.Order memory leftOrder, + LibOrder.Order memory rightOrder, + bytes memory leftSignature, + bytes memory rightSignature + ) + public + nonReentrant + returns (LibFillResults.MatchedFillResults memory matchedFillResults) + { + return _matchOrders( + leftOrder, + rightOrder, + leftSignature, + rightSignature, + true + ); + } + /// @dev Validates context for matchOrders. Succeeds or throws. /// @param leftOrder First order to match. /// @param rightOrder Second order to match. @@ -263,6 +259,473 @@ contract MixinMatchOrders is } } + /// @dev Calculates part of the matched fill results for a given situation using the fill strategy that only + /// awards profit denominated in the left maker asset. + /// @param matchedFillResults The MatchedFillResults struct to update with fill result calculations. + /// @param leftOrder The left order in the order matching situation. + /// @param rightOrder The right order in the order matching situation. + /// @param leftMakerAssetAmountRemaining The amount of the left order maker asset that can still be filled. + /// @param leftTakerAssetAmountRemaining The amount of the left order taker asset that can still be filled. + /// @param rightMakerAssetAmountRemaining The amount of the right order maker asset that can still be filled. + /// @param rightTakerAssetAmountRemaining The amount of the right order taker asset that can still be filled. + function _calculateMatchedFillResults( + MatchedFillResults memory matchedFillResults, + LibOrder.Order memory leftOrder, + LibOrder.Order memory rightOrder, + uint256 leftMakerAssetAmountRemaining, + uint256 leftTakerAssetAmountRemaining, + uint256 rightMakerAssetAmountRemaining, + uint256 rightTakerAssetAmountRemaining + ) + internal + pure + { + // Calculate fill results for maker and taker assets: at least one order will be fully filled. + // The maximum amount the left maker can buy is `leftTakerAssetAmountRemaining` + // The maximum amount the right maker can sell is `rightMakerAssetAmountRemaining` + // We have two distinct cases for calculating the fill results: + // Case 1. + // If the left maker can buy more than the right maker can sell, then only the right order is fully filled. + // If the left maker can buy exactly what the right maker can sell, then both orders are fully filled. + // Case 2. + // If the left maker cannot buy more than the right maker can sell, then only the left order is fully filled. + // Case 3. + // If the left maker can buy exactly as much as the right maker can sell, then both orders are fully filled. + if (leftTakerAssetAmountRemaining > rightMakerAssetAmountRemaining) { + // Case 1: Right order is fully filled + _calculateCompleteRightFill( + matchedFillResults, + leftOrder, + rightMakerAssetAmountRemaining, + rightTakerAssetAmountRemaining + ); + } else if (leftTakerAssetAmountRemaining < rightMakerAssetAmountRemaining) { + // Case 2: Left order is fully filled + matchedFillResults.left.makerAssetFilledAmount = leftMakerAssetAmountRemaining; + matchedFillResults.left.takerAssetFilledAmount = leftTakerAssetAmountRemaining; + matchedFillResults.right.makerAssetFilledAmount = leftTakerAssetAmountRemaining; + // Round up to ensure the maker's exchange rate does not exceed the price specified by the order. + // We favor the maker when the exchange rate must be rounded. + matchedFillResults.right.takerAssetFilledAmount = _safeGetPartialAmountCeil( + rightOrder.takerAssetAmount, + rightOrder.makerAssetAmount, + leftTakerAssetAmountRemaining // matchedFillResults.right.makerAssetFilledAmount + ); + } else { // leftTakerAssetAmountRemaining == rightMakerAssetAmountRemaining + // 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. + _calculateCompleteFillBoth( + matchedFillResults, + leftMakerAssetAmountRemaining, + leftTakerAssetAmountRemaining, + rightMakerAssetAmountRemaining, + rightTakerAssetAmountRemaining + ); + } + + // Calculate amount given to taker + matchedFillResults.profitInLeftMakerAsset = _safeSub( + matchedFillResults.left.makerAssetFilledAmount, + matchedFillResults.right.takerAssetFilledAmount + ); + } + + /// @dev Calculates part of the matched fill results for a given situation using the maximal fill order matching + /// strategy. + /// @param matchedFillResults The MatchedFillResults struct to update with fill result calculations. + /// @param leftOrder The left order in the order matching situation. + /// @param rightOrder The right order in the order matching situation. + /// @param leftMakerAssetAmountRemaining The amount of the left order maker asset that can still be filled. + /// @param leftTakerAssetAmountRemaining The amount of the left order taker asset that can still be filled. + /// @param rightMakerAssetAmountRemaining The amount of the right order maker asset that can still be filled. + /// @param rightTakerAssetAmountRemaining The amount of the right order taker asset that can still be filled. + function _calculateMatchedFillResultsWithMaximalFill( + MatchedFillResults memory matchedFillResults, + LibOrder.Order memory leftOrder, + LibOrder.Order memory rightOrder, + uint256 leftMakerAssetAmountRemaining, + uint256 leftTakerAssetAmountRemaining, + uint256 rightMakerAssetAmountRemaining, + uint256 rightTakerAssetAmountRemaining + ) + internal + pure + { + // If a maker asset is greater than the opposite taker asset, than there will be a spread denominated in that maker asset. + bool doesLeftMakerAssetProfitExist = leftMakerAssetAmountRemaining > rightTakerAssetAmountRemaining; + bool doesRightMakerAssetProfitExist = rightMakerAssetAmountRemaining > leftTakerAssetAmountRemaining; + + // Calculate the maximum fill results for the maker and taker assets. At least one of the orders will be fully filled. + // + // The maximum that the left maker can possibly buy is the amount that the right order can sell. + // The maximum that the right maker can possibly buy is the amount that the left order can sell. + // + // If the left order is fully filled, profit will be paid out in the left maker asset. If the right order is fully filled, + // the profit will be out in the right maker asset. + // + // There are three cases to consider: + // Case 1. + // If the left maker can buy more than the right maker can sell, then only the right order is fully filled. + // Case 2. + // If the right maker can buy more than the left maker can sell, then only the right order is fully filled. + // Case 3. + // If the right maker can sell the max of what the left maker can buy and the left maker can sell the max of + // what the right maker can buy, then both orders are fully filled. + if (leftTakerAssetAmountRemaining > rightMakerAssetAmountRemaining) { + // Case 1: Right order is fully filled with the profit paid in the left makerAsset + _calculateCompleteRightFill( + matchedFillResults, + leftOrder, + rightMakerAssetAmountRemaining, + rightTakerAssetAmountRemaining + ); + } else if (rightTakerAssetAmountRemaining > leftMakerAssetAmountRemaining) { + // Case 2: Left order is fully filled with the profit paid in the right makerAsset. + matchedFillResults.left.makerAssetFilledAmount = leftMakerAssetAmountRemaining; + matchedFillResults.left.takerAssetFilledAmount = leftTakerAssetAmountRemaining; + // Round down to ensure the right maker's exchange rate does not exceed the price specified by the order. + // We favor the right maker when the exchange rate must be rounded and the profit is being paid in the + // right maker asset. + matchedFillResults.right.makerAssetFilledAmount = _safeGetPartialAmountFloor( + rightOrder.makerAssetAmount, + rightOrder.takerAssetAmount, + leftMakerAssetAmountRemaining + ); + matchedFillResults.right.takerAssetFilledAmount = leftMakerAssetAmountRemaining; + } else { + // Case 3: The right and left orders are fully filled + _calculateCompleteFillBoth( + matchedFillResults, + leftMakerAssetAmountRemaining, + leftTakerAssetAmountRemaining, + rightMakerAssetAmountRemaining, + rightTakerAssetAmountRemaining + ); + } + + // Calculate amount given to taker in the left order's maker asset if the left spread will be part of the profit. + if (doesLeftMakerAssetProfitExist) { + matchedFillResults.profitInLeftMakerAsset = _safeSub( + matchedFillResults.left.makerAssetFilledAmount, + matchedFillResults.right.takerAssetFilledAmount + ); + } + + // Calculate amount given to taker in the right order's maker asset if the right spread will be part of the profit. + if (doesRightMakerAssetProfitExist) { + matchedFillResults.profitInRightMakerAsset = _safeSub( + matchedFillResults.right.makerAssetFilledAmount, + matchedFillResults.left.takerAssetFilledAmount + ); + } + } + + /// @dev Calculates the fill results for the maker and taker in the order matching and writes the results + /// to the fillResults that are being collected on the order. Both orders will be fully filled in this + /// case. + /// @param matchedFillResults The fill results object to populate with calculations. + /// @param leftMakerAssetAmountRemaining The amount of the left maker asset that is remaining to be filled. + /// @param leftTakerAssetAmountRemaining The amount of the left taker asset that is remaining to be filled. + /// @param rightMakerAssetAmountRemaining The amount of the right maker asset that is remaining to be filled. + /// @param rightTakerAssetAmountRemaining The amount of the right taker asset that is remaining to be filled. + function _calculateCompleteFillBoth( + MatchedFillResults memory matchedFillResults, + uint256 leftMakerAssetAmountRemaining, + uint256 leftTakerAssetAmountRemaining, + uint256 rightMakerAssetAmountRemaining, + uint256 rightTakerAssetAmountRemaining + ) + internal + pure + { + // Calculate the fully filled results for both orders. + matchedFillResults.left.makerAssetFilledAmount = leftMakerAssetAmountRemaining; + matchedFillResults.left.takerAssetFilledAmount = leftTakerAssetAmountRemaining; + matchedFillResults.right.makerAssetFilledAmount = rightMakerAssetAmountRemaining; + matchedFillResults.right.takerAssetFilledAmount = rightTakerAssetAmountRemaining; + } + + /// @dev Calculates the fill results for the maker and taker in the order matching and writes the results + /// to the fillResults that are being collected on the order. + /// @param matchedFillResults The fill results object to populate with calculations. + /// @param leftOrder The left order that is being maximally filled. All of the information about fill amounts + /// can be derived from this order and the right asset remaining fields. + /// @param rightMakerAssetAmountRemaining The amount of the right maker asset that is remaining to be filled. + /// @param rightTakerAssetAmountRemaining The amount of the right taker asset that is remaining to be filled. + function _calculateCompleteRightFill( + MatchedFillResults memory matchedFillResults, + LibOrder.Order memory leftOrder, + uint256 rightMakerAssetAmountRemaining, + uint256 rightTakerAssetAmountRemaining + ) + internal + pure + { + matchedFillResults.right.makerAssetFilledAmount = rightMakerAssetAmountRemaining; + matchedFillResults.right.takerAssetFilledAmount = rightTakerAssetAmountRemaining; + matchedFillResults.left.takerAssetFilledAmount = rightMakerAssetAmountRemaining; + // Round down to ensure the left maker's exchange rate does not exceed the price specified by the order. + // We favor the left maker when the exchange rate must be rounded and the profit is being paid in the + // left maker asset. + matchedFillResults.left.makerAssetFilledAmount = _safeGetPartialAmountFloor( + leftOrder.makerAssetAmount, + leftOrder.takerAssetAmount, + rightMakerAssetAmountRemaining + ); + } + + /// @dev Match complementary orders that have a profitable spread. + /// Each order is filled at their respective price point, and + /// the matcher receives a profit denominated in the left maker asset. + /// This is the reentrant version of `batchMatchOrders` and `batchMatchOrdersWithMaximalFill`. + /// @param leftOrders Set of orders with the same maker / taker asset. + /// @param rightOrders Set of orders to match against `leftOrders` + /// @param leftSignatures Proof that left orders were created by the left makers. + /// @param rightSignatures Proof that right orders were created by the right makers. + /// @param shouldMaximallyFillOrders A value that indicates whether or not the order matching + /// should be done with maximal fill. + /// @return batchMatchedFillResults Amounts filled and profit generated. + function _batchMatchOrders( + LibOrder.Order[] memory leftOrders, + LibOrder.Order[] memory rightOrders, + bytes[] memory leftSignatures, + bytes[] memory rightSignatures, + bool shouldMaximallyFillOrders + ) + private + returns (LibFillResults.BatchMatchedFillResults memory batchMatchedFillResults) + { + // Ensure that the left and right orders have nonzero lengths. + if (leftOrders.length == 0) { + LibRichErrors._rrevert(LibExchangeRichErrors.BatchMatchOrdersError( + BatchMatchOrdersErrorCodes.ZERO_LEFT_ORDERS + )); + } + if (rightOrders.length == 0) { + LibRichErrors._rrevert(LibExchangeRichErrors.BatchMatchOrdersError( + BatchMatchOrdersErrorCodes.ZERO_RIGHT_ORDERS + )); + } + + // Ensure that the left and right arrays are compatible. + if (leftOrders.length != leftSignatures.length) { + LibRichErrors._rrevert(LibExchangeRichErrors.BatchMatchOrdersError( + BatchMatchOrdersErrorCodes.INVALID_LENGTH_LEFT_SIGNATURES + )); + } + if (rightOrders.length != rightSignatures.length) { + LibRichErrors._rrevert(LibExchangeRichErrors.BatchMatchOrdersError( + BatchMatchOrdersErrorCodes.INVALID_LENGTH_RIGHT_SIGNATURES + )); + } + + batchMatchedFillResults.left = new LibFillResults.FillResults[](leftOrders.length); + batchMatchedFillResults.right = new LibFillResults.FillResults[](rightOrders.length); + + // Set up initial indices. + uint256 leftIdx = 0; + uint256 rightIdx = 0; + + // Keep local variables for orders, order info, and signatures for efficiency. + LibOrder.Order memory leftOrder = leftOrders[0]; + LibOrder.Order memory rightOrder = rightOrders[0]; + LibOrder.OrderInfo memory leftOrderInfo = getOrderInfo(leftOrder); + LibOrder.OrderInfo memory rightOrderInfo = getOrderInfo(rightOrder); + LibFillResults.FillResults memory leftFillResults; + LibFillResults.FillResults memory rightFillResults; + + // Loop infinitely (until broken inside of the loop), but keep a counter of how + // many orders have been matched. + for (;;) { + // Match the two orders that are pointed to by the left and right indices + LibFillResults.MatchedFillResults memory matchResults = _matchOrders( + leftOrder, + rightOrder, + leftSignatures[leftIdx], + rightSignatures[rightIdx], + shouldMaximallyFillOrders + ); + + // Update the orderInfo structs with the updated takerAssetFilledAmount + leftOrderInfo.orderTakerAssetFilledAmount = _safeAdd( + leftOrderInfo.orderTakerAssetFilledAmount, + matchResults.left.takerAssetFilledAmount + ); + rightOrderInfo.orderTakerAssetFilledAmount = _safeAdd( + rightOrderInfo.orderTakerAssetFilledAmount, + matchResults.right.takerAssetFilledAmount + ); + + // Aggregate the new fill results with the previous fill results for the current orders. + _addFillResults( + leftFillResults, + matchResults.left + ); + _addFillResults( + rightFillResults, + matchResults.right + ); + + // Update the profit in the left and right maker assets using the profits from + // the match. + batchMatchedFillResults.profitInLeftMakerAsset = _safeAdd( + batchMatchedFillResults.profitInLeftMakerAsset, + matchResults.profitInLeftMakerAsset + ); + batchMatchedFillResults.profitInRightMakerAsset = _safeAdd( + batchMatchedFillResults.profitInRightMakerAsset, + matchResults.profitInRightMakerAsset + ); + + // 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 (leftOrderInfo.orderTakerAssetFilledAmount >= leftOrder.takerAssetAmount) { + // Update the batched fill results once the leftIdx is updated. + batchMatchedFillResults.left[leftIdx++] = leftFillResults; + // Clear the intermediate fill results value. + leftFillResults = LibFillResults.FillResults(0, 0, 0, 0); + + // If all of the left orders have been filled, break out of the loop. + // Otherwise, update the current right order. + if (leftIdx == leftOrders.length) { + // Update the right batched fill results + batchMatchedFillResults.right[rightIdx] = rightFillResults; + break; + } else { + leftOrder = leftOrders[leftIdx]; + leftOrderInfo = getOrderInfo(leftOrder); + } + } + + // If the rightOrder is filled, update the rightIdx, rightOrder, and rightSignature, + // or break out of the loop if there are no more rightOrders to match. + if (rightOrderInfo.orderTakerAssetFilledAmount >= rightOrder.takerAssetAmount) { + // Update the batched fill results once the rightIdx is updated. + batchMatchedFillResults.right[rightIdx++] = rightFillResults; + // Clear the intermediate fill results value. + rightFillResults = LibFillResults.FillResults(0, 0, 0, 0); + + // If all of the right orders have been filled, break out of the loop. + // Otherwise, update the current right order. + if (rightIdx == rightOrders.length) { + // Update the left batched fill results + batchMatchedFillResults.left[leftIdx] = leftFillResults; + break; + } else { + rightOrder = rightOrders[rightIdx]; + rightOrderInfo = getOrderInfo(rightOrder); + } + } + } + + // Return the fill results from the batch match + return batchMatchedFillResults; + } + + /// @dev Match two complementary orders that have a profitable spread. + /// Each order is filled at their respective price point. However, the calculations are + /// carried out as though the orders are both being filled at the right order's price point. + /// The profit made by the left order goes to the taker (who matched the two orders). This + /// function is needed to allow for reentrant order matching (used by `batchMatchOrders` and + /// `batchMatchOrdersWithMaximalFill`). + /// @param leftOrder First order to match. + /// @param rightOrder Second order to match. + /// @param leftSignature Proof that order was created by the left maker. + /// @param rightSignature Proof that order was created by the right maker. + /// @param shouldMaximallyFillOrders Indicates whether or not the maximal fill matching strategy should be used + /// @return matchedFillResults Amounts filled and fees paid by maker and taker of matched orders. + function _matchOrders( + LibOrder.Order memory leftOrder, + LibOrder.Order memory rightOrder, + bytes memory leftSignature, + bytes memory rightSignature, + bool shouldMaximallyFillOrders + ) + private + returns (LibFillResults.MatchedFillResults memory matchedFillResults) + { + // We assume that rightOrder.takerAssetData == leftOrder.makerAssetData and rightOrder.makerAssetData == leftOrder.takerAssetData + // by pointing these values to the same location in memory. This is cheaper than checking equality. + // If this assumption isn't true, the match will fail at signature validation. + rightOrder.makerAssetData = leftOrder.takerAssetData; + rightOrder.takerAssetData = leftOrder.makerAssetData; + + // Get left & right order info + LibOrder.OrderInfo memory leftOrderInfo = getOrderInfo(leftOrder); + LibOrder.OrderInfo memory rightOrderInfo = getOrderInfo(rightOrder); + + // Fetch taker address + address takerAddress = _getCurrentContextAddress(); + + // Either our context is valid or we revert + _assertFillableOrder( + leftOrder, + leftOrderInfo, + takerAddress, + leftSignature + ); + _assertFillableOrder( + rightOrder, + rightOrderInfo, + takerAddress, + rightSignature + ); + _assertValidMatch(leftOrder, rightOrder); + + // Compute proportional fill amounts + matchedFillResults = calculateMatchedFillResults( + leftOrder, + rightOrder, + leftOrderInfo.orderTakerAssetFilledAmount, + rightOrderInfo.orderTakerAssetFilledAmount, + shouldMaximallyFillOrders + ); + + // Validate fill contexts + _assertValidFill( + leftOrder, + leftOrderInfo, + matchedFillResults.left.takerAssetFilledAmount, + matchedFillResults.left.takerAssetFilledAmount, + matchedFillResults.left.makerAssetFilledAmount + ); + _assertValidFill( + rightOrder, + rightOrderInfo, + matchedFillResults.right.takerAssetFilledAmount, + matchedFillResults.right.takerAssetFilledAmount, + matchedFillResults.right.makerAssetFilledAmount + ); + + // Update exchange state + _updateFilledState( + leftOrder, + takerAddress, + leftOrderInfo.orderHash, + leftOrderInfo.orderTakerAssetFilledAmount, + matchedFillResults.left + ); + _updateFilledState( + rightOrder, + takerAddress, + rightOrderInfo.orderHash, + rightOrderInfo.orderTakerAssetFilledAmount, + matchedFillResults.right + ); + + // Settle matched orders. Succeeds or throws. + _settleMatchedOrders( + leftOrderInfo.orderHash, + rightOrderInfo.orderHash, + leftOrder, + rightOrder, + takerAddress, + matchedFillResults + ); + + return matchedFillResults; + } + /// @dev Settles matched order by transferring appropriate funds between order makers, taker, and fee recipient. /// @param leftOrderHash First matched order hash. /// @param rightOrderHash Second matched order hash. @@ -325,7 +788,15 @@ contract MixinMatchOrders is leftOrder.makerAssetData, leftOrder.makerAddress, takerAddress, - matchedFillResults.leftMakerAssetSpreadAmount + matchedFillResults.profitInLeftMakerAsset + ); + + _dispatchTransferFrom( + rightOrderHash, + rightOrder.makerAssetData, + rightOrder.makerAddress, + takerAddress, + matchedFillResults.profitInRightMakerAsset ); // Settle taker fees. diff --git a/contracts/exchange/contracts/src/interfaces/IExchangeRichErrors.sol b/contracts/exchange/contracts/src/interfaces/IExchangeRichErrors.sol index b16c895130..63c122a472 100644 --- a/contracts/exchange/contracts/src/interfaces/IExchangeRichErrors.sol +++ b/contracts/exchange/contracts/src/interfaces/IExchangeRichErrors.sol @@ -8,6 +8,13 @@ contract IExchangeRichErrors { UNKNOWN_ASSET_PROXY } + enum BatchMatchOrdersErrorCodes { + ZERO_LEFT_ORDERS, + ZERO_RIGHT_ORDERS, + INVALID_LENGTH_LEFT_SIGNATURES, + INVALID_LENGTH_RIGHT_SIGNATURES + } + enum FillErrorCodes { INVALID_TAKER_AMOUNT, TAKER_OVERPAY, diff --git a/contracts/exchange/contracts/src/interfaces/IMatchOrders.sol b/contracts/exchange/contracts/src/interfaces/IMatchOrders.sol index 9eef9e9c02..fd8b67373f 100644 --- a/contracts/exchange/contracts/src/interfaces/IMatchOrders.sol +++ b/contracts/exchange/contracts/src/interfaces/IMatchOrders.sol @@ -25,6 +25,61 @@ import "@0x/contracts-exchange-libs/contracts/src/LibFillResults.sol"; contract IMatchOrders { + /// @dev Match complementary orders that have a profitable spread. + /// Each order is filled at their respective price point, and + /// the matcher receives a profit denominated in the left maker asset. + /// @param leftOrders Set of orders with the same maker / taker asset. + /// @param rightOrders Set of orders to match against `leftOrders` + /// @param leftSignatures Proof that left orders were created by the left makers. + /// @param rightSignatures Proof that right orders were created by the right makers. + /// @return batchMatchedFillResults Amounts filled and profit generated. + function batchMatchOrders( + LibOrder.Order[] memory leftOrders, + LibOrder.Order[] memory rightOrders, + bytes[] memory leftSignatures, + bytes[] memory rightSignatures + ) + public + returns (LibFillResults.BatchMatchedFillResults memory batchMatchedFillResults); + + /// @dev Match complementary orders that have a profitable spread. + /// Each order is maximally filled at their respective price point, and + /// the matcher receives a profit denominated in either the left maker asset, + /// right maker asset, or a combination of both. + /// @param leftOrders Set of orders with the same maker / taker asset. + /// @param rightOrders Set of orders to match against `leftOrders` + /// @param leftSignatures Proof that left orders were created by the left makers. + /// @param rightSignatures Proof that right orders were created by the right makers. + /// @return batchMatchedFillResults Amounts filled and profit generated. + function batchMatchOrdersWithMaximalFill( + LibOrder.Order[] memory leftOrders, + LibOrder.Order[] memory rightOrders, + bytes[] memory leftSignatures, + bytes[] memory rightSignatures + ) + public + returns (LibFillResults.BatchMatchedFillResults memory batchMatchedFillResults); + + /// @dev Calculates fill amounts for the matched orders. + /// Each order is filled at their respective price point. However, the calculations are + /// carried out as though the orders are both being filled at the right order's price point. + /// The profit made by the leftOrder order goes to the taker (who matched the two orders). + /// @param leftOrder First order to match. + /// @param rightOrder Second order to match. + /// @param leftOrderTakerAssetFilledAmount Amount of left order already filled. + /// @param rightOrderTakerAssetFilledAmount Amount of right order already filled. + /// @param matchedFillResults Amounts to fill and fees to pay by maker and taker of matched orders. + function calculateMatchedFillResults( + LibOrder.Order memory leftOrder, + LibOrder.Order memory rightOrder, + uint256 leftOrderTakerAssetFilledAmount, + uint256 rightOrderTakerAssetFilledAmount, + bool shouldMaximallyFillOrders + ) + public + pure + returns (LibFillResults.MatchedFillResults memory matchedFillResults); + /// @dev Match two complementary orders that have a profitable spread. /// Each order is filled at their respective price point. However, the calculations are /// carried out as though the orders are both being filled at the right order's price point. @@ -43,22 +98,21 @@ contract IMatchOrders { public returns (LibFillResults.MatchedFillResults memory matchedFillResults); - /// @dev Calculates fill amounts for the matched orders. - /// Each order is filled at their respective price point. However, the calculations are - /// carried out as though the orders are both being filled at the right order's price point. - /// The profit made by the leftOrder order goes to the taker (who matched the two orders). + /// @dev Match two complementary orders that have a profitable spread. + /// Each order is maximally filled at their respective price point, and + /// the matcher receives a profit denominated in either the left maker asset, + /// right maker asset, or a combination of both. /// @param leftOrder First order to match. /// @param rightOrder Second order to match. - /// @param leftOrderTakerAssetFilledAmount Amount of left order already filled. - /// @param rightOrderTakerAssetFilledAmount Amount of right order already filled. - /// @param matchedFillResults Amounts to fill and fees to pay by maker and taker of matched orders. - function calculateMatchedFillResults( + /// @param leftSignature Proof that order was created by the left maker. + /// @param rightSignature Proof that order was created by the right maker. + /// @return matchedFillResults Amounts filled by maker and taker of matched orders. + function matchOrdersWithMaximalFill( LibOrder.Order memory leftOrder, LibOrder.Order memory rightOrder, - uint256 leftOrderTakerAssetFilledAmount, - uint256 rightOrderTakerAssetFilledAmount + bytes memory leftSignature, + bytes memory rightSignature ) public - pure returns (LibFillResults.MatchedFillResults memory matchedFillResults); } diff --git a/contracts/exchange/contracts/test/ReentrantERC20Token.sol b/contracts/exchange/contracts/test/ReentrantERC20Token.sol index f1373206c7..90fe36dc2f 100644 --- a/contracts/exchange/contracts/test/ReentrantERC20Token.sol +++ b/contracts/exchange/contracts/test/ReentrantERC20Token.sol @@ -47,6 +47,9 @@ contract ReentrantERC20Token is MARKET_BUY_ORDERS, MARKET_SELL_ORDERS, MATCH_ORDERS, + MATCH_ORDERS_WITH_MAXIMAL_FILL, + BATCH_MATCH_ORDERS, + BATCH_MATCH_ORDERS_WITH_MAXIMAL_FILL, CANCEL_ORDER, BATCH_CANCEL_ORDERS, CANCEL_ORDERS_UP_TO, @@ -147,6 +150,42 @@ contract ReentrantERC20Token is signatures[0], signatures[1] ); + } else if (currentFunctionId == uint8(ExchangeFunction.MATCH_ORDERS_WITH_MAXIMAL_FILL)) { + LibOrder.Order[2] memory orders = _createMatchedOrders(); + bytes[] memory signatures = _createWalletSignatures(2); + callData = abi.encodeWithSelector( + exchange.matchOrdersWithMaximalFill.selector, + orders[0], + orders[1], + signatures[0], + signatures[1] + ); + } else if (currentFunctionId == uint8(ExchangeFunction.BATCH_MATCH_ORDERS)) { + LibOrder.Order[] memory leftOrders; + LibOrder.Order[] memory rightOrders; + (leftOrders, rightOrders) = _createBatchMatchedOrders(); + bytes[] memory leftSignatures = _createWalletSignatures(1); + bytes[] memory rightSignatures = _createWalletSignatures(1); + callData = abi.encodeWithSelector( + exchange.batchMatchOrders.selector, + leftOrders, + rightOrders, + leftSignatures, + rightSignatures + ); + } else if (currentFunctionId == uint8(ExchangeFunction.BATCH_MATCH_ORDERS_WITH_MAXIMAL_FILL)) { + LibOrder.Order[] memory leftOrders; + LibOrder.Order[] memory rightOrders; + (leftOrders, rightOrders) = _createBatchMatchedOrders(); + bytes[] memory leftSignatures = _createWalletSignatures(1); + bytes[] memory rightSignatures = _createWalletSignatures(1); + callData = abi.encodeWithSelector( + exchange.batchMatchOrders.selector, + leftOrders, + rightOrders, + leftSignatures, + rightSignatures + ); } else if (currentFunctionId == uint8(ExchangeFunction.CANCEL_ORDER)) { callData = abi.encodeWithSelector( exchange.cancelOrder.selector, @@ -238,6 +277,20 @@ contract ReentrantERC20Token is orders[1].makerAssetAmount = orders[0].takerAssetAmount; } + function _createBatchMatchedOrders() + internal + view + returns (LibOrder.Order[] memory leftOrders, LibOrder.Order[] memory rightOrders) + { + LibOrder.Order[] memory _orders = _createOrders(2); + leftOrders = new LibOrder.Order[](1); + rightOrders = new LibOrder.Order[](1); + leftOrders[0] = _orders[0]; + rightOrders[0] = _orders[1]; + rightOrders[0].takerAssetAmount = rightOrders[0].makerAssetAmount; + rightOrders[0].makerAssetAmount = leftOrders[0].takerAssetAmount; + } + function _getTakerFillAmounts( LibOrder.Order[] memory orders ) diff --git a/contracts/exchange/contracts/test/TestExchangeInternals.sol b/contracts/exchange/contracts/test/TestExchangeInternals.sol index 08d58e06c9..f40ca21797 100644 --- a/contracts/exchange/contracts/test/TestExchangeInternals.sol +++ b/contracts/exchange/contracts/test/TestExchangeInternals.sol @@ -62,110 +62,6 @@ contract TestExchangeInternals is return _calculateFillResults(order, takerAssetFilledAmount); } - /// @dev Calculates partial value given a numerator and denominator. - /// Reverts if rounding error is >= 0.1% - /// @param numerator Numerator. - /// @param denominator Denominator. - /// @param target Value to calculate partial of. - /// @return Partial value of target. - function safeGetPartialAmountFloor( - uint256 numerator, - uint256 denominator, - uint256 target - ) - public - pure - returns (uint256 partialAmount) - { - return _safeGetPartialAmountFloor(numerator, denominator, target); - } - - /// @dev Calculates partial value given a numerator and denominator. - /// Reverts if rounding error is >= 0.1% - /// @param numerator Numerator. - /// @param denominator Denominator. - /// @param target Value to calculate partial of. - /// @return Partial value of target. - function safeGetPartialAmountCeil( - uint256 numerator, - uint256 denominator, - uint256 target - ) - public - pure - returns (uint256 partialAmount) - { - return _safeGetPartialAmountCeil(numerator, denominator, target); - } - - /// @dev Calculates partial value given a numerator and denominator. - /// @param numerator Numerator. - /// @param denominator Denominator. - /// @param target Value to calculate partial of. - /// @return Partial value of target. - function getPartialAmountFloor( - uint256 numerator, - uint256 denominator, - uint256 target - ) - public - pure - returns (uint256 partialAmount) - { - return _getPartialAmountFloor(numerator, denominator, target); - } - - /// @dev Calculates partial value given a numerator and denominator. - /// @param numerator Numerator. - /// @param denominator Denominator. - /// @param target Value to calculate partial of. - /// @return Partial value of target. - function getPartialAmountCeil( - uint256 numerator, - uint256 denominator, - uint256 target - ) - public - pure - returns (uint256 partialAmount) - { - return _getPartialAmountCeil(numerator, denominator, target); - } - - /// @dev Checks if rounding error >= 0.1%. - /// @param numerator Numerator. - /// @param denominator Denominator. - /// @param target Value to multiply with numerator/denominator. - /// @return Rounding error is present. - function isRoundingErrorFloor( - uint256 numerator, - uint256 denominator, - uint256 target - ) - public - pure - returns (bool isError) - { - return _isRoundingErrorFloor(numerator, denominator, target); - } - - /// @dev Checks if rounding error >= 0.1%. - /// @param numerator Numerator. - /// @param denominator Denominator. - /// @param target Value to multiply with numerator/denominator. - /// @return Rounding error is present. - function isRoundingErrorCeil( - uint256 numerator, - uint256 denominator, - uint256 target - ) - public - pure - returns (bool isError) - { - return _isRoundingErrorCeil(numerator, denominator, target); - } - /// @dev Updates state with results of a fill order. /// @param order that was filled. /// @param takerAddress Address of taker who filled the order. diff --git a/contracts/exchange/contracts/test/TestExchangeMath.sol b/contracts/exchange/contracts/test/TestExchangeMath.sol new file mode 100644 index 0000000000..832e4c5db7 --- /dev/null +++ b/contracts/exchange/contracts/test/TestExchangeMath.sol @@ -0,0 +1,131 @@ +/* + + Copyright 2018 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.5.9; +pragma experimental ABIEncoderV2; + +import "@0x/contracts-exchange-libs/contracts/src/LibMath.sol"; + + +contract TestExchangeMath is + LibMath +{ + /// @dev Calculates partial value given a numerator and denominator. + /// Reverts if rounding error is >= 0.1% + /// @param numerator Numerator. + /// @param denominator Denominator. + /// @param target Value to calculate partial of. + /// @return Partial value of target. + function safeGetPartialAmountFloor( + uint256 numerator, + uint256 denominator, + uint256 target + ) + public + pure + returns (uint256 partialAmount) + { + return _safeGetPartialAmountFloor(numerator, denominator, target); + } + + /// @dev Calculates partial value given a numerator and denominator. + /// Reverts if rounding error is >= 0.1% + /// @param numerator Numerator. + /// @param denominator Denominator. + /// @param target Value to calculate partial of. + /// @return Partial value of target. + function safeGetPartialAmountCeil( + uint256 numerator, + uint256 denominator, + uint256 target + ) + public + pure + returns (uint256 partialAmount) + { + return _safeGetPartialAmountCeil(numerator, denominator, target); + } + + /// @dev Calculates partial value given a numerator and denominator. + /// @param numerator Numerator. + /// @param denominator Denominator. + /// @param target Value to calculate partial of. + /// @return Partial value of target. + function getPartialAmountFloor( + uint256 numerator, + uint256 denominator, + uint256 target + ) + public + pure + returns (uint256 partialAmount) + { + return _getPartialAmountFloor(numerator, denominator, target); + } + + /// @dev Calculates partial value given a numerator and denominator. + /// @param numerator Numerator. + /// @param denominator Denominator. + /// @param target Value to calculate partial of. + /// @return Partial value of target. + function getPartialAmountCeil( + uint256 numerator, + uint256 denominator, + uint256 target + ) + public + pure + returns (uint256 partialAmount) + { + return _getPartialAmountCeil(numerator, denominator, target); + } + + /// @dev Checks if rounding error >= 0.1%. + /// @param numerator Numerator. + /// @param denominator Denominator. + /// @param target Value to multiply with numerator/denominator. + /// @return Rounding error is present. + function isRoundingErrorFloor( + uint256 numerator, + uint256 denominator, + uint256 target + ) + public + pure + returns (bool isError) + { + return _isRoundingErrorFloor(numerator, denominator, target); + } + + /// @dev Checks if rounding error >= 0.1%. + /// @param numerator Numerator. + /// @param denominator Denominator. + /// @param target Value to multiply with numerator/denominator. + /// @return Rounding error is present. + function isRoundingErrorCeil( + uint256 numerator, + uint256 denominator, + uint256 target + ) + public + pure + returns (bool isError) + { + return _isRoundingErrorCeil(numerator, denominator, target); + } +} diff --git a/contracts/exchange/package.json b/contracts/exchange/package.json index 3a42e4307f..4b5fcdf796 100644 --- a/contracts/exchange/package.json +++ b/contracts/exchange/package.json @@ -34,7 +34,7 @@ "lint-contracts": "solhint -c ../.solhint.json contracts/**/**/**/**/*.sol" }, "config": { - "abis": "./generated-artifacts/@(Exchange|ExchangeWrapper|IAssetProxyDispatcher|IEIP1271Wallet|IExchange|IExchangeCore|IMatchOrders|ISignatureValidator|ITransactions|IWallet|IWrapperFunctions|ReentrantERC20Token|TestAssetProxyDispatcher|TestExchangeInternals|TestLibExchangeRichErrorDecoder|TestSignatureValidator|TestValidatorWallet|Whitelist).json", + "abis": "./generated-artifacts/@(Exchange|ExchangeWrapper|IAssetProxyDispatcher|IEIP1271Wallet|IExchange|IExchangeCore|IMatchOrders|ISignatureValidator|ITransactions|IWallet|IWrapperFunctions|ReentrantERC20Token|TestAssetProxyDispatcher|TestExchangeInternals|TestExchangeMath|TestLibExchangeRichErrorDecoder|TestSignatureValidator|TestValidatorWallet|Whitelist).json", "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually." }, "repository": { diff --git a/contracts/exchange/src/artifacts.ts b/contracts/exchange/src/artifacts.ts index 6da04da37f..b0aa72ec35 100644 --- a/contracts/exchange/src/artifacts.ts +++ b/contracts/exchange/src/artifacts.ts @@ -19,6 +19,7 @@ import * as IWrapperFunctions from '../generated-artifacts/IWrapperFunctions.jso import * as ReentrantERC20Token from '../generated-artifacts/ReentrantERC20Token.json'; import * as TestAssetProxyDispatcher from '../generated-artifacts/TestAssetProxyDispatcher.json'; import * as TestExchangeInternals from '../generated-artifacts/TestExchangeInternals.json'; +import * as TestExchangeMath from '../generated-artifacts/TestExchangeMath.json'; import * as TestLibExchangeRichErrorDecoder from '../generated-artifacts/TestLibExchangeRichErrorDecoder.json'; import * as TestSignatureValidator from '../generated-artifacts/TestSignatureValidator.json'; import * as TestValidatorWallet from '../generated-artifacts/TestValidatorWallet.json'; @@ -28,17 +29,18 @@ export const artifacts = { Whitelist: Whitelist as ContractArtifact, Exchange: Exchange as ContractArtifact, IAssetProxyDispatcher: IAssetProxyDispatcher as ContractArtifact, + IEIP1271Wallet: IEIP1271Wallet as ContractArtifact, IExchange: IExchange as ContractArtifact, IExchangeCore: IExchangeCore as ContractArtifact, IMatchOrders: IMatchOrders as ContractArtifact, ISignatureValidator: ISignatureValidator as ContractArtifact, ITransactions: ITransactions as ContractArtifact, IWallet: IWallet as ContractArtifact, - IEIP1271Wallet: IEIP1271Wallet as ContractArtifact, IWrapperFunctions: IWrapperFunctions as ContractArtifact, ReentrantERC20Token: ReentrantERC20Token as ContractArtifact, TestAssetProxyDispatcher: TestAssetProxyDispatcher as ContractArtifact, TestExchangeInternals: TestExchangeInternals as ContractArtifact, + TestExchangeMath: TestExchangeMath as ContractArtifact, TestLibExchangeRichErrorDecoder: TestLibExchangeRichErrorDecoder as ContractArtifact, TestSignatureValidator: TestSignatureValidator as ContractArtifact, TestValidatorWallet: TestValidatorWallet as ContractArtifact, diff --git a/contracts/exchange/src/wrappers.ts b/contracts/exchange/src/wrappers.ts index fc725b0fcf..719df58807 100644 --- a/contracts/exchange/src/wrappers.ts +++ b/contracts/exchange/src/wrappers.ts @@ -17,6 +17,7 @@ export * from '../generated-wrappers/i_wrapper_functions'; export * from '../generated-wrappers/reentrant_erc20_token'; export * from '../generated-wrappers/test_asset_proxy_dispatcher'; export * from '../generated-wrappers/test_exchange_internals'; +export * from '../generated-wrappers/test_exchange_math'; export * from '../generated-wrappers/test_lib_exchange_rich_error_decoder'; export * from '../generated-wrappers/test_signature_validator'; export * from '../generated-wrappers/test_validator_wallet'; diff --git a/contracts/exchange/test/internal.ts b/contracts/exchange/test/internal.ts index 9d2ecd1f3f..48aec23621 100644 --- a/contracts/exchange/test/internal.ts +++ b/contracts/exchange/test/internal.ts @@ -16,7 +16,7 @@ import { BigNumber, providerUtils, SafeMathRevertErrors } from '@0x/utils'; import * as chai from 'chai'; import * as _ from 'lodash'; -import { artifacts, TestExchangeInternalsContract } from '../src'; +import { artifacts, TestExchangeInternalsContract, TestExchangeMathContract } from '../src'; chaiSetup.configure(); const expect = chai.expect; @@ -53,10 +53,9 @@ const emptySignedOrder: SignedOrder = { const safeMathErrorForCall = new SafeMathRevertErrors.SafeMathError(); -describe('Exchange core internal functions', () => { +describe('Exchange math internal functions', () => { let chainId: number; - let testExchange: TestExchangeInternalsContract; - let safeMathErrorForSendTransaction: Error | undefined; + let testExchange: TestExchangeMathContract; let divisionByZeroErrorForCall: Error | undefined; let roundingErrorForCall: Error | undefined; @@ -71,15 +70,13 @@ describe('Exchange core internal functions', () => { emptyOrder.domain.chainId = chainId; emptySignedOrder.domain.chainId = chainId; - testExchange = await TestExchangeInternalsContract.deployFrom0xArtifactAsync( - artifacts.TestExchangeInternals, + testExchange = await TestExchangeMathContract.deployFrom0xArtifactAsync( + artifacts.TestExchangeMath, provider, txDefaults, - new BigNumber(chainId), ); divisionByZeroErrorForCall = new Error(RevertReason.DivisionByZero); roundingErrorForCall = new Error(RevertReason.RoundingError); - safeMathErrorForSendTransaction = safeMathErrorForCall; divisionByZeroErrorForCall = new LibMathRevertErrors.DivisionByZeroError(); roundingErrorForCall = new LibMathRevertErrors.RoundingError(); }); @@ -160,118 +157,6 @@ describe('Exchange core internal functions', () => { return product.dividedToIntegerBy(denominator); } - describe('addFillResults', async () => { - function makeFillResults(value: BigNumber): FillResults { - return { - makerAssetFilledAmount: value, - takerAssetFilledAmount: value, - makerFeePaid: value, - takerFeePaid: value, - }; - } - async function referenceAddFillResultsAsync( - totalValue: BigNumber, - singleValue: BigNumber, - ): Promise { - // Note(albrow): Here, each of totalFillResults and - // singleFillResults will consist of fields with the same values. - // This should be safe because none of the fields in a given - // FillResults are ever used together in a mathemetical operation. - // They are only used with the corresponding field from *the other* - // FillResults, which are different. - const totalFillResults = makeFillResults(totalValue); - const singleFillResults = makeFillResults(singleValue); - // HACK(albrow): _.mergeWith mutates the first argument! To - // workaround this we use _.cloneDeep. - return _.mergeWith( - _.cloneDeep(totalFillResults), - singleFillResults, - (totalVal: BigNumber, singleVal: BigNumber) => { - const newTotal = totalVal.plus(singleVal); - if (newTotal.isGreaterThan(MAX_UINT256)) { - throw safeMathErrorForCall; - } - return newTotal; - }, - ); - } - async function testAddFillResultsAsync(totalValue: BigNumber, singleValue: BigNumber): Promise { - const totalFillResults = makeFillResults(totalValue); - const singleFillResults = makeFillResults(singleValue); - return testExchange.addFillResults.callAsync(totalFillResults, singleFillResults); - } - await testCombinatoriallyWithReferenceFuncAsync( - 'addFillResults', - referenceAddFillResultsAsync, - testAddFillResultsAsync, - [uint256Values, uint256Values], - ); - }); - - describe('calculateFillResults', async () => { - function makeOrder( - makerAssetAmount: BigNumber, - takerAssetAmount: BigNumber, - makerFee: BigNumber, - takerFee: BigNumber, - ): Order { - return { - ...emptyOrder, - makerAssetAmount, - takerAssetAmount, - makerFee, - takerFee, - }; - } - async function referenceCalculateFillResultsAsync( - orderTakerAssetAmount: BigNumber, - takerAssetFilledAmount: BigNumber, - otherAmount: BigNumber, - ): Promise { - // Note(albrow): Here we are re-using the same value (otherAmount) - // for order.makerAssetAmount, order.makerFee, and order.takerFee. - // This should be safe because they are never used with each other - // in any mathematical operation in either the reference TypeScript - // implementation or the Solidity implementation of - // calculateFillResults. - const makerAssetFilledAmount = await referenceSafeGetPartialAmountFloorAsync( - takerAssetFilledAmount, - orderTakerAssetAmount, - otherAmount, - ); - const order = makeOrder(otherAmount, orderTakerAssetAmount, otherAmount, otherAmount); - const orderMakerAssetAmount = order.makerAssetAmount; - return { - makerAssetFilledAmount, - takerAssetFilledAmount, - makerFeePaid: await referenceSafeGetPartialAmountFloorAsync( - makerAssetFilledAmount, - orderMakerAssetAmount, - otherAmount, - ), - takerFeePaid: await referenceSafeGetPartialAmountFloorAsync( - takerAssetFilledAmount, - orderTakerAssetAmount, - otherAmount, - ), - }; - } - async function testCalculateFillResultsAsync( - orderTakerAssetAmount: BigNumber, - takerAssetFilledAmount: BigNumber, - otherAmount: BigNumber, - ): Promise { - const order = makeOrder(otherAmount, orderTakerAssetAmount, otherAmount, otherAmount); - return testExchange.calculateFillResults.callAsync(order, takerAssetFilledAmount); - } - await testCombinatoriallyWithReferenceFuncAsync( - 'calculateFillResults', - referenceCalculateFillResultsAsync, - testCalculateFillResultsAsync, - [uint256Values, uint256Values, uint256Values], - ); - }); - describe('getPartialAmountFloor', async () => { async function referenceGetPartialAmountFloorAsync( numerator: BigNumber, @@ -427,6 +312,198 @@ describe('Exchange core internal functions', () => { [uint256Values, uint256Values, uint256Values], ); }); +}); + +describe('Exchange core internal functions', () => { + let chainId: number; + let testExchange: TestExchangeInternalsContract; + let safeMathErrorForSendTransaction: Error | undefined; + let divisionByZeroErrorForCall: Error | undefined; + let roundingErrorForCall: Error | undefined; + + before(async () => { + await blockchainLifecycle.startAsync(); + }); + after(async () => { + await blockchainLifecycle.revertAsync(); + }); + before(async () => { + chainId = await providerUtils.getChainIdAsync(provider); + emptyOrder.domain.chainId = chainId; + emptySignedOrder.domain.chainId = chainId; + + testExchange = await TestExchangeInternalsContract.deployFrom0xArtifactAsync( + artifacts.TestExchangeInternals, + provider, + txDefaults, + new BigNumber(chainId), + ); + divisionByZeroErrorForCall = new Error(RevertReason.DivisionByZero); + roundingErrorForCall = new Error(RevertReason.RoundingError); + safeMathErrorForSendTransaction = safeMathErrorForCall; + divisionByZeroErrorForCall = new LibMathRevertErrors.DivisionByZeroError(); + roundingErrorForCall = new LibMathRevertErrors.RoundingError(); + }); + // Note(albrow): Don't forget to add beforeEach and afterEach calls to reset + // the blockchain state for any tests which modify it! + + async function referenceIsRoundingErrorFloorAsync( + numerator: BigNumber, + denominator: BigNumber, + target: BigNumber, + ): Promise { + if (denominator.eq(0)) { + throw divisionByZeroErrorForCall; + } + if (numerator.eq(0)) { + return false; + } + if (target.eq(0)) { + return false; + } + const product = numerator.multipliedBy(target); + const remainder = product.mod(denominator); + const remainderTimes1000 = remainder.multipliedBy('1000'); + const isError = remainderTimes1000.gte(product); + if (product.isGreaterThan(MAX_UINT256)) { + throw safeMathErrorForCall; + } + if (remainderTimes1000.isGreaterThan(MAX_UINT256)) { + throw safeMathErrorForCall; + } + return isError; + } + + async function referenceSafeGetPartialAmountFloorAsync( + numerator: BigNumber, + denominator: BigNumber, + target: BigNumber, + ): Promise { + if (denominator.eq(0)) { + throw divisionByZeroErrorForCall; + } + const isRoundingError = await referenceIsRoundingErrorFloorAsync(numerator, denominator, target); + if (isRoundingError) { + throw roundingErrorForCall; + } + const product = numerator.multipliedBy(target); + if (product.isGreaterThan(MAX_UINT256)) { + throw safeMathErrorForCall; + } + return product.dividedToIntegerBy(denominator); + } + + describe('addFillResults', async () => { + function makeFillResults(value: BigNumber): FillResults { + return { + makerAssetFilledAmount: value, + takerAssetFilledAmount: value, + makerFeePaid: value, + takerFeePaid: value, + }; + } + async function referenceAddFillResultsAsync( + totalValue: BigNumber, + singleValue: BigNumber, + ): Promise { + // Note(albrow): Here, each of totalFillResults and + // singleFillResults will consist of fields with the same values. + // This should be safe because none of the fields in a given + // FillResults are ever used together in a mathemetical operation. + // They are only used with the corresponding field from *the other* + // FillResults, which are different. + const totalFillResults = makeFillResults(totalValue); + const singleFillResults = makeFillResults(singleValue); + // HACK(albrow): _.mergeWith mutates the first argument! To + // workaround this we use _.cloneDeep. + return _.mergeWith( + _.cloneDeep(totalFillResults), + singleFillResults, + (totalVal: BigNumber, singleVal: BigNumber) => { + const newTotal = totalVal.plus(singleVal); + if (newTotal.isGreaterThan(MAX_UINT256)) { + throw safeMathErrorForCall; + } + return newTotal; + }, + ); + } + async function testAddFillResultsAsync(totalValue: BigNumber, singleValue: BigNumber): Promise { + const totalFillResults = makeFillResults(totalValue); + const singleFillResults = makeFillResults(singleValue); + return testExchange.addFillResults.callAsync(totalFillResults, singleFillResults); + } + await testCombinatoriallyWithReferenceFuncAsync( + 'addFillResults', + referenceAddFillResultsAsync, + testAddFillResultsAsync, + [uint256Values, uint256Values], + ); + }); + + describe('calculateFillResults', async () => { + function makeOrder( + makerAssetAmount: BigNumber, + takerAssetAmount: BigNumber, + makerFee: BigNumber, + takerFee: BigNumber, + ): Order { + return { + ...emptyOrder, + makerAssetAmount, + takerAssetAmount, + makerFee, + takerFee, + }; + } + async function referenceCalculateFillResultsAsync( + orderTakerAssetAmount: BigNumber, + takerAssetFilledAmount: BigNumber, + otherAmount: BigNumber, + ): Promise { + // Note(albrow): Here we are re-using the same value (otherAmount) + // for order.makerAssetAmount, order.makerFee, and order.takerFee. + // This should be safe because they are never used with each other + // in any mathematical operation in either the reference TypeScript + // implementation or the Solidity implementation of + // calculateFillResults. + const makerAssetFilledAmount = await referenceSafeGetPartialAmountFloorAsync( + takerAssetFilledAmount, + orderTakerAssetAmount, + otherAmount, + ); + const order = makeOrder(otherAmount, orderTakerAssetAmount, otherAmount, otherAmount); + const orderMakerAssetAmount = order.makerAssetAmount; + return { + makerAssetFilledAmount, + takerAssetFilledAmount, + makerFeePaid: await referenceSafeGetPartialAmountFloorAsync( + makerAssetFilledAmount, + orderMakerAssetAmount, + otherAmount, + ), + takerFeePaid: await referenceSafeGetPartialAmountFloorAsync( + takerAssetFilledAmount, + orderTakerAssetAmount, + otherAmount, + ), + }; + } + async function testCalculateFillResultsAsync( + orderTakerAssetAmount: BigNumber, + takerAssetFilledAmount: BigNumber, + otherAmount: BigNumber, + ): Promise { + const order = makeOrder(otherAmount, orderTakerAssetAmount, otherAmount, otherAmount); + return testExchange.calculateFillResults.callAsync(order, takerAssetFilledAmount); + } + await testCombinatoriallyWithReferenceFuncAsync( + 'calculateFillResults', + referenceCalculateFillResultsAsync, + testCalculateFillResultsAsync, + [uint256Values, uint256Values, uint256Values], + ); + }); describe('updateFilledState', async () => { // Note(albrow): Since updateFilledState modifies the state by calling @@ -481,3 +558,4 @@ describe('Exchange core internal functions', () => { ); }); }); +// tslint:disable-line:max-file-line-count diff --git a/contracts/exchange/test/match_orders.ts b/contracts/exchange/test/match_orders.ts index 78a35f9bcb..1e200778ce 100644 --- a/contracts/exchange/test/match_orders.ts +++ b/contracts/exchange/test/match_orders.ts @@ -11,10 +11,18 @@ import { import { ERC1155Contract as ERC1155TokenContract, Erc1155Wrapper as ERC1155Wrapper } from '@0x/contracts-erc1155'; import { DummyERC20TokenContract } from '@0x/contracts-erc20'; import { DummyERC721TokenContract } from '@0x/contracts-erc721'; -import { chaiSetup, constants, OrderFactory, provider, txDefaults, web3Wrapper } from '@0x/contracts-test-utils'; +import { + chaiSetup, + constants, + 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 { 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'; @@ -26,11 +34,12 @@ import { ExchangeContract, ExchangeWrapper, ReentrantERC20TokenContract, - TestExchangeInternalsContract, + TestExchangeMathContract, } from '../src'; import { MatchOrderTester, TokenBalances } from './utils/match_order_tester'; +const ZERO = new BigNumber(0); const ONE = new BigNumber(1); const TWO = new BigNumber(2); @@ -79,7 +88,7 @@ describe('matchOrders', () => { let matchOrderTester: MatchOrderTester; - let testExchange: TestExchangeInternalsContract; + let testExchangeMath: TestExchangeMathContract; before(async () => { await blockchainLifecycle.startAsync(); @@ -231,11 +240,10 @@ describe('matchOrders', () => { orderFactoryLeft = new OrderFactory(privateKeyLeft, defaultOrderParamsLeft); const privateKeyRight = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(makerAddressRight)]; orderFactoryRight = new OrderFactory(privateKeyRight, defaultOrderParamsRight); - testExchange = await TestExchangeInternalsContract.deployFrom0xArtifactAsync( - artifacts.TestExchangeInternals, + testExchangeMath = await TestExchangeMathContract.deployFrom0xArtifactAsync( + artifacts.TestExchangeMath, provider, txDefaults, - new BigNumber(chainId), ); // Create match order tester matchOrderTester = new MatchOrderTester(exchangeWrapper, erc20Wrapper, erc721Wrapper, erc1155ProxyWrapper); @@ -268,13 +276,13 @@ describe('matchOrders', () => { const numerator = signedOrderLeft.makerAssetAmount; const denominator = signedOrderLeft.takerAssetAmount; const target = signedOrderRight.makerAssetAmount; - const isRoundingErrorCeil = await testExchange.isRoundingErrorCeil.callAsync( + const isRoundingErrorCeil = await testExchangeMath.isRoundingErrorCeil.callAsync( numerator, denominator, target, ); expect(isRoundingErrorCeil).to.be.true(); - const isRoundingErrorFloor = await testExchange.isRoundingErrorFloor.callAsync( + const isRoundingErrorFloor = await testExchangeMath.isRoundingErrorFloor.callAsync( numerator, denominator, target, @@ -310,6 +318,7 @@ describe('matchOrders', () => { }, takerAddress, expectedTransferAmounts, + false, ); }); @@ -333,13 +342,13 @@ describe('matchOrders', () => { const numerator = signedOrderRight.takerAssetAmount; const denominator = signedOrderRight.makerAssetAmount; const target = signedOrderLeft.takerAssetAmount; - const isRoundingErrorFloor = await testExchange.isRoundingErrorFloor.callAsync( + const isRoundingErrorFloor = await testExchangeMath.isRoundingErrorFloor.callAsync( numerator, denominator, target, ); expect(isRoundingErrorFloor).to.be.true(); - const isRoundingErrorCeil = await testExchange.isRoundingErrorCeil.callAsync( + const isRoundingErrorCeil = await testExchangeMath.isRoundingErrorCeil.callAsync( numerator, denominator, target, @@ -377,6 +386,7 @@ describe('matchOrders', () => { }, takerAddress, expectedTransferAmounts, + false, ); }); @@ -430,6 +440,7 @@ describe('matchOrders', () => { }, takerAddress, expectedTransferAmounts, + false, ); }); @@ -480,6 +491,7 @@ describe('matchOrders', () => { }, takerAddress, expectedTransferAmounts, + false, ); }); @@ -526,6 +538,7 @@ describe('matchOrders', () => { }, takerAddress, expectedTransferAmounts, + false, ); }); @@ -572,6 +585,7 @@ describe('matchOrders', () => { }, takerAddress, expectedTransferAmounts, + false, ); }); @@ -632,6 +646,7 @@ describe('matchOrders', () => { }, takerAddress, expectedTransferAmounts, + false, ); }); @@ -694,6 +709,7 @@ describe('matchOrders', () => { }, takerAddress, expectedTransferAmounts, + false, ); }); @@ -727,6 +743,7 @@ describe('matchOrders', () => { }, takerAddress, expectedTransferAmounts, + false, ); }); @@ -762,6 +779,7 @@ describe('matchOrders', () => { }, takerAddress, expectedTransferAmounts, + false, ); }); @@ -797,6 +815,7 @@ describe('matchOrders', () => { }, takerAddress, expectedTransferAmounts, + false, ); }); @@ -832,6 +851,7 @@ describe('matchOrders', () => { }, takerAddress, expectedTransferAmounts, + false, ); // Construct second right order // Note: This order needs makerAssetAmount=90/takerAssetAmount=[anything <= 45] to fully fill the right order. @@ -862,6 +882,7 @@ describe('matchOrders', () => { }, takerAddress, expectedTransferAmounts2, + false, await matchOrderTester.getBalancesAsync(), ); }); @@ -898,6 +919,7 @@ describe('matchOrders', () => { }, takerAddress, expectedTransferAmounts, + false, ); // Create second left order @@ -928,6 +950,7 @@ describe('matchOrders', () => { }, takerAddress, expectedTransferAmounts2, + false, await matchOrderTester.getBalancesAsync(), ); }); @@ -965,6 +988,7 @@ describe('matchOrders', () => { }, takerAddress, expectedTransferAmounts, + false, ); }); @@ -1000,6 +1024,7 @@ describe('matchOrders', () => { }, takerAddress, expectedTransferAmounts, + false, ); }); @@ -1035,6 +1060,7 @@ describe('matchOrders', () => { }, takerAddress, expectedTransferAmounts, + false, ); }); @@ -1070,6 +1096,7 @@ describe('matchOrders', () => { }, takerAddress, expectedTransferAmounts, + false, ); }); @@ -1105,6 +1132,7 @@ describe('matchOrders', () => { }, takerAddress, expectedTransferAmounts, + false, ); }); @@ -1141,6 +1169,7 @@ describe('matchOrders', () => { }, takerAddress, expectedTransferAmounts, + false, ); }); @@ -1177,6 +1206,7 @@ describe('matchOrders', () => { }, takerAddress, expectedTransferAmounts, + false, ); }); @@ -1213,6 +1243,7 @@ describe('matchOrders', () => { }, takerAddress, expectedTransferAmounts, + false, ); }); @@ -1251,6 +1282,7 @@ describe('matchOrders', () => { }, takerAddress, expectedTransferAmounts, + false, ); }); @@ -1367,488 +1399,2492 @@ describe('matchOrders', () => { const tx = exchangeWrapper.matchOrdersAsync(signedOrderLeft, signedOrderRight, takerAddress); return expect(tx).to.revertWith(expectedError); }); - - describe('combinations', () => { - // tslint:disable: enum-naming - enum AssetType { - ERC20A = 'ERC20_A', - ERC20B = 'ERC20_B', - ERC20C = 'ERC20_C', - ERC20D = 'ERC20_D', - ERC721LeftMaker = 'ERC721_LEFT_MAKER', - ERC721RightMaker = 'ERC721_RIGHT_MAKER', - ERC721Taker = 'ERC721_TAKER', - ERC1155FungibleA = 'ERC1155_FUNGIBLE_A', - ERC1155FungibleB = 'ERC1155_FUNGIBLE_B', - ERC1155FungibleC = 'ERC1155_FUNGIBLE_C', - ERC1155FungibleD = 'ERC1155_FUNGIBLE_D', - ERC1155NonFungibleLeftMaker = 'ERC1155_NON_FUNGIBLE_LEFT_MAKER', - ERC1155NonFungibleRightMaker = 'ERC1155_NON_FUNGIBLE_RIGHT_MAKER', - ERC1155NonFungibleTaker = 'ERC1155_NON_FUNGIBLE_TAKER', - MultiAssetA = 'MULTI_ASSET_A', - MultiAssetB = 'MULTI_ASSET_B', - MultiAssetC = 'MULTI_ASSET_C', - MultiAssetD = 'MULTI_ASSET_D', - } - const fungibleTypes = [ - AssetType.ERC20A, - AssetType.ERC20B, - AssetType.ERC20C, - AssetType.ERC20D, - AssetType.ERC1155FungibleA, - AssetType.ERC1155FungibleB, - AssetType.ERC1155FungibleC, - AssetType.ERC1155FungibleD, - AssetType.MultiAssetA, - AssetType.MultiAssetB, - AssetType.MultiAssetC, - AssetType.MultiAssetD, - ]; - interface AssetCombination { - leftMaker: AssetType; - rightMaker: AssetType; - leftMakerFee: AssetType; - rightMakerFee: AssetType; - leftTakerFee: AssetType; - rightTakerFee: AssetType; - description?: string; - shouldFail?: boolean; - } - const assetCombinations: AssetCombination[] = [ - { - leftMaker: AssetType.ERC20A, - rightMaker: AssetType.ERC20B, - leftMakerFee: AssetType.ERC20C, - rightMakerFee: AssetType.ERC20C, - leftTakerFee: AssetType.ERC20C, - rightTakerFee: AssetType.ERC20C, - }, - { - leftMaker: AssetType.ERC721LeftMaker, - rightMaker: AssetType.ERC721RightMaker, - leftMakerFee: AssetType.ERC20C, - rightMakerFee: AssetType.ERC20C, - leftTakerFee: AssetType.ERC20C, - rightTakerFee: AssetType.ERC20C, - }, + }); + describe('matchOrdersWithMaximalFill', () => { + it('Should transfer correct amounts when right order is fully filled and values pass isRoundingErrorCeil but fail isRoundingErrorFloor', async () => { + // Create orders to match + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAddress: makerAddressLeft, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(17, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(98, 0), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(75, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(13, 0), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Assert is rounding error ceil & not rounding error floor + // These assertions are taken from MixinMatchOrders::calculateMatchedFillResults + // The rounding error is derived computating how much the left maker will sell. + const numerator = signedOrderLeft.makerAssetAmount; + const denominator = signedOrderLeft.takerAssetAmount; + const target = signedOrderRight.makerAssetAmount; + const isRoundingErrorCeil = await testExchangeMath.isRoundingErrorCeil.callAsync( + numerator, + denominator, + target, + ); + expect(isRoundingErrorCeil).to.be.true(); + const isRoundingErrorFloor = await testExchangeMath.isRoundingErrorFloor.callAsync( + numerator, + denominator, + target, + ); + expect(isRoundingErrorFloor).to.be.false(); + // Match signedOrderLeft with signedOrderRight + // Note that the left maker received a slightly better sell price. + // This is intentional; see note in MixinMatchOrders.calculateMatchedFillResults. + // Because the left maker received a slightly more favorable sell price, the fee + // paid by the left taker is slightly higher than that paid by the left maker. + // Fees can be thought of as a tax paid by the seller, derived from the sale price. + const expectedTransferAmounts = { + // Left Maker + leftMakerAssetSoldByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(13, 0), + leftMakerFeeAssetPaidByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount( + new BigNumber('76.4705882352941176'), + 16, + ), // 76.47% + // Right Maker + rightMakerAssetSoldByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(75, 0), + rightMakerFeeAssetPaidByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + // Taker + leftTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount( + new BigNumber('76.5306122448979591'), + 16, + ), // 76.53% + rightTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + }; + await matchOrderTester.matchOrdersAndAssertEffectsAsync( { - leftMaker: AssetType.ERC721LeftMaker, - rightMaker: AssetType.ERC20A, - leftMakerFee: AssetType.ERC20C, - rightMakerFee: AssetType.ERC20C, - leftTakerFee: AssetType.ERC20C, - rightTakerFee: AssetType.ERC20C, + leftOrder: signedOrderLeft, + rightOrder: signedOrderRight, }, + takerAddress, + expectedTransferAmounts, + true, + ); + }); + + it('Should transfer correct amounts when left order is fully filled and values pass isRoundingErrorCeil and isRoundingErrorFloor', async () => { + // Create orders to match + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAddress: makerAddressLeft, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(15, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(90, 0), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(196, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(28, 0), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Assert is rounding error floor + // These assertions are taken from MixinMatchOrders::calculateMatchedFillResults + // The rounding error is derived computating how much the right maker will buy. + const numerator = signedOrderRight.makerAssetAmount; + const denominator = signedOrderRight.takerAssetAmount; + const target = signedOrderLeft.makerAssetAmount; + const isRoundingErrorCeil = await testExchangeMath.isRoundingErrorCeil.callAsync( + numerator, + denominator, + target, + ); + expect(isRoundingErrorCeil).to.be.false(); + const isRoundingErrorFloor = await testExchangeMath.isRoundingErrorFloor.callAsync( + numerator, + denominator, + target, + ); + expect(isRoundingErrorFloor).to.be.false(); + // Match signedOrderLeft with signedOrderRight + // Note that the right maker received a slightly better purchase price. + // This is intentional; see note in MixinMatchOrders.calculateMatchedFillResults. + // Because the right maker received a slightly more favorable buy price, the fee + // paid by the right taker is slightly higher than that paid by the right maker. + // Fees can be thought of as a tax paid by the seller, derived from the sale price. + const expectedTransferAmounts = { + // Left Maker + leftMakerAssetSoldByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(15, 0), + leftMakerFeeAssetPaidByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + rightMakerAssetBoughtByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(90, 0), + // Right Maker + leftMakerAssetBoughtByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(15, 0), + rightMakerAssetSoldByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(105, 0), + rightMakerFeeAssetPaidByRightMakerAmount: Web3Wrapper.toBaseUnitAmount( + new BigNumber('53.5714285714285714'), + 16, + ), // 53.57% + // Taker + rightMakerAssetReceivedByTakerAmount: Web3Wrapper.toBaseUnitAmount(15, 0), + leftTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + rightTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount( + new BigNumber('53.5714285714285714'), + 16, + ), // 53.57% + }; + await matchOrderTester.matchOrdersAndAssertEffectsAsync( { - leftMaker: AssetType.ERC20A, - rightMaker: AssetType.ERC721RightMaker, - leftMakerFee: AssetType.ERC20C, - rightMakerFee: AssetType.ERC20C, - leftTakerFee: AssetType.ERC20C, - rightTakerFee: AssetType.ERC20C, + leftOrder: signedOrderLeft, + rightOrder: signedOrderRight, }, + takerAddress, + expectedTransferAmounts, + true, + ); + }); + + it('Should transfer correct amounts when left order is fully filled', async () => { + // Create orders to match + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAddress: makerAddressLeft, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(16, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(22, 0), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress), + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(87, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(48, 0), + feeRecipientAddress: feeRecipientAddressRight, + }); + const expectedTransferAmounts = { + // Left Maker + leftMakerAssetSoldByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(16, 0), + leftMakerFeeAssetPaidByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + rightMakerAssetBoughtByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(22, 0), + // Right Maker + rightMakerAssetSoldByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(29, 0), + leftMakerAssetBoughtByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(16, 0), + rightMakerFeeAssetPaidByRightMakerAmount: Web3Wrapper.toBaseUnitAmount( + new BigNumber('33.3333333333333333'), + 16, + ), // 33.33% + // Taker + rightMakerAssetReceivedByTakerAmount: Web3Wrapper.toBaseUnitAmount(7, 0), + leftTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + rightTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount( + new BigNumber('33.3333333333333333'), + 16, + ), // 33.33% + }; + // Match signedOrderLeft with signedOrderRight + await matchOrderTester.matchOrdersAndAssertEffectsAsync( { - leftMaker: AssetType.ERC1155FungibleA, - rightMaker: AssetType.ERC20A, - leftMakerFee: AssetType.ERC20C, - rightMakerFee: AssetType.ERC20C, - leftTakerFee: AssetType.ERC20C, - rightTakerFee: AssetType.ERC20C, + leftOrder: signedOrderLeft, + rightOrder: signedOrderRight, }, + takerAddress, + expectedTransferAmounts, + true, + ); + }); + + it('should fully fill both orders and pay out profit in both maker assets', async () => { + // Create orders to match + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAddress: makerAddressLeft, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(7, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(4, 0), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(8, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(6, 0), + }); + const expectedTransferAmounts = { + // Left Maker + leftMakerAssetSoldByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(7, 0), + leftMakerFeeAssetPaidByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + rightMakerAssetBoughtByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(4, 0), // 100% + // Right Maker + rightMakerAssetSoldByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(8, 0), + rightMakerFeeAssetPaidByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + leftMakerAssetBoughtByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(6, 0), // 100% + // Taker + leftMakerAssetReceivedByTakerAmount: Web3Wrapper.toBaseUnitAmount(1, 0), + rightMakerAssetReceivedByTakerAmount: Web3Wrapper.toBaseUnitAmount(4, 0), + leftTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + rightTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // + }; + // Match signedOrderLeft with signedOrderRight + await matchOrderTester.matchOrdersAndAssertEffectsAsync( { - leftMaker: AssetType.ERC20A, - rightMaker: AssetType.ERC1155FungibleB, - leftMakerFee: AssetType.ERC20C, - rightMakerFee: AssetType.ERC20C, - leftTakerFee: AssetType.ERC20C, - rightTakerFee: AssetType.ERC20C, + leftOrder: signedOrderLeft, + rightOrder: signedOrderRight, }, + takerAddress, + expectedTransferAmounts, + true, + ); + }); + + it('Should give left maker a better sell 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), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress), + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(89, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(1, 0), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Note: + // The maker/taker fee percentage paid on the left order differs because + // they received different sale prices. The left maker pays a fee + // slightly lower than the left taker. + const expectedTransferAmounts = { + // Left Maker + leftMakerAssetSoldByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(11, 0), + leftMakerFeeAssetPaidByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount( + new BigNumber('91.6666666666666666'), + 16, + ), // 91.6% + // Right Maker + rightMakerAssetSoldByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(89, 0), + leftMakerAssetBoughtByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(1, 0), + rightMakerFeeAssetPaidByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + // Taker + leftMakerAssetReceivedByTakerAmount: Web3Wrapper.toBaseUnitAmount(10, 0), + leftTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount( + new BigNumber('91.7525773195876288'), + 16, + ), // 91.75% + rightTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + }; + // Match signedOrderLeft with signedOrderRight + await matchOrderTester.matchOrdersAndAssertEffectsAsync( { - leftMaker: AssetType.ERC1155FungibleA, - rightMaker: AssetType.ERC1155FungibleA, - leftMakerFee: AssetType.ERC20C, - rightMakerFee: AssetType.ERC20C, - leftTakerFee: AssetType.ERC20C, - rightTakerFee: AssetType.ERC20C, + leftOrder: signedOrderLeft, + rightOrder: signedOrderRight, }, + takerAddress, + expectedTransferAmounts, + true, + ); + }); + + it('Should give right maker and right taker a favorable fee price when rounding', async () => { + // Create orders to match + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAddress: makerAddressLeft, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(16, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(22, 0), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress), + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(87, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(48, 0), + feeRecipientAddress: feeRecipientAddressRight, + makerFee: Web3Wrapper.toBaseUnitAmount(10000, 0), + takerFee: Web3Wrapper.toBaseUnitAmount(10000, 0), + }); + const expectedTransferAmounts = { + // Left Maker + leftMakerAssetSoldByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(16, 0), + leftMakerFeeAssetPaidByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + rightMakerAssetBoughtByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(22, 0), + // Right Maker + rightMakerAssetSoldByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(29, 0), + leftMakerAssetBoughtByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(16, 0), + rightMakerFeeAssetPaidByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(3333, 0), // 3333.3 repeating rounded down to 3333 + // Taker + rightMakerAssetReceivedByTakerAmount: Web3Wrapper.toBaseUnitAmount(7, 0), + leftTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + rightTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(3333, 0), // 3333.3 repeating rounded down to 3333 + }; + // Match signedOrderLeft with signedOrderRight + await matchOrderTester.matchOrdersAndAssertEffectsAsync( { - leftMaker: AssetType.ERC1155NonFungibleLeftMaker, - rightMaker: AssetType.ERC20A, - leftMakerFee: AssetType.ERC20C, - rightMakerFee: AssetType.ERC20C, - leftTakerFee: AssetType.ERC20C, - rightTakerFee: AssetType.ERC20C, - }, - { - leftMaker: AssetType.ERC20A, - rightMaker: AssetType.ERC1155NonFungibleRightMaker, - leftMakerFee: AssetType.ERC20C, - rightMakerFee: AssetType.ERC20C, - leftTakerFee: AssetType.ERC20C, - rightTakerFee: AssetType.ERC20C, + leftOrder: signedOrderLeft, + rightOrder: signedOrderRight, }, + takerAddress, + expectedTransferAmounts, + true, + ); + }); + + 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), + feeRecipientAddress: feeRecipientAddressLeft, + makerFee: Web3Wrapper.toBaseUnitAmount(10000, 0), + takerFee: Web3Wrapper.toBaseUnitAmount(10000, 0), + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress), + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(89, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(1, 0), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Note: + // The maker/taker fee percentage paid on the left order differs because + // they received different sale prices. The left maker pays a + // fee slightly lower than the left taker. + const expectedTransferAmounts = { + // Left Maker + leftMakerAssetSoldByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(11, 0), + leftMakerFeeAssetPaidByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(9166, 0), // 9166.6 rounded down to 9166 + // Right Maker + rightMakerAssetSoldByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(89, 0), + leftMakerAssetBoughtByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(1, 0), + rightMakerFeeAssetPaidByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + // Taker + leftMakerAssetReceivedByTakerAmount: Web3Wrapper.toBaseUnitAmount(10, 0), + leftTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(9175, 0), // 9175.2 rounded down to 9175 + rightTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + }; + // Match signedOrderLeft with signedOrderRight + await matchOrderTester.matchOrdersAndAssertEffectsAsync( + { + leftOrder: signedOrderLeft, + rightOrder: signedOrderRight, + }, + takerAddress, + expectedTransferAmounts, + true, + ); + }); + + it('Should give left maker a better sell 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), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress), + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(89, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(1, 0), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Note: + // The maker/taker fee percentage paid on the left order differs because + // they received different sale prices. The left maker pays a fee + // slightly lower than the left taker. + const expectedTransferAmounts = { + // Left Maker + leftMakerAssetSoldByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(11, 0), + leftMakerFeeAssetPaidByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount( + new BigNumber('91.6666666666666666'), + 16, + ), // 91.6% + // Right Maker + rightMakerAssetSoldByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(89, 0), + leftMakerAssetBoughtByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(1, 0), + rightMakerFeeAssetPaidByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + // Taker + leftMakerAssetReceivedByTakerAmount: Web3Wrapper.toBaseUnitAmount(10, 0), + leftTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount( + new BigNumber('91.7525773195876288'), + 16, + ), // 91.75% + rightTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + }; + // Match signedOrderLeft with signedOrderRight + await matchOrderTester.matchOrdersAndAssertEffectsAsync( + { + leftOrder: signedOrderLeft, + rightOrder: signedOrderRight, + }, + takerAddress, + expectedTransferAmounts, + true, + ); + }); + + it('should transfer the correct amounts when consecutive calls are used to completely fill the left order', 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), + }); + // Match orders + const expectedTransferAmounts = { + // Left Maker + leftMakerAssetSoldByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + leftMakerFeeAssetPaidByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(10, 16), // 10% + // Right Maker + rightMakerAssetSoldByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + leftMakerAssetBoughtByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + rightMakerFeeAssetPaidByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + // Taker + leftMakerAssetReceivedByTakerAmount: Web3Wrapper.toBaseUnitAmount(3, 18), + leftTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(10, 16), // 10% + rightTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + }; + // prettier-ignore + const matchResults = await matchOrderTester.matchOrdersAndAssertEffectsAsync( + { + leftOrder: signedOrderLeft, + rightOrder: signedOrderRight, + }, + takerAddress, + expectedTransferAmounts, + true, + ); + // Construct second right order + // Note: This order needs makerAssetAmount=90/takerAssetAmount=[anything <= 45] to fully fill the right order. + // However, we use 100/50 to ensure a partial fill as we want to go down the "left fill" + // branch in the contract twice for this test. + const signedOrderRight2 = await orderFactoryRight.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(100, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(50, 18), + }); + // Match signedOrderLeft with signedOrderRight2 + const expectedTransferAmounts2 = { + // Left Maker + leftMakerAssetSoldByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(45, 18), + leftMakerFeeAssetPaidByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(90, 16), // 90% (10% paid earlier) + // Right Maker + rightMakerAssetSoldByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(90, 18), + rightMakerFeeAssetPaidByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(90, 16), // 90% + // Taker + leftTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(90, 16), // 90% (10% paid earlier) + rightTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(90, 16), // 90% + }; + + await matchOrderTester.matchOrdersAndAssertEffectsAsync( + { + leftOrder: signedOrderLeft, + rightOrder: signedOrderRight2, + leftOrderTakerAssetFilledAmount: matchResults.orders.leftOrderTakerAssetFilledAmount, + }, + takerAddress, + expectedTransferAmounts2, + true, + await matchOrderTester.getBalancesAsync(), + ); + }); + + 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), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + makerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress), + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(2126, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(1063, 0), + feeRecipientAddress: feeRecipientAddressRight, + }); + const expectedTransferAmounts = { + // Left Maker + leftMakerAssetSoldByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(1000, 0), + rightMakerAssetBoughtByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(1005, 0), + leftMakerFeeAssetPaidByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + // Right Maker + // Notes: + // i. + // The left order is fully filled by the right order, so the right maker must sell 1005 units of their asset to the left maker. + // By selling 1005 units, the right maker should theoretically receive 502.5 units of the left maker's asset. + // Since the transfer amount must be an integer, this value must be rounded down to 502 or up to 503. + // ii. + // If the right order were filled via `Exchange.fillOrder` the respective fill amounts would be [1004, 502] or [1006, 503]. + // It follows that we cannot trigger a sale of 1005 units of the right maker's asset through `Exchange.fillOrder`. + // iii. + // For an optimal match, the algorithm must choose either [1005, 502] or [1005, 503] as fill amounts for the right order. + // The algorithm favors the right maker when the exchange rate must be rounded, so the final fill for the right order is [1005, 503]. + // iv. + // The right maker fee differs from the right taker fee because their exchange rate differs. + // The right maker always receives the better exchange and fee price. + rightMakerAssetSoldByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(2000, 0), + rightMakerFeeAssetPaidByRightMakerAmount: Web3Wrapper.toBaseUnitAmount( + new BigNumber('94.0733772342427093'), + 16, + ), // 94.07% + // Taker + rightMakerAssetReceivedByTakerAmount: Web3Wrapper.toBaseUnitAmount(995, 0), + leftTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + rightTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount( + new BigNumber('94.0733772342427093'), + 16, + ), // 94.07% + }; + // Match signedOrderLeft with signedOrderRight + await matchOrderTester.matchOrdersAndAssertEffectsAsync( + { + leftOrder: signedOrderLeft, + rightOrder: signedOrderRight, + }, + takerAddress, + expectedTransferAmounts, + true, + ); + }); + + const reentrancyTest = (functionNames: string[]) => { + _.forEach(functionNames, async (functionName: string, functionId: number) => { + const description = `should not allow matchOrdersWithMaximalFill to reenter the Exchange contract via ${functionName}`; + it(description, async () => { + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAssetData: assetDataUtils.encodeERC20AssetData(reentrantErc20Token.address), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + takerAssetData: assetDataUtils.encodeERC20AssetData(reentrantErc20Token.address), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + await web3Wrapper.awaitTransactionSuccessAsync( + await reentrantErc20Token.setReentrantFunction.sendTransactionAsync(functionId), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const expectedError = new ReentrancyGuardRevertErrors.IllegalReentrancyError(); + const tx = exchangeWrapper.matchOrdersWithMaximalFillAsync( + signedOrderLeft, + signedOrderRight, + takerAddress, + ); + return expect(tx).to.revertWith(expectedError); + }); + }); + }; + describe('matchOrdersWithMaximalFill reentrancy tests', () => + reentrancyTest(exchangeConstants.FUNCTIONS_WITH_MUTEX)); + + 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), + }); + // Match signedOrderLeft with signedOrderRight + const expectedTransferAmounts = { + // Left Maker + leftMakerAssetSoldByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + leftMakerFeeAssetPaidByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + // Right Maker + rightMakerAssetSoldByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + rightMakerFeeAssetPaidByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + // Taker + leftTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + rightTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + }; + // Match signedOrderLeft with signedOrderRight + await matchOrderTester.matchOrdersAndAssertEffectsAsync( + { + leftOrder: signedOrderLeft, + rightOrder: signedOrderRight, + }, + takerAddress, + expectedTransferAmounts, + true, + ); + }); + + it('should transfer the correct amounts when consecutive calls are used to completely fill the right order', async () => { + // Create orders to match + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + }); + + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(50, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(100, 18), + }); + + // Match orders + const expectedTransferAmounts = { + // Left Maker + leftMakerAssetSoldByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + leftMakerFeeAssetPaidByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + rightMakerAssetBoughtByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + // Right Maker + rightMakerAssetSoldByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + leftMakerAssetBoughtByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + rightMakerFeeAssetPaidByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(10, 16), // 10% + // Taker + rightMakerAssetReceivedByTakerAmount: Web3Wrapper.toBaseUnitAmount(3, 18), + leftTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + rightTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(10, 16), // 10% + }; + const matchResults = await matchOrderTester.matchOrdersAndAssertEffectsAsync( + { + leftOrder: signedOrderLeft, + rightOrder: signedOrderRight, + }, + takerAddress, + expectedTransferAmounts, + true, + ); + + // Create second left order + // Note: This order needs makerAssetAmount=96/takerAssetAmount=48 to fully fill the right order. + // However, we use 100/50 to ensure a partial fill as we want to go down the "right fill" + // branch in the contract twice for this test. + const signedOrderLeft2 = await orderFactoryLeft.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(100, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(50, 18), + }); + + // Match signedOrderLeft2 with signedOrderRight + const expectedTransferAmounts2 = { + // Left Maker + leftMakerAssetSoldByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(90, 18), + leftMakerFeeAssetPaidByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(90, 16), // 90% + // Right Maker + rightMakerAssetSoldByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(45, 18), + rightMakerFeeAssetPaidByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(90, 16), // 90% + // Taker + leftTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(90, 16), // 96% + rightTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(90, 16), // 90% + }; + + await matchOrderTester.matchOrdersAndAssertEffectsAsync( + { + leftOrder: signedOrderLeft2, + rightOrder: signedOrderRight, + rightOrderTakerAssetFilledAmount: matchResults.orders.rightOrderTakerAssetFilledAmount, + }, + takerAddress, + expectedTransferAmounts2, + true, + await matchOrderTester.getBalancesAsync(), + ); + }); + + it('should transfer the correct amounts if fee recipient is the same across both matched orders', async () => { + const feeRecipientAddress = feeRecipientAddressLeft; + 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, + }); + // Match orders + const expectedTransferAmounts = { + // Left Maker + leftMakerAssetSoldByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + leftMakerFeeAssetPaidByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + // Right Maker + rightMakerAssetSoldByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + leftMakerAssetBoughtByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + rightMakerFeeAssetPaidByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + // Taker + leftMakerAssetReceivedByTakerAmount: Web3Wrapper.toBaseUnitAmount(3, 18), + leftTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + rightTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + }; + await matchOrderTester.matchOrdersAndAssertEffectsAsync( + { + leftOrder: signedOrderLeft, + rightOrder: signedOrderRight, + }, + takerAddress, + expectedTransferAmounts, + true, + ); + }); + + 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), + }); + // Match orders + takerAddress = signedOrderLeft.makerAddress; + const expectedTransferAmounts = { + // Left Maker + leftMakerAssetSoldByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + leftMakerFeeAssetPaidByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + // Right Maker + rightMakerAssetSoldByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + leftMakerAssetBoughtByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + rightMakerFeeAssetPaidByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + // Taker + leftMakerAssetReceivedByTakerAmount: Web3Wrapper.toBaseUnitAmount(3, 18), + leftTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + rightTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + }; + await matchOrderTester.matchOrdersAndAssertEffectsAsync( + { + leftOrder: signedOrderLeft, + rightOrder: signedOrderRight, + }, + takerAddress, + expectedTransferAmounts, + true, + ); + }); + + it('should transfer the correct amounts if taker == rightMaker', 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), + }); + // Match orders + takerAddress = signedOrderRight.makerAddress; + const expectedTransferAmounts = { + // Left Maker + leftMakerAssetSoldByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + leftMakerFeeAssetPaidByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + // Right Maker + rightMakerAssetSoldByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + leftMakerAssetBoughtByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + rightMakerFeeAssetPaidByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + // Taker + leftMakerAssetReceivedByTakerAmount: Web3Wrapper.toBaseUnitAmount(3, 18), + leftTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + rightTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + }; + await matchOrderTester.matchOrdersAndAssertEffectsAsync( + { + leftOrder: signedOrderLeft, + rightOrder: signedOrderRight, + }, + takerAddress, + expectedTransferAmounts, + true, + ); + }); + + it('should transfer the correct amounts if taker == leftFeeRecipient', 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), + }); + // Match orders + takerAddress = feeRecipientAddressLeft; + const expectedTransferAmounts = { + // Left Maker + leftMakerAssetSoldByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + leftMakerFeeAssetPaidByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + // Right Maker + rightMakerAssetSoldByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + leftMakerAssetBoughtByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + rightMakerFeeAssetPaidByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + // Taker + leftMakerAssetReceivedByTakerAmount: Web3Wrapper.toBaseUnitAmount(3, 18), + leftTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + rightTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + }; + await matchOrderTester.matchOrdersAndAssertEffectsAsync( + { + leftOrder: signedOrderLeft, + rightOrder: signedOrderRight, + }, + takerAddress, + expectedTransferAmounts, + true, + ); + }); + + it('should transfer the correct amounts if taker == rightFeeRecipient', 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), + }); + // Match orders + takerAddress = feeRecipientAddressRight; + const expectedTransferAmounts = { + // Left Maker + leftMakerAssetSoldByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + leftMakerFeeAssetPaidByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + // Right Maker + rightMakerAssetSoldByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + leftMakerAssetBoughtByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + rightMakerFeeAssetPaidByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + // Taker + leftMakerAssetReceivedByTakerAmount: Web3Wrapper.toBaseUnitAmount(3, 18), + leftTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + rightTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + }; + await matchOrderTester.matchOrdersAndAssertEffectsAsync( { - leftMaker: AssetType.ERC1155NonFungibleLeftMaker, - rightMaker: AssetType.ERC1155NonFungibleRightMaker, - leftMakerFee: AssetType.ERC20C, - rightMakerFee: AssetType.ERC20C, - leftTakerFee: AssetType.ERC20C, - rightTakerFee: AssetType.ERC20C, - }, - { - leftMaker: AssetType.ERC1155FungibleA, - rightMaker: AssetType.ERC20A, - leftMakerFee: AssetType.ERC20C, - rightMakerFee: AssetType.ERC20C, - leftTakerFee: AssetType.ERC20C, - rightTakerFee: AssetType.ERC20C, - }, - { - leftMaker: AssetType.ERC20A, - rightMaker: AssetType.ERC1155FungibleB, - leftMakerFee: AssetType.ERC20C, - rightMakerFee: AssetType.ERC20C, - leftTakerFee: AssetType.ERC20C, - rightTakerFee: AssetType.ERC20C, - }, - { - leftMaker: AssetType.ERC1155FungibleB, - rightMaker: AssetType.ERC1155FungibleB, - leftMakerFee: AssetType.ERC20C, - rightMakerFee: AssetType.ERC20C, - leftTakerFee: AssetType.ERC20C, - rightTakerFee: AssetType.ERC20C, - }, - { - leftMaker: AssetType.MultiAssetA, - rightMaker: AssetType.ERC20A, - leftMakerFee: AssetType.ERC20C, - rightMakerFee: AssetType.ERC20C, - leftTakerFee: AssetType.ERC20C, - rightTakerFee: AssetType.ERC20C, - }, - { - leftMaker: AssetType.ERC20A, - rightMaker: AssetType.MultiAssetB, - leftMakerFee: AssetType.ERC20C, - rightMakerFee: AssetType.ERC20C, - leftTakerFee: AssetType.ERC20C, - rightTakerFee: AssetType.ERC20C, - }, - { - leftMaker: AssetType.MultiAssetA, - rightMaker: AssetType.MultiAssetB, - leftMakerFee: AssetType.ERC20C, - rightMakerFee: AssetType.ERC20C, - leftTakerFee: AssetType.ERC20C, - rightTakerFee: AssetType.ERC20C, - }, - { - leftMaker: AssetType.MultiAssetA, - rightMaker: AssetType.ERC1155FungibleA, - leftMakerFee: AssetType.ERC1155FungibleA, - rightMakerFee: AssetType.MultiAssetA, - leftTakerFee: AssetType.ERC20C, - rightTakerFee: AssetType.ERC20C, - }, - { - description: 'Paying maker fees with the same ERC20 tokens being bought.', - leftMaker: AssetType.ERC20A, - rightMaker: AssetType.ERC20B, - leftMakerFee: AssetType.ERC20B, - rightMakerFee: AssetType.ERC20A, - leftTakerFee: AssetType.ERC20B, - rightTakerFee: AssetType.ERC20A, - }, - { - description: 'Paying maker fees with the same ERC20 tokens being sold.', - leftMaker: AssetType.ERC20A, - rightMaker: AssetType.ERC20B, - leftMakerFee: AssetType.ERC20A, - rightMakerFee: AssetType.ERC20B, - leftTakerFee: AssetType.ERC20A, - rightTakerFee: AssetType.ERC20B, - }, - { - description: 'Using all the same ERC20 asset.', - leftMaker: AssetType.ERC20A, - rightMaker: AssetType.ERC20A, - leftMakerFee: AssetType.ERC20A, - rightMakerFee: AssetType.ERC20A, - leftTakerFee: AssetType.ERC20A, - rightTakerFee: AssetType.ERC20A, - }, - { - description: 'Paying fees with the same MAP assets being sold.', - leftMaker: AssetType.MultiAssetA, - rightMaker: AssetType.MultiAssetB, - leftMakerFee: AssetType.MultiAssetA, - rightMakerFee: AssetType.MultiAssetB, - leftTakerFee: AssetType.MultiAssetA, - rightTakerFee: AssetType.MultiAssetB, - }, - { - description: 'Paying fees with the same MAP assets being bought.', - leftMaker: AssetType.MultiAssetA, - rightMaker: AssetType.MultiAssetB, - leftMakerFee: AssetType.MultiAssetB, - rightMakerFee: AssetType.MultiAssetA, - leftTakerFee: AssetType.MultiAssetB, - rightTakerFee: AssetType.MultiAssetA, - }, - { - description: 'Using all the same MAP assets.', - leftMaker: AssetType.MultiAssetA, - rightMaker: AssetType.MultiAssetA, - leftMakerFee: AssetType.MultiAssetA, - rightMakerFee: AssetType.MultiAssetA, - leftTakerFee: AssetType.MultiAssetA, - rightTakerFee: AssetType.MultiAssetA, - }, - { - description: 'Swapping ERC721s then using them to pay maker fees.', - leftMaker: AssetType.ERC721LeftMaker, - rightMaker: AssetType.ERC721RightMaker, - leftMakerFee: AssetType.ERC721RightMaker, - rightMakerFee: AssetType.ERC721LeftMaker, - leftTakerFee: AssetType.ERC20A, - rightTakerFee: AssetType.ERC20A, - }, - { - description: 'Swapping ERC1155 NFTs then using them to pay maker fees.', - leftMaker: AssetType.ERC1155NonFungibleLeftMaker, - rightMaker: AssetType.ERC1155NonFungibleRightMaker, - leftMakerFee: AssetType.ERC1155NonFungibleRightMaker, - rightMakerFee: AssetType.ERC1155NonFungibleLeftMaker, - leftTakerFee: AssetType.ERC20A, - rightTakerFee: AssetType.ERC20A, - }, - { - description: 'Double-spend by trying to pay maker fees with sold ERC721 token (fail).', - leftMaker: AssetType.ERC721LeftMaker, - rightMaker: AssetType.ERC721RightMaker, - leftMakerFee: AssetType.ERC721LeftMaker, - rightMakerFee: AssetType.ERC721LeftMaker, - leftTakerFee: AssetType.ERC20A, - rightTakerFee: AssetType.ERC20A, - shouldFail: true, - }, - { - description: 'Double-spend by trying to pay maker fees with sold ERC1155 NFT (fail).', - leftMaker: AssetType.ERC20A, - rightMaker: AssetType.ERC1155NonFungibleLeftMaker, - leftMakerFee: AssetType.ERC20C, - rightMakerFee: AssetType.ERC1155NonFungibleLeftMaker, - leftTakerFee: AssetType.ERC20C, - rightTakerFee: AssetType.ERC20C, - shouldFail: true, + leftOrder: signedOrderLeft, + rightOrder: signedOrderRight, }, - ]; + takerAddress, + expectedTransferAmounts, + true, + ); + }); + + 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, + }); + // Match orders + const expectedTransferAmounts = { + // Left Maker + leftMakerAssetSoldByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + leftMakerFeeAssetPaidByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + // Right Maker + rightMakerAssetSoldByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + leftMakerAssetBoughtByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + rightMakerFeeAssetPaidByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + // Taker + leftMakerAssetReceivedByTakerAmount: Web3Wrapper.toBaseUnitAmount(3, 18), + leftTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + rightTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + }; + await matchOrderTester.matchOrdersAndAssertEffectsAsync( + { + leftOrder: signedOrderLeft, + rightOrder: signedOrderRight, + }, + takerAddress, + expectedTransferAmounts, + true, + ); + }); + + 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, + }); + // Match orders + const expectedTransferAmounts = { + // Left Maker + leftMakerAssetSoldByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + leftMakerFeeAssetPaidByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + // Right Maker + rightMakerAssetSoldByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + leftMakerAssetBoughtByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + rightMakerFeeAssetPaidByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + // Taker + leftMakerAssetReceivedByTakerAmount: Web3Wrapper.toBaseUnitAmount(3, 18), + leftTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + rightTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + }; + await matchOrderTester.matchOrdersAndAssertEffectsAsync( + { + leftOrder: signedOrderLeft, + rightOrder: signedOrderRight, + }, + takerAddress, + expectedTransferAmounts, + true, + ); + }); + + 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, + }); + // Match orders + const expectedTransferAmounts = { + // Left Maker + leftMakerAssetSoldByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + leftMakerFeeAssetPaidByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + // Right Maker + rightMakerAssetSoldByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + leftMakerAssetBoughtByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + rightMakerFeeAssetPaidByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + // Taker + leftMakerAssetReceivedByTakerAmount: Web3Wrapper.toBaseUnitAmount(3, 18), + leftTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + rightTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + }; + await matchOrderTester.matchOrdersAndAssertEffectsAsync( + { + leftOrder: signedOrderLeft, + rightOrder: signedOrderRight, + }, + takerAddress, + expectedTransferAmounts, + true, + ); + }); + + 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), + makerFeeAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress), + feeRecipientAddress: makerAddressLeft, + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + makerFeeAssetData: signedOrderLeft.makerAssetData, + feeRecipientAddress: makerAddressRight, + }); + // Match orders + const expectedTransferAmounts = { + // Left Maker + leftMakerAssetSoldByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + leftMakerFeeAssetPaidByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + // Right Maker + rightMakerAssetSoldByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + leftMakerAssetBoughtByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + rightMakerFeeAssetPaidByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + // Taker + leftMakerAssetReceivedByTakerAmount: Web3Wrapper.toBaseUnitAmount(3, 18), + leftTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + rightTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + }; + await matchOrderTester.matchOrdersAndAssertEffectsAsync( + { + leftOrder: signedOrderLeft, + rightOrder: signedOrderRight, + }, + takerAddress, + expectedTransferAmounts, + true, + ); + }); + + it('Should throw if left order is not fillable', 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 orderHashHexLeft = orderHashUtils.getOrderHashHex(signedOrderLeft); + // Cancel left order + await exchangeWrapper.cancelOrderAsync(signedOrderLeft, signedOrderLeft.makerAddress); + // Match orders + const expectedError = new ExchangeRevertErrors.OrderStatusError(orderHashHexLeft, OrderStatus.Cancelled); + const tx = exchangeWrapper.matchOrdersWithMaximalFillAsync(signedOrderLeft, signedOrderRight, takerAddress); + return expect(tx).to.revertWith(expectedError); + }); + + it('Should throw if right order is not fillable', 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 orderHashHexRight = orderHashUtils.getOrderHashHex(signedOrderRight); + // Cancel right order + await exchangeWrapper.cancelOrderAsync(signedOrderRight, signedOrderRight.makerAddress); + // Match orders + const expectedError = new ExchangeRevertErrors.OrderStatusError(orderHashHexRight, OrderStatus.Cancelled); + const tx = exchangeWrapper.matchOrdersWithMaximalFillAsync(signedOrderLeft, signedOrderRight, takerAddress); + return expect(tx).to.revertWith(expectedError); + }); + + it('should throw if there is not a positive spread', async () => { + // Create orders to match + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(100, 18), + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(1, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(200, 18), + }); + const orderHashHexLeft = orderHashUtils.getOrderHashHex(signedOrderLeft); + const orderHashHexRight = orderHashUtils.getOrderHashHex(signedOrderRight); + // Match orders + const expectedError = new ExchangeRevertErrors.NegativeSpreadError(orderHashHexLeft, orderHashHexRight); + const tx = exchangeWrapper.matchOrdersWithMaximalFillAsync(signedOrderLeft, signedOrderRight, takerAddress); + return expect(tx).to.revertWith(expectedError); + }); + + it('should throw if the left maker asset is not equal to the right taker asset ', 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({ + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20TakerAssetAddress), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + }); + // We are assuming assetData fields of the right order are the + // reverse of the left order, rather than checking equality. This + // saves a bunch of gas, but as a result if the assetData fields are + // off then the failure ends up happening at signature validation + const reconstructedOrderRight = { + ...signedOrderRight, + takerAssetData: signedOrderLeft.makerAssetData, + }; + const orderHashHex = orderHashUtils.getOrderHashHex(reconstructedOrderRight); + const expectedError = new ExchangeRevertErrors.SignatureError( + ExchangeRevertErrors.SignatureErrorCode.BadSignature, + orderHashHex, + signedOrderRight.makerAddress, + signedOrderRight.signature, + ); + // Match orders + const tx = exchangeWrapper.matchOrdersWithMaximalFillAsync(signedOrderLeft, signedOrderRight, takerAddress); + return expect(tx).to.revertWith(expectedError); + }); - let nameToERC20Asset: { [name: string]: string }; - let nameToERC721Asset: { [name: string]: [string, BigNumber] }; - let nameToERC1155FungibleAsset: { [name: string]: [string, BigNumber] }; - let nameToERC1155NonFungibleAsset: { [name: string]: [string, BigNumber] }; - let nameToMultiAssetAsset: { [name: string]: [BigNumber[], string[]] }; - - function getAssetData(assetType: AssetType): string { - const encodeERC20AssetData = assetDataUtils.encodeERC20AssetData; - const encodeERC721AssetData = assetDataUtils.encodeERC721AssetData; - const encodeERC1155AssetData = assetDataUtils.encodeERC1155AssetData; - const encodeMultiAssetData = assetDataUtils.encodeMultiAssetData; - if (nameToERC20Asset[assetType] !== undefined) { - const tokenAddress = nameToERC20Asset[assetType]; - return encodeERC20AssetData(tokenAddress); - } - if (nameToERC721Asset[assetType] !== undefined) { - const [tokenAddress, tokenId] = nameToERC721Asset[assetType]; - return encodeERC721AssetData(tokenAddress, tokenId); - } - if (nameToERC1155FungibleAsset[assetType] !== undefined) { - const [tokenAddress, tokenId] = nameToERC1155FungibleAsset[assetType]; - return encodeERC1155AssetData(tokenAddress, [tokenId], [ONE], constants.NULL_BYTES); - } - if (nameToERC1155NonFungibleAsset[assetType] !== undefined) { - const [tokenAddress, tokenId] = nameToERC1155NonFungibleAsset[assetType]; - return encodeERC1155AssetData(tokenAddress, [tokenId], [ONE], constants.NULL_BYTES); - } - if (nameToMultiAssetAsset[assetType] !== undefined) { - const [amounts, nestedAssetData] = nameToMultiAssetAsset[assetType]; - return encodeMultiAssetData(amounts, nestedAssetData); - } - throw new Error(`Unknown asset type: ${assetType}`); + it('should throw if the right maker asset is not equal to the left taker asset', async () => { + // Create orders to match + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + takerAssetData: assetDataUtils.encodeERC20AssetData(defaultERC20MakerAssetAddress), + 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 reconstructedOrderRight = { + ...signedOrderRight, + makerAssetData: signedOrderLeft.takerAssetData, + }; + const orderHashHex = orderHashUtils.getOrderHashHex(reconstructedOrderRight); + const expectedError = new ExchangeRevertErrors.SignatureError( + ExchangeRevertErrors.SignatureErrorCode.BadSignature, + orderHashHex, + signedOrderRight.makerAddress, + signedOrderRight.signature, + ); + // Match orders + const tx = exchangeWrapper.matchOrdersWithMaximalFillAsync(signedOrderLeft, signedOrderRight, takerAddress); + return expect(tx).to.revertWith(expectedError); + }); + }); + + describe('matchOrders and matchOrdersWithMaximalFill combinations', () => { + // tslint:disable: enum-naming + enum AssetType { + ERC20A = 'ERC20_A', + ERC20B = 'ERC20_B', + ERC20C = 'ERC20_C', + ERC20D = 'ERC20_D', + ERC721LeftMaker = 'ERC721_LEFT_MAKER', + ERC721RightMaker = 'ERC721_RIGHT_MAKER', + ERC721Taker = 'ERC721_TAKER', + ERC1155FungibleA = 'ERC1155_FUNGIBLE_A', + ERC1155FungibleB = 'ERC1155_FUNGIBLE_B', + ERC1155FungibleC = 'ERC1155_FUNGIBLE_C', + ERC1155FungibleD = 'ERC1155_FUNGIBLE_D', + ERC1155NonFungibleLeftMaker = 'ERC1155_NON_FUNGIBLE_LEFT_MAKER', + ERC1155NonFungibleRightMaker = 'ERC1155_NON_FUNGIBLE_RIGHT_MAKER', + ERC1155NonFungibleTaker = 'ERC1155_NON_FUNGIBLE_TAKER', + MultiAssetA = 'MULTI_ASSET_A', + MultiAssetB = 'MULTI_ASSET_B', + MultiAssetC = 'MULTI_ASSET_C', + MultiAssetD = 'MULTI_ASSET_D', + } + const fungibleTypes = [ + AssetType.ERC20A, + AssetType.ERC20B, + AssetType.ERC20C, + AssetType.ERC20D, + AssetType.ERC1155FungibleA, + AssetType.ERC1155FungibleB, + AssetType.ERC1155FungibleC, + AssetType.ERC1155FungibleD, + AssetType.MultiAssetA, + AssetType.MultiAssetB, + AssetType.MultiAssetC, + AssetType.MultiAssetD, + ]; + interface AssetCombination { + leftMaker: AssetType; + rightMaker: AssetType; + leftMakerFee: AssetType; + rightMakerFee: AssetType; + leftTakerFee: AssetType; + rightTakerFee: AssetType; + description?: string; + shouldFail?: boolean; + } + const assetCombinations: AssetCombination[] = [ + { + leftMaker: AssetType.ERC20A, + rightMaker: AssetType.ERC20B, + leftMakerFee: AssetType.ERC20C, + rightMakerFee: AssetType.ERC20C, + leftTakerFee: AssetType.ERC20C, + rightTakerFee: AssetType.ERC20C, + }, + { + leftMaker: AssetType.ERC721LeftMaker, + rightMaker: AssetType.ERC721RightMaker, + leftMakerFee: AssetType.ERC20C, + rightMakerFee: AssetType.ERC20C, + leftTakerFee: AssetType.ERC20C, + rightTakerFee: AssetType.ERC20C, + }, + { + leftMaker: AssetType.ERC721LeftMaker, + rightMaker: AssetType.ERC20A, + leftMakerFee: AssetType.ERC20C, + rightMakerFee: AssetType.ERC20C, + leftTakerFee: AssetType.ERC20C, + rightTakerFee: AssetType.ERC20C, + }, + { + leftMaker: AssetType.ERC20A, + rightMaker: AssetType.ERC721RightMaker, + leftMakerFee: AssetType.ERC20C, + rightMakerFee: AssetType.ERC20C, + leftTakerFee: AssetType.ERC20C, + rightTakerFee: AssetType.ERC20C, + }, + { + leftMaker: AssetType.ERC1155FungibleA, + rightMaker: AssetType.ERC20A, + leftMakerFee: AssetType.ERC20C, + rightMakerFee: AssetType.ERC20C, + leftTakerFee: AssetType.ERC20C, + rightTakerFee: AssetType.ERC20C, + }, + { + leftMaker: AssetType.ERC20A, + rightMaker: AssetType.ERC1155FungibleB, + leftMakerFee: AssetType.ERC20C, + rightMakerFee: AssetType.ERC20C, + leftTakerFee: AssetType.ERC20C, + rightTakerFee: AssetType.ERC20C, + }, + { + leftMaker: AssetType.ERC1155FungibleA, + rightMaker: AssetType.ERC1155FungibleA, + leftMakerFee: AssetType.ERC20C, + rightMakerFee: AssetType.ERC20C, + leftTakerFee: AssetType.ERC20C, + rightTakerFee: AssetType.ERC20C, + }, + { + leftMaker: AssetType.ERC1155NonFungibleLeftMaker, + rightMaker: AssetType.ERC20A, + leftMakerFee: AssetType.ERC20C, + rightMakerFee: AssetType.ERC20C, + leftTakerFee: AssetType.ERC20C, + rightTakerFee: AssetType.ERC20C, + }, + { + leftMaker: AssetType.ERC20A, + rightMaker: AssetType.ERC1155NonFungibleRightMaker, + leftMakerFee: AssetType.ERC20C, + rightMakerFee: AssetType.ERC20C, + leftTakerFee: AssetType.ERC20C, + rightTakerFee: AssetType.ERC20C, + }, + { + leftMaker: AssetType.ERC1155NonFungibleLeftMaker, + rightMaker: AssetType.ERC1155NonFungibleRightMaker, + leftMakerFee: AssetType.ERC20C, + rightMakerFee: AssetType.ERC20C, + leftTakerFee: AssetType.ERC20C, + rightTakerFee: AssetType.ERC20C, + }, + { + leftMaker: AssetType.ERC1155FungibleA, + rightMaker: AssetType.ERC20A, + leftMakerFee: AssetType.ERC20C, + rightMakerFee: AssetType.ERC20C, + leftTakerFee: AssetType.ERC20C, + rightTakerFee: AssetType.ERC20C, + }, + { + leftMaker: AssetType.ERC20A, + rightMaker: AssetType.ERC1155FungibleB, + leftMakerFee: AssetType.ERC20C, + rightMakerFee: AssetType.ERC20C, + leftTakerFee: AssetType.ERC20C, + rightTakerFee: AssetType.ERC20C, + }, + { + leftMaker: AssetType.ERC1155FungibleB, + rightMaker: AssetType.ERC1155FungibleB, + leftMakerFee: AssetType.ERC20C, + rightMakerFee: AssetType.ERC20C, + leftTakerFee: AssetType.ERC20C, + rightTakerFee: AssetType.ERC20C, + }, + { + leftMaker: AssetType.MultiAssetA, + rightMaker: AssetType.ERC20A, + leftMakerFee: AssetType.ERC20C, + rightMakerFee: AssetType.ERC20C, + leftTakerFee: AssetType.ERC20C, + rightTakerFee: AssetType.ERC20C, + }, + { + leftMaker: AssetType.ERC20A, + rightMaker: AssetType.MultiAssetB, + leftMakerFee: AssetType.ERC20C, + rightMakerFee: AssetType.ERC20C, + leftTakerFee: AssetType.ERC20C, + rightTakerFee: AssetType.ERC20C, + }, + { + leftMaker: AssetType.MultiAssetA, + rightMaker: AssetType.MultiAssetB, + leftMakerFee: AssetType.ERC20C, + rightMakerFee: AssetType.ERC20C, + leftTakerFee: AssetType.ERC20C, + rightTakerFee: AssetType.ERC20C, + }, + { + leftMaker: AssetType.MultiAssetA, + rightMaker: AssetType.ERC1155FungibleA, + leftMakerFee: AssetType.ERC1155FungibleA, + rightMakerFee: AssetType.MultiAssetA, + leftTakerFee: AssetType.ERC20C, + rightTakerFee: AssetType.ERC20C, + }, + { + description: 'Paying maker fees with the same ERC20 tokens being bought.', + leftMaker: AssetType.ERC20A, + rightMaker: AssetType.ERC20B, + leftMakerFee: AssetType.ERC20B, + rightMakerFee: AssetType.ERC20A, + leftTakerFee: AssetType.ERC20B, + rightTakerFee: AssetType.ERC20A, + }, + { + description: 'Paying maker fees with the same ERC20 tokens being sold.', + leftMaker: AssetType.ERC20A, + rightMaker: AssetType.ERC20B, + leftMakerFee: AssetType.ERC20A, + rightMakerFee: AssetType.ERC20B, + leftTakerFee: AssetType.ERC20A, + rightTakerFee: AssetType.ERC20B, + }, + { + description: 'Using all the same ERC20 asset.', + leftMaker: AssetType.ERC20A, + rightMaker: AssetType.ERC20A, + leftMakerFee: AssetType.ERC20A, + rightMakerFee: AssetType.ERC20A, + leftTakerFee: AssetType.ERC20A, + rightTakerFee: AssetType.ERC20A, + }, + { + description: 'Paying fees with the same MAP assets being sold.', + leftMaker: AssetType.MultiAssetA, + rightMaker: AssetType.MultiAssetB, + leftMakerFee: AssetType.MultiAssetA, + rightMakerFee: AssetType.MultiAssetB, + leftTakerFee: AssetType.MultiAssetA, + rightTakerFee: AssetType.MultiAssetB, + }, + { + description: 'Paying fees with the same MAP assets being bought.', + leftMaker: AssetType.MultiAssetA, + rightMaker: AssetType.MultiAssetB, + leftMakerFee: AssetType.MultiAssetB, + rightMakerFee: AssetType.MultiAssetA, + leftTakerFee: AssetType.MultiAssetB, + rightTakerFee: AssetType.MultiAssetA, + }, + { + description: 'Using all the same MAP assets.', + leftMaker: AssetType.MultiAssetA, + rightMaker: AssetType.MultiAssetA, + leftMakerFee: AssetType.MultiAssetA, + rightMakerFee: AssetType.MultiAssetA, + leftTakerFee: AssetType.MultiAssetA, + rightTakerFee: AssetType.MultiAssetA, + }, + { + description: 'Swapping ERC721s then using them to pay maker fees.', + leftMaker: AssetType.ERC721LeftMaker, + rightMaker: AssetType.ERC721RightMaker, + leftMakerFee: AssetType.ERC721RightMaker, + rightMakerFee: AssetType.ERC721LeftMaker, + leftTakerFee: AssetType.ERC20A, + rightTakerFee: AssetType.ERC20A, + }, + { + description: 'Swapping ERC1155 NFTs then using them to pay maker fees.', + leftMaker: AssetType.ERC1155NonFungibleLeftMaker, + rightMaker: AssetType.ERC1155NonFungibleRightMaker, + leftMakerFee: AssetType.ERC1155NonFungibleRightMaker, + rightMakerFee: AssetType.ERC1155NonFungibleLeftMaker, + leftTakerFee: AssetType.ERC20A, + rightTakerFee: AssetType.ERC20A, + }, + { + description: 'Double-spend by trying to pay maker fees with sold ERC721 token (fail).', + leftMaker: AssetType.ERC721LeftMaker, + rightMaker: AssetType.ERC721RightMaker, + leftMakerFee: AssetType.ERC721LeftMaker, + rightMakerFee: AssetType.ERC721LeftMaker, + leftTakerFee: AssetType.ERC20A, + rightTakerFee: AssetType.ERC20A, + shouldFail: true, + }, + { + description: 'Double-spend by trying to pay maker fees with sold ERC1155 NFT (fail).', + leftMaker: AssetType.ERC20A, + rightMaker: AssetType.ERC1155NonFungibleLeftMaker, + leftMakerFee: AssetType.ERC20C, + rightMakerFee: AssetType.ERC1155NonFungibleLeftMaker, + leftTakerFee: AssetType.ERC20C, + rightTakerFee: AssetType.ERC20C, + shouldFail: true, + }, + ]; + + let nameToERC20Asset: { [name: string]: string }; + let nameToERC721Asset: { [name: string]: [string, BigNumber] }; + let nameToERC1155FungibleAsset: { [name: string]: [string, BigNumber] }; + let nameToERC1155NonFungibleAsset: { [name: string]: [string, BigNumber] }; + let nameToMultiAssetAsset: { [name: string]: [BigNumber[], string[]] }; + + function getAssetData(assetType: AssetType): string { + const encodeERC20AssetData = assetDataUtils.encodeERC20AssetData; + const encodeERC721AssetData = assetDataUtils.encodeERC721AssetData; + const encodeERC1155AssetData = assetDataUtils.encodeERC1155AssetData; + const encodeMultiAssetData = assetDataUtils.encodeMultiAssetData; + if (nameToERC20Asset[assetType] !== undefined) { + const tokenAddress = nameToERC20Asset[assetType]; + return encodeERC20AssetData(tokenAddress); + } + if (nameToERC721Asset[assetType] !== undefined) { + const [tokenAddress, tokenId] = nameToERC721Asset[assetType]; + return encodeERC721AssetData(tokenAddress, tokenId); + } + if (nameToERC1155FungibleAsset[assetType] !== undefined) { + const [tokenAddress, tokenId] = nameToERC1155FungibleAsset[assetType]; + return encodeERC1155AssetData(tokenAddress, [tokenId], [ONE], constants.NULL_BYTES); } + if (nameToERC1155NonFungibleAsset[assetType] !== undefined) { + const [tokenAddress, tokenId] = nameToERC1155NonFungibleAsset[assetType]; + return encodeERC1155AssetData(tokenAddress, [tokenId], [ONE], constants.NULL_BYTES); + } + if (nameToMultiAssetAsset[assetType] !== undefined) { + const [amounts, nestedAssetData] = nameToMultiAssetAsset[assetType]; + return encodeMultiAssetData(amounts, nestedAssetData); + } + throw new Error(`Unknown asset type: ${assetType}`); + } - before(async () => { - nameToERC20Asset = { - ERC20_A: erc20Tokens[0].address, - ERC20_B: erc20Tokens[1].address, - ERC20_C: erc20Tokens[2].address, - ERC20_D: erc20Tokens[3].address, - }; - const erc721TokenIds = _.mapValues(tokenBalances.erc721, v => v[defaultERC721AssetAddress][0]); - nameToERC721Asset = { - ERC721_LEFT_MAKER: [defaultERC721AssetAddress, erc721TokenIds[makerAddressLeft]], - ERC721_RIGHT_MAKER: [defaultERC721AssetAddress, erc721TokenIds[makerAddressRight]], - ERC721_TAKER: [defaultERC721AssetAddress, erc721TokenIds[takerAddress]], - }; - const erc1155FungibleTokens = _.keys( - _.values(tokenBalances.erc1155)[0][defaultERC1155AssetAddress].fungible, - ).map(k => new BigNumber(k)); - nameToERC1155FungibleAsset = { - ERC1155_FUNGIBLE_A: [defaultERC1155AssetAddress, erc1155FungibleTokens[0]], - ERC1155_FUNGIBLE_B: [defaultERC1155AssetAddress, erc1155FungibleTokens[1]], - ERC1155_FUNGIBLE_C: [defaultERC1155AssetAddress, erc1155FungibleTokens[2]], - ERC1155_FUNGIBLE_D: [defaultERC1155AssetAddress, erc1155FungibleTokens[3]], - }; - const erc1155NonFungibleTokenIds = _.mapValues( - tokenBalances.erc1155, - v => v[defaultERC1155AssetAddress].nonFungible[0], - ); - nameToERC1155NonFungibleAsset = { - ERC1155_NON_FUNGIBLE_LEFT_MAKER: [ - defaultERC1155AssetAddress, - erc1155NonFungibleTokenIds[makerAddressLeft], - ], - ERC1155_NON_FUNGIBLE_RIGHT_MAKER: [ - defaultERC1155AssetAddress, - erc1155NonFungibleTokenIds[makerAddressRight], - ], - ERC1155_NON_FUNGIBLE_TAKER: [defaultERC1155AssetAddress, erc1155NonFungibleTokenIds[takerAddress]], - }; - nameToMultiAssetAsset = { - MULTI_ASSET_A: [ - [ONE, TWO], - [ - assetDataUtils.encodeERC20AssetData(erc20Tokens[0].address), - assetDataUtils.encodeERC1155AssetData( - defaultERC1155AssetAddress, - [erc1155FungibleTokens[0]], - [ONE], - constants.NULL_BYTES, - ), - ], + before(async () => { + nameToERC20Asset = { + ERC20_A: erc20Tokens[0].address, + ERC20_B: erc20Tokens[1].address, + ERC20_C: erc20Tokens[2].address, + ERC20_D: erc20Tokens[3].address, + }; + const erc721TokenIds = _.mapValues(tokenBalances.erc721, v => v[defaultERC721AssetAddress][0]); + nameToERC721Asset = { + ERC721_LEFT_MAKER: [defaultERC721AssetAddress, erc721TokenIds[makerAddressLeft]], + ERC721_RIGHT_MAKER: [defaultERC721AssetAddress, erc721TokenIds[makerAddressRight]], + ERC721_TAKER: [defaultERC721AssetAddress, erc721TokenIds[takerAddress]], + }; + const erc1155FungibleTokens = _.keys( + _.values(tokenBalances.erc1155)[0][defaultERC1155AssetAddress].fungible, + ).map(k => new BigNumber(k)); + nameToERC1155FungibleAsset = { + ERC1155_FUNGIBLE_A: [defaultERC1155AssetAddress, erc1155FungibleTokens[0]], + ERC1155_FUNGIBLE_B: [defaultERC1155AssetAddress, erc1155FungibleTokens[1]], + ERC1155_FUNGIBLE_C: [defaultERC1155AssetAddress, erc1155FungibleTokens[2]], + ERC1155_FUNGIBLE_D: [defaultERC1155AssetAddress, erc1155FungibleTokens[3]], + }; + const erc1155NonFungibleTokenIds = _.mapValues( + tokenBalances.erc1155, + v => v[defaultERC1155AssetAddress].nonFungible[0], + ); + nameToERC1155NonFungibleAsset = { + ERC1155_NON_FUNGIBLE_LEFT_MAKER: [ + defaultERC1155AssetAddress, + erc1155NonFungibleTokenIds[makerAddressLeft], + ], + ERC1155_NON_FUNGIBLE_RIGHT_MAKER: [ + defaultERC1155AssetAddress, + erc1155NonFungibleTokenIds[makerAddressRight], + ], + ERC1155_NON_FUNGIBLE_TAKER: [defaultERC1155AssetAddress, erc1155NonFungibleTokenIds[takerAddress]], + }; + nameToMultiAssetAsset = { + MULTI_ASSET_A: [ + [ONE, TWO], + [ + assetDataUtils.encodeERC20AssetData(erc20Tokens[0].address), + assetDataUtils.encodeERC1155AssetData( + defaultERC1155AssetAddress, + [erc1155FungibleTokens[0]], + [ONE], + constants.NULL_BYTES, + ), ], - MULTI_ASSET_B: [ - [ONE, TWO], - [ - assetDataUtils.encodeERC20AssetData(erc20Tokens[1].address), - assetDataUtils.encodeERC1155AssetData( - defaultERC1155AssetAddress, - [erc1155FungibleTokens[1]], - [ONE], - constants.NULL_BYTES, - ), - ], + ], + MULTI_ASSET_B: [ + [ONE, TWO], + [ + assetDataUtils.encodeERC20AssetData(erc20Tokens[1].address), + assetDataUtils.encodeERC1155AssetData( + defaultERC1155AssetAddress, + [erc1155FungibleTokens[1]], + [ONE], + constants.NULL_BYTES, + ), ], - MULTI_ASSET_C: [ - [ONE, TWO], - [ - assetDataUtils.encodeERC20AssetData(erc20Tokens[2].address), - assetDataUtils.encodeERC1155AssetData( - defaultERC1155AssetAddress, - [erc1155FungibleTokens[2]], - [ONE], - constants.NULL_BYTES, - ), - ], + ], + MULTI_ASSET_C: [ + [ONE, TWO], + [ + assetDataUtils.encodeERC20AssetData(erc20Tokens[2].address), + assetDataUtils.encodeERC1155AssetData( + defaultERC1155AssetAddress, + [erc1155FungibleTokens[2]], + [ONE], + constants.NULL_BYTES, + ), ], - MULTI_ASSET_D: [ - [ONE, TWO], - [ - assetDataUtils.encodeERC20AssetData(erc20Tokens[3].address), - assetDataUtils.encodeERC1155AssetData( - erc1155Token.address, - [erc1155FungibleTokens[3]], - [ONE], - constants.NULL_BYTES, - ), - ], + ], + MULTI_ASSET_D: [ + [ONE, TWO], + [ + assetDataUtils.encodeERC20AssetData(erc20Tokens[3].address), + assetDataUtils.encodeERC1155AssetData( + erc1155Token.address, + [erc1155FungibleTokens[3]], + [ONE], + constants.NULL_BYTES, + ), ], + ], + }; + }); + + // matchOrders + for (const combo of assetCombinations) { + const description = combo.description || JSON.stringify(combo); + it(description, async () => { + // Create orders to match. For ERC20s, there will be a spread. + const leftMakerAssetAmount = _.includes(fungibleTypes, combo.leftMaker) + ? Web3Wrapper.toBaseUnitAmount(15, 18) + : Web3Wrapper.toBaseUnitAmount(1, 0); + const leftTakerAssetAmount = _.includes(fungibleTypes, combo.rightMaker) + ? Web3Wrapper.toBaseUnitAmount(30, 18) + : Web3Wrapper.toBaseUnitAmount(1, 0); + const rightMakerAssetAmount = _.includes(fungibleTypes, combo.rightMaker) + ? Web3Wrapper.toBaseUnitAmount(30, 18) + : Web3Wrapper.toBaseUnitAmount(1, 0); + const rightTakerAssetAmount = _.includes(fungibleTypes, combo.leftMaker) + ? Web3Wrapper.toBaseUnitAmount(14, 18) + : Web3Wrapper.toBaseUnitAmount(1, 0); + const leftMakerFeeAssetAmount = _.includes(fungibleTypes, combo.leftMakerFee) + ? Web3Wrapper.toBaseUnitAmount(8, 12) + : Web3Wrapper.toBaseUnitAmount(1, 0); + const rightMakerFeeAssetAmount = _.includes(fungibleTypes, combo.rightMakerFee) + ? Web3Wrapper.toBaseUnitAmount(7, 12) + : Web3Wrapper.toBaseUnitAmount(1, 0); + const leftTakerFeeAssetAmount = _.includes(fungibleTypes, combo.leftTakerFee) + ? Web3Wrapper.toBaseUnitAmount(6, 12) + : Web3Wrapper.toBaseUnitAmount(1, 0); + const rightTakerFeeAssetAmount = _.includes(fungibleTypes, combo.rightTakerFee) + ? Web3Wrapper.toBaseUnitAmount(5, 12) + : Web3Wrapper.toBaseUnitAmount(1, 0); + const leftMakerAssetReceivedByTakerAmount = _.includes(fungibleTypes, combo.leftMaker) + ? leftMakerAssetAmount.minus(rightTakerAssetAmount) + : Web3Wrapper.toBaseUnitAmount(0, 0); + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAssetData: getAssetData(combo.leftMaker), + takerAssetData: getAssetData(combo.rightMaker), + makerFeeAssetData: getAssetData(combo.leftMakerFee), + takerFeeAssetData: getAssetData(combo.leftTakerFee), + makerAssetAmount: leftMakerAssetAmount, + takerAssetAmount: leftTakerAssetAmount, + makerFee: leftMakerFeeAssetAmount, + takerFee: leftTakerFeeAssetAmount, + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAssetData: getAssetData(combo.rightMaker), + takerAssetData: getAssetData(combo.leftMaker), + makerFeeAssetData: getAssetData(combo.rightMakerFee), + takerFeeAssetData: getAssetData(combo.rightTakerFee), + makerAssetAmount: rightMakerAssetAmount, + takerAssetAmount: rightTakerAssetAmount, + makerFee: rightMakerFeeAssetAmount, + takerFee: rightTakerFeeAssetAmount, + }); + // Match signedOrderLeft with signedOrderRight + const expectedTransferAmounts = { + // Left Maker + leftMakerAssetSoldByLeftMakerAmount: leftMakerAssetAmount, + leftMakerFeeAssetPaidByLeftMakerAmount: leftMakerFeeAssetAmount, + // Right Maker + rightMakerAssetSoldByRightMakerAmount: rightMakerAssetAmount, + leftMakerAssetBoughtByRightMakerAmount: rightTakerAssetAmount, + rightMakerFeeAssetPaidByRightMakerAmount: rightMakerFeeAssetAmount, + // Taker + leftMakerAssetReceivedByTakerAmount, + leftTakerFeeAssetPaidByTakerAmount: leftTakerFeeAssetAmount, + rightTakerFeeAssetPaidByTakerAmount: rightTakerFeeAssetAmount, + }; + if (!combo.shouldFail) { + await matchOrderTester.matchOrdersAndAssertEffectsAsync( + { + leftOrder: signedOrderLeft, + rightOrder: signedOrderRight, + }, + takerAddress, + expectedTransferAmounts, + false, + ); + } else { + const tx = exchangeWrapper.matchOrdersAsync(signedOrderLeft, signedOrderRight, takerAddress); + return expect(tx).to.be.rejected(); + } + }); + } + + // matchOrdersWithMaximalFill + for (const combo of assetCombinations) { + const description = combo.description || JSON.stringify(combo); + it(description, async () => { + // Create orders to match. For ERC20s, there will be a spread. + const leftMakerAssetAmount = _.includes(fungibleTypes, combo.leftMaker) + ? Web3Wrapper.toBaseUnitAmount(15, 18) + : Web3Wrapper.toBaseUnitAmount(1, 0); + const leftTakerAssetAmount = _.includes(fungibleTypes, combo.rightMaker) + ? Web3Wrapper.toBaseUnitAmount(30, 18) + : Web3Wrapper.toBaseUnitAmount(1, 0); + const rightMakerAssetAmount = _.includes(fungibleTypes, combo.rightMaker) + ? Web3Wrapper.toBaseUnitAmount(30, 18) + : Web3Wrapper.toBaseUnitAmount(1, 0); + const rightTakerAssetAmount = _.includes(fungibleTypes, combo.leftMaker) + ? Web3Wrapper.toBaseUnitAmount(14, 18) + : Web3Wrapper.toBaseUnitAmount(1, 0); + const leftMakerFeeAssetAmount = _.includes(fungibleTypes, combo.leftMakerFee) + ? Web3Wrapper.toBaseUnitAmount(8, 12) + : Web3Wrapper.toBaseUnitAmount(1, 0); + const rightMakerFeeAssetAmount = _.includes(fungibleTypes, combo.rightMakerFee) + ? Web3Wrapper.toBaseUnitAmount(7, 12) + : Web3Wrapper.toBaseUnitAmount(1, 0); + const leftTakerFeeAssetAmount = _.includes(fungibleTypes, combo.leftTakerFee) + ? Web3Wrapper.toBaseUnitAmount(6, 12) + : Web3Wrapper.toBaseUnitAmount(1, 0); + const rightTakerFeeAssetAmount = _.includes(fungibleTypes, combo.rightTakerFee) + ? Web3Wrapper.toBaseUnitAmount(5, 12) + : Web3Wrapper.toBaseUnitAmount(1, 0); + const leftMakerAssetReceivedByTakerAmount = _.includes(fungibleTypes, combo.leftMaker) + ? leftMakerAssetAmount.minus(rightTakerAssetAmount) + : Web3Wrapper.toBaseUnitAmount(0, 0); + const rightMakerAssetReceivedByTakerAmount = _.includes(fungibleTypes, combo.leftMaker) + ? rightMakerAssetAmount.minus(leftTakerAssetAmount) + : Web3Wrapper.toBaseUnitAmount(0, 0); + const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ + makerAssetData: getAssetData(combo.leftMaker), + takerAssetData: getAssetData(combo.rightMaker), + makerFeeAssetData: getAssetData(combo.leftMakerFee), + takerFeeAssetData: getAssetData(combo.leftTakerFee), + makerAssetAmount: leftMakerAssetAmount, + takerAssetAmount: leftTakerAssetAmount, + makerFee: leftMakerFeeAssetAmount, + takerFee: leftTakerFeeAssetAmount, + }); + const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ + makerAssetData: getAssetData(combo.rightMaker), + takerAssetData: getAssetData(combo.leftMaker), + makerFeeAssetData: getAssetData(combo.rightMakerFee), + takerFeeAssetData: getAssetData(combo.rightTakerFee), + makerAssetAmount: rightMakerAssetAmount, + takerAssetAmount: rightTakerAssetAmount, + makerFee: rightMakerFeeAssetAmount, + takerFee: rightTakerFeeAssetAmount, + }); + // Match signedOrderLeft with signedOrderRight + const expectedTransferAmounts = { + // Left Maker + leftMakerAssetSoldByLeftMakerAmount: leftMakerAssetAmount, + leftMakerFeeAssetPaidByLeftMakerAmount: leftMakerFeeAssetAmount, + // Right Maker + rightMakerAssetSoldByRightMakerAmount: rightMakerAssetAmount, + leftMakerAssetBoughtByRightMakerAmount: rightTakerAssetAmount, + rightMakerFeeAssetPaidByRightMakerAmount: rightMakerFeeAssetAmount, + // Taker + leftMakerAssetReceivedByTakerAmount, + rightMakerAssetReceivedByTakerAmount, + leftTakerFeeAssetPaidByTakerAmount: leftTakerFeeAssetAmount, + rightTakerFeeAssetPaidByTakerAmount: rightTakerFeeAssetAmount, }; + if (!combo.shouldFail) { + await matchOrderTester.matchOrdersAndAssertEffectsAsync( + { + leftOrder: signedOrderLeft, + rightOrder: signedOrderRight, + }, + takerAddress, + expectedTransferAmounts, + true, + ); + } else { + const tx = exchangeWrapper.matchOrdersWithMaximalFillAsync( + signedOrderLeft, + signedOrderRight, + takerAddress, + ); + return expect(tx).to.be.rejected(); + } }); + } + }); + + describe('batchMatchOrders and batchMatchOrdersWithMaximalFill rich errors', async () => { + it('should fail if there are zero leftOrders with the ZeroLeftOrders rich error reason', async () => { + const leftOrders: SignedOrder[] = []; + const rightOrders = [ + await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(1, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(2, 0), + feeRecipientAddress: feeRecipientAddressRight, + }), + ]; + const expectedError = new ExchangeRevertErrors.BatchMatchOrdersError( + ExchangeRevertErrors.BatchMatchOrdersErrorCodes.ZeroLeftOrders, + ); + let tx = exchangeWrapper.batchMatchOrdersAsync(leftOrders, rightOrders, takerAddress); + await expect(tx).to.revertWith(expectedError); + tx = exchangeWrapper.batchMatchOrdersWithMaximalFillAsync(leftOrders, rightOrders, takerAddress); + return expect(tx).to.revertWith(expectedError); + }); + 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 expectedError = new ExchangeRevertErrors.BatchMatchOrdersError( + ExchangeRevertErrors.BatchMatchOrdersErrorCodes.ZeroRightOrders, + ); + let tx = exchangeWrapper.batchMatchOrdersAsync(leftOrders, rightOrders, takerAddress); + await expect(tx).to.revertWith(expectedError); + tx = exchangeWrapper.batchMatchOrdersWithMaximalFillAsync(leftOrders, rightOrders, takerAddress); + return expect(tx).to.revertWith(expectedError); + }); + it('should fail if there are a different number of left orders and signatures', async () => { + const leftOrders = [ + await orderFactoryLeft.newSignedOrderAsync({ + makerAddress: makerAddressLeft, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(2, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(1, 0), + feeRecipientAddress: feeRecipientAddressLeft, + }), + await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(1, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(2, 0), + feeRecipientAddress: feeRecipientAddressRight, + }), + ]; + 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 params = orderUtils.createBatchMatchOrders(leftOrders, rightOrders); + // Set params left signatures to only include the first left signature + params.leftSignatures = [params.leftSignatures[0]]; + const expectedError = new ExchangeRevertErrors.BatchMatchOrdersError( + ExchangeRevertErrors.BatchMatchOrdersErrorCodes.InvalidLengthLeftSignatures, + ); + let tx = exchangeWrapper.batchMatchOrdersRawAsync(params, takerAddress); + await expect(tx).to.revertWith(expectedError); + tx = exchangeWrapper.batchMatchOrdersWithMaximalFillRawAsync(params, takerAddress); + return expect(tx).to.revertWith(expectedError); + }); + it('should fail if there are a different number of right orders and signatures', async () => { + const leftOrders = [ + await orderFactoryLeft.newSignedOrderAsync({ + makerAddress: makerAddressLeft, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(2, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(1, 0), + feeRecipientAddress: feeRecipientAddressLeft, + }), + await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(1, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(2, 0), + feeRecipientAddress: feeRecipientAddressRight, + }), + ]; + 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 params = orderUtils.createBatchMatchOrders(leftOrders, rightOrders); + // Set params right signatures to only include the first right signature + params.rightSignatures = [params.rightSignatures[0]]; + const expectedError = new ExchangeRevertErrors.BatchMatchOrdersError( + ExchangeRevertErrors.BatchMatchOrdersErrorCodes.InvalidLengthRightSignatures, + ); + let tx = exchangeWrapper.batchMatchOrdersRawAsync(params, takerAddress); + await expect(tx).to.revertWith(expectedError); + tx = exchangeWrapper.batchMatchOrdersWithMaximalFillRawAsync(params, takerAddress); + return expect(tx).to.revertWith(expectedError); + }); + }); + describe('batchMatchOrders', () => { + 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, + false, + ); + }); + it('Should correctly match a partial fill', 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, + }), + ]; + 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% + }, + ]; + await matchOrderTester.batchMatchOrdersAndAssertEffectsAsync( + { + leftOrders, + rightOrders, + leftOrdersTakerAssetFilledAmounts: [ZERO], + rightOrdersTakerAssetFilledAmounts: [ZERO], + }, + takerAddress, + [[0, 0]], + expectedTransferAmounts, + false, + ); + }); + 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 + leftMakerAssetBoughtByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(2, 0), + 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 + leftMakerAssetBoughtByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(2, 0), + 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, + false, + ); + }); + 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, + false, + ); + }); + it('should correctly match one left order to two right orders, where the last should not be touched', 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, + }), + 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, ZERO], + }, + takerAddress, + [[0, 0]], + expectedTransferAmounts, + false, + ); + }); + it('should have three order matchings with only two left orders and two right orders', async () => { + const leftOrders = [ + await orderFactoryLeft.newSignedOrderAsync({ + makerAddress: makerAddressLeft, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(4, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(2, 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(1, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(2, 0), + feeRecipientAddress: feeRecipientAddressRight, + }), + 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(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(50, 16), // 50% + // Taker + leftTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(50, 16), // 50% + rightTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(50, 16), // 50% + }, + { + // 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), // 100% + rightTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(50, 16), // 50% + }, + ]; + await matchOrderTester.batchMatchOrdersAndAssertEffectsAsync( + { + leftOrders, + rightOrders, + leftOrdersTakerAssetFilledAmounts: [ZERO, ZERO], + rightOrdersTakerAssetFilledAmounts: [ZERO, ZERO], + }, + takerAddress, + [[0, 0], [0, 1], [1, 1]], + expectedTransferAmounts, + false, + ); + }); - for (const combo of assetCombinations) { - const description = combo.description || JSON.stringify(combo); + const reentrancyTest = (functionNames: string[]) => { + _.forEach(functionNames, async (functionName: string, functionId: number) => { + const description = `should not allow batchMatchOrders to reenter the Exchange contract via ${functionName}`; it(description, async () => { - // Create orders to match. For ERC20s, there will be a spread. - const leftMakerAssetAmount = _.includes(fungibleTypes, combo.leftMaker) - ? Web3Wrapper.toBaseUnitAmount(15, 18) - : Web3Wrapper.toBaseUnitAmount(1, 0); - const leftTakerAssetAmount = _.includes(fungibleTypes, combo.rightMaker) - ? Web3Wrapper.toBaseUnitAmount(30, 18) - : Web3Wrapper.toBaseUnitAmount(1, 0); - const rightMakerAssetAmount = _.includes(fungibleTypes, combo.rightMaker) - ? Web3Wrapper.toBaseUnitAmount(30, 18) - : Web3Wrapper.toBaseUnitAmount(1, 0); - const rightTakerAssetAmount = _.includes(fungibleTypes, combo.leftMaker) - ? Web3Wrapper.toBaseUnitAmount(14, 18) - : Web3Wrapper.toBaseUnitAmount(1, 0); - const leftMakerFeeAssetAmount = _.includes(fungibleTypes, combo.leftMakerFee) - ? Web3Wrapper.toBaseUnitAmount(8, 12) - : Web3Wrapper.toBaseUnitAmount(1, 0); - const rightMakerFeeAssetAmount = _.includes(fungibleTypes, combo.rightMakerFee) - ? Web3Wrapper.toBaseUnitAmount(7, 12) - : Web3Wrapper.toBaseUnitAmount(1, 0); - const leftTakerFeeAssetAmount = _.includes(fungibleTypes, combo.leftTakerFee) - ? Web3Wrapper.toBaseUnitAmount(6, 12) - : Web3Wrapper.toBaseUnitAmount(1, 0); - const rightTakerFeeAssetAmount = _.includes(fungibleTypes, combo.rightTakerFee) - ? Web3Wrapper.toBaseUnitAmount(5, 12) - : Web3Wrapper.toBaseUnitAmount(1, 0); - const leftMakerAssetReceivedByTakerAmount = _.includes(fungibleTypes, combo.leftMaker) - ? leftMakerAssetAmount.minus(rightTakerAssetAmount) - : Web3Wrapper.toBaseUnitAmount(0, 0); - const signedOrderLeft = await orderFactoryLeft.newSignedOrderAsync({ - makerAssetData: getAssetData(combo.leftMaker), - takerAssetData: getAssetData(combo.rightMaker), - makerFeeAssetData: getAssetData(combo.leftMakerFee), - takerFeeAssetData: getAssetData(combo.leftTakerFee), - makerAssetAmount: leftMakerAssetAmount, - takerAssetAmount: leftTakerAssetAmount, - makerFee: leftMakerFeeAssetAmount, - takerFee: leftTakerFeeAssetAmount, - }); - const signedOrderRight = await orderFactoryRight.newSignedOrderAsync({ - makerAssetData: getAssetData(combo.rightMaker), - takerAssetData: getAssetData(combo.leftMaker), - makerFeeAssetData: getAssetData(combo.rightMakerFee), - takerFeeAssetData: getAssetData(combo.rightTakerFee), - makerAssetAmount: rightMakerAssetAmount, - takerAssetAmount: rightTakerAssetAmount, - makerFee: rightMakerFeeAssetAmount, - takerFee: rightTakerFeeAssetAmount, - }); - // Match signedOrderLeft with signedOrderRight - const expectedTransferAmounts = { - // Left Maker - leftMakerAssetSoldByLeftMakerAmount: leftMakerAssetAmount, - leftMakerFeeAssetPaidByLeftMakerAmount: leftMakerFeeAssetAmount, - // Right Maker - rightMakerAssetSoldByRightMakerAmount: rightMakerAssetAmount, - leftMakerAssetBoughtByRightMakerAmount: rightTakerAssetAmount, - rightMakerFeeAssetPaidByRightMakerAmount: rightMakerFeeAssetAmount, - // Taker - leftMakerAssetReceivedByTakerAmount, - leftTakerFeeAssetPaidByTakerAmount: leftTakerFeeAssetAmount, - rightTakerFeeAssetPaidByTakerAmount: rightTakerFeeAssetAmount, - }; - if (!combo.shouldFail) { - await matchOrderTester.matchOrdersAndAssertEffectsAsync( - { - leftOrder: signedOrderLeft, - rightOrder: signedOrderRight, - }, - takerAddress, - expectedTransferAmounts, - ); - } else { - const tx = exchangeWrapper.matchOrdersAsync(signedOrderLeft, signedOrderRight, takerAddress); - return expect(tx).to.be.rejected(); - } + const leftOrders = [ + await orderFactoryLeft.newSignedOrderAsync({ + makerAssetData: assetDataUtils.encodeERC20AssetData(reentrantErc20Token.address), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + }), + ]; + const rightOrders = [ + await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + takerAssetData: assetDataUtils.encodeERC20AssetData(reentrantErc20Token.address), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + feeRecipientAddress: feeRecipientAddressRight, + }), + ]; + await web3Wrapper.awaitTransactionSuccessAsync( + await reentrantErc20Token.setReentrantFunction.sendTransactionAsync(functionId), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const expectedError = new ReentrancyGuardRevertErrors.IllegalReentrancyError(); + const tx = exchangeWrapper.batchMatchOrdersAsync(leftOrders, rightOrders, takerAddress); + return expect(tx).to.revertWith(expectedError); }); - } + }); + }; + describe('batchMatchOrders reentrancy tests', () => reentrancyTest(exchangeConstants.FUNCTIONS_WITH_MUTEX)); + }); + describe('batchMatchOrdersWithMaximalFill', () => { + it('should fully fill the the right order and pay the profit denominated in the left maker asset', async () => { + // Create orders to match + const leftOrders = [ + await orderFactoryLeft.newSignedOrderAsync({ + makerAddress: makerAddressLeft, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(17, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(98, 0), + feeRecipientAddress: feeRecipientAddressLeft, + }), + ]; + const rightOrders = [ + await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(75, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(13, 0), + feeRecipientAddress: feeRecipientAddressRight, + }), + ]; + const expectedTransferAmounts = [ + { + // Left Maker + leftMakerAssetSoldByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(13, 0), + leftMakerFeeAssetPaidByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount( + new BigNumber('76.4705882352941176'), + 16, + ), // 76.47% + // Right Maker + rightMakerAssetSoldByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(75, 0), + rightMakerFeeAssetPaidByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + // Taker + leftTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount( + new BigNumber('76.5306122448979591'), + 16, + ), // 76.53% + rightTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + }, + ]; + await matchOrderTester.batchMatchOrdersAndAssertEffectsAsync( + { + leftOrders, + rightOrders, + leftOrdersTakerAssetFilledAmounts: [ZERO], + rightOrdersTakerAssetFilledAmounts: [ZERO], + }, + takerAddress, + [[0, 0]], + expectedTransferAmounts, + true, + ); + }); + it('Should transfer correct amounts when left order is fully filled', async () => { + // Create orders to match + const leftOrders = [ + await orderFactoryLeft.newSignedOrderAsync({ + makerAddress: makerAddressLeft, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(15, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(90, 0), + feeRecipientAddress: feeRecipientAddressLeft, + }), + ]; + const rightOrders = [ + await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(196, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(28, 0), + feeRecipientAddress: feeRecipientAddressRight, + }), + ]; + // Match signedOrderLeft with signedOrderRight + // Note that the right maker received a slightly better purchase price. + // This is intentional; see note in MixinMatchOrders.calculateMatchedFillResults. + // Because the right maker received a slightly more favorable buy price, the fee + // paid by the right taker is slightly higher than that paid by the right maker. + // Fees can be thought of as a tax paid by the seller, derived from the sale price. + const expectedTransferAmounts = [ + { + // Left Maker + leftMakerAssetSoldByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(15, 0), + leftMakerFeeAssetPaidByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + rightMakerAssetBoughtByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(90, 0), + // Right Maker + leftMakerAssetBoughtByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(15, 0), + rightMakerAssetSoldByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(105, 0), + rightMakerFeeAssetPaidByRightMakerAmount: Web3Wrapper.toBaseUnitAmount( + new BigNumber('53.5714285714285714'), + 16, + ), // 53.57% + // Taker + rightMakerAssetReceivedByTakerAmount: Web3Wrapper.toBaseUnitAmount(15, 0), + leftTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + rightTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount( + new BigNumber('53.5714285714285714'), + 16, + ), // 53.57% + }, + ]; + await matchOrderTester.batchMatchOrdersAndAssertEffectsAsync( + { + leftOrders, + rightOrders, + leftOrdersTakerAssetFilledAmounts: [ZERO], + rightOrdersTakerAssetFilledAmounts: [ZERO], + }, + takerAddress, + [[0, 0]], + expectedTransferAmounts, + true, + ); + }); + it('should correctly match one left order to two right orders, where the last should not be touched', 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, + }), + 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, ZERO], + }, + takerAddress, + [[0, 0]], + expectedTransferAmounts, + true, + ); + }); + it('should correctly fill all four orders in three matches', 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(72, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(36, 0), + feeRecipientAddress: feeRecipientAddressLeft, + }), + ]; + const rightOrders = [ + await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(15, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(30, 0), + feeRecipientAddress: feeRecipientAddressRight, + }), + await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(22, 0), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(44, 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( + new BigNumber('6.6666666666666666'), + 16, + ), // 6.66% + // Taker + leftTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + rightTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount( + new BigNumber('6.6666666666666666'), + 16, + ), // 6.66% + }, + { + // Left Maker + leftMakerAssetSoldByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(28, 0), + leftMakerFeeAssetPaidByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount( + new BigNumber('38.8888888888888888'), + 16, + ), // 38.88% + // Right Maker + rightMakerAssetSoldByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(14, 0), + rightMakerFeeAssetPaidByRightMakerAmount: Web3Wrapper.toBaseUnitAmount( + new BigNumber('93.3333333333333333'), + 16, + ), // 93.33% + // Taker + leftTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount( + new BigNumber('38.8888888888888888'), + 16, + ), // 38.88% + rightTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount( + new BigNumber('93.3333333333333333'), + 16, + ), // 93.33% + }, + { + // Left Maker + leftMakerAssetSoldByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount(44, 0), + leftMakerFeeAssetPaidByLeftMakerAmount: Web3Wrapper.toBaseUnitAmount( + new BigNumber('61.1111111111111111'), + 16, + ), // 61.11% + // Right Maker + rightMakerAssetSoldByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(22, 0), + rightMakerFeeAssetPaidByRightMakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + // Taker + leftTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount( + new BigNumber('61.1111111111111111'), + 16, + ), // 61.11% + rightTakerFeeAssetPaidByTakerAmount: Web3Wrapper.toBaseUnitAmount(100, 16), // 100% + }, + ]; + await matchOrderTester.batchMatchOrdersAndAssertEffectsAsync( + { + leftOrders, + rightOrders, + leftOrdersTakerAssetFilledAmounts: [ZERO, ZERO], + rightOrdersTakerAssetFilledAmounts: [ZERO, ZERO], + }, + takerAddress, + [[0, 0], [1, 0], [1, 1]], + expectedTransferAmounts, + true, + ); }); + + const reentrancyTest = (functionNames: string[]) => { + _.forEach(functionNames, async (functionName: string, functionId: number) => { + const description = `should not allow batchMatchOrdersWithMaximalFill to reenter the Exchange contract via ${functionName}`; + it(description, async () => { + const leftOrders = [ + await orderFactoryLeft.newSignedOrderAsync({ + makerAssetData: assetDataUtils.encodeERC20AssetData(reentrantErc20Token.address), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(5, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + }), + ]; + const rightOrders = [ + await orderFactoryRight.newSignedOrderAsync({ + makerAddress: makerAddressRight, + takerAssetData: assetDataUtils.encodeERC20AssetData(reentrantErc20Token.address), + makerAssetAmount: Web3Wrapper.toBaseUnitAmount(10, 18), + takerAssetAmount: Web3Wrapper.toBaseUnitAmount(2, 18), + feeRecipientAddress: feeRecipientAddressRight, + }), + ]; + await web3Wrapper.awaitTransactionSuccessAsync( + await reentrantErc20Token.setReentrantFunction.sendTransactionAsync(functionId), + constants.AWAIT_TRANSACTION_MINED_MS, + ); + const expectedError = new ReentrancyGuardRevertErrors.IllegalReentrancyError(); + const tx = exchangeWrapper.batchMatchOrdersAsync(leftOrders, rightOrders, takerAddress); + return expect(tx).to.revertWith(expectedError); + }); + }); + }; + describe('batchMatchOrdersWithMaximalFill reentrancy tests', () => + reentrancyTest(exchangeConstants.FUNCTIONS_WITH_MUTEX)); }); }); // tslint:disable-line:max-file-line-count diff --git a/contracts/exchange/test/utils/constants.ts b/contracts/exchange/test/utils/constants.ts index 483b49416b..817ad27acf 100644 --- a/contracts/exchange/test/utils/constants.ts +++ b/contracts/exchange/test/utils/constants.ts @@ -9,6 +9,9 @@ export const constants = { 'MARKET_BUY_ORDERS', 'MARKET_SELL_ORDERS', 'MATCH_ORDERS', + 'MATCH_ORDERS_WITH_MAXIMAL_FILL', + 'BATCH_MATCH_ORDERS', + 'BATCH_MATCH_ORDERS_WITH_MAXIMAL_FILL', 'CANCEL_ORDER', 'BATCH_CANCEL_ORDERS', 'CANCEL_ORDERS_UP_TO', diff --git a/contracts/exchange/test/utils/exchange_wrapper.ts b/contracts/exchange/test/utils/exchange_wrapper.ts index 75cd042d86..1fe35b3cd9 100644 --- a/contracts/exchange/test/utils/exchange_wrapper.ts +++ b/contracts/exchange/test/utils/exchange_wrapper.ts @@ -1,7 +1,16 @@ import { artifacts as erc1155Artifacts } from '@0x/contracts-erc1155'; import { artifacts as erc20Artifacts } from '@0x/contracts-erc20'; import { artifacts as erc721Artifacts } from '@0x/contracts-erc721'; -import { FillResults, LogDecoder, OrderInfo, orderUtils, Web3ProviderEngine } from '@0x/contracts-test-utils'; +import { + BatchMatchedFillResults, + BatchMatchOrder, + FillResults, + LogDecoder, + MatchedFillResults, + OrderInfo, + orderUtils, + Web3ProviderEngine, +} from '@0x/contracts-test-utils'; import { SignedOrder, SignedZeroExTransaction } from '@0x/types'; import { AbiEncoder, BigNumber } from '@0x/utils'; import { Web3Wrapper } from '@0x/web3-wrapper'; @@ -266,6 +275,96 @@ 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 batchMatchOrdersRawAsync( + params: BatchMatchOrder, + from: string, + ): Promise { + 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 getBatchMatchOrdersResultsAsync( + signedOrdersLeft: SignedOrder[], + signedOrdersRight: SignedOrder[], + from: string, + ): Promise { + const params = orderUtils.createBatchMatchOrders(signedOrdersLeft, signedOrdersRight); + const batchMatchedFillResults = await this._exchange.batchMatchOrders.callAsync( + params.leftOrders, + params.rightOrders, + params.leftSignatures, + params.rightSignatures, + { from }, + ); + return batchMatchedFillResults; + } + public async batchMatchOrdersWithMaximalFillAsync( + signedOrdersLeft: SignedOrder[], + signedOrdersRight: SignedOrder[], + from: string, + ): Promise { + const params = orderUtils.createBatchMatchOrders(signedOrdersLeft, signedOrdersRight); + const txHash = await this._exchange.batchMatchOrdersWithMaximalFill.sendTransactionAsync( + params.leftOrders, + params.rightOrders, + params.leftSignatures, + params.rightSignatures, + { from }, + ); + const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); + return tx; + } + public async batchMatchOrdersWithMaximalFillRawAsync( + params: BatchMatchOrder, + from: string, + ): Promise { + const txHash = await this._exchange.batchMatchOrdersWithMaximalFill.sendTransactionAsync( + params.leftOrders, + params.rightOrders, + params.leftSignatures, + params.rightSignatures, + { from }, + ); + const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); + return tx; + } + public async getBatchMatchOrdersWithMaximalFillResultsAsync( + signedOrdersLeft: SignedOrder[], + signedOrdersRight: SignedOrder[], + from: string, + ): Promise { + const params = orderUtils.createBatchMatchOrders(signedOrdersLeft, signedOrdersRight); + const batchMatchedFillResults = await this._exchange.batchMatchOrdersWithMaximalFill.callAsync( + params.leftOrders, + params.rightOrders, + params.leftSignatures, + params.rightSignatures, + { from }, + ); + return batchMatchedFillResults; + } public async matchOrdersAsync( signedOrderLeft: SignedOrder, signedOrderRight: SignedOrder, @@ -282,6 +381,52 @@ export class ExchangeWrapper { const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); return tx; } + public async getMatchOrdersResultsAsync( + signedOrderLeft: SignedOrder, + signedOrderRight: SignedOrder, + from: string, + ): Promise { + const params = orderUtils.createMatchOrders(signedOrderLeft, signedOrderRight); + const matchedFillResults = await this._exchange.matchOrders.callAsync( + params.left, + params.right, + params.leftSignature, + params.rightSignature, + { from }, + ); + return matchedFillResults; + } + public async matchOrdersWithMaximalFillAsync( + signedOrderLeft: SignedOrder, + signedOrderRight: SignedOrder, + from: string, + ): Promise { + const params = orderUtils.createMatchOrders(signedOrderLeft, signedOrderRight); + const txHash = await this._exchange.matchOrdersWithMaximalFill.sendTransactionAsync( + params.left, + params.right, + params.leftSignature, + params.rightSignature, + { from }, + ); + const tx = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); + return tx; + } + public async getMatchOrdersWithMaximalFillResultsAsync( + signedOrderLeft: SignedOrder, + signedOrderRight: SignedOrder, + from: string, + ): Promise { + const params = orderUtils.createMatchOrders(signedOrderLeft, signedOrderRight); + const matchedFillResults = await this._exchange.matchOrdersWithMaximalFill.callAsync( + params.left, + params.right, + params.leftSignature, + params.rightSignature, + { from }, + ); + return matchedFillResults; + } public async getFillOrderResultsAsync( signedOrder: SignedOrder, from: string, diff --git a/contracts/exchange/test/utils/match_order_tester.ts b/contracts/exchange/test/utils/match_order_tester.ts index 8a569aed44..9d8572e6b0 100644 --- a/contracts/exchange/test/utils/match_order_tester.ts +++ b/contracts/exchange/test/utils/match_order_tester.ts @@ -1,5 +1,12 @@ import { ERC1155ProxyWrapper, ERC20Wrapper, ERC721Wrapper } from '@0x/contracts-asset-proxy'; -import { chaiSetup, ERC1155HoldingsByOwner, OrderStatus } from '@0x/contracts-test-utils'; +import { + BatchMatchedFillResults, + chaiSetup, + ERC1155HoldingsByOwner, + FillResults, + MatchedFillResults, + OrderStatus, +} from '@0x/contracts-test-utils'; import { assetDataUtils, orderHashUtils } from '@0x/order-utils'; import { AssetProxyId, SignedOrder } from '@0x/types'; import { BigNumber } from '@0x/utils'; @@ -56,6 +63,13 @@ export interface MatchResults { balances: TokenBalances; } +export interface BatchMatchResults { + matches: MatchResults[]; + filledAmounts: Array<[SignedOrder, BigNumber, string]>; + leftFilledResults: FillEventArgs[]; + rightFilledResults: FillEventArgs[]; +} + export interface ERC1155Holdings { [owner: string]: { [contract: string]: { @@ -81,6 +95,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 +109,12 @@ export interface MatchedOrders { rightOrderTakerAssetFilledAmount?: BigNumber; } +export type BatchMatchOrdersAsyncCall = ( + leftOrders: SignedOrder[], + rightOrders: SignedOrder[], + takerAddress: string, +) => Promise; + export type MatchOrdersAsyncCall = ( leftOrder: SignedOrder, rightOrder: SignedOrder, @@ -99,7 +126,10 @@ export class MatchOrderTester { public erc20Wrapper: ERC20Wrapper; public erc721Wrapper: ERC721Wrapper; public erc1155ProxyWrapper: ERC1155ProxyWrapper; + public batchMatchOrdersCallAsync?: BatchMatchOrdersAsyncCall; + public batchMatchOrdersWithMaximalFillCallAsync?: BatchMatchOrdersAsyncCall; public matchOrdersCallAsync?: MatchOrdersAsyncCall; + public matchOrdersWithMaximalFillCallAsync?: MatchOrdersAsyncCall; private readonly _initialTokenBalancesPromise: Promise; /** @@ -108,24 +138,114 @@ 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 batchMatchOrdersWithMaximalFillCallAsync Optional, custom caller for + * `ExchangeWrapper.batchMatchOrdersAsync()`. * @param matchOrdersCallAsync Optional, custom caller for * `ExchangeWrapper.matchOrdersAsync()`. + * @param matchOrdersWithMaximalFillCallAsync Optional, custom caller for + * `ExchangeWrapper.matchOrdersAsync()`. */ constructor( exchangeWrapper: ExchangeWrapper, erc20Wrapper: ERC20Wrapper, erc721Wrapper: ERC721Wrapper, erc1155ProxyWrapper: ERC1155ProxyWrapper, + batchMatchOrdersCallAsync?: BatchMatchOrdersAsyncCall, + batchMatchOrdersWithMaximalFillCallAsync?: BatchMatchOrdersAsyncCall, matchOrdersCallAsync?: MatchOrdersAsyncCall, + matchOrdersWithMaximalFillCallAsync?: MatchOrdersAsyncCall, ) { this.exchangeWrapper = exchangeWrapper; this.erc20Wrapper = erc20Wrapper; this.erc721Wrapper = erc721Wrapper; this.erc1155ProxyWrapper = erc1155ProxyWrapper; + this.batchMatchOrdersCallAsync = batchMatchOrdersCallAsync; + this.batchMatchOrdersWithMaximalFillCallAsync = batchMatchOrdersWithMaximalFillCallAsync; this.matchOrdersCallAsync = matchOrdersCallAsync; + this.matchOrdersWithMaximalFillCallAsync = matchOrdersWithMaximalFillCallAsync; 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. + * @param withMaximalFill A boolean that indicates whether the "maximal fill" order matching + * strategy should be used. + * @return Results of `batchMatchOrders()`. + */ + public async batchMatchOrdersAndAssertEffectsAsync( + orders: BatchMatchedOrders, + takerAddress: string, + matchPairs: Array<[number, number]>, + expectedTransferAmounts: Array>, + withMaximalFill: boolean, + 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()` + let actualBatchMatchResults; + let transactionReceipt; + if (withMaximalFill) { + actualBatchMatchResults = await this.exchangeWrapper.getBatchMatchOrdersWithMaximalFillResultsAsync( + orders.leftOrders, + orders.rightOrders, + takerAddress, + ); + transactionReceipt = await this._executeBatchMatchOrdersWithMaximalFillAsync( + orders.leftOrders, + orders.rightOrders, + takerAddress, + ); + } else { + actualBatchMatchResults = await this.exchangeWrapper.getBatchMatchOrdersResultsAsync( + orders.leftOrders, + orders.rightOrders, + takerAddress, + ); + transactionReceipt = await this._executeBatchMatchOrdersAsync( + orders.leftOrders, + orders.rightOrders, + takerAddress, + ); + } + // Simulate the batch order match. + const expectedBatchMatchResults = simulateBatchMatchOrders( + orders, + takerAddress, + _initialTokenBalances, + matchPairs, + expectedTransferAmounts, + ); + const expectedResults = convertToBatchMatchResults(expectedBatchMatchResults); + expect(actualBatchMatchResults).to.be.eql(expectedResults); + // Validate the simulation against reality. + await assertBatchMatchResultsAsync( + expectedBatchMatchResults, + transactionReceipt, + await this.getBalancesAsync(), + _initialTokenBalances, + this.exchangeWrapper, + ); + return expectedBatchMatchResults; + } + /** * Matches two complementary orders and asserts results. * @param orders The matched orders and filled states. @@ -133,12 +253,15 @@ export class MatchOrderTester { * @param expectedTransferAmounts Expected amounts transferred as a result of order matching. * Omitted fields are either set to 0 or their complementary * field. + * @param withMaximalFill A boolean that indicates whether the "maximal fill" order matching + * strategy should be used. * @return Results of `matchOrders()`. */ public async matchOrdersAndAssertEffectsAsync( orders: MatchedOrders, takerAddress: string, expectedTransferAmounts: Partial, + withMaximalFill: boolean, initialTokenBalances?: TokenBalances, ): Promise { await assertInitialOrderStatesAsync(orders, this.exchangeWrapper); @@ -147,26 +270,44 @@ export class MatchOrderTester { ? initialTokenBalances : await this._initialTokenBalancesPromise; // Execute `matchOrders()` - const transactionReceipt = await this._executeMatchOrdersAsync( - orders.leftOrder, - orders.rightOrder, - takerAddress, - ); + let actualMatchResults; + let transactionReceipt; + if (withMaximalFill) { + actualMatchResults = await this.exchangeWrapper.getMatchOrdersWithMaximalFillResultsAsync( + orders.leftOrder, + orders.rightOrder, + takerAddress, + ); + transactionReceipt = await this._executeMatchOrdersWithMaximalFillAsync( + orders.leftOrder, + orders.rightOrder, + takerAddress, + ); + } else { + actualMatchResults = await this.exchangeWrapper.getMatchOrdersResultsAsync( + orders.leftOrder, + orders.rightOrder, + takerAddress, + ); + transactionReceipt = await this._executeMatchOrdersAsync(orders.leftOrder, orders.rightOrder, takerAddress); + } // Simulate the fill. - const matchResults = simulateMatchOrders( + const expectedMatchResults = simulateMatchOrders( orders, takerAddress, _initialTokenBalances, toFullMatchTransferAmounts(expectedTransferAmounts), ); - // Validate the simulation against realit. + const expectedResults = convertToMatchResults(expectedMatchResults); + expect(actualMatchResults).to.be.eql(expectedResults); + // Validate the simulation against reality. await assertMatchResultsAsync( - matchResults, + expectedMatchResults, transactionReceipt, await this.getBalancesAsync(), this.exchangeWrapper, ); - return matchResults; + return expectedMatchResults; } /** @@ -176,6 +317,30 @@ 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 _executeBatchMatchOrdersWithMaximalFillAsync( + leftOrders: SignedOrder[], + rightOrders: SignedOrder[], + takerAddress: string, + ): Promise { + const caller = + this.batchMatchOrdersWithMaximalFillCallAsync || + (async (_leftOrders: SignedOrder[], _rightOrders: SignedOrder[], _takerAddress: string) => + this.exchangeWrapper.batchMatchOrdersWithMaximalFillAsync(_leftOrders, _rightOrders, _takerAddress)); + return caller(leftOrders, rightOrders, takerAddress); + } + private async _executeMatchOrdersAsync( leftOrder: SignedOrder, rightOrder: SignedOrder, @@ -187,6 +352,18 @@ export class MatchOrderTester { this.exchangeWrapper.matchOrdersAsync(_leftOrder, _rightOrder, _takerAddress)); return caller(leftOrder, rightOrder, takerAddress); } + + private async _executeMatchOrdersWithMaximalFillAsync( + leftOrder: SignedOrder, + rightOrder: SignedOrder, + takerAddress: string, + ): Promise { + const caller = + this.matchOrdersWithMaximalFillCallAsync || + (async (_leftOrder: SignedOrder, _rightOrder: SignedOrder, _takerAddress: string) => + this.exchangeWrapper.matchOrdersWithMaximalFillAsync(_leftOrder, _rightOrder, _takerAddress)); + return caller(leftOrder, rightOrder, takerAddress); + } } /** @@ -227,6 +404,128 @@ 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 leftIdx = 0; + let rightIdx = 0; + let lastLeftIdx = -1; + let lastRightIdx = -1; + let matchedOrders: MatchedOrders; + const batchMatchResults: BatchMatchResults = { + matches: [], + filledAmounts: [], + leftFilledResults: [], + rightFilledResults: [], + }; + + // Loop over all of the matched pairs from the round + for (let i = 0; i < matchPairs.length; i++) { + leftIdx = matchPairs[i][0]; + 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', + ]); + } + } + + // Add the latest match to the batch match results + batchMatchResults.matches.push( + simulateMatchOrders( + matchedOrders, + takerAddress, + tokenBalances, + toFullMatchTransferAmounts(transferAmounts[i]), + ), + ); + + // Update the left and right fill results + if (lastLeftIdx === leftIdx) { + addFillResults(batchMatchResults.leftFilledResults[leftIdx], getLastMatch(batchMatchResults).fills[0]); + } else { + batchMatchResults.leftFilledResults.push({ ...getLastMatch(batchMatchResults).fills[0] }); + } + if (lastRightIdx === rightIdx) { + addFillResults(batchMatchResults.rightFilledResults[rightIdx], getLastMatch(batchMatchResults).fills[1]); + } else { + batchMatchResults.rightFilledResults.push({ ...getLastMatch(batchMatchResults).fills[1] }); + } + + lastLeftIdx = leftIdx; + lastRightIdx = rightIdx; + } + + for (let i = leftIdx + 1; i < orders.leftOrders.length; i++) { + batchMatchResults.leftFilledResults.push(emptyFillEventArgs()); + } + + for (let i = rightIdx + 1; i < orders.rightOrders.length; i++) { + batchMatchResults.rightFilledResults.push(emptyFillEventArgs()); + } + + // 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. @@ -327,6 +626,7 @@ function simulateMatchOrders( orders.rightOrder.takerFeeAssetData, matchResults, ); + return matchResults; } @@ -406,6 +706,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()`. @@ -524,13 +852,40 @@ function extractFillEventsfromReceipt(receipt: TransactionReceiptWithDecodedLogs * @param actualBalances Actual balances. */ function assertBalances(expectedBalances: TokenBalances, actualBalances: TokenBalances): void { - expect(encodeTokenBalances(expectedBalances)).to.deep.equal(encodeTokenBalances(actualBalances)); + expect(encodeTokenBalances(actualBalances)).to.deep.equal(encodeTokenBalances(expectedBalances)); } /** - * 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 +895,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 +921,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 +1032,225 @@ 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)); +} + +/** + * Add a new fill results object to a total fill results object destructively. + * @param total The total fill results that should be updated. + * @param fill The new fill results that should be used to accumulate. + */ +function addFillResults(total: FillEventArgs, fill: FillEventArgs): void { + // Ensure that the total and fill are compatibe fill events + expect(total.orderHash).to.be.eq(fill.orderHash); + expect(total.makerAddress).to.be.eq(fill.makerAddress); + expect(total.takerAddress).to.be.eq(fill.takerAddress); + // Add the fill results together + total.makerAssetFilledAmount = total.makerAssetFilledAmount.plus(fill.makerAssetFilledAmount); + total.takerAssetFilledAmount = total.takerAssetFilledAmount.plus(fill.takerAssetFilledAmount); + total.makerFeePaid = total.makerFeePaid.plus(fill.makerFeePaid); + total.takerFeePaid = total.takerFeePaid.plus(fill.takerFeePaid); +} + +/** + * 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)), + ); + } + } + // ERC1155 + for (const owner of _.keys(totalBalances.erc1155)) { + for (const contract of _.keys(totalBalances.erc1155[owner])) { + // Fungible + for (const tokenId of _.keys(totalBalances.erc1155[owner][contract].fungible)) { + const difference = balances.erc1155[owner][contract].fungible[tokenId].minus( + initialBalances.erc1155[owner][contract].fungible[tokenId], + ); + totalBalances.erc1155[owner][contract].fungible[tokenId] = totalBalances.erc1155[owner][ + contract + ].fungible[tokenId].plus(difference); + } + + // Nonfungible + let isDuplicate = false; + for (const value of balances.erc1155[owner][contract].nonFungible) { + // If the value is in the initial balances or the total balances, skip the + // value since it will already be added. + for (const val of totalBalances.erc1155[owner][contract].nonFungible) { + if (value.isEqualTo(val)) { + isDuplicate = true; + } + } + + if (!isDuplicate) { + for (const val of initialBalances.erc1155[owner][contract].nonFungible) { + if (value.isEqualTo(val)) { + isDuplicate = true; + } + } + } + + if (!isDuplicate) { + totalBalances.erc1155[owner][contract].nonFungible.push(value); + } + isDuplicate = false; + } + } + } + return totalBalances; +} + +/** + * Converts a BatchMatchResults object to the associated value that correspondes to a value that could be + * returned by `batchMatchOrders` or `batchMatchOrdersWithMaximalFill`. + * @param results The results object to convert + * @return The associated object that can be compared to the return value of `batchMatchOrders` + */ +function convertToBatchMatchResults(results: BatchMatchResults): BatchMatchedFillResults { + // Initialize the results object + const batchMatchedFillResults: BatchMatchedFillResults = { + left: [], + right: [], + profitInLeftMakerAsset: ZERO, + profitInRightMakerAsset: ZERO, + }; + for (const match of results.matches) { + const leftSpread = match.fills[0].makerAssetFilledAmount.minus(match.fills[1].takerAssetFilledAmount); + // If the left maker spread is positive for match, update the profitInLeftMakerAsset + if (leftSpread.isGreaterThan(ZERO)) { + batchMatchedFillResults.profitInLeftMakerAsset = batchMatchedFillResults.profitInLeftMakerAsset.plus( + leftSpread, + ); + } + const rightSpread = match.fills[1].makerAssetFilledAmount.minus(match.fills[0].takerAssetFilledAmount); + // If the right maker spread is positive for match, update the profitInRightMakerAsset + if (rightSpread.isGreaterThan(ZERO)) { + batchMatchedFillResults.profitInRightMakerAsset = batchMatchedFillResults.profitInRightMakerAsset.plus( + rightSpread, + ); + } + } + for (const fill of results.leftFilledResults) { + batchMatchedFillResults.left.push(convertToFillResults(fill)); + } + for (const fill of results.rightFilledResults) { + batchMatchedFillResults.right.push(convertToFillResults(fill)); + } + return batchMatchedFillResults; +} + +/** + * Converts a MatchResults object to the associated value that correspondes to a value that could be + * returned by `matchOrders` or `matchOrdersWithMaximalFill`. + * @param results The results object to convert + * @return The associated object that can be compared to the return value of `matchOrders` + */ +function convertToMatchResults(result: MatchResults): MatchedFillResults { + // If the left spread is negative, set it to zero + let profitInLeftMakerAsset = result.fills[0].makerAssetFilledAmount.minus(result.fills[1].takerAssetFilledAmount); + if (profitInLeftMakerAsset.isLessThanOrEqualTo(ZERO)) { + profitInLeftMakerAsset = ZERO; + } + + // If the right spread is negative, set it to zero + let profitInRightMakerAsset = result.fills[1].makerAssetFilledAmount.minus(result.fills[0].takerAssetFilledAmount); + if (profitInRightMakerAsset.isLessThanOrEqualTo(ZERO)) { + profitInRightMakerAsset = ZERO; + } + + const matchedFillResults: MatchedFillResults = { + left: { + makerAssetFilledAmount: result.fills[0].makerAssetFilledAmount, + takerAssetFilledAmount: result.fills[0].takerAssetFilledAmount, + makerFeePaid: result.fills[0].makerFeePaid, + takerFeePaid: result.fills[0].takerFeePaid, + }, + right: { + makerAssetFilledAmount: result.fills[1].makerAssetFilledAmount, + takerAssetFilledAmount: result.fills[1].takerAssetFilledAmount, + makerFeePaid: result.fills[1].makerFeePaid, + takerFeePaid: result.fills[1].takerFeePaid, + }, + profitInLeftMakerAsset, + profitInRightMakerAsset, + }; + return matchedFillResults; +} + +/** + * Converts a fill event args object to the associated FillResults object. + * @param result The result to be converted to a FillResults object. + * @return The converted value. + */ +function convertToFillResults(result: FillEventArgs): FillResults { + const fillResults: FillResults = { + makerAssetFilledAmount: result.makerAssetFilledAmount, + takerAssetFilledAmount: result.takerAssetFilledAmount, + makerFeePaid: result.makerFeePaid, + takerFeePaid: result.takerFeePaid, + }; + return fillResults; +} + +/** + * Creates an empty FillEventArgs object. + * @return The empty FillEventArgs object. + */ +function emptyFillEventArgs(): FillEventArgs { + const empty: FillEventArgs = { + orderHash: '', + makerAddress: '', + takerAddress: '', + makerAssetFilledAmount: new BigNumber(0), + takerAssetFilledAmount: new BigNumber(0), + makerFeePaid: new BigNumber(0), + takerFeePaid: new BigNumber(0), + }; + return empty; +} // tslint:disable-line:max-file-line-count diff --git a/contracts/exchange/tsconfig.json b/contracts/exchange/tsconfig.json index 6f878d304c..6d8ad9c993 100644 --- a/contracts/exchange/tsconfig.json +++ b/contracts/exchange/tsconfig.json @@ -17,6 +17,7 @@ "generated-artifacts/ReentrantERC20Token.json", "generated-artifacts/TestAssetProxyDispatcher.json", "generated-artifacts/TestExchangeInternals.json", + "generated-artifacts/TestExchangeMath.json", "generated-artifacts/TestLibExchangeRichErrorDecoder.json", "generated-artifacts/TestSignatureValidator.json", "generated-artifacts/TestValidatorWallet.json", diff --git a/contracts/test-utils/src/index.ts b/contracts/test-utils/src/index.ts index 44c101adcc..b31c2c40f4 100644 --- a/contracts/test-utils/src/index.ts +++ b/contracts/test-utils/src/index.ts @@ -29,6 +29,8 @@ export { TransactionFactory } from './transaction_factory'; export { testWithReferenceFuncAsync } from './test_with_reference'; export { hexConcat } from './hex_utils'; export { + BatchMatchedFillResults, + BatchMatchOrder, ContractName, ERC20BalancesByOwner, ERC1155FungibleHoldingsByOwner, @@ -38,6 +40,7 @@ export { FillResults, MarketBuyOrders, MarketSellOrders, + MatchedFillResults, OrderInfo, OrderStatus, Token, 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..b085a34d0c 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; @@ -143,3 +150,17 @@ export interface FillResults { makerFeePaid: BigNumber; takerFeePaid: BigNumber; } + +export interface MatchedFillResults { + left: FillResults; + right: FillResults; + profitInLeftMakerAsset: BigNumber; + profitInRightMakerAsset: BigNumber; +} + +export interface BatchMatchedFillResults { + left: FillResults[]; + right: FillResults[]; + profitInLeftMakerAsset: BigNumber; + profitInRightMakerAsset: BigNumber; +} diff --git a/packages/order-utils/src/exchange_revert_errors.ts b/packages/order-utils/src/exchange_revert_errors.ts index 66286d0542..4ef59c6706 100644 --- a/packages/order-utils/src/exchange_revert_errors.ts +++ b/packages/order-utils/src/exchange_revert_errors.ts @@ -4,6 +4,13 @@ import * as _ from 'lodash'; // tslint:disable:max-classes-per-file +export enum BatchMatchOrdersErrorCodes { + ZeroLeftOrders, + ZeroRightOrders, + InvalidLengthLeftSignatures, + InvalidLengthRightSignatures, +} + export enum FillErrorCode { InvalidTakerAmount, TakerOverpay, @@ -30,6 +37,12 @@ export enum TransactionErrorCode { Expired, } +export class BatchMatchOrdersError extends RevertError { + constructor(error?: BatchMatchOrdersErrorCodes) { + super('BatchMatchOrdersError', 'BatchMatchOrdersError(uint8 error)', { error }); + } +} + export class SignatureError extends RevertError { constructor(error?: SignatureErrorCode, hash?: string, signer?: string, signature?: string) { super('SignatureError', 'SignatureError(uint8 error, bytes32 hash, address signer, bytes signature)', { @@ -200,6 +213,7 @@ export class IncompleteFillError extends RevertError { } const types = [ + BatchMatchOrdersError, OrderStatusError, SignatureError, SignatureValidatorNotApprovedError,