diff options
author | Greg Hysen <hysz@users.noreply.github.com> | 2018-05-22 05:29:56 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-05-22 05:29:56 +0800 |
commit | d460c0e8b9c6f4081803fff4e2d2347be8cd5ce3 (patch) | |
tree | 596681d82624683e76cdd507e31c46ffad4df73b /packages/contracts | |
parent | 0fdf32cf6f0f1def41a1733687e977feb3a652f5 (diff) | |
parent | 11b6d290da18ea4690424a2a518413fb39a4a885 (diff) | |
download | dexon-sol-tools-d460c0e8b9c6f4081803fff4e2d2347be8cd5ce3.tar dexon-sol-tools-d460c0e8b9c6f4081803fff4e2d2347be8cd5ce3.tar.gz dexon-sol-tools-d460c0e8b9c6f4081803fff4e2d2347be8cd5ce3.tar.bz2 dexon-sol-tools-d460c0e8b9c6f4081803fff4e2d2347be8cd5ce3.tar.lz dexon-sol-tools-d460c0e8b9c6f4081803fff4e2d2347be8cd5ce3.tar.xz dexon-sol-tools-d460c0e8b9c6f4081803fff4e2d2347be8cd5ce3.tar.zst dexon-sol-tools-d460c0e8b9c6f4081803fff4e2d2347be8cd5ce3.zip |
Merge pull request #572 from 0xProject/feature/contracts/atomicMatching
Atomic Order Matching
Diffstat (limited to 'packages/contracts')
62 files changed, 2500 insertions, 243 deletions
diff --git a/packages/contracts/compiler.json b/packages/contracts/compiler.json index 9b0a1e161..bf6cc2376 100644 --- a/packages/contracts/compiler.json +++ b/packages/contracts/compiler.json @@ -4,7 +4,7 @@ "compilerSettings": { "optimizer": { "enabled": true, - "runs": 200 + "runs": 0 }, "outputSelection": { "*": { diff --git a/packages/contracts/package.json b/packages/contracts/package.json index e3bf83ae0..9c7142cdb 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -12,7 +12,7 @@ }, "scripts": { "watch": "tsc -w", - "prebuild": "run-s clean copy_artifacts generate_contract_wrappers", + "prebuild": "run-s clean compile copy_artifacts generate_contract_wrappers", "copy_artifacts": "copyfiles -u 4 '../migrations/artifacts/2.0.0/**/*' ./lib/src/artifacts;", "build": "tsc", "test": "run-s build run_mocha", @@ -58,7 +58,7 @@ "npm-run-all": "^4.1.2", "prettier": "^1.11.1", "shx": "^0.2.2", - "solc": "^0.4.23", + "solc": "^0.4.24", "tslint": "5.8.0", "typescript": "2.7.1", "yargs": "^10.0.3" diff --git a/packages/contracts/src/contracts/current/protocol/AssetProxy/ERC20Proxy.sol b/packages/contracts/src/contracts/current/protocol/AssetProxy/ERC20Proxy.sol index c02536d67..ee0c66fdc 100644 --- a/packages/contracts/src/contracts/current/protocol/AssetProxy/ERC20Proxy.sol +++ b/packages/contracts/src/contracts/current/protocol/AssetProxy/ERC20Proxy.sol @@ -16,7 +16,7 @@ */ -pragma solidity ^0.4.23; +pragma solidity ^0.4.24; pragma experimental ABIEncoderV2; import "../../utils/LibBytes/LibBytes.sol"; diff --git a/packages/contracts/src/contracts/current/protocol/AssetProxy/ERC721Proxy.sol b/packages/contracts/src/contracts/current/protocol/AssetProxy/ERC721Proxy.sol index 475359087..94aab9139 100644 --- a/packages/contracts/src/contracts/current/protocol/AssetProxy/ERC721Proxy.sol +++ b/packages/contracts/src/contracts/current/protocol/AssetProxy/ERC721Proxy.sol @@ -16,7 +16,7 @@ */ -pragma solidity ^0.4.23; +pragma solidity ^0.4.24; pragma experimental ABIEncoderV2; import "../../utils/LibBytes/LibBytes.sol"; diff --git a/packages/contracts/src/contracts/current/protocol/AssetProxy/MixinAssetProxy.sol b/packages/contracts/src/contracts/current/protocol/AssetProxy/MixinAssetProxy.sol index d58cfc2dd..4ec31304f 100644 --- a/packages/contracts/src/contracts/current/protocol/AssetProxy/MixinAssetProxy.sol +++ b/packages/contracts/src/contracts/current/protocol/AssetProxy/MixinAssetProxy.sol @@ -16,7 +16,7 @@ */ -pragma solidity ^0.4.23; +pragma solidity ^0.4.24; pragma experimental ABIEncoderV2; import "./mixins/MAssetProxy.sol"; diff --git a/packages/contracts/src/contracts/current/protocol/AssetProxy/MixinAuthorizable.sol b/packages/contracts/src/contracts/current/protocol/AssetProxy/MixinAuthorizable.sol index b66b783ea..0bbd3b318 100644 --- a/packages/contracts/src/contracts/current/protocol/AssetProxy/MixinAuthorizable.sol +++ b/packages/contracts/src/contracts/current/protocol/AssetProxy/MixinAuthorizable.sol @@ -16,7 +16,7 @@ */ -pragma solidity ^0.4.23; +pragma solidity ^0.4.24; pragma experimental ABIEncoderV2; import "./mixins/MAuthorizable.sol"; diff --git a/packages/contracts/src/contracts/current/protocol/AssetProxy/interfaces/IAssetProxy.sol b/packages/contracts/src/contracts/current/protocol/AssetProxy/interfaces/IAssetProxy.sol index eca6524e5..8b30dfabb 100644 --- a/packages/contracts/src/contracts/current/protocol/AssetProxy/interfaces/IAssetProxy.sol +++ b/packages/contracts/src/contracts/current/protocol/AssetProxy/interfaces/IAssetProxy.sol @@ -16,7 +16,7 @@ */ -pragma solidity ^0.4.23; +pragma solidity ^0.4.24; pragma experimental ABIEncoderV2; import "./IAuthorizable.sol"; diff --git a/packages/contracts/src/contracts/current/protocol/AssetProxy/interfaces/IAuthorizable.sol b/packages/contracts/src/contracts/current/protocol/AssetProxy/interfaces/IAuthorizable.sol index 3120be7ec..d6fe03898 100644 --- a/packages/contracts/src/contracts/current/protocol/AssetProxy/interfaces/IAuthorizable.sol +++ b/packages/contracts/src/contracts/current/protocol/AssetProxy/interfaces/IAuthorizable.sol @@ -16,7 +16,7 @@ */ -pragma solidity ^0.4.23; +pragma solidity ^0.4.24; pragma experimental ABIEncoderV2; import "../../../utils/Ownable/IOwnable.sol"; diff --git a/packages/contracts/src/contracts/current/protocol/AssetProxy/mixins/MAssetProxy.sol b/packages/contracts/src/contracts/current/protocol/AssetProxy/mixins/MAssetProxy.sol index e0ec8c4e1..3800bf04c 100644 --- a/packages/contracts/src/contracts/current/protocol/AssetProxy/mixins/MAssetProxy.sol +++ b/packages/contracts/src/contracts/current/protocol/AssetProxy/mixins/MAssetProxy.sol @@ -16,7 +16,7 @@ */ -pragma solidity ^0.4.23; +pragma solidity ^0.4.24; pragma experimental ABIEncoderV2; import "../interfaces/IAssetProxy.sol"; diff --git a/packages/contracts/src/contracts/current/protocol/AssetProxy/mixins/MAuthorizable.sol b/packages/contracts/src/contracts/current/protocol/AssetProxy/mixins/MAuthorizable.sol index 71d1910e5..cdf60bdee 100644 --- a/packages/contracts/src/contracts/current/protocol/AssetProxy/mixins/MAuthorizable.sol +++ b/packages/contracts/src/contracts/current/protocol/AssetProxy/mixins/MAuthorizable.sol @@ -16,7 +16,7 @@ */ -pragma solidity ^0.4.23; +pragma solidity ^0.4.24; pragma experimental ABIEncoderV2; import "../interfaces/IAuthorizable.sol"; diff --git a/packages/contracts/src/contracts/current/protocol/Exchange/Exchange.sol b/packages/contracts/src/contracts/current/protocol/Exchange/Exchange.sol index 85e2bc47e..b7b308069 100644 --- a/packages/contracts/src/contracts/current/protocol/Exchange/Exchange.sol +++ b/packages/contracts/src/contracts/current/protocol/Exchange/Exchange.sol @@ -16,7 +16,7 @@ */ -pragma solidity ^0.4.23; +pragma solidity ^0.4.24; pragma experimental ABIEncoderV2; import "./MixinExchangeCore.sol"; @@ -25,24 +25,29 @@ import "./MixinSettlement.sol"; import "./MixinWrapperFunctions.sol"; import "./MixinAssetProxyDispatcher.sol"; import "./MixinTransactions.sol"; +import "./MixinMatchOrders.sol"; contract Exchange is - MixinWrapperFunctions, MixinExchangeCore, + MixinMatchOrders, MixinSettlement, MixinSignatureValidator, MixinTransactions, - MixinAssetProxyDispatcher + MixinAssetProxyDispatcher, + MixinWrapperFunctions { + string constant public VERSION = "2.0.1-alpha"; + // Mixins are instantiated in the order they are inherited constructor (bytes memory _zrxProxyData) public MixinExchangeCore() - MixinSignatureValidator() + MixinMatchOrders() MixinSettlement(_zrxProxyData) - MixinWrapperFunctions() - MixinAssetProxyDispatcher() + MixinSignatureValidator() MixinTransactions() + MixinAssetProxyDispatcher() + MixinWrapperFunctions() {} } diff --git a/packages/contracts/src/contracts/current/protocol/Exchange/MixinAssetProxyDispatcher.sol b/packages/contracts/src/contracts/current/protocol/Exchange/MixinAssetProxyDispatcher.sol index 308dace32..3b38d1f37 100644 --- a/packages/contracts/src/contracts/current/protocol/Exchange/MixinAssetProxyDispatcher.sol +++ b/packages/contracts/src/contracts/current/protocol/Exchange/MixinAssetProxyDispatcher.sol @@ -16,7 +16,7 @@ */ -pragma solidity ^0.4.23; +pragma solidity ^0.4.24; import "../../utils/Ownable/Ownable.sol"; import "../AssetProxy/interfaces/IAssetProxy.sol"; diff --git a/packages/contracts/src/contracts/current/protocol/Exchange/MixinExchangeCore.sol b/packages/contracts/src/contracts/current/protocol/Exchange/MixinExchangeCore.sol index 82c7a2ddc..1c2420374 100644 --- a/packages/contracts/src/contracts/current/protocol/Exchange/MixinExchangeCore.sol +++ b/packages/contracts/src/contracts/current/protocol/Exchange/MixinExchangeCore.sol @@ -16,12 +16,13 @@ */ -pragma solidity ^0.4.23; +pragma solidity ^0.4.24; pragma experimental ABIEncoderV2; import "./libs/LibFillResults.sol"; import "./libs/LibOrder.sol"; import "./libs/LibMath.sol"; +import "./libs/LibStatus.sol"; import "./libs/LibExchangeErrors.sol"; import "./mixins/MExchangeCore.sol"; import "./mixins/MSettlement.sol"; @@ -29,9 +30,11 @@ import "./mixins/MSignatureValidator.sol"; import "./mixins/MTransactions.sol"; contract MixinExchangeCore is + SafeMath, + LibMath, + LibStatus, LibOrder, LibFillResults, - LibMath, LibExchangeErrors, MExchangeCore, MSettlement, @@ -48,6 +51,8 @@ contract MixinExchangeCore is // Orders with a salt less than their maker's epoch are considered cancelled mapping (address => uint256) public makerEpoch; + ////// Core exchange functions ////// + /// @dev Cancels all orders reated by sender with a salt less than or equal to the specified salt value. /// @param salt Orders created with a salt less or equal to this value will be cancelled. function cancelOrdersUpTo(uint256 salt) @@ -70,42 +75,243 @@ contract MixinExchangeCore is function fillOrder( Order memory order, uint256 takerAssetFillAmount, - bytes memory signature) + bytes memory signature + ) public returns (FillResults memory fillResults) { + // Fetch order info + OrderInfo memory orderInfo = getOrderInfo(order); + + // Fetch taker address + address takerAddress = getCurrentContextAddress(); + + // Either our context is valid or we revert + assertValidFill( + order, + orderInfo.orderStatus, + orderInfo.orderHash, + takerAddress, + orderInfo.orderTakerAssetFilledAmount, + takerAssetFillAmount, + signature + ); + + // Compute proportional fill amounts + uint8 status; + (status, fillResults) = calculateFillResults( + order, + orderInfo.orderStatus, + orderInfo.orderTakerAssetFilledAmount, + takerAssetFillAmount + ); + if (status != uint8(Status.SUCCESS)) { + emit ExchangeStatus(uint8(status), orderInfo.orderHash); + return getNullFillResults(); + } + + // Settle order + settleOrder(order, takerAddress, fillResults); + + // Update exchange internal state + updateFilledState( + order, + takerAddress, + orderInfo.orderHash, + orderInfo.orderTakerAssetFilledAmount, + fillResults + ); + return fillResults; + } + + /// @dev After calling, the order can not be filled anymore. + /// Throws if order is invalid or sender does not have permission to cancel. + /// @param order Order to cancel. Order must be Status.FILLABLE. + /// @return True if the order state changed to cancelled. + /// False if the order was valid, but in an + /// unfillable state (see LibStatus.STATUS for order states) + function cancelOrder(Order memory order) + public + returns (bool) + { + // Fetch current order status + OrderInfo memory orderInfo = getOrderInfo(order); + + // Validate context + assertValidCancel(order, orderInfo.orderStatus, orderInfo.orderHash); + + // Perform cancel + return updateCancelledState(order, orderInfo.orderStatus, orderInfo.orderHash); + } + + /// @dev Gets information about an order: status, hash, and amount filled. + /// @param order Order to gather information on. + /// @return OrderInfo Information about the order and its state. + /// See LibOrder.OrderInfo for a complete description. + function getOrderInfo(Order memory order) + public + view + returns (LibOrder.OrderInfo memory orderInfo) + { // Compute the order hash - bytes32 orderHash = getOrderHash(order); + orderInfo.orderHash = getOrderHash(order); - // Check if order has been cancelled by salt value - if (order.salt < makerEpoch[order.makerAddress]) { - emit ExchangeError(uint8(Errors.ORDER_CANCELLED), orderHash); - return fillResults; + // If order.makerAssetAmount is zero, we also reject the order. + // While the Exchange contract handles them correctly, they create + // edge cases in the supporting infrastructure because they have + // an 'infinite' price when computed by a simple division. + if (order.makerAssetAmount == 0) { + orderInfo.orderStatus = uint8(Status.ORDER_INVALID_MAKER_ASSET_AMOUNT); + return orderInfo; } - // Check if order has been cancelled by orderHash - if (cancelled[orderHash]) { - emit ExchangeError(uint8(Errors.ORDER_CANCELLED), orderHash); - return fillResults; + // If order.takerAssetAmount is zero, then the order will always + // be considered filled because 0 == takerAssetAmount == orderTakerAssetFilledAmount + // Instead of distinguishing between unfilled and filled zero taker + // amount orders, we choose not to support them. + if (order.takerAssetAmount == 0) { + orderInfo.orderStatus = uint8(Status.ORDER_INVALID_TAKER_ASSET_AMOUNT); + return orderInfo; } - // Validate order and maker only if first time seen - // TODO: Read filled and cancelled only once - if (filled[orderHash] == 0) { - require( - order.makerAssetAmount > 0, - GT_ZERO_AMOUNT_REQUIRED - ); - require( - order.takerAssetAmount > 0, - GT_ZERO_AMOUNT_REQUIRED - ); + // Validate order expiration + if (block.timestamp >= order.expirationTimeSeconds) { + orderInfo.orderStatus = uint8(Status.ORDER_EXPIRED); + return orderInfo; + } + + // Check if order has been cancelled + if (cancelled[orderInfo.orderHash]) { + orderInfo.orderStatus = uint8(Status.ORDER_CANCELLED); + return orderInfo; + } + if (makerEpoch[order.makerAddress] > order.salt) { + orderInfo.orderStatus = uint8(Status.ORDER_CANCELLED); + return orderInfo; + } + + // Fetch filled amount and validate order availability + orderInfo.orderTakerAssetFilledAmount = filled[orderInfo.orderHash]; + if (orderInfo.orderTakerAssetFilledAmount >= order.takerAssetAmount) { + orderInfo.orderStatus = uint8(Status.ORDER_FULLY_FILLED); + return orderInfo; + } + + // All other statuses are ruled out: order is Fillable + orderInfo.orderStatus = uint8(Status.ORDER_FILLABLE); + return orderInfo; + } + + /// @dev Calculates amounts filled and fees paid by maker and taker. + /// @param order to be filled. + /// @param orderStatus Status of order to be filled. + /// @param orderTakerAssetFilledAmount Amount of order already filled. + /// @param takerAssetFillAmount Desired amount of order to fill by taker. + /// @return status Return status of calculating fill amounts. Returns Status.SUCCESS on success. + /// @return fillResults Amounts filled and fees paid by maker and taker. + function calculateFillResults( + Order memory order, + uint8 orderStatus, + uint256 orderTakerAssetFilledAmount, + uint256 takerAssetFillAmount + ) + public + pure + returns ( + uint8 status, + FillResults memory fillResults + ) + { + // Fill amount must be greater than 0 + if (takerAssetFillAmount == 0) { + status = uint8(Status.TAKER_ASSET_FILL_AMOUNT_TOO_LOW); + return (status, fillResults); + } + + // Ensure the order is fillable + if (orderStatus != uint8(Status.ORDER_FILLABLE)) { + status = orderStatus; + return (status, fillResults); + } + + // Compute takerAssetFilledAmount + uint256 remainingTakerAssetAmount = safeSub(order.takerAssetAmount, orderTakerAssetFilledAmount); + uint256 takerAssetFilledAmount = min256(takerAssetFillAmount, remainingTakerAssetAmount); + + // Validate fill order rounding + if (isRoundingError( + takerAssetFilledAmount, + order.takerAssetAmount, + order.makerAssetAmount)) + { + status = uint8(Status.ROUNDING_ERROR_TOO_LARGE); + return (status, fillResults); + } + + // Compute proportional transfer amounts + // TODO: All three are multiplied by the same fraction. This can + // potentially be optimized. + fillResults.takerAssetFilledAmount = takerAssetFilledAmount; + fillResults.makerAssetFilledAmount = getPartialAmount( + fillResults.takerAssetFilledAmount, + order.takerAssetAmount, + order.makerAssetAmount + ); + fillResults.makerFeePaid = getPartialAmount( + fillResults.takerAssetFilledAmount, + order.takerAssetAmount, + order.makerFee + ); + fillResults.takerFeePaid = getPartialAmount( + fillResults.takerAssetFilledAmount, + order.takerAssetAmount, + order.takerFee + ); + + status = uint8(Status.SUCCESS); + return (status, fillResults); + } + + /// @dev Validates context for fillOrder. Succeeds or throws. + /// @param order to be filled. + /// @param orderStatus Status of order to be filled. + /// @param orderHash Hash of order to be filled. + /// @param takerAddress Address of order taker. + /// @param orderTakerAssetFilledAmount Amount of order already filled. + /// @param takerAssetFillAmount Desired amount of order to fill by taker. + /// @param signature Proof that the orders was created by its maker. + function assertValidFill( + Order memory order, + uint8 orderStatus, + bytes32 orderHash, + address takerAddress, + uint256 orderTakerAssetFilledAmount, + uint256 takerAssetFillAmount, + bytes memory signature + ) + internal + { + // Ensure order is valid + // An order can only be filled if its status is FILLABLE; + // however, only invalid statuses result in a throw. + // See LibStatus for a complete description of order statuses. + require( + orderStatus != uint8(Status.ORDER_INVALID_MAKER_ASSET_AMOUNT), + INVALID_ORDER_MAKER_ASSET_AMOUNT + ); + require( + orderStatus != uint8(Status.ORDER_INVALID_TAKER_ASSET_AMOUNT), + INVALID_ORDER_TAKER_ASSET_AMOUNT + ); + + // Validate Maker signature (check only if first time seen) + if (orderTakerAssetFilledAmount == 0) { require( isValidSignature(orderHash, order.makerAddress, signature), SIGNATURE_VALIDATION_FAILED ); } - + // Validate sender is allowed to fill this order if (order.senderAddress != address(0)) { require( @@ -115,7 +321,6 @@ contract MixinExchangeCore is } // Validate taker is allowed to fill this order - address takerAddress = getCurrentContextAddress(); if (order.takerAddress != address(0)) { require( order.takerAddress == takerAddress, @@ -126,88 +331,108 @@ contract MixinExchangeCore is takerAssetFillAmount > 0, GT_ZERO_AMOUNT_REQUIRED ); + } - // Validate order expiration - if (block.timestamp >= order.expirationTimeSeconds) { - emit ExchangeError(uint8(Errors.ORDER_EXPIRED), orderHash); - return fillResults; - } - - // Validate order availability - uint256 remainingTakerAssetFillAmount = safeSub(order.takerAssetAmount, filled[orderHash]); - if (remainingTakerAssetFillAmount == 0) { - emit ExchangeError(uint8(Errors.ORDER_FULLY_FILLED), orderHash); - return fillResults; - } - - // Validate fill order rounding - fillResults.takerAssetFilledAmount = min256(takerAssetFillAmount, remainingTakerAssetFillAmount); - if (isRoundingError(fillResults.takerAssetFilledAmount, order.takerAssetAmount, order.makerAssetAmount)) { - emit ExchangeError(uint8(Errors.ROUNDING_ERROR_TOO_LARGE), orderHash); - fillResults.takerAssetFilledAmount = 0; - return fillResults; - } - + /// @dev Updates state with results of a fill order. + /// @param order that was filled. + /// @param takerAddress Address of taker who filled the order. + /// @param orderTakerAssetFilledAmount Amount of order already filled. + /// @return fillResults Amounts filled and fees paid by maker and taker. + function updateFilledState( + Order memory order, + address takerAddress, + bytes32 orderHash, + uint256 orderTakerAssetFilledAmount, + FillResults memory fillResults + ) + internal + { // Update state - filled[orderHash] = safeAdd(filled[orderHash], fillResults.takerAssetFilledAmount); - - // Settle order - (fillResults.makerAssetFilledAmount, fillResults.makerFeePaid, fillResults.takerFeePaid) = - settleOrder(order, takerAddress, fillResults.takerAssetFilledAmount); + filled[orderHash] = safeAdd(orderTakerAssetFilledAmount, fillResults.takerAssetFilledAmount); // Log order - emitFillEvent(order, takerAddress, orderHash, fillResults); - return fillResults; + emit Fill( + order.makerAddress, + takerAddress, + order.feeRecipientAddress, + fillResults.makerAssetFilledAmount, + fillResults.takerAssetFilledAmount, + fillResults.makerFeePaid, + fillResults.takerFeePaid, + orderHash, + order.makerAssetData, + order.takerAssetData + ); } - /// @dev After calling, the order can not be filled anymore. - /// @param order Order struct containing order specifications. - /// @return True if the order state changed to cancelled. - /// False if the transaction was already cancelled or expired. - function cancelOrder(Order memory order) - public - returns (bool) + /// @dev Validates context for cancelOrder. Succeeds or throws. + /// @param order that was cancelled. + /// @param orderStatus Status of order that was cancelled. + /// @param orderHash Hash of order that was cancelled. + function assertValidCancel( + Order memory order, + uint8 orderStatus, + bytes32 orderHash + ) + internal { - // Compute the order hash - bytes32 orderHash = getOrderHash(order); - - // Validate the order + // Ensure order is valid + // An order can only be cancelled if its status is FILLABLE; + // however, only invalid statuses result in a throw. + // See LibStatus for a complete description of order statuses. require( - order.makerAssetAmount > 0, - GT_ZERO_AMOUNT_REQUIRED + orderStatus != uint8(Status.ORDER_INVALID_MAKER_ASSET_AMOUNT), + INVALID_ORDER_MAKER_ASSET_AMOUNT ); require( - order.takerAssetAmount > 0, - GT_ZERO_AMOUNT_REQUIRED + orderStatus != uint8(Status.ORDER_INVALID_TAKER_ASSET_AMOUNT), + INVALID_ORDER_TAKER_ASSET_AMOUNT ); - // Validate sender is allowed to cancel this order - if (order.senderAddress != address(0)) { - require( - order.senderAddress == msg.sender, - INVALID_SENDER - ); - } - // Validate transaction signed by maker address makerAddress = getCurrentContextAddress(); require( order.makerAddress == makerAddress, INVALID_CONTEXT ); - - if (block.timestamp >= order.expirationTimeSeconds) { - emit ExchangeError(uint8(Errors.ORDER_EXPIRED), orderHash); - return false; + + // Validate sender is allowed to cancel this order + if (order.senderAddress != address(0)) { + require( + order.senderAddress == msg.sender, + INVALID_SENDER + ); } + } - if (cancelled[orderHash]) { - emit ExchangeError(uint8(Errors.ORDER_CANCELLED), orderHash); - return false; + /// @dev Updates state with results of cancelling an order. + /// State is only updated if the order is currently fillable. + /// Otherwise, updating state would have no effect. + /// @param order that was cancelled. + /// @param orderStatus Status of order that was cancelled. + /// @param orderHash Hash of order that was cancelled. + /// @return stateUpdated Returns true only if state was updated. + function updateCancelledState( + Order memory order, + uint8 orderStatus, + bytes32 orderHash + ) + internal + returns (bool stateUpdated) + { + // Ensure order is fillable (otherwise cancelling does nothing) + // See LibStatus for a complete description of order statuses. + if (orderStatus != uint8(Status.ORDER_FILLABLE)) { + emit ExchangeStatus(uint8(orderStatus), orderHash); + stateUpdated = false; + return stateUpdated; } + // Perform cancel cancelled[orderHash] = true; + stateUpdated = true; + // Log cancel emit Cancel( order.makerAddress, order.feeRecipientAddress, @@ -215,29 +440,7 @@ contract MixinExchangeCore is order.makerAssetData, order.takerAssetData ); - return true; - } - /// @dev Logs a Fill event with the given arguments. - /// The sole purpose of this function is to get around the stack variable limit. - function emitFillEvent( - Order memory order, - address takerAddress, - bytes32 orderHash, - FillResults memory fillResults) - internal - { - emit Fill( - order.makerAddress, - takerAddress, - order.feeRecipientAddress, - fillResults.makerAssetFilledAmount, - fillResults.takerAssetFilledAmount, - fillResults.makerFeePaid, - fillResults.takerFeePaid, - orderHash, - order.makerAssetData, - order.takerAssetData - ); + return stateUpdated; } } diff --git a/packages/contracts/src/contracts/current/protocol/Exchange/MixinMatchOrders.sol b/packages/contracts/src/contracts/current/protocol/Exchange/MixinMatchOrders.sol new file mode 100644 index 000000000..9d8b521c0 --- /dev/null +++ b/packages/contracts/src/contracts/current/protocol/Exchange/MixinMatchOrders.sol @@ -0,0 +1,297 @@ +/* + Copyright 2018 ZeroEx Intl. + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +pragma solidity ^0.4.24; +pragma experimental ABIEncoderV2; + +import "./mixins/MExchangeCore.sol"; +import "./mixins/MMatchOrders.sol"; +import "./mixins/MSettlement.sol"; +import "./mixins/MTransactions.sol"; +import "../../utils/SafeMath/SafeMath.sol"; +import "./libs/LibMath.sol"; +import "./libs/LibOrder.sol"; +import "./libs/LibStatus.sol"; +import "../../utils/LibBytes/LibBytes.sol"; +import "./libs/LibExchangeErrors.sol"; + +contract MixinMatchOrders is + SafeMath, + LibBytes, + LibMath, + LibStatus, + LibOrder, + LibFillResults, + LibExchangeErrors, + MExchangeCore, + MMatchOrders, + MSettlement, + MTransactions +{ + + /// @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. + /// TODO: Make this function external once supported by Solidity (See Solidity Issues #3199, #1603) + function matchOrders( + Order memory leftOrder, + Order memory rightOrder, + bytes memory leftSignature, + bytes memory rightSignature + ) + public + returns (MatchedFillResults memory matchedFillResults) + { + // Get left & right order info + OrderInfo memory leftOrderInfo = getOrderInfo(leftOrder); + OrderInfo memory rightOrderInfo = getOrderInfo(rightOrder); + + // Fetch taker address + address takerAddress = getCurrentContextAddress(); + + // Either our context is valid or we revert + assertValidMatch(leftOrder, rightOrder); + + // Compute proportional fill amounts + matchedFillResults = calculateMatchedFillResults( + leftOrder, + rightOrder, + leftOrderInfo.orderStatus, + rightOrderInfo.orderStatus, + leftOrderInfo.orderTakerAssetFilledAmount, + rightOrderInfo.orderTakerAssetFilledAmount + ); + + // Validate fill contexts + assertValidFill( + leftOrder, + leftOrderInfo.orderStatus, + leftOrderInfo.orderHash, + takerAddress, + leftOrderInfo.orderTakerAssetFilledAmount, + matchedFillResults.left.takerAssetFilledAmount, + leftSignature + ); + assertValidFill( + rightOrder, + rightOrderInfo.orderStatus, + rightOrderInfo.orderHash, + takerAddress, + rightOrderInfo.orderTakerAssetFilledAmount, + matchedFillResults.right.takerAssetFilledAmount, + rightSignature + ); + + // Settle matched orders. Succeeds or throws. + settleMatchedOrders( + leftOrder, + rightOrder, + takerAddress, + matchedFillResults + ); + + // Update exchange state + updateFilledState( + leftOrder, + takerAddress, + leftOrderInfo.orderHash, + leftOrderInfo.orderTakerAssetFilledAmount, + matchedFillResults.left + ); + updateFilledState( + rightOrder, + takerAddress, + rightOrderInfo.orderHash, + rightOrderInfo.orderTakerAssetFilledAmount, + matchedFillResults.right + ); + + return matchedFillResults; + } + + /// @dev Validates context for matchOrders. Succeeds or throws. + /// @param leftOrder First order to match. + /// @param rightOrder Second order to match. + function assertValidMatch( + Order memory leftOrder, + Order memory rightOrder + ) + internal + { + // The leftOrder maker asset must be the same as the rightOrder taker asset. + // TODO: Can we safely assume equality and expect a later failure otherwise? + require( + areBytesEqual(leftOrder.makerAssetData, rightOrder.takerAssetData), + ASSET_MISMATCH_MAKER_TAKER + ); + + // The leftOrder taker asset must be the same as the rightOrder maker asset. + // TODO: Can we safely assume equality and expect a later failure otherwise? + require( + areBytesEqual(leftOrder.takerAssetData, rightOrder.makerAssetData), + ASSET_MISMATCH_TAKER_MAKER + ); + + // Make sure there is a profitable spread. + // There is a profitable spread iff the cost per unit bought (OrderA.MakerAmount/OrderA.TakerAmount) for each order is greater + // than the profit per unit sold of the matched order (OrderB.TakerAmount/OrderB.MakerAmount). + // This is satisfied by the equations below: + // <leftOrder.makerAssetAmount> / <leftOrder.takerAssetAmount> >= <rightOrder.takerAssetAmount> / <rightOrder.makerAssetAmount> + // AND + // <rightOrder.makerAssetAmount> / <rightOrder.takerAssetAmount> >= <leftOrder.takerAssetAmount> / <leftOrder.makerAssetAmount> + // These equations can be combined to get the following: + require( + safeMul(leftOrder.makerAssetAmount, rightOrder.makerAssetAmount) >= + safeMul(leftOrder.takerAssetAmount, rightOrder.takerAssetAmount), + NEGATIVE_SPREAD + ); + } + + /// @dev Validates matched fill results. Succeeds or throws. + /// @param matchedFillResults Amounts to fill and fees to pay by maker and taker of matched orders. + function assertValidMatchResults(MatchedFillResults memory matchedFillResults) + internal + { + // If the amount transferred from the left order is different than what is transferred, it is a rounding error amount. + // Ensure this difference is negligible by dividing the values with each other. The result should equal to ~1. + uint256 amountSpentByLeft = safeAdd( + matchedFillResults.right.takerAssetFilledAmount, + matchedFillResults.takerFillAmount + ); + require( + !isRoundingError( + matchedFillResults.left.makerAssetFilledAmount, + amountSpentByLeft, + 1 + ), + ROUNDING_ERROR_TRANSFER_AMOUNTS + ); + + // If the amount transferred from the right order is different than what is transferred, it is a rounding error amount. + // Ensure this difference is negligible by dividing the values with each other. The result should equal to ~1. + require( + !isRoundingError( + matchedFillResults.right.makerAssetFilledAmount, + matchedFillResults.left.takerAssetFilledAmount, + 1 + ), + ROUNDING_ERROR_TRANSFER_AMOUNTS + ); + } + + /// @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 leftOrderStatus Order status of left order. + /// @param rightOrderStatus Order status of right order. + /// @param leftOrderFilledAmount Amount of left order already filled. + /// @param rightOrderFilledAmount Amount of right order already filled. + /// @param matchedFillResults Amounts to fill and fees to pay by maker and taker of matched orders. + function calculateMatchedFillResults( + Order memory leftOrder, + Order memory rightOrder, + uint8 leftOrderStatus, + uint8 rightOrderStatus, + uint256 leftOrderFilledAmount, + uint256 rightOrderFilledAmount + ) + internal + returns (MatchedFillResults memory matchedFillResults) + { + // We settle orders at the exchange rate of the right order. + // The amount saved by the left maker goes to the taker. + // Either the left or right order will be fully filled; possibly both. + // The left order is fully filled iff the right order can sell more than left can buy. + // That is: the amount required to fill the left order is less than or equal to + // the amount we can spend from the right order: + // <leftTakerAssetAmountRemaining> <= <rightTakerAssetAmountRemaining> * <rightMakerToTakerRatio> + // <leftTakerAssetAmountRemaining> <= <rightTakerAssetAmountRemaining> * <rightOrder.makerAssetAmount> / <rightOrder.takerAssetAmount> + // <leftTakerAssetAmountRemaining> * <rightOrder.takerAssetAmount> <= <rightTakerAssetAmountRemaining> * <rightOrder.makerAssetAmount> + uint256 rightTakerAssetAmountRemaining = safeSub(rightOrder.takerAssetAmount, rightOrderFilledAmount); + uint256 leftTakerAssetAmountRemaining = safeSub(leftOrder.takerAssetAmount, leftOrderFilledAmount); + uint256 leftOrderAmountToFill; + uint256 rightOrderAmountToFill; + if ( + safeMul(leftTakerAssetAmountRemaining, rightOrder.takerAssetAmount) <= + safeMul(rightTakerAssetAmountRemaining, rightOrder.makerAssetAmount) + ) { + // Left order will be fully filled: maximally fill left + leftOrderAmountToFill = leftTakerAssetAmountRemaining; + + // The right order receives an amount proportional to how much was spent. + // TODO: Can we ensure rounding error is in the correct direction? + rightOrderAmountToFill = safeGetPartialAmount( + rightOrder.takerAssetAmount, + rightOrder.makerAssetAmount, + leftOrderAmountToFill + ); + } else { + // Right order will be fully filled: maximally fill right + rightOrderAmountToFill = rightTakerAssetAmountRemaining; + + // The left order receives an amount proportional to how much was spent. + // TODO: Can we ensure rounding error is in the correct direction? + leftOrderAmountToFill = safeGetPartialAmount( + rightOrder.makerAssetAmount, + rightOrder.takerAssetAmount, + rightOrderAmountToFill + ); + } + + // Calculate fill results for left order + uint8 status; + (status, matchedFillResults.left) = calculateFillResults( + leftOrder, + leftOrderStatus, + leftOrderFilledAmount, + leftOrderAmountToFill + ); + require( + status == uint8(Status.SUCCESS), + FAILED_TO_CALCULATE_FILL_RESULTS_FOR_LEFT_ORDER + ); + + // Calculate fill results for right order + (status, matchedFillResults.right) = calculateFillResults( + rightOrder, + rightOrderStatus, + rightOrderFilledAmount, + rightOrderAmountToFill + ); + require( + status == uint8(Status.SUCCESS), + FAILED_TO_CALCULATE_FILL_RESULTS_FOR_RIGHT_ORDER + ); + + // Calculate amount given to taker + matchedFillResults.takerFillAmount = safeSub( + matchedFillResults.left.makerAssetFilledAmount, + matchedFillResults.right.takerAssetFilledAmount + ); + + // Validate the fill results + assertValidMatchResults(matchedFillResults); + + // Return fill results + return matchedFillResults; + } +} diff --git a/packages/contracts/src/contracts/current/protocol/Exchange/MixinSettlement.sol b/packages/contracts/src/contracts/current/protocol/Exchange/MixinSettlement.sol index 30f1bb49b..7c03bde75 100644 --- a/packages/contracts/src/contracts/current/protocol/Exchange/MixinSettlement.sol +++ b/packages/contracts/src/contracts/current/protocol/Exchange/MixinSettlement.sol @@ -16,15 +16,22 @@ */ -pragma solidity ^0.4.23; +pragma solidity ^0.4.24; +pragma experimental ABIEncoderV2; import "./mixins/MSettlement.sol"; import "./mixins/MAssetProxyDispatcher.sol"; import "./libs/LibOrder.sol"; import "./libs/LibMath.sol"; +import "./libs/LibExchangeErrors.sol"; +import "./libs/LibFillResults.sol"; +import "./mixins/MMatchOrders.sol"; contract MixinSettlement is LibMath, + LibFillResults, + LibExchangeErrors, + MMatchOrders, MSettlement, MAssetProxyDispatcher { @@ -54,46 +61,111 @@ contract MixinSettlement is /// @dev Settles an order by transferring assets between counterparties. /// @param order Order struct containing order specifications. /// @param takerAddress Address selling takerAsset and buying makerAsset. - /// @param takerAssetFilledAmount The amount of takerAsset that will be transferred to the order's maker. - /// @return Amount filled by maker and fees paid by maker/taker. + /// @param fillResults Amounts to be filled and fees paid by maker and taker. function settleOrder( LibOrder.Order memory order, address takerAddress, - uint256 takerAssetFilledAmount) + FillResults memory fillResults + ) internal - returns ( - uint256 makerAssetFilledAmount, - uint256 makerFeePaid, - uint256 takerFeePaid - ) { - makerAssetFilledAmount = getPartialAmount(takerAssetFilledAmount, order.takerAssetAmount, order.makerAssetAmount); dispatchTransferFrom( order.makerAssetData, order.makerAddress, takerAddress, - makerAssetFilledAmount + fillResults.makerAssetFilledAmount ); dispatchTransferFrom( order.takerAssetData, takerAddress, order.makerAddress, - takerAssetFilledAmount + fillResults.takerAssetFilledAmount ); - makerFeePaid = getPartialAmount(takerAssetFilledAmount, order.takerAssetAmount, order.makerFee); dispatchTransferFrom( ZRX_PROXY_DATA, order.makerAddress, order.feeRecipientAddress, - makerFeePaid + fillResults.makerFeePaid ); - takerFeePaid = getPartialAmount(takerAssetFilledAmount, order.takerAssetAmount, order.takerFee); dispatchTransferFrom( ZRX_PROXY_DATA, takerAddress, order.feeRecipientAddress, - takerFeePaid + fillResults.takerFeePaid ); - return (makerAssetFilledAmount, makerFeePaid, takerFeePaid); + } + + /// @dev Settles matched order by transferring appropriate funds between order makers, taker, and fee recipient. + /// @param leftOrder First matched order. + /// @param rightOrder Second matched order. + /// @param takerAddress Address that matched the orders. The taker receives the spread between orders as profit. + /// @param matchedFillResults Struct holding amounts to transfer between makers, taker, and fee recipients. + function settleMatchedOrders( + LibOrder.Order memory leftOrder, + LibOrder.Order memory rightOrder, + address takerAddress, + MatchedFillResults memory matchedFillResults + ) + internal + { + // Order makers and taker + dispatchTransferFrom( + leftOrder.makerAssetData, + leftOrder.makerAddress, + rightOrder.makerAddress, + matchedFillResults.right.takerAssetFilledAmount + ); + dispatchTransferFrom( + rightOrder.makerAssetData, + rightOrder.makerAddress, + leftOrder.makerAddress, + matchedFillResults.left.takerAssetFilledAmount + ); + dispatchTransferFrom( + leftOrder.makerAssetData, + leftOrder.makerAddress, + takerAddress, + matchedFillResults.takerFillAmount + ); + + // Maker fees + dispatchTransferFrom( + ZRX_PROXY_DATA, + leftOrder.makerAddress, + leftOrder.feeRecipientAddress, + matchedFillResults.left.makerFeePaid + ); + dispatchTransferFrom( + ZRX_PROXY_DATA, + rightOrder.makerAddress, + rightOrder.feeRecipientAddress, + matchedFillResults.right.makerFeePaid + ); + + // Taker fees + if (leftOrder.feeRecipientAddress == rightOrder.feeRecipientAddress) { + dispatchTransferFrom( + ZRX_PROXY_DATA, + takerAddress, + leftOrder.feeRecipientAddress, + safeAdd( + matchedFillResults.left.takerFeePaid, + matchedFillResults.right.takerFeePaid + ) + ); + } else { + dispatchTransferFrom( + ZRX_PROXY_DATA, + takerAddress, + leftOrder.feeRecipientAddress, + matchedFillResults.left.takerFeePaid + ); + dispatchTransferFrom( + ZRX_PROXY_DATA, + takerAddress, + rightOrder.feeRecipientAddress, + matchedFillResults.right.takerFeePaid + ); + } } } diff --git a/packages/contracts/src/contracts/current/protocol/Exchange/MixinSignatureValidator.sol b/packages/contracts/src/contracts/current/protocol/Exchange/MixinSignatureValidator.sol index 2322625d9..f7fcd36b6 100644 --- a/packages/contracts/src/contracts/current/protocol/Exchange/MixinSignatureValidator.sol +++ b/packages/contracts/src/contracts/current/protocol/Exchange/MixinSignatureValidator.sol @@ -16,7 +16,7 @@ */ -pragma solidity ^0.4.23; +pragma solidity ^0.4.24; import "./mixins/MSignatureValidator.sol"; import "./interfaces/ISigner.sol"; diff --git a/packages/contracts/src/contracts/current/protocol/Exchange/MixinTransactions.sol b/packages/contracts/src/contracts/current/protocol/Exchange/MixinTransactions.sol index 7f12834a3..f93a80705 100644 --- a/packages/contracts/src/contracts/current/protocol/Exchange/MixinTransactions.sol +++ b/packages/contracts/src/contracts/current/protocol/Exchange/MixinTransactions.sol @@ -15,7 +15,7 @@ limitations under the License. */ -pragma solidity ^0.4.21; +pragma solidity ^0.4.24; import "./mixins/MSignatureValidator.sol"; import "./mixins/MTransactions.sol"; diff --git a/packages/contracts/src/contracts/current/protocol/Exchange/MixinWrapperFunctions.sol b/packages/contracts/src/contracts/current/protocol/Exchange/MixinWrapperFunctions.sol index 42517221e..15f1a2e0b 100644 --- a/packages/contracts/src/contracts/current/protocol/Exchange/MixinWrapperFunctions.sol +++ b/packages/contracts/src/contracts/current/protocol/Exchange/MixinWrapperFunctions.sol @@ -16,7 +16,7 @@ */ -pragma solidity ^0.4.23; +pragma solidity ^0.4.24; pragma experimental ABIEncoderV2; import "../../utils/LibBytes/LibBytes.sol"; @@ -27,10 +27,11 @@ import "./libs/LibFillResults.sol"; import "./libs/LibExchangeErrors.sol"; contract MixinWrapperFunctions is + SafeMath, + LibBytes, + LibMath, LibOrder, LibFillResults, - LibMath, - LibBytes, LibExchangeErrors, MExchangeCore { diff --git a/packages/contracts/src/contracts/current/protocol/Exchange/interfaces/IAssetProxyDispatcher.sol b/packages/contracts/src/contracts/current/protocol/Exchange/interfaces/IAssetProxyDispatcher.sol index 4e4ed6be8..3ce5ef157 100644 --- a/packages/contracts/src/contracts/current/protocol/Exchange/interfaces/IAssetProxyDispatcher.sol +++ b/packages/contracts/src/contracts/current/protocol/Exchange/interfaces/IAssetProxyDispatcher.sol @@ -16,7 +16,7 @@ */ -pragma solidity ^0.4.23; +pragma solidity ^0.4.24; contract IAssetProxyDispatcher { diff --git a/packages/contracts/src/contracts/current/protocol/Exchange/interfaces/IExchange.sol b/packages/contracts/src/contracts/current/protocol/Exchange/interfaces/IExchange.sol index 9fba3491b..fc428e9c0 100644 --- a/packages/contracts/src/contracts/current/protocol/Exchange/interfaces/IExchange.sol +++ b/packages/contracts/src/contracts/current/protocol/Exchange/interfaces/IExchange.sol @@ -16,19 +16,23 @@ */ -pragma solidity ^0.4.23; +pragma solidity ^0.4.24; pragma experimental ABIEncoderV2; import "./IExchangeCore.sol"; -import "./ISignatureValidator.sol"; -import "./IAssetProxyDispatcher.sol"; -import "./ITransactions.sol"; -import "./IWrapperFunctions.sol"; +import "./IMatchOrders"; +import "./ISettlement"; +import "./ISignatureValidator"; +import "./ITransactions"; +import "./IAssetProxyDispatcher"; +import "./IWrapperFunctions"; contract IExchange is - IWrapperFunctions, IExchangeCore, + IMatchOrders, + ISettlement, ISignatureValidator, ITransactions, - IAssetProxyDispatcher + IAssetProxyDispatcher, + IWrapperFunctions {} diff --git a/packages/contracts/src/contracts/current/protocol/Exchange/interfaces/IExchangeCore.sol b/packages/contracts/src/contracts/current/protocol/Exchange/interfaces/IExchangeCore.sol index 0f19525ca..fc0157d75 100644 --- a/packages/contracts/src/contracts/current/protocol/Exchange/interfaces/IExchangeCore.sol +++ b/packages/contracts/src/contracts/current/protocol/Exchange/interfaces/IExchangeCore.sol @@ -16,7 +16,7 @@ */ -pragma solidity ^0.4.23; +pragma solidity ^0.4.24; pragma experimental ABIEncoderV2; import "../libs/LibOrder.sol"; @@ -48,4 +48,33 @@ contract IExchangeCore { function cancelOrder(LibOrder.Order memory order) public returns (bool); + + /// @dev Gets information about an order: status, hash, and amount filled. + /// @param order Order to gather information on. + /// @return OrderInfo Information about the order and its state. + /// See LibOrder.OrderInfo for a complete description. + function getOrderInfo(LibOrder.Order memory order) + public + view + returns (LibOrder.OrderInfo memory orderInfo); + + /// @dev Calculates amounts filled and fees paid by maker and taker. + /// @param order to be filled. + /// @param orderStatus Status of order to be filled. + /// @param orderTakerAssetFilledAmount Amount of order already filled. + /// @param takerAssetFillAmount Desired amount of order to fill by taker. + /// @return status Return status of calculating fill amounts. Returns Status.SUCCESS on success. + /// @return fillResults Amounts filled and fees paid by maker and taker. + function calculateFillResults( + LibOrder.Order memory order, + uint8 orderStatus, + uint256 orderTakerAssetFilledAmount, + uint256 takerAssetFillAmount + ) + public + pure + returns ( + uint8 status, + LibFillResults.FillResults memory fillResults + ); } diff --git a/packages/contracts/src/contracts/current/protocol/Exchange/interfaces/IMatchOrders.sol b/packages/contracts/src/contracts/current/protocol/Exchange/interfaces/IMatchOrders.sol new file mode 100644 index 000000000..df009d063 --- /dev/null +++ b/packages/contracts/src/contracts/current/protocol/Exchange/interfaces/IMatchOrders.sol @@ -0,0 +1,44 @@ +/* + + Copyright 2018 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ +pragma solidity ^0.4.24; +pragma experimental ABIEncoderV2; + +import "../libs/LibOrder.sol"; +import "../libs/LibFillResults.sol"; + +contract IMatchOrders { + + /// @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. + /// TODO: Make this function external once supported by Solidity (See Solidity Issues #3199, #1603) + function matchOrders( + LibOrder.Order memory leftOrder, + LibOrder.Order memory rightOrder, + bytes memory leftSignature, + bytes memory rightSignature + ) + public + returns (LibFillResults.MatchedFillResults memory matchedFillResults); +} diff --git a/packages/contracts/src/contracts/current/protocol/Exchange/interfaces/ISignatureValidator.sol b/packages/contracts/src/contracts/current/protocol/Exchange/interfaces/ISignatureValidator.sol index b4a238472..65ff45f7b 100644 --- a/packages/contracts/src/contracts/current/protocol/Exchange/interfaces/ISignatureValidator.sol +++ b/packages/contracts/src/contracts/current/protocol/Exchange/interfaces/ISignatureValidator.sol @@ -16,7 +16,7 @@ */ -pragma solidity ^0.4.23; +pragma solidity ^0.4.24; contract ISignatureValidator { diff --git a/packages/contracts/src/contracts/current/protocol/Exchange/interfaces/ISigner.sol b/packages/contracts/src/contracts/current/protocol/Exchange/interfaces/ISigner.sol index e065cfd8a..53c41d331 100644 --- a/packages/contracts/src/contracts/current/protocol/Exchange/interfaces/ISigner.sol +++ b/packages/contracts/src/contracts/current/protocol/Exchange/interfaces/ISigner.sol @@ -16,7 +16,7 @@ */ -pragma solidity ^0.4.23; +pragma solidity ^0.4.24; contract ISigner { diff --git a/packages/contracts/src/contracts/current/protocol/Exchange/interfaces/ITransactions.sol b/packages/contracts/src/contracts/current/protocol/Exchange/interfaces/ITransactions.sol index 76e4cf2fe..d973bf001 100644 --- a/packages/contracts/src/contracts/current/protocol/Exchange/interfaces/ITransactions.sol +++ b/packages/contracts/src/contracts/current/protocol/Exchange/interfaces/ITransactions.sol @@ -15,7 +15,7 @@ limitations under the License. */ -pragma solidity ^0.4.23; +pragma solidity ^0.4.24; contract ITransactions { diff --git a/packages/contracts/src/contracts/current/protocol/Exchange/interfaces/IWrapperFunctions.sol b/packages/contracts/src/contracts/current/protocol/Exchange/interfaces/IWrapperFunctions.sol index d23170b1c..1eb1233ed 100644 --- a/packages/contracts/src/contracts/current/protocol/Exchange/interfaces/IWrapperFunctions.sol +++ b/packages/contracts/src/contracts/current/protocol/Exchange/interfaces/IWrapperFunctions.sol @@ -16,17 +16,17 @@ */ -pragma solidity ^0.4.23; +pragma solidity ^0.4.24; pragma experimental ABIEncoderV2; import "./libs/LibOrder.sol"; import "./libs/LibFillResults.sol"; contract IWrapperFunctions is + LibBytes, + LibMath, LibOrder, LibFillResults, - LibMath, - LibBytes, LibExchangeErrors, MExchangeCore { diff --git a/packages/contracts/src/contracts/current/protocol/Exchange/libs/LibExchangeErrors.sol b/packages/contracts/src/contracts/current/protocol/Exchange/libs/LibExchangeErrors.sol index 6ffbdfdca..4712ee36c 100644 --- a/packages/contracts/src/contracts/current/protocol/Exchange/libs/LibExchangeErrors.sol +++ b/packages/contracts/src/contracts/current/protocol/Exchange/libs/LibExchangeErrors.sol @@ -16,21 +16,10 @@ */ -pragma solidity ^0.4.23; +pragma solidity ^0.4.24; contract LibExchangeErrors { - // Error Codes - enum Errors { - ORDER_EXPIRED, // Order has already expired - ORDER_FULLY_FILLED, // Order has already been fully filled - ORDER_CANCELLED, // Order has already been cancelled - ROUNDING_ERROR_TOO_LARGE, // Rounding error too large - INSUFFICIENT_BALANCE_OR_ALLOWANCE // Insufficient balance or allowance for token transfer - } - - event ExchangeError(uint8 indexed errorId, bytes32 indexed orderHash); - // Core revert reasons string constant GT_ZERO_AMOUNT_REQUIRED = "Amount must be greater than 0."; string constant SIGNATURE_VALIDATION_FAILED = "Signature validation failed."; @@ -38,6 +27,10 @@ contract LibExchangeErrors { string constant INVALID_CONTEXT = "Function called in an invalid context."; string constant INVALID_NEW_MAKER_EPOCH = "Specified salt must be greater than or equal to existing makerEpoch."; + // Order revert reasons + string constant INVALID_ORDER_TAKER_ASSET_AMOUNT = "Invalid order taker asset amount: expected a non-zero value."; + string constant INVALID_ORDER_MAKER_ASSET_AMOUNT = "Invalid order maker asset amount: expected a non-zero value."; + // Transaction revert reasons string constant DUPLICATE_TRANSACTION_HASH = "Transaction has already been executed."; string constant TRANSACTION_EXECUTION_FAILED = "Transaction execution failed."; @@ -55,4 +48,13 @@ contract LibExchangeErrors { string constant INVALID_SIGNATURE_LENGTH = "Invalid signature length."; string constant ILLEGAL_SIGNATURE_TYPE = "Illegal signature type."; string constant UNSUPPORTED_SIGNATURE_TYPE = "Unsupported signature type."; + + // Order matching revert reasons + string constant ASSET_MISMATCH_MAKER_TAKER = "Left order maker asset is different from right order taker asset."; + string constant ASSET_MISMATCH_TAKER_MAKER = "Left order taker asset is different from right order maker asset."; + string constant NEGATIVE_SPREAD = "Matched orders must have a positive spread."; + string constant MISCALCULATED_TRANSFER_AMOUNTS = "A miscalculation occurred: the left maker would receive more than the right maker would spend."; + string constant ROUNDING_ERROR_TRANSFER_AMOUNTS = "A rounding error occurred when calculating transfer amounts for matched orders."; + string constant FAILED_TO_CALCULATE_FILL_RESULTS_FOR_LEFT_ORDER = "Failed to calculate fill results for left order."; + string constant FAILED_TO_CALCULATE_FILL_RESULTS_FOR_RIGHT_ORDER = "Failed to calculate fill results for right order."; } diff --git a/packages/contracts/src/contracts/current/protocol/Exchange/libs/LibFillResults.sol b/packages/contracts/src/contracts/current/protocol/Exchange/libs/LibFillResults.sol index 3b8f2acf9..aa54598fa 100644 --- a/packages/contracts/src/contracts/current/protocol/Exchange/libs/LibFillResults.sol +++ b/packages/contracts/src/contracts/current/protocol/Exchange/libs/LibFillResults.sol @@ -16,7 +16,7 @@ */ -pragma solidity ^0.4.23; +pragma solidity ^0.4.24; import "../../../utils/SafeMath/SafeMath.sol"; @@ -31,6 +31,12 @@ contract LibFillResults is uint256 takerFeePaid; } + struct MatchedFillResults { + LibFillResults.FillResults left; + LibFillResults.FillResults right; + uint256 takerFillAmount; + } + /// @dev Adds properties of both FillResults instances. /// Modifies the first FillResults instance specified. /// @param totalFillResults Fill results instance that will be added onto. @@ -44,4 +50,19 @@ contract LibFillResults is totalFillResults.makerFeePaid = safeAdd(totalFillResults.makerFeePaid, singleFillResults.makerFeePaid); totalFillResults.takerFeePaid = safeAdd(totalFillResults.takerFeePaid, singleFillResults.takerFeePaid); } -}
\ No newline at end of file + + /// @dev Returns a null fill results struct + function getNullFillResults() + internal + pure + returns (FillResults memory) + { + // returns zeroed out FillResults instance + return FillResults({ + makerAssetFilledAmount: 0, + takerAssetFilledAmount: 0, + makerFeePaid: 0, + takerFeePaid: 0 + }); + } +} diff --git a/packages/contracts/src/contracts/current/protocol/Exchange/libs/LibMath.sol b/packages/contracts/src/contracts/current/protocol/Exchange/libs/LibMath.sol index b48602d19..ea8c138d6 100644 --- a/packages/contracts/src/contracts/current/protocol/Exchange/libs/LibMath.sol +++ b/packages/contracts/src/contracts/current/protocol/Exchange/libs/LibMath.sol @@ -16,13 +16,14 @@ */ -pragma solidity ^0.4.23; +pragma solidity ^0.4.24; import "../../../utils/SafeMath/SafeMath.sol"; contract LibMath is SafeMath { + string constant ROUNDING_ERROR_ON_PARTIAL_AMOUNT = "A rounding error occurred when calculating partial transfer amounts."; /// @dev Calculates partial value given a numerator and denominator. /// @param numerator Numerator. @@ -44,6 +45,26 @@ contract LibMath is return partialAmount; } + /// @dev Calculates partial value given a numerator and denominator. + /// Throws if there is a rounding error. + /// @param numerator Numerator. + /// @param denominator Denominator. + /// @param target Value to calculate partial of. + /// @return Partial value of target. + function safeGetPartialAmount( + uint256 numerator, + uint256 denominator, + uint256 target) + internal pure + returns (uint256 partialAmount) + { + require( + !isRoundingError(numerator, denominator, target), + ROUNDING_ERROR_ON_PARTIAL_AMOUNT + ); + return getPartialAmount(numerator, denominator, target); + } + /// @dev Checks if rounding error > 0.1%. /// @param numerator Numerator. /// @param denominator Denominator. diff --git a/packages/contracts/src/contracts/current/protocol/Exchange/libs/LibOrder.sol b/packages/contracts/src/contracts/current/protocol/Exchange/libs/LibOrder.sol index 0fe8c8c96..7d8328c67 100644 --- a/packages/contracts/src/contracts/current/protocol/Exchange/libs/LibOrder.sol +++ b/packages/contracts/src/contracts/current/protocol/Exchange/libs/LibOrder.sol @@ -16,7 +16,7 @@ */ -pragma solidity ^0.4.23; +pragma solidity ^0.4.24; contract LibOrder { @@ -51,6 +51,15 @@ contract LibOrder { bytes takerAssetData; } + struct OrderInfo { + // See LibStatus for a complete description of order statuses + uint8 orderStatus; + // Keccak-256 EIP712 hash of the order + bytes32 orderHash; + // Amount of order that has been filled + uint256 orderTakerAssetFilledAmount; + } + /// @dev Calculates Keccak-256 hash of the order. /// @param order The order structure. /// @return Keccak-256 EIP712 hash of the order. diff --git a/packages/contracts/src/contracts/current/protocol/Exchange/libs/LibStatus.sol b/packages/contracts/src/contracts/current/protocol/Exchange/libs/LibStatus.sol new file mode 100644 index 000000000..f72b7d65f --- /dev/null +++ b/packages/contracts/src/contracts/current/protocol/Exchange/libs/LibStatus.sol @@ -0,0 +1,51 @@ +/* + + Copyright 2018 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.4.24; +pragma experimental ABIEncoderV2; + +contract LibStatus { + + // Exchange Status Codes + enum Status { + /// Default Status /// + INVALID, // General invalid status + + /// General Exchange Statuses /// + SUCCESS, // Indicates a successful operation + ROUNDING_ERROR_TOO_LARGE, // Rounding error too large + INSUFFICIENT_BALANCE_OR_ALLOWANCE, // Insufficient balance or allowance for token transfer + TAKER_ASSET_FILL_AMOUNT_TOO_LOW, // takerAssetFillAmount is <= 0 + INVALID_SIGNATURE, // Invalid signature + INVALID_SENDER, // Invalid sender + INVALID_TAKER, // Invalid taker + INVALID_MAKER, // Invalid maker + + /// Order State Statuses /// + // A valid order remains fillable until it is expired, fully filled, or cancelled. + // An order's state is unaffected by external factors, like account balances. + ORDER_INVALID_MAKER_ASSET_AMOUNT, // Order does not have a valid maker asset amount + ORDER_INVALID_TAKER_ASSET_AMOUNT, // Order does not have a valid taker asset amount + ORDER_FILLABLE, // Order is fillable + ORDER_EXPIRED, // Order has already expired + ORDER_FULLY_FILLED, // Order is fully filled + ORDER_CANCELLED // Order has been cancelled + } + + event ExchangeStatus(uint8 indexed statusId, bytes32 indexed orderHash); +} diff --git a/packages/contracts/src/contracts/current/protocol/Exchange/mixins/MAssetProxyDispatcher.sol b/packages/contracts/src/contracts/current/protocol/Exchange/mixins/MAssetProxyDispatcher.sol index 77ee77797..ccc960d6e 100644 --- a/packages/contracts/src/contracts/current/protocol/Exchange/mixins/MAssetProxyDispatcher.sol +++ b/packages/contracts/src/contracts/current/protocol/Exchange/mixins/MAssetProxyDispatcher.sol @@ -16,7 +16,8 @@ */ -pragma solidity ^0.4.23; +pragma solidity ^0.4.24; +pragma experimental ABIEncoderV2; import "../interfaces/IAssetProxyDispatcher.sol"; diff --git a/packages/contracts/src/contracts/current/protocol/Exchange/mixins/MExchangeCore.sol b/packages/contracts/src/contracts/current/protocol/Exchange/mixins/MExchangeCore.sol index 5b78e70da..ae1e50637 100644 --- a/packages/contracts/src/contracts/current/protocol/Exchange/mixins/MExchangeCore.sol +++ b/packages/contracts/src/contracts/current/protocol/Exchange/mixins/MExchangeCore.sol @@ -16,7 +16,8 @@ */ -pragma solidity ^0.4.23; +pragma solidity ^0.4.24; +pragma experimental ABIEncoderV2; import "../libs/LibOrder.sol"; import "../libs/LibFillResults.sol"; @@ -55,12 +56,62 @@ contract MExchangeCore is uint256 makerEpoch ); - /// @dev Logs a Fill event with the given arguments. - /// The sole purpose of this function is to get around the stack variable limit. - function emitFillEvent( + /// @dev Validates context for fillOrder. Succeeds or throws. + /// @param order to be filled. + /// @param orderStatus Status of order to be filled. + /// @param orderHash Hash of order to be filled. + /// @param takerAddress Address of order taker. + /// @param orderTakerAssetFilledAmount Amount of order already filled. + /// @param takerAssetFillAmount Desired amount of order to fill by taker. + /// @param signature Proof that the orders was created by its maker. + function assertValidFill( + LibOrder.Order memory order, + uint8 orderStatus, + bytes32 orderHash, + address takerAddress, + uint256 orderTakerAssetFilledAmount, + uint256 takerAssetFillAmount, + bytes memory signature + ) + internal; + + /// @dev Updates state with results of a fill order. + /// @param order that was filled. + /// @param takerAddress Address of taker who filled the order. + /// @param orderTakerAssetFilledAmount Amount of order already filled. + /// @return fillResults Amounts filled and fees paid by maker and taker. + function updateFilledState( LibOrder.Order memory order, address takerAddress, bytes32 orderHash, - LibFillResults.FillResults memory fillResults) + uint256 orderTakerAssetFilledAmount, + LibFillResults.FillResults memory fillResults + ) internal; + + /// @dev Validates context for cancelOrder. Succeeds or throws. + /// @param order that was cancelled. + /// @param orderStatus Status of order that was cancelled. + /// @param orderHash Hash of order that was cancelled. + function assertValidCancel( + LibOrder.Order memory order, + uint8 orderStatus, + bytes32 orderHash + ) + internal; + + /// @dev Updates state with results of cancelling an order. + /// State is only updated if the order is currently fillable. + /// Otherwise, updating state would have no effect. + /// @param order that was cancelled. + /// @param orderStatus Status of order that was cancelled. + /// @param orderHash Hash of order that was cancelled. + /// @return stateUpdated Returns true only if state was updated. + function updateCancelledState( + LibOrder.Order memory order, + uint8 orderStatus, + bytes32 orderHash + ) + internal + returns (bool stateUpdated); } diff --git a/packages/contracts/src/contracts/current/protocol/Exchange/mixins/MMatchOrders.sol b/packages/contracts/src/contracts/current/protocol/Exchange/mixins/MMatchOrders.sol new file mode 100644 index 000000000..7dd608cf2 --- /dev/null +++ b/packages/contracts/src/contracts/current/protocol/Exchange/mixins/MMatchOrders.sol @@ -0,0 +1,65 @@ +/* + + Copyright 2018 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ +pragma solidity ^0.4.24; +pragma experimental ABIEncoderV2; + +import "../libs/LibOrder.sol"; +import "../libs/LibFillResults.sol"; +import "./MExchangeCore.sol"; +import "../interfaces/IMatchOrders.sol"; + +contract MMatchOrders is + IMatchOrders +{ + + /// @dev Validates context for matchOrders. Succeeds or throws. + /// @param leftOrder First order to match. + /// @param rightOrder Second order to match. + function assertValidMatch( + LibOrder.Order memory leftOrder, + LibOrder.Order memory rightOrder + ) + internal; + + /// @dev Validates matched fill results. Succeeds or throws. + /// @param matchedFillResults Amounts to fill and fees to pay by maker and taker of matched orders. + function assertValidMatchResults(LibFillResults.MatchedFillResults memory matchedFillResults) + internal; + + /// @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 leftOrderStatus Order status of left order. + /// @param rightOrderStatus Order status of right order. + /// @param leftOrderFilledAmount Amount of left order already filled. + /// @param rightOrderFilledAmount 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, + uint8 leftOrderStatus, + uint8 rightOrderStatus, + uint256 leftOrderFilledAmount, + uint256 rightOrderFilledAmount + ) + internal + returns (LibFillResults.MatchedFillResults memory matchedFillResults); +} diff --git a/packages/contracts/src/contracts/current/protocol/Exchange/mixins/MSettlement.sol b/packages/contracts/src/contracts/current/protocol/Exchange/mixins/MSettlement.sol index 5e2edbf99..50b62e79f 100644 --- a/packages/contracts/src/contracts/current/protocol/Exchange/mixins/MSettlement.sol +++ b/packages/contracts/src/contracts/current/protocol/Exchange/mixins/MSettlement.sol @@ -16,26 +16,35 @@ */ -pragma solidity ^0.4.23; +pragma solidity ^0.4.24; import "../libs/LibOrder.sol"; +import "./MMatchOrders.sol"; +import "../libs/LibFillResults.sol"; contract MSettlement { - /// @dev Settles an order by transfering assets between counterparties. + /// @dev Settles an order by transferring assets between counterparties. /// @param order Order struct containing order specifications. /// @param takerAddress Address selling takerAsset and buying makerAsset. - /// @param takerAssetFilledAmount The amount of takerAsset that will be transfered to the order's maker. - /// @return Amount filled by maker and fees paid by maker/taker. + /// @param fillResults Amounts to be filled and fees paid by maker and taker. function settleOrder( LibOrder.Order memory order, address takerAddress, - uint256 takerAssetFilledAmount) - internal - returns ( - uint256 makerAssetFilledAmount, - uint256 makerFeePaid, - uint256 takerFeePaid - ); + LibFillResults.FillResults memory fillResults + ) + internal; + + /// @dev Settles matched order by transferring appropriate funds between order makers, taker, and fee recipient. + /// @param leftOrder First matched order. + /// @param rightOrder Second matched order. + /// @param takerAddress Address that matched the orders. The taker receives the spread between orders as profit. + /// @param matchedFillResults Struct holding amounts to transfer between makers, taker, and fee recipients. + function settleMatchedOrders( + LibOrder.Order memory leftOrder, + LibOrder.Order memory rightOrder, + address takerAddress, + LibFillResults.MatchedFillResults memory matchedFillResults + ) + internal; } - diff --git a/packages/contracts/src/contracts/current/protocol/Exchange/mixins/MSignatureValidator.sol b/packages/contracts/src/contracts/current/protocol/Exchange/mixins/MSignatureValidator.sol index ba26c07f6..3658e7c6f 100644 --- a/packages/contracts/src/contracts/current/protocol/Exchange/mixins/MSignatureValidator.sol +++ b/packages/contracts/src/contracts/current/protocol/Exchange/mixins/MSignatureValidator.sol @@ -16,7 +16,7 @@ */ -pragma solidity ^0.4.23; +pragma solidity ^0.4.24; import "../interfaces/ISignatureValidator.sol"; diff --git a/packages/contracts/src/contracts/current/protocol/Exchange/mixins/MTransactions.sol b/packages/contracts/src/contracts/current/protocol/Exchange/mixins/MTransactions.sol index 159ed1527..e2f89de01 100644 --- a/packages/contracts/src/contracts/current/protocol/Exchange/mixins/MTransactions.sol +++ b/packages/contracts/src/contracts/current/protocol/Exchange/mixins/MTransactions.sol @@ -15,7 +15,7 @@ limitations under the License. */ -pragma solidity ^0.4.23; +pragma solidity ^0.4.24; import "../interfaces/ITransactions.sol"; diff --git a/packages/contracts/src/contracts/current/test/DummyERC20Token/DummyERC20Token.sol b/packages/contracts/src/contracts/current/test/DummyERC20Token/DummyERC20Token.sol index ab5311e0c..0c7b18c0c 100644 --- a/packages/contracts/src/contracts/current/test/DummyERC20Token/DummyERC20Token.sol +++ b/packages/contracts/src/contracts/current/test/DummyERC20Token/DummyERC20Token.sol @@ -16,7 +16,7 @@ */ -pragma solidity ^0.4.23; +pragma solidity ^0.4.24; pragma experimental ABIEncoderV2; import "../Mintable/Mintable.sol"; diff --git a/packages/contracts/src/contracts/current/test/DummyERC721Token/DummyERC721Token.sol b/packages/contracts/src/contracts/current/test/DummyERC721Token/DummyERC721Token.sol index 22ebbd3c1..369a2950d 100644 --- a/packages/contracts/src/contracts/current/test/DummyERC721Token/DummyERC721Token.sol +++ b/packages/contracts/src/contracts/current/test/DummyERC721Token/DummyERC721Token.sol @@ -16,7 +16,7 @@ */ -pragma solidity ^0.4.23; +pragma solidity ^0.4.24; pragma experimental ABIEncoderV2; import "../../tokens/ERC721Token/ERC721Token.sol"; diff --git a/packages/contracts/src/contracts/current/test/Mintable/Mintable.sol b/packages/contracts/src/contracts/current/test/Mintable/Mintable.sol index fd944f244..a91bfee9e 100644 --- a/packages/contracts/src/contracts/current/test/Mintable/Mintable.sol +++ b/packages/contracts/src/contracts/current/test/Mintable/Mintable.sol @@ -16,7 +16,7 @@ */ -pragma solidity ^0.4.23; +pragma solidity ^0.4.24; pragma experimental ABIEncoderV2; import "../../tokens/UnlimitedAllowanceToken/UnlimitedAllowanceToken.sol"; diff --git a/packages/contracts/src/contracts/current/test/TestAssetProxyDispatcher/TestAssetProxyDispatcher.sol b/packages/contracts/src/contracts/current/test/TestAssetProxyDispatcher/TestAssetProxyDispatcher.sol index 93ec3cef3..11ca0617d 100644 --- a/packages/contracts/src/contracts/current/test/TestAssetProxyDispatcher/TestAssetProxyDispatcher.sol +++ b/packages/contracts/src/contracts/current/test/TestAssetProxyDispatcher/TestAssetProxyDispatcher.sol @@ -16,7 +16,7 @@ */ -pragma solidity ^0.4.23; +pragma solidity ^0.4.24; pragma experimental ABIEncoderV2; import "../../protocol/Exchange/MixinAssetProxyDispatcher.sol"; diff --git a/packages/contracts/src/contracts/current/test/TestLibBytes/TestLibBytes.sol b/packages/contracts/src/contracts/current/test/TestLibBytes/TestLibBytes.sol index 1597ff8d5..ac4602933 100644 --- a/packages/contracts/src/contracts/current/test/TestLibBytes/TestLibBytes.sol +++ b/packages/contracts/src/contracts/current/test/TestLibBytes/TestLibBytes.sol @@ -16,7 +16,7 @@ */ -pragma solidity ^0.4.23; +pragma solidity ^0.4.24; pragma experimental ABIEncoderV2; import "../../utils/LibBytes/LibBytes.sol"; diff --git a/packages/contracts/src/contracts/current/test/TestLibs/TestLibs.sol b/packages/contracts/src/contracts/current/test/TestLibs/TestLibs.sol index 0dc7785b2..b8fc90af1 100644 --- a/packages/contracts/src/contracts/current/test/TestLibs/TestLibs.sol +++ b/packages/contracts/src/contracts/current/test/TestLibs/TestLibs.sol @@ -16,7 +16,7 @@ */ -pragma solidity ^0.4.23; +pragma solidity ^0.4.24; pragma experimental ABIEncoderV2; import "../../protocol/Exchange/libs/LibMath.sol"; @@ -77,4 +77,4 @@ contract TestLibs is addFillResults(totalFillResults, singleFillResults); return totalFillResults; } -}
\ No newline at end of file +} diff --git a/packages/contracts/src/contracts/current/test/TestSignatureValidator/TestSignatureValidator.sol b/packages/contracts/src/contracts/current/test/TestSignatureValidator/TestSignatureValidator.sol index 20202cd7b..15d9ca189 100644 --- a/packages/contracts/src/contracts/current/test/TestSignatureValidator/TestSignatureValidator.sol +++ b/packages/contracts/src/contracts/current/test/TestSignatureValidator/TestSignatureValidator.sol @@ -16,7 +16,7 @@ */ -pragma solidity ^0.4.23; +pragma solidity ^0.4.24; pragma experimental ABIEncoderV2; import "../../protocol/Exchange/MixinSignatureValidator.sol"; @@ -38,4 +38,4 @@ contract TestSignatureValidator is MixinSignatureValidator { ); return isValid; } -}
\ No newline at end of file +} diff --git a/packages/contracts/src/contracts/current/tokens/ERC20Token/ERC20Token.sol b/packages/contracts/src/contracts/current/tokens/ERC20Token/ERC20Token.sol index 6497d3c7a..f0bcdafef 100644 --- a/packages/contracts/src/contracts/current/tokens/ERC20Token/ERC20Token.sol +++ b/packages/contracts/src/contracts/current/tokens/ERC20Token/ERC20Token.sol @@ -16,7 +16,7 @@ */ -pragma solidity ^0.4.23; +pragma solidity ^0.4.24; pragma experimental ABIEncoderV2; import "./IERC20Token.sol"; diff --git a/packages/contracts/src/contracts/current/tokens/ERC20Token/IERC20Token.sol b/packages/contracts/src/contracts/current/tokens/ERC20Token/IERC20Token.sol index 537f5a83d..eb879b6a8 100644 --- a/packages/contracts/src/contracts/current/tokens/ERC20Token/IERC20Token.sol +++ b/packages/contracts/src/contracts/current/tokens/ERC20Token/IERC20Token.sol @@ -16,7 +16,7 @@ */ -pragma solidity ^0.4.23; +pragma solidity ^0.4.24; pragma experimental ABIEncoderV2; contract IERC20Token { diff --git a/packages/contracts/src/contracts/current/tokens/ERC721Token/ERC721Token.sol b/packages/contracts/src/contracts/current/tokens/ERC721Token/ERC721Token.sol index b3493bc99..41ba149e3 100644 --- a/packages/contracts/src/contracts/current/tokens/ERC721Token/ERC721Token.sol +++ b/packages/contracts/src/contracts/current/tokens/ERC721Token/ERC721Token.sol @@ -23,7 +23,7 @@ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -pragma solidity ^0.4.23; +pragma solidity ^0.4.24; import "./IERC721Token.sol"; import "./IERC721Receiver.sol"; @@ -403,4 +403,4 @@ contract ERC721Token is assembly { size := extcodesize(addr) } // solium-disable-line security/no-inline-assembly return size > 0; } -}
\ No newline at end of file +} diff --git a/packages/contracts/src/contracts/current/tokens/ERC721Token/IERC721Receiver.sol b/packages/contracts/src/contracts/current/tokens/ERC721Token/IERC721Receiver.sol index 3484bf824..b0fff3c90 100644 --- a/packages/contracts/src/contracts/current/tokens/ERC721Token/IERC721Receiver.sol +++ b/packages/contracts/src/contracts/current/tokens/ERC721Token/IERC721Receiver.sol @@ -23,7 +23,7 @@ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -pragma solidity ^0.4.23; +pragma solidity ^0.4.24; /** * @title ERC721 token receiver interface @@ -57,4 +57,4 @@ contract IERC721Receiver { bytes _data) public returns (bytes4); -}
\ No newline at end of file +} diff --git a/packages/contracts/src/contracts/current/tokens/ERC721Token/IERC721Token.sol b/packages/contracts/src/contracts/current/tokens/ERC721Token/IERC721Token.sol index 81e1b97af..345712d67 100644 --- a/packages/contracts/src/contracts/current/tokens/ERC721Token/IERC721Token.sol +++ b/packages/contracts/src/contracts/current/tokens/ERC721Token/IERC721Token.sol @@ -23,7 +23,7 @@ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -pragma solidity ^0.4.23; +pragma solidity ^0.4.24; /** * @title ERC721 Non-Fungible Token Standard basic interface @@ -102,4 +102,4 @@ contract IERC721Token { uint256 _tokenId, bytes _data) public; -}
\ No newline at end of file +} diff --git a/packages/contracts/src/contracts/current/tokens/UnlimitedAllowanceToken/UnlimitedAllowanceToken.sol b/packages/contracts/src/contracts/current/tokens/UnlimitedAllowanceToken/UnlimitedAllowanceToken.sol index 395e5d356..f62602ab3 100644 --- a/packages/contracts/src/contracts/current/tokens/UnlimitedAllowanceToken/UnlimitedAllowanceToken.sol +++ b/packages/contracts/src/contracts/current/tokens/UnlimitedAllowanceToken/UnlimitedAllowanceToken.sol @@ -16,7 +16,7 @@ */ -pragma solidity ^0.4.23; +pragma solidity ^0.4.24; pragma experimental ABIEncoderV2; import "../ERC20Token/ERC20Token.sol"; diff --git a/packages/contracts/src/contracts/current/utils/LibBytes/LibBytes.sol b/packages/contracts/src/contracts/current/utils/LibBytes/LibBytes.sol index 3c5531e35..2c5d9e756 100644 --- a/packages/contracts/src/contracts/current/utils/LibBytes/LibBytes.sol +++ b/packages/contracts/src/contracts/current/utils/LibBytes/LibBytes.sol @@ -16,7 +16,7 @@ */ -pragma solidity ^0.4.23; +pragma solidity ^0.4.24; contract LibBytes { diff --git a/packages/contracts/src/contracts/current/utils/Ownable/IOwnable.sol b/packages/contracts/src/contracts/current/utils/Ownable/IOwnable.sol index 63b04945f..e77680903 100644 --- a/packages/contracts/src/contracts/current/utils/Ownable/IOwnable.sol +++ b/packages/contracts/src/contracts/current/utils/Ownable/IOwnable.sol @@ -1,4 +1,4 @@ -pragma solidity ^0.4.23; +pragma solidity ^0.4.24; pragma experimental ABIEncoderV2; /* diff --git a/packages/contracts/src/contracts/current/utils/Ownable/Ownable.sol b/packages/contracts/src/contracts/current/utils/Ownable/Ownable.sol index 933aa168a..296c6c856 100644 --- a/packages/contracts/src/contracts/current/utils/Ownable/Ownable.sol +++ b/packages/contracts/src/contracts/current/utils/Ownable/Ownable.sol @@ -1,4 +1,4 @@ -pragma solidity ^0.4.23; +pragma solidity ^0.4.24; pragma experimental ABIEncoderV2; /* diff --git a/packages/contracts/src/contracts/current/utils/SafeMath/SafeMath.sol b/packages/contracts/src/contracts/current/utils/SafeMath/SafeMath.sol index 1ab27eebc..e137f6ca5 100644 --- a/packages/contracts/src/contracts/current/utils/SafeMath/SafeMath.sol +++ b/packages/contracts/src/contracts/current/utils/SafeMath/SafeMath.sol @@ -1,4 +1,4 @@ -pragma solidity ^0.4.23; +pragma solidity ^0.4.24; pragma experimental ABIEncoderV2; contract SafeMath { diff --git a/packages/contracts/src/utils/asset_proxy_utils.ts b/packages/contracts/src/utils/asset_proxy_utils.ts index dc31c3497..c042da5d0 100644 --- a/packages/contracts/src/utils/asset_proxy_utils.ts +++ b/packages/contracts/src/utils/asset_proxy_utils.ts @@ -2,12 +2,15 @@ import { BigNumber } from '@0xproject/utils'; import BN = require('bn.js'); import ethUtil = require('ethereumjs-util'); -import { AssetProxyId } from './types'; +import { AssetProxyId, ERC20ProxyData, ERC721ProxyData, ProxyData } from './types'; export const assetProxyUtils = { encodeAssetProxyId(assetProxyId: AssetProxyId): Buffer { return ethUtil.toBuffer(assetProxyId); }, + decodeAssetProxyId(encodedAssetProxyId: Buffer): AssetProxyId { + return ethUtil.bufferToInt(encodedAssetProxyId); + }, encodeAddress(address: string): Buffer { if (!ethUtil.isValidAddress(address)) { throw new Error(`Invalid Address: ${address}`); @@ -15,12 +18,24 @@ export const assetProxyUtils = { const encodedAddress = ethUtil.toBuffer(address); return encodedAddress; }, + decodeAddress(encodedAddress: Buffer): string { + const address = ethUtil.bufferToHex(encodedAddress); + if (!ethUtil.isValidAddress(address)) { + throw new Error(`Invalid Address: ${address}`); + } + return address; + }, encodeUint256(value: BigNumber): Buffer { const formattedValue = new BN(value.toString(10)); const encodedValue = ethUtil.toBuffer(formattedValue); const paddedValue = ethUtil.setLengthLeft(encodedValue, 32); return paddedValue; }, + decodeUint256(encodedValue: Buffer): BigNumber { + const formattedValue = ethUtil.bufferToHex(encodedValue); + const value = new BigNumber(formattedValue, 16); + return value; + }, encodeERC20ProxyData(tokenAddress: string): string { const encodedAssetProxyId = assetProxyUtils.encodeAssetProxyId(AssetProxyId.ERC20); const encodedAddress = assetProxyUtils.encodeAddress(tokenAddress); @@ -28,6 +43,32 @@ export const assetProxyUtils = { const encodedMetadataHex = ethUtil.bufferToHex(encodedMetadata); return encodedMetadataHex; }, + decodeERC20ProxyData(proxyData: string): ERC20ProxyData { + const encodedProxyMetadata = ethUtil.toBuffer(proxyData); + if (encodedProxyMetadata.byteLength !== 21) { + throw new Error( + `Could not decode ERC20 Proxy Data. Expected length of encoded data to be 21. Got ${ + encodedProxyMetadata.byteLength + }`, + ); + } + const encodedAssetProxyId = encodedProxyMetadata.slice(0, 1); + const assetProxyId = assetProxyUtils.decodeAssetProxyId(encodedAssetProxyId); + if (assetProxyId !== AssetProxyId.ERC20) { + throw new Error( + `Could not decode ERC20 Proxy Data. Expected Asset Proxy Id to be ERC20 (${ + AssetProxyId.ERC20 + }), but got ${assetProxyId}`, + ); + } + const encodedTokenAddress = encodedProxyMetadata.slice(1, 21); + const tokenAddress = assetProxyUtils.decodeAddress(encodedTokenAddress); + const erc20ProxyData = { + assetProxyId, + tokenAddress, + }; + return erc20ProxyData; + }, encodeERC721ProxyData(tokenAddress: string, tokenId: BigNumber): string { const encodedAssetProxyId = assetProxyUtils.encodeAssetProxyId(AssetProxyId.ERC721); const encodedAddress = assetProxyUtils.encodeAddress(tokenAddress); @@ -36,4 +77,68 @@ export const assetProxyUtils = { const encodedMetadataHex = ethUtil.bufferToHex(encodedMetadata); return encodedMetadataHex; }, + decodeERC721ProxyData(proxyData: string): ERC721ProxyData { + const encodedProxyMetadata = ethUtil.toBuffer(proxyData); + if (encodedProxyMetadata.byteLength !== 53) { + throw new Error( + `Could not decode ERC20 Proxy Data. Expected length of encoded data to be 53. Got ${ + encodedProxyMetadata.byteLength + }`, + ); + } + const encodedAssetProxyId = encodedProxyMetadata.slice(0, 1); + const assetProxyId = assetProxyUtils.decodeAssetProxyId(encodedAssetProxyId); + if (assetProxyId !== AssetProxyId.ERC721) { + throw new Error( + `Could not decode ERC721 Proxy Data. Expected Asset Proxy Id to be ERC721 (${ + AssetProxyId.ERC721 + }), but got ${assetProxyId}`, + ); + } + const encodedTokenAddress = encodedProxyMetadata.slice(1, 21); + const tokenAddress = assetProxyUtils.decodeAddress(encodedTokenAddress); + const encodedTokenId = encodedProxyMetadata.slice(21, 53); + const tokenId = assetProxyUtils.decodeUint256(encodedTokenId); + const erc721ProxyData = { + assetProxyId, + tokenAddress, + tokenId, + }; + return erc721ProxyData; + }, + decodeProxyDataId(proxyData: string): AssetProxyId { + const encodedProxyMetadata = ethUtil.toBuffer(proxyData); + if (encodedProxyMetadata.byteLength < 1) { + throw new Error( + `Could not decode Proxy Data. Expected length of encoded data to be at least 1. Got ${ + encodedProxyMetadata.byteLength + }`, + ); + } + const encodedAssetProxyId = encodedProxyMetadata.slice(0, 1); + const assetProxyId = assetProxyUtils.decodeAssetProxyId(encodedAssetProxyId); + return assetProxyId; + }, + decodeProxyData(proxyData: string): ProxyData { + const assetProxyId = assetProxyUtils.decodeProxyDataId(proxyData); + switch (assetProxyId) { + case AssetProxyId.ERC20: + const erc20ProxyData = assetProxyUtils.decodeERC20ProxyData(proxyData); + const generalizedERC20ProxyData = { + assetProxyId, + tokenAddress: erc20ProxyData.tokenAddress, + }; + return generalizedERC20ProxyData; + case AssetProxyId.ERC721: + const erc721ProxyData = assetProxyUtils.decodeERC721ProxyData(proxyData); + const generaliedERC721ProxyData = { + assetProxyId, + tokenAddress: erc721ProxyData.tokenAddress, + data: erc721ProxyData.tokenId, + }; + return generaliedERC721ProxyData; + default: + throw new Error(`Unrecognized asset proxy id: ${assetProxyId}`); + } + }, }; diff --git a/packages/contracts/src/utils/exchange_wrapper.ts b/packages/contracts/src/utils/exchange_wrapper.ts index 27fdd698f..46531fa3f 100644 --- a/packages/contracts/src/utils/exchange_wrapper.ts +++ b/packages/contracts/src/utils/exchange_wrapper.ts @@ -9,7 +9,7 @@ import { constants } from './constants'; import { formatters } from './formatters'; import { LogDecoder } from './log_decoder'; import { orderUtils } from './order_utils'; -import { AssetProxyId, SignedOrder, SignedTransaction } from './types'; +import { AssetProxyId, OrderInfo, SignedOrder, SignedTransaction } from './types'; export class ExchangeWrapper { private _exchange: ExchangeContract; @@ -225,6 +225,26 @@ export class ExchangeWrapper { const filledAmount = new BigNumber(await this._exchange.filled.callAsync(orderHashHex)); return filledAmount; } + public async getOrderInfoAsync(signedOrder: SignedOrder): Promise<OrderInfo> { + const orderInfo = (await this._exchange.getOrderInfo.callAsync(signedOrder)) as OrderInfo; + return orderInfo; + } + public async matchOrdersAsync( + signedOrderLeft: SignedOrder, + signedOrderRight: SignedOrder, + from: string, + ): Promise<TransactionReceiptWithDecodedLogs> { + const params = orderUtils.createMatchOrders(signedOrderLeft, signedOrderRight); + const txHash = await this._exchange.matchOrders.sendTransactionAsync( + params.left, + params.right, + params.leftSignature, + params.rightSignature, + { from }, + ); + const tx = await this._getTxWithDecodedExchangeLogsAsync(txHash); + return tx; + } private async _getTxWithDecodedExchangeLogsAsync(txHash: string) { const tx = await this._zeroEx.awaitTransactionMinedAsync(txHash); tx.logs = _.filter(tx.logs, log => log.address === this._exchange.address); diff --git a/packages/contracts/src/utils/order_utils.ts b/packages/contracts/src/utils/order_utils.ts index 10bbf4f7c..7a482ad9e 100644 --- a/packages/contracts/src/utils/order_utils.ts +++ b/packages/contracts/src/utils/order_utils.ts @@ -80,4 +80,13 @@ export const orderUtils = { const orderHashHex = `0x${orderHashBuff.toString('hex')}`; return orderHashHex; }, + createMatchOrders(signedOrderLeft: SignedOrder, signedOrderRight: SignedOrder) { + const fill = { + left: orderUtils.getOrderStruct(signedOrderLeft), + right: orderUtils.getOrderStruct(signedOrderRight), + leftSignature: signedOrderLeft.signature, + rightSignature: signedOrderRight.signature, + }; + return fill; + }, }; diff --git a/packages/contracts/src/utils/types.ts b/packages/contracts/src/utils/types.ts index 234b14ef9..8d81adece 100644 --- a/packages/contracts/src/utils/types.ts +++ b/packages/contracts/src/utils/types.ts @@ -74,12 +74,22 @@ export interface Token { swarmHash: string; } -export enum ExchangeContractErrs { - ERROR_ORDER_EXPIRED, - ERROR_ORDER_FULLY_FILLED, - ERROR_ORDER_CANCELLED, - ERROR_ROUNDING_ERROR_TOO_LARGE, - ERROR_INSUFFICIENT_BALANCE_OR_ALLOWANCE, +export enum ExchangeStatus { + INVALID, + SUCCESS, + ROUNDING_ERROR_TOO_LARGE, + INSUFFICIENT_BALANCE_OR_ALLOWANCE, + TAKER_ASSET_FILL_AMOUNT_TOO_LOW, + INVALID_SIGNATURE, + INVALID_SENDER, + INVALID_TAKER, + INVALID_MAKER, + ORDER_INVALID_MAKER_ASSET_AMOUNT, + ORDER_INVALID_TAKER_ASSET_AMOUNT, + ORDER_FILLABLE, + ORDER_EXPIRED, + ORDER_FULLY_FILLED, + ORDER_CANCELLED, } export enum ContractName { @@ -143,3 +153,47 @@ export interface SignedTransaction { data: string; signature: string; } + +export interface TransferAmountsByMatchOrders { + // Left Maker + amountBoughtByLeftMaker: BigNumber; + amountSoldByLeftMaker: BigNumber; + amountReceivedByLeftMaker: BigNumber; + feePaidByLeftMaker: BigNumber; + // Right Maker + amountBoughtByRightMaker: BigNumber; + amountSoldByRightMaker: BigNumber; + amountReceivedByRightMaker: BigNumber; + feePaidByRightMaker: BigNumber; + // Taker + amountReceivedByTaker: BigNumber; + feePaidByTakerLeft: BigNumber; + feePaidByTakerRight: BigNumber; + totalFeePaidByTaker: BigNumber; + // Fee Recipients + feeReceivedLeft: BigNumber; + feeReceivedRight: BigNumber; +} + +export interface OrderInfo { + orderStatus: number; + orderHash: string; + orderTakerAssetFilledAmount: BigNumber; +} + +export interface ERC20ProxyData { + assetProxyId: AssetProxyId; + tokenAddress: string; +} + +export interface ERC721ProxyData { + assetProxyId: AssetProxyId; + tokenAddress: string; + tokenId: BigNumber; +} + +export interface ProxyData { + assetProxyId: AssetProxyId; + tokenAddress?: string; + data?: any; +} diff --git a/packages/contracts/test/exchange/core.ts b/packages/contracts/test/exchange/core.ts index e8fb87efb..be3252800 100644 --- a/packages/contracts/test/exchange/core.ts +++ b/packages/contracts/test/exchange/core.ts @@ -12,7 +12,7 @@ import { ERC721ProxyContract } from '../../src/contract_wrappers/generated/e_r_c import { CancelContractEventArgs, ExchangeContract, - ExchangeErrorContractEventArgs, + ExchangeStatusContractEventArgs, FillContractEventArgs, } from '../../src/contract_wrappers/generated/exchange'; import { artifacts } from '../../src/utils/artifacts'; @@ -25,7 +25,7 @@ import { ERC721Wrapper } from '../../src/utils/erc721_wrapper'; import { ExchangeWrapper } from '../../src/utils/exchange_wrapper'; import { OrderFactory } from '../../src/utils/order_factory'; import { orderUtils } from '../../src/utils/order_utils'; -import { AssetProxyId, ERC20BalancesByOwner, ExchangeContractErrs, SignedOrder } from '../../src/utils/types'; +import { AssetProxyId, ContractName, ERC20BalancesByOwner, ExchangeStatus, SignedOrder } from '../../src/utils/types'; import { provider, txDefaults, web3Wrapper } from '../../src/utils/web3_wrapper'; chaiSetup.configure(); @@ -556,9 +556,9 @@ describe('Exchange core', () => { const res = await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress); expect(res.logs).to.have.length(1); - const log = res.logs[0] as LogWithDecodedArgs<ExchangeErrorContractEventArgs>; - const errCode = log.args.errorId; - expect(errCode).to.be.equal(ExchangeContractErrs.ERROR_ORDER_EXPIRED); + const log = res.logs[0] as LogWithDecodedArgs<ExchangeStatusContractEventArgs>; + const statusCode = log.args.statusId; + expect(statusCode).to.be.equal(ExchangeStatus.ORDER_EXPIRED); }); it('should log an error event if no value is filled', async () => { @@ -567,9 +567,9 @@ describe('Exchange core', () => { const res = await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress); expect(res.logs).to.have.length(1); - const log = res.logs[0] as LogWithDecodedArgs<ExchangeErrorContractEventArgs>; - const errCode = log.args.errorId; - expect(errCode).to.be.equal(ExchangeContractErrs.ERROR_ORDER_FULLY_FILLED); + const log = res.logs[0] as LogWithDecodedArgs<ExchangeStatusContractEventArgs>; + const statusCode = log.args.statusId; + expect(statusCode).to.be.equal(ExchangeStatus.ORDER_FULLY_FILLED); }); }); @@ -635,9 +635,9 @@ describe('Exchange core', () => { const res = await exchangeWrapper.cancelOrderAsync(signedOrder, makerAddress); expect(res.logs).to.have.length(1); - const log = res.logs[0] as LogWithDecodedArgs<ExchangeErrorContractEventArgs>; - const errCode = log.args.errorId; - expect(errCode).to.be.equal(ExchangeContractErrs.ERROR_ORDER_CANCELLED); + const log = res.logs[0] as LogWithDecodedArgs<ExchangeStatusContractEventArgs>; + const statusCode = log.args.statusId; + expect(statusCode).to.be.equal(ExchangeStatus.ORDER_CANCELLED); }); it('should log error if order is expired', async () => { @@ -647,9 +647,9 @@ describe('Exchange core', () => { const res = await exchangeWrapper.cancelOrderAsync(signedOrder, makerAddress); expect(res.logs).to.have.length(1); - const log = res.logs[0] as LogWithDecodedArgs<ExchangeErrorContractEventArgs>; - const errCode = log.args.errorId; - expect(errCode).to.be.equal(ExchangeContractErrs.ERROR_ORDER_EXPIRED); + const log = res.logs[0] as LogWithDecodedArgs<ExchangeStatusContractEventArgs>; + const statusCode = log.args.statusId; + expect(statusCode).to.be.equal(ExchangeStatus.ORDER_EXPIRED); }); }); diff --git a/packages/contracts/test/exchange/match_orders.ts b/packages/contracts/test/exchange/match_orders.ts new file mode 100644 index 000000000..94cdf4598 --- /dev/null +++ b/packages/contracts/test/exchange/match_orders.ts @@ -0,0 +1,831 @@ +import { LogWithDecodedArgs, ZeroEx } from '0x.js'; +import { BlockchainLifecycle } from '@0xproject/dev-utils'; +import { BigNumber } from '@0xproject/utils'; +import * as chai from 'chai'; +import ethUtil = require('ethereumjs-util'); +import * as _ from 'lodash'; + +import { DummyERC20TokenContract } from '../../src/contract_wrappers/generated/dummy_e_r_c20_token'; +import { DummyERC721TokenContract } from '../../src/contract_wrappers/generated/dummy_e_r_c721_token'; +import { ERC20ProxyContract } from '../../src/contract_wrappers/generated/e_r_c20_proxy'; +import { ERC721ProxyContract } from '../../src/contract_wrappers/generated/e_r_c721_proxy'; +import { + CancelContractEventArgs, + ExchangeContract, + ExchangeStatusContractEventArgs, + FillContractEventArgs, +} from '../../src/contract_wrappers/generated/exchange'; +import { artifacts } from '../../src/utils/artifacts'; +import { assetProxyUtils } from '../../src/utils/asset_proxy_utils'; +import { chaiSetup } from '../../src/utils/chai_setup'; +import { constants } from '../../src/utils/constants'; +import { crypto } from '../../src/utils/crypto'; +import { ERC20Wrapper } from '../../src/utils/erc20_wrapper'; +import { ERC721Wrapper } from '../../src/utils/erc721_wrapper'; +import { ExchangeWrapper } from '../../src/utils/exchange_wrapper'; +import { OrderFactory } from '../../src/utils/order_factory'; +import { orderUtils } from '../../src/utils/order_utils'; +import { + AssetProxyId, + ContractName, + ERC20BalancesByOwner, + ERC721TokenIdsByOwner, + ExchangeStatus, + OrderInfo, + SignedOrder, +} from '../../src/utils/types'; +import { provider, txDefaults, web3Wrapper } from '../../src/utils/web3_wrapper'; + +import { MatchOrderTester } from '../utils/match_order_tester'; + +chaiSetup.configure(); +const expect = chai.expect; +const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); + +describe('matchOrders', () => { + let makerAddressLeft: string; + let makerAddressRight: string; + let owner: string; + let takerAddress: string; + let feeRecipientAddressLeft: string; + let feeRecipientAddressRight: string; + + let erc20TokenA: DummyERC20TokenContract; + let erc20TokenB: DummyERC20TokenContract; + let zrxToken: DummyERC20TokenContract; + let erc721Token: DummyERC721TokenContract; + let exchange: ExchangeContract; + let erc20Proxy: ERC20ProxyContract; + let erc721Proxy: ERC721ProxyContract; + + let erc20BalancesByOwner: ERC20BalancesByOwner; + let erc721TokenIdsByOwner: ERC721TokenIdsByOwner; + let exchangeWrapper: ExchangeWrapper; + let erc20Wrapper: ERC20Wrapper; + let erc721Wrapper: ERC721Wrapper; + let orderFactoryLeft: OrderFactory; + let orderFactoryRight: OrderFactory; + + let erc721LeftMakerAssetIds: BigNumber[]; + let erc721RightMakerAssetIds: BigNumber[]; + let erc721TakerAssetIds: BigNumber[]; + + let defaultERC20MakerAssetAddress: string; + let defaultERC20TakerAssetAddress: string; + let defaultERC721AssetAddress: string; + + let matchOrderTester: MatchOrderTester; + + let zeroEx: ZeroEx; + + before(async () => { + // Create accounts + const accounts = await web3Wrapper.getAvailableAddressesAsync(); + const usedAddresses = ([ + owner, + makerAddressLeft, + makerAddressRight, + takerAddress, + feeRecipientAddressLeft, + feeRecipientAddressRight, + ] = accounts); + // Create wrappers + erc20Wrapper = new ERC20Wrapper(provider, usedAddresses, owner); + erc721Wrapper = new ERC721Wrapper(provider, usedAddresses, owner); + // Deploy ERC20 token & ERC20 proxy + [erc20TokenA, erc20TokenB, zrxToken] = await erc20Wrapper.deployDummyTokensAsync(); + erc20Proxy = await erc20Wrapper.deployProxyAsync(); + await erc20Wrapper.setBalancesAndAllowancesAsync(); + // Deploy ERC721 token and proxy + [erc721Token] = await erc721Wrapper.deployDummyTokensAsync(); + erc721Proxy = await erc721Wrapper.deployProxyAsync(); + await erc721Wrapper.setBalancesAndAllowancesAsync(); + const erc721Balances = await erc721Wrapper.getBalancesAsync(); + erc721LeftMakerAssetIds = erc721Balances[makerAddressLeft][erc721Token.address]; + erc721RightMakerAssetIds = erc721Balances[makerAddressRight][erc721Token.address]; + erc721TakerAssetIds = erc721Balances[takerAddress][erc721Token.address]; + // Depoy exchange + exchange = await ExchangeContract.deployFrom0xArtifactAsync( + artifacts.Exchange, + provider, + txDefaults, + assetProxyUtils.encodeERC20ProxyData(zrxToken.address), + ); + zeroEx = new ZeroEx(provider, { + exchangeContractAddress: exchange.address, + networkId: constants.TESTRPC_NETWORK_ID, + }); + exchangeWrapper = new ExchangeWrapper(exchange, zeroEx); + await exchangeWrapper.registerAssetProxyAsync(AssetProxyId.ERC20, erc20Proxy.address, owner); + await exchangeWrapper.registerAssetProxyAsync(AssetProxyId.ERC721, erc721Proxy.address, owner); + // Authorize ERC20 and ERC721 trades by exchange + await erc20Proxy.addAuthorizedAddress.sendTransactionAsync(exchange.address, { + from: owner, + }); + await erc721Proxy.addAuthorizedAddress.sendTransactionAsync(exchange.address, { + from: owner, + }); + // Set default addresses + defaultERC20MakerAssetAddress = erc20TokenA.address; + defaultERC20TakerAssetAddress = erc20TokenB.address; + defaultERC721AssetAddress = erc721Token.address; + // Create default order parameters + const defaultOrderParams = { + ...constants.STATIC_ORDER_PARAMS, + exchangeAddress: exchange.address, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + }; + const privateKeyLeft = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(makerAddressLeft)]; + orderFactoryLeft = new OrderFactory(privateKeyLeft, defaultOrderParams); + const privateKeyRight = constants.TESTRPC_PRIVATE_KEYS[accounts.indexOf(makerAddressRight)]; + orderFactoryRight = new OrderFactory(privateKeyRight, defaultOrderParams); + // Set match order tester + matchOrderTester = new MatchOrderTester(exchangeWrapper, erc20Wrapper, erc721Wrapper, zrxToken.address); + }); + beforeEach(async () => { + await blockchainLifecycle.startAsync(); + }); + afterEach(async () => { + await blockchainLifecycle.revertAsync(); + }); + describe('matchOrders', () => { + beforeEach(async () => { + erc20BalancesByOwner = await erc20Wrapper.getBalancesAsync(); + erc721TokenIdsByOwner = await erc721Wrapper.getBalancesAsync(); + }); + + it('should transfer the correct amounts when orders completely fill each other', async () => { + // Create orders to match + const signedOrderLeft = orderFactoryLeft.newSignedOrder({ + makerAddress: makerAddressLeft, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(5), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(10), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = orderFactoryRight.newSignedOrder({ + makerAddress: makerAddressRight, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(2), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Match signedOrderLeft with signedOrderRight + await matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft, + signedOrderRight, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ); + // Verify left order was fully filled + const leftOrderInfo: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderLeft); + expect(leftOrderInfo.orderStatus as ExchangeStatus).to.be.equal(ExchangeStatus.ORDER_FULLY_FILLED); + // Verify right order was fully filled + const rightOrderInfo: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderRight); + expect(rightOrderInfo.orderStatus as ExchangeStatus).to.be.equal(ExchangeStatus.ORDER_FULLY_FILLED); + }); + + 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 = orderFactoryLeft.newSignedOrder({ + makerAddress: makerAddressLeft, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(5), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(10), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = orderFactoryRight.newSignedOrder({ + makerAddress: makerAddressRight, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(5), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Store original taker balance + const takerInitialBalances = _.cloneDeep(erc20BalancesByOwner[takerAddress][defaultERC20MakerAssetAddress]); + // Match signedOrderLeft with signedOrderRight + let newERC20BalancesByOwner: ERC20BalancesByOwner; + let newERC721TokenIdsByOwner: ERC721TokenIdsByOwner; + [ + newERC20BalancesByOwner, + newERC721TokenIdsByOwner, + ] = await matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft, + signedOrderRight, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ); + // Verify left order was fully filled + const leftOrderInfo: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderLeft); + expect(leftOrderInfo.orderStatus as ExchangeStatus).to.be.equal(ExchangeStatus.ORDER_FULLY_FILLED); + // Verify right order was fully filled + const rightOrderInfo: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderRight); + expect(rightOrderInfo.orderStatus as ExchangeStatus).to.be.equal(ExchangeStatus.ORDER_FULLY_FILLED); + // Verify taker did not take a profit + expect(takerInitialBalances).to.be.deep.equal( + newERC20BalancesByOwner[takerAddress][defaultERC20MakerAssetAddress], + ); + }); + + it('should transfer the correct amounts when left order is completely filled and right order is partially filled', async () => { + // Create orders to match + const signedOrderLeft = orderFactoryLeft.newSignedOrder({ + makerAddress: makerAddressLeft, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(5), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(10), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = orderFactoryRight.newSignedOrder({ + makerAddress: makerAddressRight, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(20), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(4), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Match orders + await matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft, + signedOrderRight, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ); + // Verify left order was fully filled + const leftOrderInfo: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderLeft); + expect(leftOrderInfo.orderStatus as ExchangeStatus).to.be.equal(ExchangeStatus.ORDER_FULLY_FILLED); + // Verify right order was partially filled + const rightOrderInfo: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderRight); + expect(rightOrderInfo.orderStatus as ExchangeStatus).to.be.equal(ExchangeStatus.ORDER_FILLABLE); + }); + + it('should transfer the correct amounts when right order is completely filled and left order is partially filled', async () => { + // Create orders to match + const signedOrderLeft = orderFactoryLeft.newSignedOrder({ + makerAddress: makerAddressLeft, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(50), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(100), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = orderFactoryRight.newSignedOrder({ + makerAddress: makerAddressRight, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(2), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Match orders + await matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft, + signedOrderRight, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ); + // Verify left order was partially filled + const leftOrderInfo: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderLeft); + expect(leftOrderInfo.orderStatus as ExchangeStatus).to.be.equal(ExchangeStatus.ORDER_FILLABLE); + // Verify right order was fully filled + const rightOrderInfo: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderRight); + expect(rightOrderInfo.orderStatus as ExchangeStatus).to.be.equal(ExchangeStatus.ORDER_FULLY_FILLED); + }); + + it('should transfer the correct amounts when consecutive calls are used to completely fill the left order', async () => { + // Create orders to match + const signedOrderLeft = orderFactoryLeft.newSignedOrder({ + makerAddress: makerAddressLeft, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(50), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(100), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = orderFactoryRight.newSignedOrder({ + makerAddress: makerAddressRight, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(2), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Match orders + let newERC20BalancesByOwner: ERC20BalancesByOwner; + let newERC721TokenIdsByOwner: ERC721TokenIdsByOwner; + [ + newERC20BalancesByOwner, + newERC721TokenIdsByOwner, + ] = await matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft, + signedOrderRight, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ); + // Verify left order was partially filled + const leftOrderInfo: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderLeft); + expect(leftOrderInfo.orderStatus as ExchangeStatus).to.be.equal(ExchangeStatus.ORDER_FILLABLE); + // Verify right order was fully filled + const rightOrderInfo: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderRight); + expect(rightOrderInfo.orderStatus as ExchangeStatus).to.be.equal(ExchangeStatus.ORDER_FULLY_FILLED); + // 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 = orderFactoryRight.newSignedOrder({ + makerAddress: makerAddressRight, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(100), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(50), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Match signedOrderLeft with signedOrderRight2 + const leftTakerAssetFilledAmount = signedOrderRight.makerAssetAmount; + const rightTakerAssetFilledAmount = new BigNumber(0); + await matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft, + signedOrderRight2, + takerAddress, + newERC20BalancesByOwner, + erc721TokenIdsByOwner, + leftTakerAssetFilledAmount, + rightTakerAssetFilledAmount, + ); + // Verify left order was fully filled + const leftOrderInfo2: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderLeft); + expect(leftOrderInfo2.orderStatus as ExchangeStatus).to.be.equal(ExchangeStatus.ORDER_FULLY_FILLED); + // Verify second right order was partially filled + const rightOrderInfo2: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderRight2); + expect(rightOrderInfo2.orderStatus as ExchangeStatus).to.be.equal(ExchangeStatus.ORDER_FILLABLE); + }); + + it('should transfer the correct amounts when consecutive calls are used to completely fill the right order', async () => { + // Create orders to match + const signedOrderLeft = orderFactoryLeft.newSignedOrder({ + makerAddress: makerAddressLeft, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(2), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + + const signedOrderRight = orderFactoryRight.newSignedOrder({ + makerAddress: makerAddressRight, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(50), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(100), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Match orders + let newERC20BalancesByOwner: ERC20BalancesByOwner; + let newERC721TokenIdsByOwner: ERC721TokenIdsByOwner; + [ + newERC20BalancesByOwner, + newERC721TokenIdsByOwner, + ] = await matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft, + signedOrderRight, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ); + // Verify left order was partially filled + const leftOrderInfo: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderLeft); + expect(leftOrderInfo.orderStatus as ExchangeStatus).to.be.equal(ExchangeStatus.ORDER_FULLY_FILLED); + // Verify right order was fully filled + const rightOrderInfo: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderRight); + expect(rightOrderInfo.orderStatus as ExchangeStatus).to.be.equal(ExchangeStatus.ORDER_FILLABLE); + // 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 = orderFactoryLeft.newSignedOrder({ + makerAddress: makerAddressLeft, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(100), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(50), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + // Match signedOrderLeft2 with signedOrderRight + const leftTakerAssetFilledAmount = new BigNumber(0); + const takerAmountReceived = newERC20BalancesByOwner[takerAddress][defaultERC20MakerAssetAddress].minus( + erc20BalancesByOwner[takerAddress][defaultERC20MakerAssetAddress], + ); + const rightTakerAssetFilledAmount = signedOrderLeft.makerAssetAmount.minus(takerAmountReceived); + await matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft2, + signedOrderRight, + takerAddress, + newERC20BalancesByOwner, + erc721TokenIdsByOwner, + leftTakerAssetFilledAmount, + rightTakerAssetFilledAmount, + ); + // Verify second left order was partially filled + const leftOrderInfo2: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderLeft2); + expect(leftOrderInfo2.orderStatus as ExchangeStatus).to.be.equal(ExchangeStatus.ORDER_FILLABLE); + // Verify right order was fully filled + const rightOrderInfo2: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderRight); + expect(rightOrderInfo2.orderStatus as ExchangeStatus).to.be.equal(ExchangeStatus.ORDER_FULLY_FILLED); + }); + + it('should transfer the correct amounts if fee recipient is the same across both matched orders', async () => { + const feeRecipientAddress = feeRecipientAddressLeft; + const signedOrderLeft = orderFactoryLeft.newSignedOrder({ + makerAddress: makerAddressLeft, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(5), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(10), 18), + feeRecipientAddress, + }); + const signedOrderRight = orderFactoryRight.newSignedOrder({ + makerAddress: makerAddressRight, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(2), 18), + feeRecipientAddress, + }); + // Match orders + await matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft, + signedOrderRight, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ); + }); + + it('should transfer the correct amounts if taker is also the left order maker', async () => { + // Create orders to match + const signedOrderLeft = orderFactoryLeft.newSignedOrder({ + makerAddress: makerAddressLeft, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(5), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(10), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = orderFactoryRight.newSignedOrder({ + makerAddress: makerAddressRight, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(2), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Match orders + takerAddress = signedOrderLeft.makerAddress; + await matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft, + signedOrderRight, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ); + }); + + it('should transfer the correct amounts if taker is also the right order maker', async () => { + // Create orders to match + const signedOrderLeft = orderFactoryLeft.newSignedOrder({ + makerAddress: makerAddressLeft, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(5), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(10), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = orderFactoryRight.newSignedOrder({ + makerAddress: makerAddressRight, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(2), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Match orders + takerAddress = signedOrderRight.makerAddress; + await matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft, + signedOrderRight, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ); + }); + + it('should transfer the correct amounts if taker is also the left fee recipient', async () => { + // Create orders to match + const signedOrderLeft = orderFactoryLeft.newSignedOrder({ + makerAddress: makerAddressLeft, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(5), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(10), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = orderFactoryRight.newSignedOrder({ + makerAddress: makerAddressRight, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(2), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Match orders + takerAddress = feeRecipientAddressLeft; + await matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft, + signedOrderRight, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ); + }); + + it('should transfer the correct amounts if taker is also the right fee recipient', async () => { + // Create orders to match + const signedOrderLeft = orderFactoryLeft.newSignedOrder({ + makerAddress: makerAddressLeft, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(5), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(10), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = orderFactoryRight.newSignedOrder({ + makerAddress: makerAddressRight, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(2), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Match orders + takerAddress = feeRecipientAddressRight; + await matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft, + signedOrderRight, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ); + }); + + it('should transfer the correct amounts if left maker is the left fee recipient and right maker is the right fee recipient', async () => { + // Create orders to match + const signedOrderLeft = orderFactoryLeft.newSignedOrder({ + makerAddress: makerAddressLeft, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(5), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(10), 18), + feeRecipientAddress: makerAddressLeft, + }); + const signedOrderRight = orderFactoryRight.newSignedOrder({ + makerAddress: makerAddressRight, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(2), 18), + feeRecipientAddress: makerAddressRight, + }); + // Match orders + await matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft, + signedOrderRight, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ); + }); + + it('Should throw if left order is not fillable', async () => { + // Create orders to match + const signedOrderLeft = orderFactoryLeft.newSignedOrder({ + makerAddress: makerAddressLeft, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(5), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(10), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = orderFactoryRight.newSignedOrder({ + makerAddress: makerAddressRight, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(2), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Cancel left order + await exchangeWrapper.cancelOrderAsync(signedOrderLeft, signedOrderLeft.makerAddress); + // Match orders + return expect( + exchangeWrapper.matchOrdersAsync(signedOrderLeft, signedOrderRight, takerAddress), + ).to.be.rejectedWith(constants.REVERT); + }); + + it('Should throw if right order is not fillable', async () => { + // Create orders to match + const signedOrderLeft = orderFactoryLeft.newSignedOrder({ + makerAddress: makerAddressLeft, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(5), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(10), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = orderFactoryRight.newSignedOrder({ + makerAddress: makerAddressRight, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(2), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Cancel right order + await exchangeWrapper.cancelOrderAsync(signedOrderRight, signedOrderRight.makerAddress); + // Match orders + return expect( + exchangeWrapper.matchOrdersAsync(signedOrderLeft, signedOrderRight, takerAddress), + ).to.be.rejectedWith(constants.REVERT); + }); + + it('should throw if there is not a positive spread', async () => { + // Create orders to match + const signedOrderLeft = orderFactoryLeft.newSignedOrder({ + makerAddress: makerAddressLeft, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(5), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(100), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = orderFactoryRight.newSignedOrder({ + makerAddress: makerAddressRight, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(1), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(200), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Match orders + return expect( + matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft, + signedOrderRight, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ), + ).to.be.rejectedWith(constants.REVERT); + }); + + it('should throw if the left maker asset is not equal to the right taker asset ', async () => { + // Create orders to match + const signedOrderLeft = orderFactoryLeft.newSignedOrder({ + makerAddress: makerAddressLeft, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(5), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(10), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = orderFactoryRight.newSignedOrder({ + makerAddress: makerAddressRight, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(2), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Match orders + return expect( + matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft, + signedOrderRight, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ), + ).to.be.rejectedWith(constants.REVERT); + }); + + it('should throw if the right maker asset is not equal to the left taker asset', async () => { + // Create orders to match + const signedOrderLeft = orderFactoryLeft.newSignedOrder({ + makerAddress: makerAddressLeft, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(5), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(10), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = orderFactoryRight.newSignedOrder({ + makerAddress: makerAddressRight, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(2), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Match orders + return expect( + matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft, + signedOrderRight, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ), + ).to.be.rejectedWith(constants.REVERT); + }); + + it('should transfer correct amounts when left order maker asset is an ERC721 token', async () => { + // Create orders to match + const erc721TokenToTransfer = erc721LeftMakerAssetIds[0]; + const signedOrderLeft = orderFactoryLeft.newSignedOrder({ + makerAddress: makerAddressLeft, + makerAssetData: assetProxyUtils.encodeERC721ProxyData(defaultERC721AssetAddress, erc721TokenToTransfer), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + makerAssetAmount: new BigNumber(1), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(10), 18), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = orderFactoryRight.newSignedOrder({ + makerAddress: makerAddressRight, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20TakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC721ProxyData(defaultERC721AssetAddress, erc721TokenToTransfer), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: new BigNumber(1), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Match orders + await matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft, + signedOrderRight, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ); + // Verify left order was fully filled + const leftOrderInfo: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderLeft); + expect(leftOrderInfo.orderStatus as ExchangeStatus).to.be.equal(ExchangeStatus.ORDER_FULLY_FILLED); + // Verify right order was fully filled + const rightOrderInfo: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderRight); + expect(rightOrderInfo.orderStatus as ExchangeStatus).to.be.equal(ExchangeStatus.ORDER_FULLY_FILLED); + }); + + it('should transfer correct amounts when right order maker asset is an ERC721 token', async () => { + // Create orders to match + const erc721TokenToTransfer = erc721RightMakerAssetIds[0]; + const signedOrderLeft = orderFactoryLeft.newSignedOrder({ + makerAddress: makerAddressLeft, + makerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + takerAssetData: assetProxyUtils.encodeERC721ProxyData(defaultERC721AssetAddress, erc721TokenToTransfer), + makerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(10), 18), + takerAssetAmount: new BigNumber(1), + feeRecipientAddress: feeRecipientAddressLeft, + }); + const signedOrderRight = orderFactoryRight.newSignedOrder({ + makerAddress: makerAddressRight, + makerAssetData: assetProxyUtils.encodeERC721ProxyData(defaultERC721AssetAddress, erc721TokenToTransfer), + takerAssetData: assetProxyUtils.encodeERC20ProxyData(defaultERC20MakerAssetAddress), + makerAssetAmount: new BigNumber(1), + takerAssetAmount: ZeroEx.toBaseUnitAmount(new BigNumber(10), 18), + feeRecipientAddress: feeRecipientAddressRight, + }); + // Match orders + await matchOrderTester.matchOrdersAndVerifyBalancesAsync( + signedOrderLeft, + signedOrderRight, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + ); + // Verify left order was fully filled + const leftOrderInfo: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderLeft); + expect(leftOrderInfo.orderStatus as ExchangeStatus).to.be.equal(ExchangeStatus.ORDER_FULLY_FILLED); + // Verify right order was fully filled + const rightOrderInfo: OrderInfo = await exchangeWrapper.getOrderInfoAsync(signedOrderRight); + expect(rightOrderInfo.orderStatus as ExchangeStatus).to.be.equal(ExchangeStatus.ORDER_FULLY_FILLED); + }); + }); +}); // tslint:disable-line:max-file-line-count diff --git a/packages/contracts/test/exchange/transactions.ts b/packages/contracts/test/exchange/transactions.ts index a71b50a61..482475554 100644 --- a/packages/contracts/test/exchange/transactions.ts +++ b/packages/contracts/test/exchange/transactions.ts @@ -21,7 +21,7 @@ import { TransactionFactory } from '../../src/utils/transaction_factory'; import { AssetProxyId, ERC20BalancesByOwner, - ExchangeContractErrs, + ExchangeStatus, OrderStruct, SignatureType, SignedOrder, @@ -197,7 +197,7 @@ describe('Exchange transactions', () => { it('should cancel the order when signed by maker and called by sender', async () => { await exchangeWrapper.executeTransactionAsync(signedTx, senderAddress); - const res = await exchangeWrapper.fillOrderAsync(signedOrder, takerAddress); + const res = await exchangeWrapper.fillOrderAsync(signedOrder, senderAddress); const newBalances = await erc20Wrapper.getBalancesAsync(); expect(newBalances).to.deep.equal(erc20Balances); }); diff --git a/packages/contracts/test/utils/match_order_tester.ts b/packages/contracts/test/utils/match_order_tester.ts new file mode 100644 index 000000000..14930de08 --- /dev/null +++ b/packages/contracts/test/utils/match_order_tester.ts @@ -0,0 +1,353 @@ +import { LogWithDecodedArgs, ZeroEx } from '0x.js'; +import { BlockchainLifecycle } from '@0xproject/dev-utils'; +import { BigNumber } from '@0xproject/utils'; +import * as chai from 'chai'; +import ethUtil = require('ethereumjs-util'); +import * as _ from 'lodash'; + +import { DummyERC20TokenContract } from '../../src/contract_wrappers/generated/dummy_e_r_c20_token'; +import { DummyERC721TokenContract } from '../../src/contract_wrappers/generated/dummy_e_r_c721_token'; +import { ERC20ProxyContract } from '../../src/contract_wrappers/generated/e_r_c20_proxy'; +import { ERC721ProxyContract } from '../../src/contract_wrappers/generated/e_r_c721_proxy'; +import { + CancelContractEventArgs, + ExchangeContract, + FillContractEventArgs, +} from '../../src/contract_wrappers/generated/exchange'; +import { assetProxyUtils } from '../../src/utils/asset_proxy_utils'; +import { chaiSetup } from '../../src/utils/chai_setup'; +import { constants } from '../../src/utils/constants'; +import { crypto } from '../../src/utils/crypto'; +import { ERC20Wrapper } from '../../src/utils/erc20_wrapper'; +import { ERC721Wrapper } from '../../src/utils/erc721_wrapper'; +import { ExchangeWrapper } from '../../src/utils/exchange_wrapper'; +import { OrderFactory } from '../../src/utils/order_factory'; +import { orderUtils } from '../../src/utils/order_utils'; +import { + AssetProxyId, + ContractName, + ERC20BalancesByOwner, + ERC721TokenIdsByOwner, + ExchangeStatus, + SignedOrder, + TransferAmountsByMatchOrders as TransferAmounts, +} from '../../src/utils/types'; +import { provider, web3Wrapper } from '../../src/utils/web3_wrapper'; + +chaiSetup.configure(); +const expect = chai.expect; +const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); + +export class MatchOrderTester { + private _exchangeWrapper: ExchangeWrapper; + private _erc20Wrapper: ERC20Wrapper; + private _erc721Wrapper: ERC721Wrapper; + private _feeTokenAddress: string; + + /// @dev Compares a pair of ERC20 balances and a pair of ERC721 token owners. + /// @param expectedNewERC20BalancesByOwner Expected ERC20 balances. + /// @param realERC20BalancesByOwner Actual ERC20 balances. + /// @param expectedNewERC721TokenIdsByOwner Expected ERC721 token owners. + /// @param realERC721TokenIdsByOwner Actual ERC20 token owners. + /// @return True only if ERC20 balances match and ERC721 token owners match. + private static _compareExpectedAndRealBalances( + expectedNewERC20BalancesByOwner: ERC20BalancesByOwner, + realERC20BalancesByOwner: ERC20BalancesByOwner, + expectedNewERC721TokenIdsByOwner: ERC721TokenIdsByOwner, + realERC721TokenIdsByOwner: ERC721TokenIdsByOwner, + ) { + // ERC20 Balances + const erc20BalancesMatch = _.isEqual(expectedNewERC20BalancesByOwner, realERC20BalancesByOwner); + if (!erc20BalancesMatch) { + return false; + } + // ERC721 Token Ids + const sortedExpectedNewERC721TokenIdsByOwner = _.mapValues( + expectedNewERC721TokenIdsByOwner, + tokenIdsByOwner => { + _.mapValues(tokenIdsByOwner, tokenIds => { + _.sortBy(tokenIds); + }); + }, + ); + const sortedNewERC721TokenIdsByOwner = _.mapValues(realERC721TokenIdsByOwner, tokenIdsByOwner => { + _.mapValues(tokenIdsByOwner, tokenIds => { + _.sortBy(tokenIds); + }); + }); + const erc721TokenIdsMatch = _.isEqual(sortedExpectedNewERC721TokenIdsByOwner, sortedNewERC721TokenIdsByOwner); + return erc721TokenIdsMatch; + } + /// @dev Constructs new MatchOrderTester. + /// @param exchangeWrapper Used to call to the Exchange. + /// @param erc20Wrapper Used to fetch ERC20 balances. + /// @param erc721Wrapper Used to fetch ERC721 token owners. + /// @param feeTokenAddress Address of ERC20 fee token. + constructor( + exchangeWrapper: ExchangeWrapper, + erc20Wrapper: ERC20Wrapper, + erc721Wrapper: ERC721Wrapper, + feeTokenAddress: string, + ) { + this._exchangeWrapper = exchangeWrapper; + this._erc20Wrapper = erc20Wrapper; + this._erc721Wrapper = erc721Wrapper; + this._feeTokenAddress = feeTokenAddress; + } + /// @dev Matches two complementary orders and validates results. + /// Validation either succeeds or throws. + /// @param signedOrderLeft First matched order. + /// @param signedOrderRight Second matched order. + /// @param takerAddress Address of taker (the address who matched the two orders) + /// @param erc20BalancesByOwner Current ERC20 balances. + /// @param erc721TokenIdsByOwner Current ERC721 token owners. + /// @param initialTakerAssetFilledAmountLeft Current amount the left order has been filled. + /// @param initialTakerAssetFilledAmountRight Current amount the right order has been filled. + /// @return New ERC20 balances & ERC721 token owners. + public async matchOrdersAndVerifyBalancesAsync( + signedOrderLeft: SignedOrder, + signedOrderRight: SignedOrder, + takerAddress: string, + erc20BalancesByOwner: ERC20BalancesByOwner, + erc721TokenIdsByOwner: ERC721TokenIdsByOwner, + initialTakerAssetFilledAmountLeft?: BigNumber, + initialTakerAssetFilledAmountRight?: BigNumber, + ): Promise<[ERC20BalancesByOwner, ERC721TokenIdsByOwner]> { + // Test setup & verify preconditions + const makerAddressLeft = signedOrderLeft.makerAddress; + const makerAddressRight = signedOrderRight.makerAddress; + const feeRecipientAddressLeft = signedOrderLeft.feeRecipientAddress; + const feeRecipientAddressRight = signedOrderRight.feeRecipientAddress; + // Verify Left order preconditions + const orderTakerAssetFilledAmountLeft = await this._exchangeWrapper.getTakerAssetFilledAmountAsync( + orderUtils.getOrderHashHex(signedOrderLeft), + ); + const expectedOrderFilledAmountLeft = initialTakerAssetFilledAmountLeft + ? initialTakerAssetFilledAmountLeft + : new BigNumber(0); + expect(expectedOrderFilledAmountLeft).to.be.bignumber.equal(orderTakerAssetFilledAmountLeft); + // Verify Right order preconditions + const orderTakerAssetFilledAmountRight = await this._exchangeWrapper.getTakerAssetFilledAmountAsync( + orderUtils.getOrderHashHex(signedOrderRight), + ); + const expectedOrderFilledAmountRight = initialTakerAssetFilledAmountRight + ? initialTakerAssetFilledAmountRight + : new BigNumber(0); + expect(expectedOrderFilledAmountRight).to.be.bignumber.equal(orderTakerAssetFilledAmountRight); + // Match left & right orders + await this._exchangeWrapper.matchOrdersAsync(signedOrderLeft, signedOrderRight, takerAddress); + const newERC20BalancesByOwner = await this._erc20Wrapper.getBalancesAsync(); + const newERC721TokenIdsByOwner = await this._erc721Wrapper.getBalancesAsync(); + // Calculate expected balance changes + const expectedTransferAmounts = await this._calculateExpectedTransferAmountsAsync( + signedOrderLeft, + signedOrderRight, + orderTakerAssetFilledAmountLeft, + orderTakerAssetFilledAmountRight, + ); + let expectedERC20BalancesByOwner: ERC20BalancesByOwner; + let expectedERC721TokenIdsByOwner: ERC721TokenIdsByOwner; + [expectedERC20BalancesByOwner, expectedERC721TokenIdsByOwner] = this._calculateExpectedBalances( + signedOrderLeft, + signedOrderRight, + takerAddress, + erc20BalancesByOwner, + erc721TokenIdsByOwner, + expectedTransferAmounts, + ); + // Assert our expected balances are equal to the actual balances + const expectedBalancesMatchRealBalances = MatchOrderTester._compareExpectedAndRealBalances( + expectedERC20BalancesByOwner, + newERC20BalancesByOwner, + expectedERC721TokenIdsByOwner, + newERC721TokenIdsByOwner, + ); + expect(expectedBalancesMatchRealBalances).to.be.true(); + return [newERC20BalancesByOwner, newERC721TokenIdsByOwner]; + } + /// @dev Calculates expected transfer amounts between order makers, fee recipients, and + /// the taker when two orders are matched. + /// @param signedOrderLeft First matched order. + /// @param signedOrderRight Second matched order. + /// @param orderTakerAssetFilledAmountLeft How much left order has been filled, prior to matching orders. + /// @param orderTakerAssetFilledAmountRight How much the right order has been filled, prior to matching orders. + /// @return TransferAmounts A struct containing the expected transfer amounts. + private async _calculateExpectedTransferAmountsAsync( + signedOrderLeft: SignedOrder, + signedOrderRight: SignedOrder, + orderTakerAssetFilledAmountLeft: BigNumber, + orderTakerAssetFilledAmountRight: BigNumber, + ): Promise<TransferAmounts> { + let amountBoughtByLeftMaker = await this._exchangeWrapper.getTakerAssetFilledAmountAsync( + orderUtils.getOrderHashHex(signedOrderLeft), + ); + amountBoughtByLeftMaker = amountBoughtByLeftMaker.minus(orderTakerAssetFilledAmountLeft); + const amountSoldByLeftMaker = amountBoughtByLeftMaker + .times(signedOrderLeft.makerAssetAmount) + .dividedToIntegerBy(signedOrderLeft.takerAssetAmount); + const amountReceivedByRightMaker = amountBoughtByLeftMaker + .times(signedOrderRight.takerAssetAmount) + .dividedToIntegerBy(signedOrderRight.makerAssetAmount); + const amountReceivedByTaker = amountSoldByLeftMaker.minus(amountReceivedByRightMaker); + let amountBoughtByRightMaker = await this._exchangeWrapper.getTakerAssetFilledAmountAsync( + orderUtils.getOrderHashHex(signedOrderRight), + ); + amountBoughtByRightMaker = amountBoughtByRightMaker.minus(orderTakerAssetFilledAmountRight); + const amountSoldByRightMaker = amountBoughtByRightMaker + .times(signedOrderRight.makerAssetAmount) + .dividedToIntegerBy(signedOrderRight.takerAssetAmount); + const amountReceivedByLeftMaker = amountSoldByRightMaker; + const feePaidByLeftMaker = signedOrderLeft.makerFee + .times(amountSoldByLeftMaker) + .dividedToIntegerBy(signedOrderLeft.makerAssetAmount); + const feePaidByRightMaker = signedOrderRight.makerFee + .times(amountSoldByRightMaker) + .dividedToIntegerBy(signedOrderRight.makerAssetAmount); + const feePaidByTakerLeft = signedOrderLeft.takerFee + .times(amountSoldByLeftMaker) + .dividedToIntegerBy(signedOrderLeft.makerAssetAmount); + const feePaidByTakerRight = signedOrderRight.takerFee + .times(amountSoldByRightMaker) + .dividedToIntegerBy(signedOrderRight.makerAssetAmount); + const totalFeePaidByTaker = feePaidByTakerLeft.add(feePaidByTakerRight); + const feeReceivedLeft = feePaidByLeftMaker.add(feePaidByTakerLeft); + const feeReceivedRight = feePaidByRightMaker.add(feePaidByTakerRight); + // Return values + const expectedTransferAmounts = { + // Left Maker + amountBoughtByLeftMaker, + amountSoldByLeftMaker, + amountReceivedByLeftMaker, + feePaidByLeftMaker, + // Right Maker + amountBoughtByRightMaker, + amountSoldByRightMaker, + amountReceivedByRightMaker, + feePaidByRightMaker, + // Taker + amountReceivedByTaker, + feePaidByTakerLeft, + feePaidByTakerRight, + totalFeePaidByTaker, + // Fee Recipients + feeReceivedLeft, + feeReceivedRight, + }; + return expectedTransferAmounts; + } + /// @dev Calculates the expected balances of order makers, fee recipients, and the taker, + /// as a result of matching two orders. + /// @param signedOrderLeft First matched order. + /// @param signedOrderRight Second matched order. + /// @param takerAddress Address of taker (the address who matched the two orders) + /// @param erc20BalancesByOwner Current ERC20 balances. + /// @param erc721TokenIdsByOwner Current ERC721 token owners. + /// @param expectedTransferAmounts A struct containing the expected transfer amounts. + /// @return Expected ERC20 balances & ERC721 token owners after orders have been matched. + private _calculateExpectedBalances( + signedOrderLeft: SignedOrder, + signedOrderRight: SignedOrder, + takerAddress: string, + erc20BalancesByOwner: ERC20BalancesByOwner, + erc721TokenIdsByOwner: ERC721TokenIdsByOwner, + expectedTransferAmounts: TransferAmounts, + ): [ERC20BalancesByOwner, ERC721TokenIdsByOwner] { + const makerAddressLeft = signedOrderLeft.makerAddress; + const makerAddressRight = signedOrderRight.makerAddress; + const feeRecipientAddressLeft = signedOrderLeft.feeRecipientAddress; + const feeRecipientAddressRight = signedOrderRight.feeRecipientAddress; + // Operations are performed on copies of the balances + const expectedNewERC20BalancesByOwner = _.cloneDeep(erc20BalancesByOwner); + const expectedNewERC721TokenIdsByOwner = _.cloneDeep(erc721TokenIdsByOwner); + // Left Maker Asset (Right Taker Asset) + const makerAssetProxyIdLeft = assetProxyUtils.decodeProxyDataId(signedOrderLeft.makerAssetData); + if (makerAssetProxyIdLeft === AssetProxyId.ERC20) { + // Decode asset data + const erc20ProxyData = assetProxyUtils.decodeERC20ProxyData(signedOrderLeft.makerAssetData); + const makerAssetAddressLeft = erc20ProxyData.tokenAddress; + const takerAssetAddressRight = makerAssetAddressLeft; + // Left Maker + expectedNewERC20BalancesByOwner[makerAddressLeft][makerAssetAddressLeft] = expectedNewERC20BalancesByOwner[ + makerAddressLeft + ][makerAssetAddressLeft].minus(expectedTransferAmounts.amountSoldByLeftMaker); + // Right Maker + expectedNewERC20BalancesByOwner[makerAddressRight][ + takerAssetAddressRight + ] = expectedNewERC20BalancesByOwner[makerAddressRight][takerAssetAddressRight].add( + expectedTransferAmounts.amountReceivedByRightMaker, + ); + // Taker + expectedNewERC20BalancesByOwner[takerAddress][makerAssetAddressLeft] = expectedNewERC20BalancesByOwner[ + takerAddress + ][makerAssetAddressLeft].add(expectedTransferAmounts.amountReceivedByTaker); + } else if (makerAssetProxyIdLeft === AssetProxyId.ERC721) { + // Decode asset data + const erc721ProxyData = assetProxyUtils.decodeERC721ProxyData(signedOrderLeft.makerAssetData); + const makerAssetAddressLeft = erc721ProxyData.tokenAddress; + const makerAssetIdLeft = erc721ProxyData.tokenId; + const takerAssetAddressRight = makerAssetAddressLeft; + const takerAssetIdRight = makerAssetIdLeft; + // Left Maker + _.remove(expectedNewERC721TokenIdsByOwner[makerAddressLeft][makerAssetAddressLeft], makerAssetIdLeft); + // Right Maker + expectedNewERC721TokenIdsByOwner[makerAddressRight][takerAssetAddressRight].push(takerAssetIdRight); + // Taker: Since there is only 1 asset transferred, the taker does not receive any of the left maker asset. + } + // Left Taker Asset (Right Maker Asset) + // Note: This exchange is only between the order makers: the Taker does not receive any of the left taker asset. + const takerAssetProxyIdLeft = assetProxyUtils.decodeProxyDataId(signedOrderLeft.takerAssetData); + if (takerAssetProxyIdLeft === AssetProxyId.ERC20) { + // Decode asset data + const erc20ProxyData = assetProxyUtils.decodeERC20ProxyData(signedOrderLeft.takerAssetData); + const takerAssetAddressLeft = erc20ProxyData.tokenAddress; + const makerAssetAddressRight = takerAssetAddressLeft; + // Left Maker + expectedNewERC20BalancesByOwner[makerAddressLeft][takerAssetAddressLeft] = expectedNewERC20BalancesByOwner[ + makerAddressLeft + ][takerAssetAddressLeft].add(expectedTransferAmounts.amountReceivedByLeftMaker); + // Right Maker + expectedNewERC20BalancesByOwner[makerAddressRight][ + makerAssetAddressRight + ] = expectedNewERC20BalancesByOwner[makerAddressRight][makerAssetAddressRight].minus( + expectedTransferAmounts.amountSoldByRightMaker, + ); + } else if (takerAssetProxyIdLeft === AssetProxyId.ERC721) { + // Decode asset data + const erc721ProxyData = assetProxyUtils.decodeERC721ProxyData(signedOrderRight.makerAssetData); + const makerAssetAddressRight = erc721ProxyData.tokenAddress; + const makerAssetIdRight = erc721ProxyData.tokenId; + const takerAssetAddressLeft = makerAssetAddressRight; + const takerAssetIdLeft = makerAssetIdRight; + // Right Maker + _.remove(expectedNewERC721TokenIdsByOwner[makerAddressRight][makerAssetAddressRight], makerAssetIdRight); + // Left Maker + expectedNewERC721TokenIdsByOwner[makerAddressLeft][takerAssetAddressLeft].push(takerAssetIdLeft); + } + // Left Maker Fees + expectedNewERC20BalancesByOwner[makerAddressLeft][this._feeTokenAddress] = expectedNewERC20BalancesByOwner[ + makerAddressLeft + ][this._feeTokenAddress].minus(expectedTransferAmounts.feePaidByLeftMaker); + // Right Maker Fees + expectedNewERC20BalancesByOwner[makerAddressRight][this._feeTokenAddress] = expectedNewERC20BalancesByOwner[ + makerAddressRight + ][this._feeTokenAddress].minus(expectedTransferAmounts.feePaidByRightMaker); + // Taker Fees + expectedNewERC20BalancesByOwner[takerAddress][this._feeTokenAddress] = expectedNewERC20BalancesByOwner[ + takerAddress + ][this._feeTokenAddress].minus(expectedTransferAmounts.totalFeePaidByTaker); + // Left Fee Recipient Fees + expectedNewERC20BalancesByOwner[feeRecipientAddressLeft][ + this._feeTokenAddress + ] = expectedNewERC20BalancesByOwner[feeRecipientAddressLeft][this._feeTokenAddress].add( + expectedTransferAmounts.feeReceivedLeft, + ); + // Right Fee Recipient Fees + expectedNewERC20BalancesByOwner[feeRecipientAddressRight][ + this._feeTokenAddress + ] = expectedNewERC20BalancesByOwner[feeRecipientAddressRight][this._feeTokenAddress].add( + expectedTransferAmounts.feeReceivedRight, + ); + + return [expectedNewERC20BalancesByOwner, expectedNewERC721TokenIdsByOwner]; + } +} |