diff --git a/contracts/utils/CHANGELOG.json b/contracts/utils/CHANGELOG.json index f1aad91e73..b65e3e3c6a 100644 --- a/contracts/utils/CHANGELOG.json +++ b/contracts/utils/CHANGELOG.json @@ -1,4 +1,13 @@ [ + { + "version": "4.6.0", + "changes": [ + { + "note": "Add `uint128` functions to `LibSafeMathV06`", + "pr": 27 + } + ] + }, { "timestamp": 1605302002, "version": "4.5.8", diff --git a/contracts/utils/contracts/src/v06/LibSafeMathV06.sol b/contracts/utils/contracts/src/v06/LibSafeMathV06.sol index f494e5a2cc..3a3940d9d8 100644 --- a/contracts/utils/contracts/src/v06/LibSafeMathV06.sol +++ b/contracts/utils/contracts/src/v06/LibSafeMathV06.sol @@ -105,4 +105,86 @@ library LibSafeMathV06 { { return a < b ? a : b; } + + function safeMul128(uint128 a, uint128 b) + internal + pure + returns (uint128) + { + if (a == 0) { + return 0; + } + uint128 c = a * b; + if (c / a != b) { + LibRichErrorsV06.rrevert(LibSafeMathRichErrorsV06.Uint256BinOpError( + LibSafeMathRichErrorsV06.BinOpErrorCodes.MULTIPLICATION_OVERFLOW, + a, + b + )); + } + return c; + } + + function safeDiv128(uint128 a, uint128 b) + internal + pure + returns (uint128) + { + if (b == 0) { + LibRichErrorsV06.rrevert(LibSafeMathRichErrorsV06.Uint256BinOpError( + LibSafeMathRichErrorsV06.BinOpErrorCodes.DIVISION_BY_ZERO, + a, + b + )); + } + uint128 c = a / b; + return c; + } + + function safeSub128(uint128 a, uint128 b) + internal + pure + returns (uint128) + { + if (b > a) { + LibRichErrorsV06.rrevert(LibSafeMathRichErrorsV06.Uint256BinOpError( + LibSafeMathRichErrorsV06.BinOpErrorCodes.SUBTRACTION_UNDERFLOW, + a, + b + )); + } + return a - b; + } + + function safeAdd128(uint128 a, uint128 b) + internal + pure + returns (uint128) + { + uint128 c = a + b; + if (c < a) { + LibRichErrorsV06.rrevert(LibSafeMathRichErrorsV06.Uint256BinOpError( + LibSafeMathRichErrorsV06.BinOpErrorCodes.ADDITION_OVERFLOW, + a, + b + )); + } + return c; + } + + function max128(uint128 a, uint128 b) + internal + pure + returns (uint128) + { + return a >= b ? a : b; + } + + function min128(uint128 a, uint128 b) + internal + pure + returns (uint128) + { + return a < b ? a : b; + } } diff --git a/contracts/zero-ex/CHANGELOG.json b/contracts/zero-ex/CHANGELOG.json index 412fdc8f38..528f9d5659 100644 --- a/contracts/zero-ex/CHANGELOG.json +++ b/contracts/zero-ex/CHANGELOG.json @@ -55,6 +55,10 @@ { "note": "Add `LibSignature` library", "pr": 21 + }, + { + "note": "Add `LimitOrdersFeature`", + "pr": 27 } ], "timestamp": 1604355662 diff --git a/contracts/zero-ex/compiler.json b/contracts/zero-ex/compiler.json index 20f8637f21..f5e0b42163 100644 --- a/contracts/zero-ex/compiler.json +++ b/contracts/zero-ex/compiler.json @@ -19,7 +19,8 @@ "evm.bytecode.object", "evm.bytecode.sourceMap", "evm.deployedBytecode.object", - "evm.deployedBytecode.sourceMap" + "evm.deployedBytecode.sourceMap", + "evm.methodIdentifiers" ] } } diff --git a/contracts/zero-ex/contracts/src/IZeroEx.sol b/contracts/zero-ex/contracts/src/IZeroEx.sol index beae4e5e2c..160ff71153 100644 --- a/contracts/zero-ex/contracts/src/IZeroEx.sol +++ b/contracts/zero-ex/contracts/src/IZeroEx.sol @@ -27,6 +27,7 @@ import "./features/ITransformERC20Feature.sol"; import "./features/IMetaTransactionsFeature.sol"; import "./features/IUniswapFeature.sol"; import "./features/ILiquidityProviderFeature.sol"; +import "./features/INativeOrdersFeature.sol"; /// @dev Interface for a fully featured Exchange Proxy. @@ -38,7 +39,8 @@ interface IZeroEx is ITransformERC20Feature, IMetaTransactionsFeature, IUniswapFeature, - ILiquidityProviderFeature + ILiquidityProviderFeature, + INativeOrdersFeature { // solhint-disable state-visibility diff --git a/contracts/zero-ex/contracts/src/errors/LibNativeOrdersRichErrors.sol b/contracts/zero-ex/contracts/src/errors/LibNativeOrdersRichErrors.sol new file mode 100644 index 0000000000..83c6e44a6f --- /dev/null +++ b/contracts/zero-ex/contracts/src/errors/LibNativeOrdersRichErrors.sol @@ -0,0 +1,172 @@ +/* + + Copyright 2020 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.6.5; + + +library LibNativeOrdersRichErrors { + + // solhint-disable func-name-mixedcase + + function ProtocolFeeRefundFailed( + address receiver, + uint256 refundAmount + ) + internal + pure + returns (bytes memory) + { + return abi.encodeWithSelector( + bytes4(keccak256("ProtocolFeeRefundFailed(address,uint256)")), + receiver, + refundAmount + ); + } + + function OrderNotFillableByOriginError( + bytes32 orderHash, + address txOrigin, + address orderTxOrigin + ) + internal + pure + returns (bytes memory) + { + return abi.encodeWithSelector( + bytes4(keccak256("OrderNotFillableByOriginError(bytes32,address,address)")), + orderHash, + txOrigin, + orderTxOrigin + ); + } + + function OrderNotFillableError( + bytes32 orderHash, + uint8 orderStatus + ) + internal + pure + returns (bytes memory) + { + return abi.encodeWithSelector( + bytes4(keccak256("OrderNotFillableError(bytes32,uint8)")), + orderHash, + orderStatus + ); + } + + function OrderNotSignedByMakerError( + bytes32 orderHash, + address signer, + address maker + ) + internal + pure + returns (bytes memory) + { + return abi.encodeWithSelector( + bytes4(keccak256("OrderNotSignedByMakerError(bytes32,address,address)")), + orderHash, + signer, + maker + ); + } + + function OrderNotFillableBySenderError( + bytes32 orderHash, + address sender, + address orderSender + ) + internal + pure + returns (bytes memory) + { + return abi.encodeWithSelector( + bytes4(keccak256("OrderNotFillableBySenderError(bytes32,address,address)")), + orderHash, + sender, + orderSender + ); + } + + function OrderNotFillableByTakerError( + bytes32 orderHash, + address taker, + address orderTaker + ) + internal + pure + returns (bytes memory) + { + return abi.encodeWithSelector( + bytes4(keccak256("OrderNotFillableByTakerError(bytes32,address,address)")), + orderHash, + taker, + orderTaker + ); + } + + function CancelSaltTooLowError( + uint256 minValidSalt, + uint256 oldMinValidSalt + ) + internal + pure + returns (bytes memory) + { + return abi.encodeWithSelector( + bytes4(keccak256("CancelSaltTooLowError(uint256,uint256)")), + minValidSalt, + oldMinValidSalt + ); + } + + function FillOrKillFailedError( + bytes32 orderHash, + uint256 takerTokenFilledAmount, + uint256 takerTokenFillAmount + ) + internal + pure + returns (bytes memory) + { + return abi.encodeWithSelector( + bytes4(keccak256("FillOrKillFailedError(bytes32,uint256,uint256)")), + orderHash, + takerTokenFilledAmount, + takerTokenFillAmount + ); + } + + function OnlyOrderMakerAllowed( + bytes32 orderHash, + address sender, + address maker + ) + internal + pure + returns (bytes memory) + { + return abi.encodeWithSelector( + bytes4(keccak256("OnlyOrderMakerAllowed(bytes32,address,address)")), + orderHash, + sender, + maker + ); + } +} diff --git a/contracts/zero-ex/contracts/src/external/FeeCollector.sol b/contracts/zero-ex/contracts/src/external/FeeCollector.sol index 0845b1ff5c..38e128518a 100644 --- a/contracts/zero-ex/contracts/src/external/FeeCollector.sol +++ b/contracts/zero-ex/contracts/src/external/FeeCollector.sol @@ -58,8 +58,8 @@ contract FeeCollector is AuthorizableV06 { onlyAuthorized { // Leave 1 wei behind to avoid expensive zero-->non-zero state change. - if (address(this).balance > 1) { - weth.deposit{value: address(this).balance - 1}(); + if (address(this).balance > 0) { + weth.deposit{value: address(this).balance}(); } } } diff --git a/contracts/zero-ex/contracts/src/features/INativeOrdersFeature.sol b/contracts/zero-ex/contracts/src/features/INativeOrdersFeature.sol new file mode 100644 index 0000000000..0df85ab6be --- /dev/null +++ b/contracts/zero-ex/contracts/src/features/INativeOrdersFeature.sol @@ -0,0 +1,328 @@ +/* + + Copyright 2020 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.6.5; +pragma experimental ABIEncoderV2; + +import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol"; +import "./libs/LibSignature.sol"; +import "./libs/LibNativeOrder.sol"; + + +/// @dev Feature for interacting with limit orders. +interface INativeOrdersFeature { + + /// @dev Emitted whenever a `LimitOrder` is filled. + /// @param orderHash The canonical hash of the order. + /// @param maker The maker of the order. + /// @param taker The taker of the order. + /// @param feeRecipient Fee recipient of the order. + /// @param takerTokenFilledAmount How much taker token was filled. + /// @param makerTokenFilledAmount How much maker token was filled. + /// @param protocolFeePaid How much protocol fee was paid. + /// @param pool The fee pool associated with this order. + event LimitOrderFilled( + bytes32 orderHash, + address maker, + address taker, + address feeRecipient, + address makerToken, + address takerToken, + uint128 takerTokenFilledAmount, + uint128 makerTokenFilledAmount, + uint128 takerTokenFeeFilledAmount, + uint256 protocolFeePaid, + bytes32 pool + ); + + /// @dev Emitted whenever an `RfqOrder` is filled. + /// @param orderHash The canonical hash of the order. + /// @param maker The maker of the order. + /// @param taker The taker of the order. + /// @param takerTokenFilledAmount How much taker token was filled. + /// @param makerTokenFilledAmount How much maker token was filled. + /// @param protocolFeePaid How much protocol fee was paid. + /// @param pool The fee pool associated with this order. + event RfqOrderFilled( + bytes32 orderHash, + address maker, + address taker, + address makerToken, + address takerToken, + uint128 takerTokenFilledAmount, + uint128 makerTokenFilledAmount, + uint256 protocolFeePaid, + bytes32 pool + ); + + /// @dev Emitted whenever a limit or RFQ order is cancelled. + /// @param orderHash The canonical hash of the order. + /// @param maker The order maker. + event OrderCancelled( + bytes32 orderHash, + address maker + ); + + /// @dev Emitted whenever limit or RFQ orders are cancelled by pair by a maker. + /// @param maker The maker of the order. + /// @param makerToken The maker token in a pair for the orders cancelled. + /// @param takerToken The taker token in a pair for the orders cancelled. + /// @param minValidSalt The new minimum valid salt an order with this pair must + /// have. + event PairOrdersCancelled( + address maker, + address makerToken, + address takerToken, + uint256 minValidSalt + ); + + /// @dev Transfers protocol fees from the `FeeCollector` pools into + /// the staking contract. + /// @param poolIds Staking pool IDs + function transferProtocolFeesForPools(bytes32[] calldata poolIds) + external; + + /// @dev Fill a limit order. The taker and sender will be the caller. + /// @param order The limit order. ETH protocol fees can be + /// attached to this call. Any unspent ETH will be refunded to + /// the caller. + /// @param signature The order signature. + /// @param takerTokenFillAmount Maximum taker token amount to fill this order with. + /// @return takerTokenFilledAmount How much maker token was filled. + /// @return makerTokenFilledAmount How much maker token was filled. + function fillLimitOrder( + LibNativeOrder.LimitOrder calldata order, + LibSignature.Signature calldata signature, + uint128 takerTokenFillAmount + ) + external + payable + returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount); + + /// @dev Fill an RFQ order for up to `takerTokenFillAmount` taker tokens. + /// The taker will be the caller. ETH should be attached to pay the + /// protocol fee. + /// @param order The RFQ order. + /// @param signature The order signature. + /// @param takerTokenFillAmount Maximum taker token amount to fill this order with. + /// @return takerTokenFilledAmount How much maker token was filled. + /// @return makerTokenFilledAmount How much maker token was filled. + function fillRfqOrder( + LibNativeOrder.RfqOrder calldata order, + LibSignature.Signature calldata signature, + uint128 takerTokenFillAmount + ) + external + payable + returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount); + + /// @dev Fill an RFQ order for exactly `takerTokenFillAmount` taker tokens. + /// The taker will be the caller. ETH protocol fees can be + /// attached to this call. Any unspent ETH will be refunded to + /// the caller. + /// @param order The limit order. + /// @param signature The order signature. + /// @param takerTokenFillAmount How much taker token to fill this order with. + /// @return makerTokenFilledAmount How much maker token was filled. + function fillOrKillLimitOrder( + LibNativeOrder.LimitOrder calldata order, + LibSignature.Signature calldata signature, + uint128 takerTokenFillAmount + ) + external + payable + returns (uint128 makerTokenFilledAmount); + + /// @dev Fill an RFQ order for exactly `takerTokenFillAmount` taker tokens. + /// The taker will be the caller. ETH protocol fees can be + /// attached to this call. Any unspent ETH will be refunded to + /// the caller. + /// @param order The RFQ order. + /// @param signature The order signature. + /// @param takerTokenFillAmount How much taker token to fill this order with. + /// @return makerTokenFilledAmount How much maker token was filled. + function fillOrKillRfqOrder( + LibNativeOrder.RfqOrder calldata order, + LibSignature.Signature calldata signature, + uint128 takerTokenFillAmount + ) + external + payable + returns (uint128 makerTokenFilledAmount); + + /// @dev Fill a limit order. Internal variant. ETH protocol fees can be + /// attached to this call. Any unspent ETH will be refunded to + /// `msg.sender` (not `sender`). + /// @param order The limit order. + /// @param signature The order signature. + /// @param takerTokenFillAmount Maximum taker token to fill this order with. + /// @param taker The order taker. + /// @param sender The order sender. + /// @return takerTokenFilledAmount How much maker token was filled. + /// @return makerTokenFilledAmount How much maker token was filled. + function _fillLimitOrder( + LibNativeOrder.LimitOrder calldata order, + LibSignature.Signature calldata signature, + uint128 takerTokenFillAmount, + address taker, + address sender + ) + external + payable + returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount); + + /// @dev Fill an RFQ order. Internal variant. ETH protocol fees can be + /// attached to this call. Any unspent ETH will be refunded to + /// `msg.sender` (not `sender`). + /// @param order The RFQ order. + /// @param signature The order signature. + /// @param takerTokenFillAmount Maximum taker token to fill this order with. + /// @param taker The order taker. + /// @return takerTokenFilledAmount How much maker token was filled. + /// @return makerTokenFilledAmount How much maker token was filled. + function _fillRfqOrder( + LibNativeOrder.RfqOrder calldata order, + LibSignature.Signature calldata signature, + uint128 takerTokenFillAmount, + address taker + ) + external + payable + returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount); + + /// @dev Cancel a single limit order. The caller must be the maker. + /// Silently succeeds if the order has already been cancelled. + /// @param order The limit order. + function cancelLimitOrder(LibNativeOrder.LimitOrder calldata order) + external; + + /// @dev Cancel a single RFQ order. The caller must be the maker. + /// Silently succeeds if the order has already been cancelled. + /// @param order The RFQ order. + function cancelRfqOrder(LibNativeOrder.RfqOrder calldata order) + external; + + /// @dev Cancel multiple limit orders. The caller must be the maker. + /// Silently succeeds if the order has already been cancelled. + /// @param orders The limit orders. + function batchCancelLimitOrders(LibNativeOrder.LimitOrder[] calldata orders) + external; + + /// @dev Cancel multiple RFQ orders. The caller must be the maker. + /// Silently succeeds if the order has already been cancelled. + /// @param orders The RFQ orders. + function batchCancelRfqOrders(LibNativeOrder.RfqOrder[] calldata orders) + external; + + /// @dev Cancel all limit orders for a given maker and pair with a salt less + /// than the value provided. The caller must be the maker. Subsequent + /// calls to this function with the same caller and pair require the + /// new salt to be >= the old salt. + /// @param makerToken The maker token. + /// @param takerToken The taker token. + /// @param minValidSalt The new minimum valid salt. + function cancelPairLimitOrders( + IERC20TokenV06 makerToken, + IERC20TokenV06 takerToken, + uint256 minValidSalt + ) + external; + + /// @dev Cancel all limit orders for a given maker and pair with a salt less + /// than the value provided. The caller must be the maker. Subsequent + /// calls to this function with the same caller and pair require the + /// new salt to be >= the old salt. + /// @param makerTokens The maker tokens. + /// @param takerTokens The taker tokens. + /// @param minValidSalts The new minimum valid salts. + function batchCancelPairLimitOrders( + IERC20TokenV06[] calldata makerTokens, + IERC20TokenV06[] calldata takerTokens, + uint256[] calldata minValidSalts + ) + external; + + /// @dev Cancel all RFQ orders for a given maker and pair with a salt less + /// than the value provided. The caller must be the maker. Subsequent + /// calls to this function with the same caller and pair require the + /// new salt to be >= the old salt. + /// @param makerToken The maker token. + /// @param takerToken The taker token. + /// @param minValidSalt The new minimum valid salt. + function cancelPairRfqOrders( + IERC20TokenV06 makerToken, + IERC20TokenV06 takerToken, + uint256 minValidSalt + ) + external; + + /// @dev Cancel all RFQ orders for a given maker and pair with a salt less + /// than the value provided. The caller must be the maker. Subsequent + /// calls to this function with the same caller and pair require the + /// new salt to be >= the old salt. + /// @param makerTokens The maker tokens. + /// @param takerTokens The taker tokens. + /// @param minValidSalts The new minimum valid salts. + function batchCancelPairRfqOrders( + IERC20TokenV06[] calldata makerTokens, + IERC20TokenV06[] calldata takerTokens, + uint256[] calldata minValidSalts + ) + external; + + /// @dev Get the order info for a limit order. + /// @param order The limit order. + /// @return orderInfo Info about the order. + function getLimitOrderInfo(LibNativeOrder.LimitOrder calldata order) + external + view + returns (LibNativeOrder.OrderInfo memory orderInfo); + + /// @dev Get the order info for an RFQ order. + /// @param order The RFQ order. + /// @return orderInfo Info about the order. + function getRfqOrderInfo(LibNativeOrder.RfqOrder calldata order) + external + view + returns (LibNativeOrder.OrderInfo memory orderInfo); + + /// @dev Get the canonical hash of a limit order. + /// @param order The limit order. + /// @return orderHash The order hash. + function getLimitOrderHash(LibNativeOrder.LimitOrder calldata order) + external + view + returns (bytes32 orderHash); + + /// @dev Get the canonical hash of an RFQ order. + /// @param order The RFQ order. + /// @return orderHash The order hash. + function getRfqOrderHash(LibNativeOrder.RfqOrder calldata order) + external + view + returns (bytes32 orderHash); + + /// @dev Get the protocol fee multiplier. This should be multiplied by the + /// gas price to arrive at the required protocol fee to fill a native order. + /// @return multiplier The protocol fee multiplier. + function getProtocolFeeMultiplier() + external + view + returns (uint32 multiplier); + +} diff --git a/contracts/zero-ex/contracts/src/features/NativeOrdersFeature.sol b/contracts/zero-ex/contracts/src/features/NativeOrdersFeature.sol new file mode 100644 index 0000000000..5a4b4ede1e --- /dev/null +++ b/contracts/zero-ex/contracts/src/features/NativeOrdersFeature.sol @@ -0,0 +1,984 @@ +/* + + Copyright 2020 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.6.5; +pragma experimental ABIEncoderV2; + +import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol"; +import "@0x/contracts-erc20/contracts/src/v06/IEtherTokenV06.sol"; +import "@0x/contracts-erc20/contracts/src/v06/LibERC20TokenV06.sol"; +import "@0x/contracts-utils/contracts/src/v06/errors/LibRichErrorsV06.sol"; +import "@0x/contracts-utils/contracts/src/v06/LibSafeMathV06.sol"; +import "@0x/contracts-utils/contracts/src/v06/LibMathV06.sol"; +import "../fixins/FixinCommon.sol"; +import "../fixins/FixinProtocolFees.sol"; +import "../fixins/FixinEIP712.sol"; +import "../errors/LibNativeOrdersRichErrors.sol"; +import "../migrations/LibMigrate.sol"; +import "../storage/LibNativeOrdersStorage.sol"; +import "../vendor/v3/IStaking.sol"; +import "./libs/LibTokenSpender.sol"; +import "./libs/LibSignature.sol"; +import "./libs/LibNativeOrder.sol"; +import "./INativeOrdersFeature.sol"; +import "./IFeature.sol"; + + +/// @dev Feature for interacting with limit orders. +contract NativeOrdersFeature is + IFeature, + INativeOrdersFeature, + FixinCommon, + FixinProtocolFees, + FixinEIP712 +{ + using LibSafeMathV06 for uint256; + using LibSafeMathV06 for uint128; + using LibRichErrorsV06 for bytes; + + /// @dev Params for `_settleOrder()`. + struct SettleOrderInfo { + // Order hash. + bytes32 orderHash; + // Maker of the order. + address maker; + // Taker of the order. + address taker; + // Maker token. + IERC20TokenV06 makerToken; + // Taker token. + IERC20TokenV06 takerToken; + // Maker token amount. + uint128 makerAmount; + // Taker token amount. + uint128 takerAmount; + // Maximum taker token amount to fill. + uint128 takerTokenFillAmount; + // How much taker token amount has already been filled in this order. + uint128 takerTokenFilledAmount; + } + + /// @dev Params for `_fillLimitOrderPrivate()` + struct FillLimitOrderPrivateParams { + // The limit order. + LibNativeOrder.LimitOrder order; + // The order signature. + LibSignature.Signature signature; + // Maximum taker token to fill this order with. + uint128 takerTokenFillAmount; + // The order taker. + address taker; + // The order sender. + address sender; + } + + // @dev Fill results returned by `_fillLimitOrderPrivate()` and + /// `_fillRfqOrderPrivate()`. + struct FillNativeOrderResults { + uint256 ethProtocolFeePaid; + uint128 takerTokenFilledAmount; + uint128 makerTokenFilledAmount; + uint128 takerTokenFeeFilledAmount; + } + + /// @dev Name of this feature. + string public constant override FEATURE_NAME = "LimitOrders"; + /// @dev Version of this feature. + uint256 public immutable override FEATURE_VERSION = _encodeVersion(1, 0, 0); + /// @dev Highest bit of a uint256, used to flag cancelled orders. + uint256 private constant HIGH_BIT = 1 << 255; + + constructor( + address zeroExAddress, + IEtherTokenV06 weth, + IStaking staking, + uint32 protocolFeeMultiplier + ) + public + FixinEIP712(zeroExAddress) + FixinProtocolFees(weth, staking, protocolFeeMultiplier) + { + // solhint-disable no-empty-blocks + } + + /// @dev Initialize and register this feature. + /// Should be delegatecalled by `Migrate.migrate()`. + /// @return success `LibMigrate.SUCCESS` on success. + function migrate() + external + returns (bytes4 success) + { + _registerFeatureFunction(this.transferProtocolFeesForPools.selector); + _registerFeatureFunction(this.fillLimitOrder.selector); + _registerFeatureFunction(this.fillRfqOrder.selector); + _registerFeatureFunction(this.fillOrKillLimitOrder.selector); + _registerFeatureFunction(this.fillOrKillRfqOrder.selector); + _registerFeatureFunction(this._fillLimitOrder.selector); + _registerFeatureFunction(this._fillRfqOrder.selector); + _registerFeatureFunction(this.cancelLimitOrder.selector); + _registerFeatureFunction(this.cancelRfqOrder.selector); + _registerFeatureFunction(this.batchCancelLimitOrders.selector); + _registerFeatureFunction(this.batchCancelRfqOrders.selector); + _registerFeatureFunction(this.cancelPairLimitOrders.selector); + _registerFeatureFunction(this.batchCancelPairLimitOrders.selector); + _registerFeatureFunction(this.cancelPairRfqOrders.selector); + _registerFeatureFunction(this.batchCancelPairRfqOrders.selector); + _registerFeatureFunction(this.getLimitOrderInfo.selector); + _registerFeatureFunction(this.getRfqOrderInfo.selector); + _registerFeatureFunction(this.getLimitOrderHash.selector); + _registerFeatureFunction(this.getRfqOrderHash.selector); + _registerFeatureFunction(this.getProtocolFeeMultiplier.selector); + return LibMigrate.MIGRATE_SUCCESS; + } + + /// @dev Transfers protocol fees from the `FeeCollector` pools into + /// the staking contract. + /// @param poolIds Staking pool IDs + function transferProtocolFeesForPools(bytes32[] calldata poolIds) + external + override + { + for (uint256 i = 0; i < poolIds.length; ++i) { + _transferFeesForPool(poolIds[i]); + } + } + + /// @dev Fill a limit order. The taker and sender will be the caller. + /// @param order The limit order. ETH protocol fees can be + /// attached to this call. Any unspent ETH will be refunded to + /// the caller. + /// @param signature The order signature. + /// @param takerTokenFillAmount Maximum taker token amount to fill this order with. + /// @return takerTokenFilledAmount How much maker token was filled. + /// @return makerTokenFilledAmount How much maker token was filled. + function fillLimitOrder( + LibNativeOrder.LimitOrder memory order, + LibSignature.Signature memory signature, + uint128 takerTokenFillAmount + ) + public + override + payable + returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount) + { + FillNativeOrderResults memory results = + _fillLimitOrderPrivate(FillLimitOrderPrivateParams({ + order: order, + signature: signature, + takerTokenFillAmount: takerTokenFillAmount, + taker: msg.sender, + sender: msg.sender + })); + _refundExcessProtocolFeeToSender(results.ethProtocolFeePaid); + (takerTokenFilledAmount, makerTokenFilledAmount) = ( + results.takerTokenFilledAmount, + results.makerTokenFilledAmount + ); + } + + /// @dev Fill an RFQ order for up to `takerTokenFillAmount` taker tokens. + /// The taker will be the caller. ETH should be attached to pay the + /// protocol fee. + /// @param order The RFQ order. + /// @param signature The order signature. + /// @param takerTokenFillAmount Maximum taker token amount to fill this order with. + /// @return takerTokenFilledAmount How much maker token was filled. + /// @return makerTokenFilledAmount How much maker token was filled. + function fillRfqOrder( + LibNativeOrder.RfqOrder memory order, + LibSignature.Signature memory signature, + uint128 takerTokenFillAmount + ) + public + override + payable + returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount) + { + FillNativeOrderResults memory results = + _fillRfqOrderPrivate( + order, + signature, + takerTokenFillAmount, + msg.sender + ); + _refundExcessProtocolFeeToSender(results.ethProtocolFeePaid); + (takerTokenFilledAmount, makerTokenFilledAmount) = ( + results.takerTokenFilledAmount, + results.makerTokenFilledAmount + ); + } + + /// @dev Fill an RFQ order for exactly `takerTokenFillAmount` taker tokens. + /// The taker will be the caller. ETH protocol fees can be + /// attached to this call. Any unspent ETH will be refunded to + /// the caller. + /// @param order The limit order. + /// @param signature The order signature. + /// @param takerTokenFillAmount How much taker token to fill this order with. + /// @return makerTokenFilledAmount How much maker token was filled. + function fillOrKillLimitOrder( + LibNativeOrder.LimitOrder memory order, + LibSignature.Signature memory signature, + uint128 takerTokenFillAmount + ) + public + override + payable + returns (uint128 makerTokenFilledAmount) + { + FillNativeOrderResults memory results = + _fillLimitOrderPrivate(FillLimitOrderPrivateParams({ + order: order, + signature: signature, + takerTokenFillAmount: takerTokenFillAmount, + taker: msg.sender, + sender: msg.sender + })); + // Must have filled exactly the amount requested. + if (results.takerTokenFilledAmount < takerTokenFillAmount) { + LibNativeOrdersRichErrors.FillOrKillFailedError( + getLimitOrderHash(order), + results.takerTokenFilledAmount, + takerTokenFillAmount + ).rrevert(); + } + _refundExcessProtocolFeeToSender(results.ethProtocolFeePaid); + makerTokenFilledAmount = results.makerTokenFilledAmount; + } + + /// @dev Fill an RFQ order for exactly `takerTokenFillAmount` taker tokens. + /// The taker will be the caller. ETH protocol fees can be + /// attached to this call. Any unspent ETH will be refunded to + /// the caller. + /// @param order The RFQ order. + /// @param signature The order signature. + /// @param takerTokenFillAmount How much taker token to fill this order with. + /// @return makerTokenFilledAmount How much maker token was filled. + function fillOrKillRfqOrder( + LibNativeOrder.RfqOrder memory order, + LibSignature.Signature memory signature, + uint128 takerTokenFillAmount + ) + public + override + payable + returns (uint128 makerTokenFilledAmount) + { + FillNativeOrderResults memory results = + _fillRfqOrderPrivate( + order, + signature, + takerTokenFillAmount, + msg.sender + ); + // Must have filled exactly the amount requested. + if (results.takerTokenFilledAmount < takerTokenFillAmount) { + LibNativeOrdersRichErrors.FillOrKillFailedError( + getRfqOrderHash(order), + results.takerTokenFilledAmount, + takerTokenFillAmount + ).rrevert(); + } + _refundExcessProtocolFeeToSender(results.ethProtocolFeePaid); + makerTokenFilledAmount = results.makerTokenFilledAmount; + } + + /// @dev Fill a limit order. Internal variant. ETH protocol fees can be + /// attached to this call. Any unspent ETH will be refunded to + /// `msg.sender` (not `sender`). + /// @param order The limit order. + /// @param signature The order signature. + /// @param takerTokenFillAmount Maximum taker token to fill this order with. + /// @param taker The order taker. + /// @param sender The order sender. + /// @return takerTokenFilledAmount How much maker token was filled. + /// @return makerTokenFilledAmount How much maker token was filled. + function _fillLimitOrder( + LibNativeOrder.LimitOrder memory order, + LibSignature.Signature memory signature, + uint128 takerTokenFillAmount, + address taker, + address sender + ) + public + override + payable + onlySelf + returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount) + { + FillNativeOrderResults memory results = + _fillLimitOrderPrivate(FillLimitOrderPrivateParams({ + order: order, + signature: signature, + takerTokenFillAmount: takerTokenFillAmount, + taker: taker, + sender: sender + })); + _refundExcessProtocolFeeToSender(results.ethProtocolFeePaid); + (takerTokenFilledAmount, makerTokenFilledAmount) = ( + results.takerTokenFilledAmount, + results.makerTokenFilledAmount + ); + } + + /// @dev Fill an RFQ order. Internal variant. ETH protocol fees can be + /// attached to this call. Any unspent ETH will be refunded to + /// `msg.sender` (not `sender`). + /// @param order The RFQ order. + /// @param signature The order signature. + /// @param takerTokenFillAmount Maximum taker token to fill this order with. + /// @param taker The order taker. + /// @return takerTokenFilledAmount How much maker token was filled. + /// @return makerTokenFilledAmount How much maker token was filled. + function _fillRfqOrder( + LibNativeOrder.RfqOrder memory order, + LibSignature.Signature memory signature, + uint128 takerTokenFillAmount, + address taker + ) + public + override + payable + onlySelf + returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount) + { + FillNativeOrderResults memory results = + _fillRfqOrderPrivate( + order, + signature, + takerTokenFillAmount, + taker + ); + _refundExcessProtocolFeeToSender(results.ethProtocolFeePaid); + (takerTokenFilledAmount, makerTokenFilledAmount) = ( + results.takerTokenFilledAmount, + results.makerTokenFilledAmount + ); + } + + /// @dev Cancel a single limit order. The caller must be the maker. + /// Silently succeeds if the order has already been cancelled. + /// @param order The limit order. + function cancelLimitOrder(LibNativeOrder.LimitOrder memory order) + public + override + { + bytes32 orderHash = getLimitOrderHash(order); + if (msg.sender != order.maker) { + LibNativeOrdersRichErrors.OnlyOrderMakerAllowed( + orderHash, + msg.sender, + order.maker + ).rrevert(); + } + _cancelOrderHash(orderHash, order.maker); + } + + /// @dev Cancel a single RFQ order. The caller must be the maker. + /// Silently succeeds if the order has already been cancelled. + /// @param order The RFQ order. + function cancelRfqOrder(LibNativeOrder.RfqOrder memory order) + public + override + { + bytes32 orderHash = getRfqOrderHash(order); + if (msg.sender != order.maker) { + LibNativeOrdersRichErrors.OnlyOrderMakerAllowed( + orderHash, + msg.sender, + order.maker + ).rrevert(); + } + _cancelOrderHash(orderHash, order.maker); + } + + /// @dev Cancel multiple limit orders. The caller must be the maker. + /// Silently succeeds if the order has already been cancelled. + /// @param orders The limit orders. + function batchCancelLimitOrders(LibNativeOrder.LimitOrder[] memory orders) + public + override + { + for (uint256 i = 0; i < orders.length; ++i) { + cancelLimitOrder(orders[i]); + } + } + + /// @dev Cancel multiple RFQ orders. The caller must be the maker. + /// Silently succeeds if the order has already been cancelled. + /// @param orders The RFQ orders. + function batchCancelRfqOrders(LibNativeOrder.RfqOrder[] memory orders) + public + override + { + for (uint256 i = 0; i < orders.length; ++i) { + cancelRfqOrder(orders[i]); + } + } + + /// @dev Cancel all limit orders for a given maker and pair with a salt less + /// than the value provided. The caller must be the maker. Subsequent + /// calls to this function with the same caller and pair require the + /// new salt to be >= the old salt. + /// @param makerToken The maker token. + /// @param takerToken The taker token. + /// @param minValidSalt The new minimum valid salt. + function cancelPairLimitOrders( + IERC20TokenV06 makerToken, + IERC20TokenV06 takerToken, + uint256 minValidSalt + ) + public + override + { + LibNativeOrdersStorage.Storage storage stor = + LibNativeOrdersStorage.getStorage(); + + uint256 oldMinValidSalt = + stor.limitOrdersMakerToMakerTokenToTakerTokenToMinValidOrderSalt + [msg.sender] + [address(makerToken)] + [address(takerToken)]; + + // New min salt must >= the old one. + if (oldMinValidSalt > minValidSalt) { + LibNativeOrdersRichErrors. + CancelSaltTooLowError(minValidSalt, oldMinValidSalt) + .rrevert(); + } + + stor.limitOrdersMakerToMakerTokenToTakerTokenToMinValidOrderSalt + [msg.sender] + [address(makerToken)] + [address(takerToken)] = minValidSalt; + + emit PairOrdersCancelled( + msg.sender, + address(makerToken), + address(takerToken), + minValidSalt + ); + } + + /// @dev Cancel all limit orders for a given maker and pair with a salt less + /// than the value provided. The caller must be the maker. Subsequent + /// calls to this function with the same caller and pair require the + /// new salt to be >= the old salt. + /// @param makerTokens The maker tokens. + /// @param takerTokens The taker tokens. + /// @param minValidSalts The new minimum valid salts. + function batchCancelPairLimitOrders( + IERC20TokenV06[] memory makerTokens, + IERC20TokenV06[] memory takerTokens, + uint256[] memory minValidSalts + ) + public + override + { + require( + makerTokens.length == takerTokens.length && + makerTokens.length == minValidSalts.length, + "LimitOrdersFeature/MISMATCHED_PAIR_ORDERS_ARRAY_LENGTHS" + ); + + for (uint256 i = 0; i < makerTokens.length; ++i) { + cancelPairLimitOrders( + makerTokens[i], + takerTokens[i], + minValidSalts[i] + ); + } + } + + /// @dev Cancel all RFQ orders for a given maker and pair with a salt less + /// than the value provided. The caller must be the maker. Subsequent + /// calls to this function with the same caller and pair require the + /// new salt to be >= the old salt. + /// @param makerToken The maker token. + /// @param takerToken The taker token. + /// @param minValidSalt The new minimum valid salt. + function cancelPairRfqOrders( + IERC20TokenV06 makerToken, + IERC20TokenV06 takerToken, + uint256 minValidSalt + ) + public + override + { + LibNativeOrdersStorage.Storage storage stor = + LibNativeOrdersStorage.getStorage(); + + uint256 oldMinValidSalt = + stor.rfqOrdersMakerToMakerTokenToTakerTokenToMinValidOrderSalt + [msg.sender] + [address(makerToken)] + [address(takerToken)]; + + // New min salt must >= the old one. + if (oldMinValidSalt > minValidSalt) { + LibNativeOrdersRichErrors. + CancelSaltTooLowError(minValidSalt, oldMinValidSalt) + .rrevert(); + } + + stor.rfqOrdersMakerToMakerTokenToTakerTokenToMinValidOrderSalt + [msg.sender] + [address(makerToken)] + [address(takerToken)] = minValidSalt; + + emit PairOrdersCancelled( + msg.sender, + address(makerToken), + address(takerToken), + minValidSalt + ); + } + + /// @dev Cancel all RFQ orders for a given maker and pair with a salt less + /// than the value provided. The caller must be the maker. Subsequent + /// calls to this function with the same caller and pair require the + /// new salt to be >= the old salt. + /// @param makerTokens The maker tokens. + /// @param takerTokens The taker tokens. + /// @param minValidSalts The new minimum valid salts. + function batchCancelPairRfqOrders( + IERC20TokenV06[] memory makerTokens, + IERC20TokenV06[] memory takerTokens, + uint256[] memory minValidSalts + ) + public + override + { + require( + makerTokens.length == takerTokens.length && + makerTokens.length == minValidSalts.length, + "LimitOrdersFeature/MISMATCHED_PAIR_ORDERS_ARRAY_LENGTHS" + ); + + for (uint256 i = 0; i < makerTokens.length; ++i) { + cancelPairRfqOrders( + makerTokens[i], + takerTokens[i], + minValidSalts[i] + ); + } + } + + /// @dev Get the order info for a limit order. + /// @param order The limit order. + /// @return orderInfo Info about the order. + function getLimitOrderInfo(LibNativeOrder.LimitOrder memory order) + public + override + view + returns (LibNativeOrder.OrderInfo memory orderInfo) + { + // Recover maker and compute order hash. + orderInfo.orderHash = getLimitOrderHash(order); + uint256 minValidSalt = LibNativeOrdersStorage.getStorage() + .limitOrdersMakerToMakerTokenToTakerTokenToMinValidOrderSalt + [order.maker] + [address(order.makerToken)] + [address(order.takerToken)]; + _populateCommonOrderInfoFields( + orderInfo, + order.takerAmount, + order.expiry, + order.salt, + minValidSalt + ); + } + + /// @dev Get the order info for an RFQ order. + /// @param order The RFQ order. + /// @return orderInfo Info about the order. + function getRfqOrderInfo(LibNativeOrder.RfqOrder memory order) + public + override + view + returns (LibNativeOrder.OrderInfo memory orderInfo) + { + // Recover maker and compute order hash. + orderInfo.orderHash = getRfqOrderHash(order); + uint256 minValidSalt = LibNativeOrdersStorage.getStorage() + .rfqOrdersMakerToMakerTokenToTakerTokenToMinValidOrderSalt + [order.maker] + [address(order.makerToken)] + [address(order.takerToken)]; + _populateCommonOrderInfoFields( + orderInfo, + order.takerAmount, + order.expiry, + order.salt, + minValidSalt + ); + } + + /// @dev Get the canonical hash of a limit order. + /// @param order The limit order. + /// @return orderHash The order hash. + function getLimitOrderHash(LibNativeOrder.LimitOrder memory order) + public + override + view + returns (bytes32 orderHash) + { + return _getEIP712Hash( + LibNativeOrder.getLimitOrderStructHash(order) + ); + } + + /// @dev Get the canonical hash of an RFQ order. + /// @param order The RFQ order. + /// @return orderHash The order hash. + function getRfqOrderHash(LibNativeOrder.RfqOrder memory order) + public + override + view + returns (bytes32 orderHash) + { + return _getEIP712Hash( + LibNativeOrder.getRfqOrderStructHash(order) + ); + } + + /// @dev Get the protocol fee multiplier. This should be multiplied by the + /// gas price to arrive at the required protocol fee to fill a native order. + /// @return multiplier The protocol fee multiplier. + function getProtocolFeeMultiplier() + external + override + view + returns (uint32 multiplier) + { + return PROTOCOL_FEE_MULTIPLIER; + } + + /// @dev Populate `status` and `takerTokenFilledAmount` fields in + /// `orderInfo`, which use the same code path for both limit and + /// RFQ orders. + /// @param orderInfo `OrderInfo` with `orderHash` and `maker` filled. + /// @param takerAmount The order's taker token amount.. + /// @param expiry The order's expiry. + /// @param salt The order's salt. + /// @param salt The minimum valid salt for the maker and pair combination. + function _populateCommonOrderInfoFields( + LibNativeOrder.OrderInfo memory orderInfo, + uint128 takerAmount, + uint64 expiry, + uint256 salt, + uint256 minValidSalt + ) + private + view + { + LibNativeOrdersStorage.Storage storage stor = + LibNativeOrdersStorage.getStorage(); + + // Get the filled and direct cancel state. + { + // The high bit of the raw taker token filled amount will be set + // if the order was cancelled. + uint256 rawTakerTokenFilledAmount = + stor.orderHashToTakerTokenFilledAmount[orderInfo.orderHash]; + orderInfo.takerTokenFilledAmount = uint128(rawTakerTokenFilledAmount); + if (orderInfo.takerTokenFilledAmount >= takerAmount) { + orderInfo.status = LibNativeOrder.OrderStatus.FILLED; + return; + } + if (rawTakerTokenFilledAmount & HIGH_BIT != 0) { + orderInfo.status = LibNativeOrder.OrderStatus.CANCELLED; + return; + } + } + + // Check for expiration. + if (expiry <= uint64(block.timestamp)) { + orderInfo.status = LibNativeOrder.OrderStatus.EXPIRED; + return; + } + + // Check if the order was cancelled by salt. + if (minValidSalt > salt) { + orderInfo.status = LibNativeOrder.OrderStatus.CANCELLED; + return; + } + orderInfo.status = LibNativeOrder.OrderStatus.FILLABLE; + } + + /// @dev Cancel a limit or RFQ order directly by its order hash. + /// @param orderHash The order's order hash. + /// @param maker The order's maker. + function _cancelOrderHash(bytes32 orderHash, address maker) + private + { + LibNativeOrdersStorage.Storage storage stor = + LibNativeOrdersStorage.getStorage(); + // Set the high bit on the raw taker token fill amount to indicate + // a cancel. It's OK to cancel twice. + stor.orderHashToTakerTokenFilledAmount[orderHash] |= HIGH_BIT; + + emit OrderCancelled(orderHash, msg.sender); + } + + /// @dev Fill a limit order. Private variant. Does not refund protocol fees. + /// @param params Function params. + /// @return results Results of the fill. + function _fillLimitOrderPrivate(FillLimitOrderPrivateParams memory params) + private + returns (FillNativeOrderResults memory results) + { + LibNativeOrder.OrderInfo memory orderInfo = getLimitOrderInfo(params.order); + + // Must be fillable. + if (orderInfo.status != LibNativeOrder.OrderStatus.FILLABLE) { + LibNativeOrdersRichErrors.OrderNotFillableError( + orderInfo.orderHash, + uint8(orderInfo.status) + ).rrevert(); + } + + // Must be fillable by the taker. + if (params.order.taker != address(0) && params.order.taker != params.taker) { + LibNativeOrdersRichErrors.OrderNotFillableByTakerError( + orderInfo.orderHash, + params.taker, + params.order.taker + ).rrevert(); + } + + // Must be fillable by the sender. + if (params.order.sender != address(0) && params.order.sender != params.sender) { + LibNativeOrdersRichErrors.OrderNotFillableBySenderError( + orderInfo.orderHash, + params.sender, + params.order.sender + ).rrevert(); + } + + // Signature must be valid for the order. + { + address signer = LibSignature.getSignerOfHash( + orderInfo.orderHash, + params.signature + ); + if (signer != params.order.maker) { + LibNativeOrdersRichErrors.OrderNotSignedByMakerError( + orderInfo.orderHash, + signer, + params.order.maker + ).rrevert(); + } + } + + // Pay the protocol fee. + results.ethProtocolFeePaid = _collectProtocolFee(params.order.pool); + + // Settle between the maker and taker. + (results.takerTokenFilledAmount, results.makerTokenFilledAmount) = _settleOrder( + SettleOrderInfo({ + orderHash: orderInfo.orderHash, + maker: params.order.maker, + taker: params.taker, + makerToken: IERC20TokenV06(params.order.makerToken), + takerToken: IERC20TokenV06(params.order.takerToken), + makerAmount: params.order.makerAmount, + takerAmount: params.order.takerAmount, + takerTokenFillAmount: params.takerTokenFillAmount, + takerTokenFilledAmount: orderInfo.takerTokenFilledAmount + }) + ); + + // Pay the fee recipient. + if (params.order.takerTokenFeeAmount > 0) { + results.takerTokenFeeFilledAmount = uint128(LibMathV06.getPartialAmountFloor( + results.takerTokenFilledAmount, + params.order.takerAmount, + params.order.takerTokenFeeAmount + )); + LibTokenSpender.spendERC20Tokens( + params.order.takerToken, + params.taker, + params.order.feeRecipient, + uint256(results.takerTokenFeeFilledAmount) + ); + } + + emit LimitOrderFilled( + orderInfo.orderHash, + params.order.maker, + params.taker, + params.order.feeRecipient, + address(params.order.makerToken), + address(params.order.takerToken), + results.takerTokenFilledAmount, + results.makerTokenFilledAmount, + results.takerTokenFeeFilledAmount, + results.ethProtocolFeePaid, + params.order.pool + ); + } + + /// @dev Fill an RFQ order. Private variant. Does not refund protocol fees. + /// @param order The RFQ order. + /// @param signature The order signature. + /// @param takerTokenFillAmount Maximum taker token to fill this order with. + /// @param taker The order taker. + /// @return results Results of the fill. + function _fillRfqOrderPrivate( + LibNativeOrder.RfqOrder memory order, + LibSignature.Signature memory signature, + uint128 takerTokenFillAmount, + address taker + ) + private + returns (FillNativeOrderResults memory results) + { + LibNativeOrder.OrderInfo memory orderInfo = getRfqOrderInfo(order); + + // Must be fillable. + if (orderInfo.status != LibNativeOrder.OrderStatus.FILLABLE) { + LibNativeOrdersRichErrors.OrderNotFillableError( + orderInfo.orderHash, + uint8(orderInfo.status) + ).rrevert(); + } + + // Must be fillable by the tx.origin. + if (order.txOrigin != address(0) && order.txOrigin != tx.origin) { + LibNativeOrdersRichErrors.OrderNotFillableByOriginError( + orderInfo.orderHash, + tx.origin, + order.txOrigin + ).rrevert(); + } + + // Signature must be valid for the order. + { + address signer = LibSignature.getSignerOfHash(orderInfo.orderHash, signature); + if (signer != order.maker) { + LibNativeOrdersRichErrors.OrderNotSignedByMakerError( + orderInfo.orderHash, + signer, + order.maker + ).rrevert(); + } + } + + // Pay the protocol fee. + results.ethProtocolFeePaid = _collectProtocolFee(order.pool); + + // Settle between the maker and taker. + (results.takerTokenFilledAmount, results.makerTokenFilledAmount) = _settleOrder( + SettleOrderInfo({ + orderHash: orderInfo.orderHash, + maker: order.maker, + taker: taker, + makerToken: IERC20TokenV06(order.makerToken), + takerToken: IERC20TokenV06(order.takerToken), + makerAmount: order.makerAmount, + takerAmount: order.takerAmount, + takerTokenFillAmount: takerTokenFillAmount, + takerTokenFilledAmount: orderInfo.takerTokenFilledAmount + }) + ); + + emit RfqOrderFilled( + orderInfo.orderHash, + order.maker, + taker, + address(order.makerToken), + address(order.takerToken), + results.takerTokenFilledAmount, + results.makerTokenFilledAmount, + results.ethProtocolFeePaid, + order.pool + ); + } + + /// @dev Settle the trade between an order's maker and taker. + /// @param settleInfo Information needed to execute the settlement. + /// @return takerTokenFilledAmount How much taker token was filled. + /// @return makerTokenFilledAmount How much maker token was filled. + function _settleOrder(SettleOrderInfo memory settleInfo) + private + returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount) + { + // Clamp the taker token fill amount to the fillable amount. + takerTokenFilledAmount = LibSafeMathV06.min128( + settleInfo.takerTokenFillAmount, + settleInfo.takerAmount.safeSub128(settleInfo.takerTokenFilledAmount) + ); + // Compute the maker token amount. + // This should never overflow because the values are all clamped to + // (2^128-1). + makerTokenFilledAmount = uint128(LibMathV06.getPartialAmountFloor( + uint256(takerTokenFilledAmount), + uint256(settleInfo.takerAmount), + uint256(settleInfo.makerAmount) + )); + + if (takerTokenFilledAmount == 0 || makerTokenFilledAmount == 0) { + // Nothing to do. + return (0, 0); + } + + // Update filled state for the order. + LibNativeOrdersStorage + .getStorage() + .orderHashToTakerTokenFilledAmount[settleInfo.orderHash] = + // OK to overwrite the whole word because we shouldn't get to this + // function if the order is cancelled. + settleInfo.takerTokenFilledAmount.safeAdd128(takerTokenFilledAmount); + + // Transfer taker -> maker. + LibTokenSpender.spendERC20Tokens( + settleInfo.takerToken, + settleInfo.taker, + settleInfo.maker, + takerTokenFilledAmount + ); + + // Transfer maker -> taker. + LibTokenSpender.spendERC20Tokens( + settleInfo.makerToken, + settleInfo.maker, + settleInfo.taker, + makerTokenFilledAmount + ); + } + + /// @dev Refund any leftover protocol fees in `msg.value` to `msg.sender`. + /// @param ethProtocolFeePaid How much ETH was paid in protocol fees. + function _refundExcessProtocolFeeToSender(uint256 ethProtocolFeePaid) + private + { + if (msg.value > ethProtocolFeePaid && msg.sender != address(this)) { + uint256 refundAmount = msg.value.safeSub(ethProtocolFeePaid); + (bool success,) = msg + .sender + .call{value: refundAmount}(""); + if (!success) { + LibNativeOrdersRichErrors.ProtocolFeeRefundFailed( + msg.sender, + refundAmount + ).rrevert(); + } + } + } +} diff --git a/contracts/zero-ex/contracts/src/features/libs/LibNativeOrder.sol b/contracts/zero-ex/contracts/src/features/libs/LibNativeOrder.sol new file mode 100644 index 0000000000..d1298e6744 --- /dev/null +++ b/contracts/zero-ex/contracts/src/features/libs/LibNativeOrder.sol @@ -0,0 +1,213 @@ +/* + + Copyright 2020 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.6.5; +pragma experimental ABIEncoderV2; + +import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol"; + + +/// @dev A library for common native order operations. +library LibNativeOrder { + + enum OrderStatus { + INVALID, + FILLABLE, + FILLED, + CANCELLED, + EXPIRED + } + + /// @dev A standard OTC or OO limit order. + struct LimitOrder { + IERC20TokenV06 makerToken; + IERC20TokenV06 takerToken; + uint128 makerAmount; + uint128 takerAmount; + uint128 takerTokenFeeAmount; + address maker; + address taker; + address sender; + address feeRecipient; + bytes32 pool; + uint64 expiry; + uint256 salt; + } + + /// @dev An RFQ limit order. + struct RfqOrder { + IERC20TokenV06 makerToken; + IERC20TokenV06 takerToken; + uint128 makerAmount; + uint128 takerAmount; + address maker; + address txOrigin; + bytes32 pool; + uint64 expiry; + uint256 salt; + } + + /// @dev Info on a limit or RFQ order. + struct OrderInfo { + bytes32 orderHash; + OrderStatus status; + uint128 takerTokenFilledAmount; + } + + uint256 private constant UINT_128_MASK = (1 << 128) - 1; + uint256 private constant UINT_64_MASK = (1 << 64) - 1; + uint256 private constant ADDRESS_MASK = (1 << 160) - 1; + + // The type hash for limit orders, which is: + // keccak256(abi.encodePacked( + // "LimitOrder(", + // "address makerToken,", + // "address takerToken,", + // "uint128 makerAmount,", + // "uint128 takerAmount,", + // "uint128 takerTokenFeeAmount,", + // "address maker,", + // "address taker,", + // "address sender,", + // "address feeRecipient,", + // "bytes32 pool,", + // "uint64 expiry,", + // "uint256 salt" + // ")" + // )) + uint256 private constant _LIMIT_ORDER_TYPEHASH = + 0xce918627cb55462ddbb85e73de69a8b322f2bc88f4507c52fcad6d4c33c29d49; + + // The type hash for RFQ orders, which is: + // keccak256(abi.encodePacked( + // "RfqOrder(", + // "address makerToken,", + // "address takerToken,", + // "uint128 makerAmount,", + // "uint128 takerAmount,", + // "address maker,", + // "address txOrigin,", + // "bytes32 pool,", + // "uint64 expiry,", + // "uint256 salt" + // ")" + // )) + uint256 private constant _RFQ_ORDER_TYPEHASH = + 0xc6b3034376598bc7f28b05e81db7ed88486dcdb6b4a6c7300353fffc5f31f382; + + /// @dev Get the struct hash of a limit order. + /// @param order The limit order. + /// @return structHash The struct hash of the order. + function getLimitOrderStructHash(LimitOrder memory order) + internal + pure + returns (bytes32 structHash) + { + // The struct hash is: + // keccak256(abi.encode( + // TYPE_HASH, + // order.makerToken, + // order.takerToken, + // order.makerAmount, + // order.takerAmount, + // order.takerTokenFeeAmount, + // order.maker, + // order.taker, + // order.sender, + // order.feeRecipient, + // order.pool, + // order.expiry, + // order.salt, + // )) + assembly { + let mem := mload(0x40) + mstore(mem, _LIMIT_ORDER_TYPEHASH) + // order.makerToken; + mstore(add(mem, 0x20), and(ADDRESS_MASK, mload(order))) + // order.takerToken; + mstore(add(mem, 0x40), and(ADDRESS_MASK, mload(add(order, 0x20)))) + // order.makerAmount; + mstore(add(mem, 0x60), and(UINT_128_MASK, mload(add(order, 0x40)))) + // order.takerAmount; + mstore(add(mem, 0x80), and(UINT_128_MASK, mload(add(order, 0x60)))) + // order.takerTokenFeeAmount; + mstore(add(mem, 0xA0), and(UINT_128_MASK, mload(add(order, 0x80)))) + // order.maker; + mstore(add(mem, 0xC0), and(ADDRESS_MASK, mload(add(order, 0xA0)))) + // order.taker; + mstore(add(mem, 0xE0), and(ADDRESS_MASK, mload(add(order, 0xC0)))) + // order.sender; + mstore(add(mem, 0x100), and(ADDRESS_MASK, mload(add(order, 0xE0)))) + // order.feeRecipient; + mstore(add(mem, 0x120), and(ADDRESS_MASK, mload(add(order, 0x100)))) + // order.pool; + mstore(add(mem, 0x140), mload(add(order, 0x120))) + // order.expiry; + mstore(add(mem, 0x160), and(UINT_64_MASK, mload(add(order, 0x140)))) + // order.salt; + mstore(add(mem, 0x180), mload(add(order, 0x160))) + structHash := keccak256(mem, 0x1A0) + } + } + + /// @dev Get the struct hash of a RFQ order. + /// @param order The RFQ order. + /// @return structHash The struct hash of the order. + function getRfqOrderStructHash(RfqOrder memory order) + internal + pure + returns (bytes32 structHash) + { + // The struct hash is: + // keccak256(abi.encode( + // TYPE_HASH, + // order.makerToken, + // order.takerToken, + // order.makerAmount, + // order.takerAmount, + // order.maker, + // order.txOrigin, + // order.pool, + // order.expiry, + // order.salt, + // )) + assembly { + let mem := mload(0x40) + mstore(mem, _RFQ_ORDER_TYPEHASH) + // order.makerToken; + mstore(add(mem, 0x20), and(ADDRESS_MASK, mload(order))) + // order.takerToken; + mstore(add(mem, 0x40), and(ADDRESS_MASK, mload(add(order, 0x20)))) + // order.makerAmount; + mstore(add(mem, 0x60), and(UINT_128_MASK, mload(add(order, 0x40)))) + // order.takerAmount; + mstore(add(mem, 0x80), and(UINT_128_MASK, mload(add(order, 0x60)))) + // order.maker; + mstore(add(mem, 0xA0), and(ADDRESS_MASK, mload(add(order, 0x80)))) + // order.txOrigin; + mstore(add(mem, 0xC0), and(ADDRESS_MASK, mload(add(order, 0xA0)))) + // order.pool; + mstore(add(mem, 0xE0), mload(add(order, 0xC0))) + // order.expiry; + mstore(add(mem, 0x100), and(UINT_64_MASK, mload(add(order, 0xE0)))) + // order.salt; + mstore(add(mem, 0x120), mload(add(order, 0x100))) + structHash := keccak256(mem, 0x140) + } + } +} diff --git a/contracts/zero-ex/contracts/src/fixins/FixinProtocolFees.sol b/contracts/zero-ex/contracts/src/fixins/FixinProtocolFees.sol index 18703dc95c..84bc832f01 100644 --- a/contracts/zero-ex/contracts/src/fixins/FixinProtocolFees.sol +++ b/contracts/zero-ex/contracts/src/fixins/FixinProtocolFees.sol @@ -22,48 +22,55 @@ pragma experimental ABIEncoderV2; import "@0x/contracts-erc20/contracts/src/v06/IEtherTokenV06.sol"; import "../external/FeeCollector.sol"; import "../features/libs/LibTokenSpender.sol"; +import "../vendor/v3/IStaking.sol"; + /// @dev Helpers for collecting protocol fees. abstract contract FixinProtocolFees { - bytes32 immutable feeCollectorCodeHash; + /// @dev The protocol fee multiplier. + uint32 public immutable PROTOCOL_FEE_MULTIPLIER; + /// @dev Hash of the fee collector init code. + bytes32 private immutable FEE_COLLECTOR_INIT_CODE_HASH; + /// @dev The WETH token contract. + IEtherTokenV06 private immutable WETH; + /// @dev The staking contract. + IStaking private immutable STAKING; - constructor() internal { - feeCollectorCodeHash = keccak256(type(FeeCollector).creationCode); - } - - /// @dev Collect the specified protocol fee in either WETH or ETH. If - /// msg.value is non-zero, the fee will be paid in ETH. Otherwise, - /// this function attempts to transfer the fee in WETH. Either way, - /// The fee is stored in a per-pool fee collector contract. - /// @param poolId The pool ID for which a fee is being collected. - /// @param amount The amount of ETH/WETH to be collected. - /// @param weth The WETH token contract. - function _collectProtocolFee( - bytes32 poolId, - uint256 amount, - IERC20TokenV06 weth + constructor( + IEtherTokenV06 weth, + IStaking staking, + uint32 protocolFeeMultiplier ) internal { - FeeCollector feeCollector = _getFeeCollector(poolId); + FEE_COLLECTOR_INIT_CODE_HASH = keccak256(type(FeeCollector).creationCode); + WETH = weth; + STAKING = staking; + PROTOCOL_FEE_MULTIPLIER = protocolFeeMultiplier; + } - if (msg.value == 0) { - // WETH - LibTokenSpender.spendERC20Tokens(weth, msg.sender, address(feeCollector), amount); - } else { - // ETH - (bool success,) = address(feeCollector).call{value: amount}(""); - require(success, "FixinProtocolFees/ETHER_TRANSFER_FALIED"); + /// @dev Collect the specified protocol fee in ETH. + /// The fee is stored in a per-pool fee collector contract. + /// @param poolId The pool ID for which a fee is being collected. + /// @return ethProtocolFeePaid How much protocol fee was collected in ETH. + function _collectProtocolFee(bytes32 poolId) + internal + returns (uint256 ethProtocolFeePaid) + { + uint256 protocolFeePaid = _getSingleProtocolFee(); + if (protocolFeePaid == 0) { + // Nothing to do. + return 0; } + FeeCollector feeCollector = _getFeeCollector(poolId); + (bool success,) = address(feeCollector).call{value: protocolFeePaid}(""); + require(success, "FixinProtocolFees/ETHER_TRANSFER_FALIED"); + return protocolFeePaid; } /// @dev Transfer fees for a given pool to the staking contract. /// @param poolId Identifies the pool whose fees are being paid. - function _transferFeesForPool( - bytes32 poolId, - IStaking staking, - IEtherTokenV06 weth - ) + function _transferFeesForPool(bytes32 poolId) internal { FeeCollector feeCollector = _getFeeCollector(poolId); @@ -75,18 +82,18 @@ abstract contract FixinProtocolFees { if (codeSize == 0) { // Create and initialize the contract if necessary. - new FeeCollector{salt: poolId}(); - feeCollector.initialize(weth, staking, poolId); + new FeeCollector{salt: bytes32(poolId)}(); + feeCollector.initialize(WETH, STAKING, poolId); } if (address(feeCollector).balance > 1) { - feeCollector.convertToWeth(weth); + feeCollector.convertToWeth(WETH); } - uint256 bal = weth.balanceOf(address(feeCollector)); + uint256 bal = WETH.balanceOf(address(feeCollector)); if (bal > 1) { // Leave 1 wei behind to avoid high SSTORE cost of zero-->non-zero. - staking.payProtocolFee( + STAKING.payProtocolFee( address(feeCollector), address(feeCollector), bal - 1); @@ -95,9 +102,7 @@ abstract contract FixinProtocolFees { /// @dev Compute the CREATE2 address for a fee collector. /// @param poolId The fee collector's pool ID. - function _getFeeCollector( - bytes32 poolId - ) + function _getFeeCollector(bytes32 poolId) internal view returns (FeeCollector) @@ -107,8 +112,18 @@ abstract contract FixinProtocolFees { byte(0xff), address(this), poolId, // pool ID is salt - feeCollectorCodeHash + FEE_COLLECTOR_INIT_CODE_HASH )))); return FeeCollector(addr); } + + /// @dev Get the cost of a single protocol fee. + /// @return protocolFeeAmount The protocol fee amount, in ETH/WETH. + function _getSingleProtocolFee() + internal + view + returns (uint256 protocolFeeAmount) + { + return uint256(PROTOCOL_FEE_MULTIPLIER) * tx.gasprice; + } } diff --git a/contracts/zero-ex/contracts/src/migrations/FullMigration.sol b/contracts/zero-ex/contracts/src/migrations/FullMigration.sol index 5127be284d..254e2a8ee1 100644 --- a/contracts/zero-ex/contracts/src/migrations/FullMigration.sol +++ b/contracts/zero-ex/contracts/src/migrations/FullMigration.sol @@ -25,6 +25,7 @@ import "../features/TokenSpenderFeature.sol"; import "../features/TransformERC20Feature.sol"; import "../features/SignatureValidatorFeature.sol"; import "../features/MetaTransactionsFeature.sol"; +import "../features/NativeOrdersFeature.sol"; import "../external/AllowanceTarget.sol"; import "./InitialMigration.sol"; @@ -42,6 +43,7 @@ contract FullMigration { TransformERC20Feature transformERC20; SignatureValidatorFeature signatureValidator; MetaTransactionsFeature metaTransactions; + NativeOrdersFeature nativeOrders; } /// @dev Parameters needed to initialize features. @@ -84,7 +86,7 @@ contract FullMigration { /// @param features Features to add to the proxy. /// @return _zeroEx The configured ZeroEx contract. Same as the `zeroEx` parameter. /// @param migrateOpts Parameters needed to initialize features. - function initializeZeroEx( + function migrateZeroEx( address payable owner, ZeroEx zeroEx, Features memory features, @@ -195,5 +197,16 @@ contract FullMigration { address(this) ); } + // NativeOrdersFeature + { + // Register the feature. + ownable.migrate( + address(features.nativeOrders), + abi.encodeWithSelector( + NativeOrdersFeature.migrate.selector + ), + address(this) + ); + } } } diff --git a/contracts/zero-ex/contracts/src/storage/LibNativeOrdersStorage.sol b/contracts/zero-ex/contracts/src/storage/LibNativeOrdersStorage.sol new file mode 100644 index 0000000000..093dc79607 --- /dev/null +++ b/contracts/zero-ex/contracts/src/storage/LibNativeOrdersStorage.sol @@ -0,0 +1,54 @@ +/* + + Copyright 2020 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.6.5; +pragma experimental ABIEncoderV2; + +import "./LibStorage.sol"; + + +/// @dev Storage helpers for `NativeOrdersFeature`. +library LibNativeOrdersStorage { + + /// @dev Storage bucket for this feature. + struct Storage { + // How much taker token has been filled in order. + // The lower `uint128` is the taker token fill amount. + // The high bit will be `1` if the order was directly cancelled. + mapping(bytes32 => uint256) orderHashToTakerTokenFilledAmount; + // The minimum valid order salt for a given maker and order pair (maker, taker) + // for limit orders. + mapping(address => mapping(address => mapping(address => uint256))) + limitOrdersMakerToMakerTokenToTakerTokenToMinValidOrderSalt; + // The minimum valid order salt for a given maker and order pair (maker, taker) + // for RFQ orders. + mapping(address => mapping(address => mapping(address => uint256))) + rfqOrdersMakerToMakerTokenToTakerTokenToMinValidOrderSalt; + } + + /// @dev Get the storage bucket for this contract. + function getStorage() internal pure returns (Storage storage stor) { + uint256 storageSlot = LibStorage.getStorageSlot( + LibStorage.StorageId.NativeOrders + ); + // Dip into assembly to change the slot pointed to by the local + // variable `stor`. + // See https://solidity.readthedocs.io/en/v0.6.8/assembly.html?highlight=slot#access-to-external-variables-functions-and-libraries + assembly { stor_slot := storageSlot } + } +} diff --git a/contracts/zero-ex/contracts/src/storage/LibStorage.sol b/contracts/zero-ex/contracts/src/storage/LibStorage.sol index 809977d4a8..9679e6984f 100644 --- a/contracts/zero-ex/contracts/src/storage/LibStorage.sol +++ b/contracts/zero-ex/contracts/src/storage/LibStorage.sol @@ -36,7 +36,8 @@ library LibStorage { TokenSpender, TransformERC20, MetaTransactions, - ReentrancyGuard + ReentrancyGuard, + NativeOrders } /// @dev Get the storage slot given a storage ID. We assign unique, well-spaced diff --git a/contracts/zero-ex/contracts/test/TestBridge.sol b/contracts/zero-ex/contracts/test/TestBridge.sol index f16b1c39a1..4fac6db6f4 100644 --- a/contracts/zero-ex/contracts/test/TestBridge.sol +++ b/contracts/zero-ex/contracts/test/TestBridge.sol @@ -37,14 +37,13 @@ contract TestBridge is /// @param from Address to transfer asset from. /// @param to Address to transfer asset to. /// @param amount Amount of asset to transfer. - /// @param bridgeData Arbitrary asset data needed by the bridge contract. /// @return success The magic bytes `0xdc1600f3` if successful. function bridgeTransferFrom( address tokenAddress, address from, address to, uint256 amount, - bytes calldata bridgeData + bytes calldata /* bridgeData */ ) external override diff --git a/contracts/zero-ex/contracts/test/TestProtocolFees.sol b/contracts/zero-ex/contracts/test/TestFixinProtocolFees.sol similarity index 61% rename from contracts/zero-ex/contracts/test/TestProtocolFees.sol rename to contracts/zero-ex/contracts/test/TestFixinProtocolFees.sol index 5982eb3150..b24081c65e 100644 --- a/contracts/zero-ex/contracts/test/TestProtocolFees.sol +++ b/contracts/zero-ex/contracts/test/TestFixinProtocolFees.sol @@ -21,26 +21,31 @@ pragma experimental ABIEncoderV2; import "../src/fixins/FixinProtocolFees.sol"; -contract TestProtocolFees is FixinProtocolFees { - function collectProtocolFee( - bytes32 poolId, - uint256 amount, - IERC20TokenV06 weth +contract TestFixinProtocolFees is + FixinProtocolFees +{ + constructor( + IEtherTokenV06 weth, + IStaking staking, + uint32 protocolFeeMultiplier ) + public + FixinProtocolFees(weth, staking, protocolFeeMultiplier) + { + // solhint-disalbe no-empty-blocks + } + + function collectProtocolFee(bytes32 poolId) external payable { - _collectProtocolFee(poolId, amount, weth); + _collectProtocolFee(poolId); } - function transferFeesForPool( - bytes32 poolId, - IStaking staking, - IEtherTokenV06 weth - ) + function transferFeesForPool(bytes32 poolId) external { - _transferFeesForPool(poolId, staking, weth); + _transferFeesForPool(poolId); } function getFeeCollector( @@ -52,4 +57,12 @@ contract TestProtocolFees is FixinProtocolFees { { return _getFeeCollector(poolId); } + + function getSingleProtocolFee() + external + view + returns (uint256 protocolFeeAmount) + { + return _getSingleProtocolFee(); + } } diff --git a/contracts/zero-ex/contracts/test/TestLibNativeOrder.sol b/contracts/zero-ex/contracts/test/TestLibNativeOrder.sol new file mode 100644 index 0000000000..52ae9d3b92 --- /dev/null +++ b/contracts/zero-ex/contracts/test/TestLibNativeOrder.sol @@ -0,0 +1,24 @@ +pragma solidity ^0.6; +pragma experimental ABIEncoderV2; + +import "../src/features/libs/LibNativeOrder.sol"; + + +contract TestLibNativeOrder { + + function getLimitOrderStructHash(LibNativeOrder.LimitOrder calldata order) + external + pure + returns (bytes32 structHash) + { + return LibNativeOrder.getLimitOrderStructHash(order); + } + + function getRfqOrderStructHash(LibNativeOrder.RfqOrder calldata order) + external + pure + returns (bytes32 structHash) + { + return LibNativeOrder.getRfqOrderStructHash(order); + } +} diff --git a/contracts/zero-ex/contracts/test/TestNativeOrdersFeature.sol b/contracts/zero-ex/contracts/test/TestNativeOrdersFeature.sol new file mode 100644 index 0000000000..3ff64eefbc --- /dev/null +++ b/contracts/zero-ex/contracts/test/TestNativeOrdersFeature.sol @@ -0,0 +1,24 @@ +pragma solidity ^0.6; +pragma experimental ABIEncoderV2; + +import "../src/features/NativeOrdersFeature.sol"; + +contract TestNativeOrdersFeature is + NativeOrdersFeature +{ + constructor( + address zeroExAddress, + IEtherTokenV06 weth, + IStaking staking, + uint32 protocolFeeMultiplier + ) + public + NativeOrdersFeature(zeroExAddress, weth, staking, protocolFeeMultiplier) + { + // solhint-disable no-empty-blocks + } + + modifier onlySelf() override { + _; + } +} diff --git a/contracts/zero-ex/contracts/test/TestTransformerBase.sol b/contracts/zero-ex/contracts/test/TestTransformerBase.sol index ba6415310b..553616b23b 100644 --- a/contracts/zero-ex/contracts/test/TestTransformerBase.sol +++ b/contracts/zero-ex/contracts/test/TestTransformerBase.sol @@ -28,7 +28,7 @@ contract TestTransformerBase is IERC20Transformer, Transformer { - function transform(TransformContext calldata context) + function transform(TransformContext calldata /* context */) external override returns (bytes4 success) diff --git a/contracts/zero-ex/package.json b/contracts/zero-ex/package.json index 2b6ce06cfa..844fd93e87 100644 --- a/contracts/zero-ex/package.json +++ b/contracts/zero-ex/package.json @@ -40,9 +40,9 @@ "publish:private": "yarn build && gitpkg publish" }, "config": { - "publicInterfaceContracts": "IZeroEx,ZeroEx,FullMigration,InitialMigration,IFlashWallet,IAllowanceTarget,IERC20Transformer,IOwnableFeature,ISimpleFunctionRegistryFeature,ITokenSpenderFeature,ITransformERC20Feature,FillQuoteTransformer,PayTakerTransformer,WethTransformer,OwnableFeature,SimpleFunctionRegistryFeature,TransformERC20Feature,TokenSpenderFeature,AffiliateFeeTransformer,SignatureValidatorFeature,MetaTransactionsFeature,LogMetadataTransformer,BridgeAdapter,LiquidityProviderFeature,ILiquidityProvider", + "publicInterfaceContracts": "IZeroEx,ZeroEx,FullMigration,InitialMigration,IFlashWallet,IAllowanceTarget,IERC20Transformer,IOwnableFeature,ISimpleFunctionRegistryFeature,ITokenSpenderFeature,ITransformERC20Feature,FillQuoteTransformer,PayTakerTransformer,WethTransformer,OwnableFeature,SimpleFunctionRegistryFeature,TransformERC20Feature,TokenSpenderFeature,AffiliateFeeTransformer,SignatureValidatorFeature,MetaTransactionsFeature,LogMetadataTransformer,BridgeAdapter,LiquidityProviderFeature,ILiquidityProviderFeature,NativeOrdersFeature,INativeOrdersFeature", "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.", - "abis": "./test/generated-artifacts/@(AffiliateFeeTransformer|AllowanceTarget|BootstrapFeature|BridgeAdapter|FeeCollector|FillQuoteTransformer|FixinCommon|FixinEIP712|FixinProtocolFees|FixinReentrancyGuard|FlashWallet|FullMigration|IAllowanceTarget|IBootstrapFeature|IBridgeAdapter|IERC20Bridge|IERC20Transformer|IExchange|IFeature|IFlashWallet|IGasToken|ILiquidityProvider|ILiquidityProviderFeature|ILiquidityProviderSandbox|IMetaTransactionsFeature|IOwnableFeature|ISignatureValidatorFeature|ISimpleFunctionRegistryFeature|IStaking|ITestSimpleFunctionRegistryFeature|ITokenSpenderFeature|ITransformERC20Feature|IUniswapFeature|IZeroEx|InitialMigration|LibBootstrap|LibCommonRichErrors|LibERC20Transformer|LibLiquidityProviderRichErrors|LibMetaTransactionsRichErrors|LibMetaTransactionsStorage|LibMigrate|LibOrderHash|LibOwnableRichErrors|LibOwnableStorage|LibProxyRichErrors|LibProxyStorage|LibReentrancyGuardStorage|LibSignature|LibSignatureRichErrors|LibSignedCallData|LibSimpleFunctionRegistryRichErrors|LibSimpleFunctionRegistryStorage|LibSpenderRichErrors|LibStorage|LibTokenSpender|LibTokenSpenderStorage|LibTransformERC20RichErrors|LibTransformERC20Storage|LibWalletRichErrors|LiquidityProviderFeature|LiquidityProviderSandbox|LogMetadataTransformer|MetaTransactionsFeature|MixinAdapterAddresses|MixinBalancer|MixinCurve|MixinDodo|MixinKyber|MixinMStable|MixinMooniswap|MixinOasis|MixinShell|MixinSushiswap|MixinUniswap|MixinUniswapV2|MixinZeroExBridge|OwnableFeature|PayTakerTransformer|SignatureValidatorFeature|SimpleFunctionRegistryFeature|TestBridge|TestCallTarget|TestDelegateCaller|TestFillQuoteTransformerBridge|TestFillQuoteTransformerExchange|TestFillQuoteTransformerHost|TestFullMigration|TestInitialMigration|TestLibSignature|TestLibTokenSpender|TestLiquidityProvider|TestMetaTransactionsTransformERC20Feature|TestMigrator|TestMintTokenERC20Transformer|TestMintableERC20Token|TestProtocolFees|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestStaking|TestTokenSpender|TestTokenSpenderERC20Token|TestTransformERC20|TestTransformerBase|TestTransformerDeployerTransformer|TestTransformerHost|TestWeth|TestWethTransformerHost|TestZeroExFeature|TokenSpenderFeature|TransformERC20Feature|Transformer|TransformerDeployer|UniswapFeature|WethTransformer|ZeroEx).json" + "abis": "./test/generated-artifacts/@(AffiliateFeeTransformer|AllowanceTarget|BootstrapFeature|BridgeAdapter|FeeCollector|FillQuoteTransformer|FixinCommon|FixinEIP712|FixinProtocolFees|FixinReentrancyGuard|FlashWallet|FullMigration|IAllowanceTarget|IBootstrapFeature|IBridgeAdapter|IERC20Bridge|IERC20Transformer|IExchange|IFeature|IFlashWallet|IGasToken|ILiquidityProvider|ILiquidityProviderFeature|ILiquidityProviderSandbox|IMetaTransactionsFeature|INativeOrdersFeature|IOwnableFeature|ISignatureValidatorFeature|ISimpleFunctionRegistryFeature|IStaking|ITestSimpleFunctionRegistryFeature|ITokenSpenderFeature|ITransformERC20Feature|IUniswapFeature|IZeroEx|InitialMigration|LibBootstrap|LibCommonRichErrors|LibERC20Transformer|LibLiquidityProviderRichErrors|LibMetaTransactionsRichErrors|LibMetaTransactionsStorage|LibMigrate|LibNativeOrder|LibNativeOrdersRichErrors|LibNativeOrdersStorage|LibOrderHash|LibOwnableRichErrors|LibOwnableStorage|LibProxyRichErrors|LibProxyStorage|LibReentrancyGuardStorage|LibSignature|LibSignatureRichErrors|LibSignedCallData|LibSimpleFunctionRegistryRichErrors|LibSimpleFunctionRegistryStorage|LibSpenderRichErrors|LibStorage|LibTokenSpender|LibTokenSpenderStorage|LibTransformERC20RichErrors|LibTransformERC20Storage|LibWalletRichErrors|LiquidityProviderFeature|LiquidityProviderSandbox|LogMetadataTransformer|MetaTransactionsFeature|MixinAdapterAddresses|MixinBalancer|MixinCurve|MixinDodo|MixinKyber|MixinMStable|MixinMooniswap|MixinOasis|MixinShell|MixinSushiswap|MixinUniswap|MixinUniswapV2|MixinZeroExBridge|NativeOrdersFeature|OwnableFeature|PayTakerTransformer|SignatureValidatorFeature|SimpleFunctionRegistryFeature|TestBridge|TestCallTarget|TestDelegateCaller|TestFillQuoteTransformerBridge|TestFillQuoteTransformerExchange|TestFillQuoteTransformerHost|TestFixinProtocolFees|TestFullMigration|TestInitialMigration|TestLibNativeOrder|TestLibSignature|TestLibTokenSpender|TestLiquidityProvider|TestMetaTransactionsTransformERC20Feature|TestMigrator|TestMintTokenERC20Transformer|TestMintableERC20Token|TestNativeOrdersFeature|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestStaking|TestTokenSpender|TestTokenSpenderERC20Token|TestTransformERC20|TestTransformerBase|TestTransformerDeployerTransformer|TestTransformerHost|TestWeth|TestWethTransformerHost|TestZeroExFeature|TokenSpenderFeature|TransformERC20Feature|Transformer|TransformerDeployer|UniswapFeature|WethTransformer|ZeroEx).json" }, "repository": { "type": "git", diff --git a/contracts/zero-ex/src/artifacts.ts b/contracts/zero-ex/src/artifacts.ts index cb7a510fb1..922fc55d4f 100644 --- a/contracts/zero-ex/src/artifacts.ts +++ b/contracts/zero-ex/src/artifacts.ts @@ -12,7 +12,8 @@ import * as FullMigration from '../generated-artifacts/FullMigration.json'; import * as IAllowanceTarget from '../generated-artifacts/IAllowanceTarget.json'; import * as IERC20Transformer from '../generated-artifacts/IERC20Transformer.json'; import * as IFlashWallet from '../generated-artifacts/IFlashWallet.json'; -import * as ILiquidityProvider from '../generated-artifacts/ILiquidityProvider.json'; +import * as ILiquidityProviderFeature from '../generated-artifacts/ILiquidityProviderFeature.json'; +import * as INativeOrdersFeature from '../generated-artifacts/INativeOrdersFeature.json'; import * as InitialMigration from '../generated-artifacts/InitialMigration.json'; import * as IOwnableFeature from '../generated-artifacts/IOwnableFeature.json'; import * as ISimpleFunctionRegistryFeature from '../generated-artifacts/ISimpleFunctionRegistryFeature.json'; @@ -22,6 +23,7 @@ import * as IZeroEx from '../generated-artifacts/IZeroEx.json'; import * as LiquidityProviderFeature from '../generated-artifacts/LiquidityProviderFeature.json'; import * as LogMetadataTransformer from '../generated-artifacts/LogMetadataTransformer.json'; import * as MetaTransactionsFeature from '../generated-artifacts/MetaTransactionsFeature.json'; +import * as NativeOrdersFeature from '../generated-artifacts/NativeOrdersFeature.json'; import * as OwnableFeature from '../generated-artifacts/OwnableFeature.json'; import * as PayTakerTransformer from '../generated-artifacts/PayTakerTransformer.json'; import * as SignatureValidatorFeature from '../generated-artifacts/SignatureValidatorFeature.json'; @@ -55,5 +57,7 @@ export const artifacts = { LogMetadataTransformer: LogMetadataTransformer as ContractArtifact, BridgeAdapter: BridgeAdapter as ContractArtifact, LiquidityProviderFeature: LiquidityProviderFeature as ContractArtifact, - ILiquidityProvider: ILiquidityProvider as ContractArtifact, + ILiquidityProviderFeature: ILiquidityProviderFeature as ContractArtifact, + NativeOrdersFeature: NativeOrdersFeature as ContractArtifact, + INativeOrdersFeature: INativeOrdersFeature as ContractArtifact, }; diff --git a/contracts/zero-ex/src/eip712_utils.ts b/contracts/zero-ex/src/eip712_utils.ts new file mode 100644 index 0000000000..3cbc5e9675 --- /dev/null +++ b/contracts/zero-ex/src/eip712_utils.ts @@ -0,0 +1,70 @@ +import { hexUtils, NULL_ADDRESS } from '@0x/utils'; + +export interface EIP712Domain { + name: string; + version: string; + chainId: number; + verifyingContract: string; +} + +export const EIP712_DOMAIN_PARAMETERS = [ + { name: 'name', type: 'string' }, + { name: 'version', type: 'string' }, + { name: 'chainId', type: 'uint256' }, + { name: 'verifyingContract', type: 'address' }, +]; + +const EXCHANGE_PROXY_EIP712_DOMAIN_DEFAULT = { + chainId: 1, + verifyingContract: NULL_ADDRESS, + name: 'ZeroEx', + version: '1.0.0', +}; + +const EXCHANGE_PROXY_DOMAIN_TYPEHASH = hexUtils.hash( + hexUtils.toHex( + Buffer.from( + [ + 'EIP712Domain(', + ['string name', 'string version', 'uint256 chainId', 'address verifyingContract'].join(','), + ')', + ].join(''), + ), + ), +); + +/** + * Create an exchange proxy EIP712 domain. + */ +export function createExchangeProxyEIP712Domain(chainId?: number, verifyingContract?: string): EIP712Domain { + return { + ...EXCHANGE_PROXY_EIP712_DOMAIN_DEFAULT, + ...(chainId ? { chainId } : {}), + ...(verifyingContract ? { verifyingContract } : {}), + }; +} + +/** + * Get the hash of the exchange proxy EIP712 domain. + */ +export function getExchangeProxyEIP712DomainHash(chainId?: number, verifyingContract?: string): string { + const domain = createExchangeProxyEIP712Domain(chainId, verifyingContract); + return hexUtils.hash( + hexUtils.concat( + EXCHANGE_PROXY_DOMAIN_TYPEHASH, + hexUtils.hash(hexUtils.toHex(Buffer.from(domain.name))), + hexUtils.hash(hexUtils.toHex(Buffer.from(domain.version))), + hexUtils.leftPad(domain.chainId), + hexUtils.leftPad(domain.verifyingContract), + ), + ); +} + +/** + * Compute a complete EIP712 hash given a struct hash. + */ +export function getExchangeProxyEIP712Hash(structHash: string, chainId?: number, verifyingContract?: string): string { + return hexUtils.hash( + hexUtils.concat('0x1901', getExchangeProxyEIP712DomainHash(chainId, verifyingContract), structHash), + ); +} diff --git a/contracts/zero-ex/src/index.ts b/contracts/zero-ex/src/index.ts index b055abed6c..dcc34bc76f 100644 --- a/contracts/zero-ex/src/index.ts +++ b/contracts/zero-ex/src/index.ts @@ -33,6 +33,9 @@ export * from './migration'; export * from './nonce_utils'; export * from './signed_call_data'; export * from './signature_utils'; +export * from './orders'; +export * from './eip712_utils'; +export * from './revert_errors'; export { AffiliateFeeTransformerContract, BridgeAdapterContract, @@ -49,6 +52,5 @@ export { WethTransformerContract, ZeroExContract, } from './wrappers'; -export * from './revert_errors'; export { EIP712TypedData } from '@0x/types'; export { SupportedProvider } from '@0x/subproviders'; diff --git a/contracts/zero-ex/src/migration.ts b/contracts/zero-ex/src/migration.ts index 6277b8931d..6e27624b94 100644 --- a/contracts/zero-ex/src/migration.ts +++ b/contracts/zero-ex/src/migration.ts @@ -1,4 +1,6 @@ import { SupportedProvider } from '@0x/subproviders'; +import { SimpleContractArtifact } from '@0x/types'; +import { NULL_ADDRESS } from '@0x/utils'; import { TxData } from 'ethereum-types'; import * as _ from 'lodash'; @@ -8,6 +10,7 @@ import { InitialMigrationContract, IZeroExContract, MetaTransactionsFeatureContract, + NativeOrdersFeatureContract, OwnableFeatureContract, SignatureValidatorFeatureContract, SimpleFunctionRegistryFeatureContract, @@ -26,6 +29,19 @@ export interface BootstrapFeatures { ownable: string; } +/** + * Artifacts to use when deploying bootstrap features. + */ +export interface BootstrapFeatureArtifacts { + registry: SimpleContractArtifact; + ownable: SimpleContractArtifact; +} + +const DEFAULT_BOOTSTRAP_FEATURE_ARTIFACTS = { + registry: artifacts.SimpleFunctionRegistryFeature, + ownable: artifacts.OwnableFeature, +}; + /** * Deploy the minimum features of the Exchange Proxy. */ @@ -33,12 +49,17 @@ export async function deployBootstrapFeaturesAsync( provider: SupportedProvider, txDefaults: Partial, features: Partial = {}, + featureArtifacts: Partial = {}, ): Promise { + const _featureArtifacts = { + ...DEFAULT_BOOTSTRAP_FEATURE_ARTIFACTS, + ...featureArtifacts, + }; return { registry: features.registry || (await SimpleFunctionRegistryFeatureContract.deployFrom0xArtifactAsync( - artifacts.SimpleFunctionRegistryFeature, + _featureArtifacts.registry, provider, txDefaults, artifacts, @@ -46,7 +67,7 @@ export async function deployBootstrapFeaturesAsync( ownable: features.ownable || (await OwnableFeatureContract.deployFrom0xArtifactAsync( - artifacts.OwnableFeature, + _featureArtifacts.ownable, provider, txDefaults, artifacts, @@ -90,30 +111,73 @@ export interface FullFeatures extends BootstrapFeatures { transformERC20: string; signatureValidator: string; metaTransactions: string; + nativeOrders: string; } /** - * Extra configuration options for a full migration of the Exchange Proxy. + * Artifacts to use when deploying full features. */ -export interface FullMigrationOpts { - transformerDeployer: string; +export interface FullFeatureArtifacts extends BootstrapFeatureArtifacts { + tokenSpender: SimpleContractArtifact; + transformERC20: SimpleContractArtifact; + signatureValidator: SimpleContractArtifact; + metaTransactions: SimpleContractArtifact; + nativeOrders: SimpleContractArtifact; } +/** + * Configuration for deploying full features.. + */ +export interface FullFeaturesDeployConfig { + zeroExAddress: string; + wethAddress: string; + stakingAddress: string; + protocolFeeMultiplier: number; +} + +/** + * Configuration options for a full migration of the Exchange Proxy. + */ +export interface FullMigrationConfig extends FullFeaturesDeployConfig { + transformerDeployer?: string; +} + +const DEFAULT_FULL_FEATURES_DEPLOY_CONFIG = { + zeroExAddress: NULL_ADDRESS, + wethAddress: NULL_ADDRESS, + stakingAddress: NULL_ADDRESS, + protocolFeeMultiplier: 70e3, +}; + +const DEFAULT_FULL_FEATURES_ARTIFACTS = { + tokenSpender: artifacts.TokenSpenderFeature, + transformERC20: artifacts.TransformERC20Feature, + signatureValidator: artifacts.SignatureValidatorFeature, + metaTransactions: artifacts.MetaTransactionsFeature, + nativeOrders: artifacts.NativeOrdersFeature, +}; + /** * Deploy all the features for a full Exchange Proxy. */ export async function deployFullFeaturesAsync( provider: SupportedProvider, txDefaults: Partial, - zeroExAddress: string, + config: Partial = {}, features: Partial = {}, + featureArtifacts: Partial = {}, ): Promise { + const _config = { ...DEFAULT_FULL_FEATURES_DEPLOY_CONFIG, ...config }; + const _featureArtifacts = { + ...DEFAULT_FULL_FEATURES_ARTIFACTS, + ...featureArtifacts, + }; return { ...(await deployBootstrapFeaturesAsync(provider, txDefaults)), tokenSpender: features.tokenSpender || (await TokenSpenderFeatureContract.deployFrom0xArtifactAsync( - artifacts.TokenSpenderFeature, + _featureArtifacts.tokenSpender, provider, txDefaults, artifacts, @@ -121,7 +185,7 @@ export async function deployFullFeaturesAsync( transformERC20: features.transformERC20 || (await TransformERC20FeatureContract.deployFrom0xArtifactAsync( - artifacts.TransformERC20Feature, + _featureArtifacts.transformERC20, provider, txDefaults, artifacts, @@ -129,7 +193,7 @@ export async function deployFullFeaturesAsync( signatureValidator: features.signatureValidator || (await SignatureValidatorFeatureContract.deployFrom0xArtifactAsync( - artifacts.SignatureValidatorFeature, + _featureArtifacts.signatureValidator, provider, txDefaults, artifacts, @@ -137,11 +201,23 @@ export async function deployFullFeaturesAsync( metaTransactions: features.metaTransactions || (await MetaTransactionsFeatureContract.deployFrom0xArtifactAsync( - artifacts.MetaTransactionsFeature, + _featureArtifacts.metaTransactions, provider, txDefaults, artifacts, - zeroExAddress, + _config.zeroExAddress, + )).address, + nativeOrders: + features.nativeOrders || + (await NativeOrdersFeatureContract.deployFrom0xArtifactAsync( + _featureArtifacts.nativeOrders, + provider, + txDefaults, + artifacts, + _config.zeroExAddress, + _config.wethAddress, + _config.stakingAddress, + _config.protocolFeeMultiplier, )).address, }; } @@ -154,7 +230,8 @@ export async function fullMigrateAsync( provider: SupportedProvider, txDefaults: Partial, features: Partial = {}, - opts: Partial = {}, + config: Partial = {}, + featureArtifacts: Partial = {}, ): Promise { const migrator = await FullMigrationContract.deployFrom0xArtifactAsync( artifacts.FullMigration, @@ -170,11 +247,12 @@ export async function fullMigrateAsync( artifacts, await migrator.getBootstrapper().callAsync(), ); - const _features = await deployFullFeaturesAsync(provider, txDefaults, zeroEx.address, features); - const _opts = { + const _config = { ...config, zeroExAddress: zeroEx.address }; + const _features = await deployFullFeaturesAsync(provider, txDefaults, _config, features, featureArtifacts); + const migrateOpts = { transformerDeployer: txDefaults.from as string, - ...opts, + ..._config, }; - await migrator.initializeZeroEx(owner, zeroEx.address, _features, _opts).awaitTransactionSuccessAsync(); + await migrator.migrateZeroEx(owner, zeroEx.address, _features, migrateOpts).awaitTransactionSuccessAsync(); return new IZeroExContract(zeroEx.address, provider, txDefaults); } diff --git a/contracts/zero-ex/src/orders.ts b/contracts/zero-ex/src/orders.ts new file mode 100644 index 0000000000..6a57e37a51 --- /dev/null +++ b/contracts/zero-ex/src/orders.ts @@ -0,0 +1,333 @@ +// tslint:disable: max-classes-per-file +import { SupportedProvider } from '@0x/subproviders'; +import { EIP712TypedData } from '@0x/types'; +import { BigNumber, hexUtils, NULL_ADDRESS } from '@0x/utils'; + +import { createExchangeProxyEIP712Domain, EIP712_DOMAIN_PARAMETERS, getExchangeProxyEIP712Hash } from './eip712_utils'; +import { + eip712SignTypedDataWithKey, + eip712SignTypedDataWithProviderAsync, + ethSignHashWithKey, + ethSignHashWithProviderAsync, + Signature, + SignatureType, +} from './signature_utils'; + +const ZERO = new BigNumber(0); +const COMMON_ORDER_DEFAULT_VALUES = { + makerToken: NULL_ADDRESS, + takerToken: NULL_ADDRESS, + makerAmount: ZERO, + takerAmount: ZERO, + maker: NULL_ADDRESS, + pool: hexUtils.leftPad(0), + expiry: ZERO, + salt: ZERO, + chainId: 1, + verifyingContract: '0xdef1c0ded9bec7f1a1670819833240f027b25eff', +}; +const LIMIT_ORDER_DEFAULT_VALUES = { + ...COMMON_ORDER_DEFAULT_VALUES, + takerTokenFeeAmount: ZERO, + taker: NULL_ADDRESS, + sender: NULL_ADDRESS, + feeRecipient: NULL_ADDRESS, +}; +const RFQ_ORDER_DEFAULT_VALUES = { + ...COMMON_ORDER_DEFAULT_VALUES, + txOrigin: NULL_ADDRESS, +}; + +export type CommonOrderFields = typeof COMMON_ORDER_DEFAULT_VALUES; +export type LimitOrderFields = typeof LIMIT_ORDER_DEFAULT_VALUES; +export type RfqOrderFields = typeof RFQ_ORDER_DEFAULT_VALUES; + +export enum OrderStatus { + Invalid = 0, + Fillable = 1, + Filled = 2, + Cancelled = 3, + Expired = 4, +} + +export interface OrderInfo { + status: OrderStatus; + orderHash: string; + takerTokenFilledAmount: BigNumber; +} + +export abstract class OrderBase { + public makerToken: string; + public takerToken: string; + public makerAmount: BigNumber; + public takerAmount: BigNumber; + public maker: string; + public pool: string; + public expiry: BigNumber; + public salt: BigNumber; + public chainId: number; + public verifyingContract: string; + + protected constructor(fields: Partial = {}) { + const _fields = { ...COMMON_ORDER_DEFAULT_VALUES, ...fields }; + this.makerToken = _fields.makerToken; + this.takerToken = _fields.takerToken; + this.makerAmount = _fields.makerAmount; + this.takerAmount = _fields.takerAmount; + this.maker = _fields.maker; + this.pool = _fields.pool; + this.expiry = _fields.expiry; + this.salt = _fields.salt; + this.chainId = _fields.chainId; + this.verifyingContract = _fields.verifyingContract; + } + + public abstract getStructHash(): string; + public abstract getEIP712TypedData(): EIP712TypedData; + + public getHash(): string { + return getExchangeProxyEIP712Hash(this.getStructHash(), this.chainId, this.verifyingContract); + } + + public async getSignatureWithProviderAsync( + provider: SupportedProvider, + type: SignatureType = SignatureType.EthSign, + ): Promise { + switch (type) { + case SignatureType.EIP712: + return eip712SignTypedDataWithProviderAsync(this.getEIP712TypedData(), this.maker, provider); + case SignatureType.EthSign: + return ethSignHashWithProviderAsync(this.getHash(), this.maker, provider); + default: + throw new Error(`Cannot sign with signature type: ${type}`); + } + } + + public getSignatureWithKey(key: string, type: SignatureType = SignatureType.EthSign): Signature { + switch (type) { + case SignatureType.EIP712: + return eip712SignTypedDataWithKey(this.getEIP712TypedData(), key); + case SignatureType.EthSign: + return ethSignHashWithKey(this.getHash(), key); + default: + throw new Error(`Cannot sign with signature type: ${type}`); + } + } +} + +export class LimitOrder extends OrderBase { + public static readonly TYPE_HASH = hexUtils.hash( + hexUtils.toHex( + Buffer.from( + [ + 'LimitOrder(', + [ + 'address makerToken', + 'address takerToken', + 'uint128 makerAmount', + 'uint128 takerAmount', + 'uint128 takerTokenFeeAmount', + 'address maker', + 'address taker', + 'address sender', + 'address feeRecipient', + 'bytes32 pool', + 'uint64 expiry', + 'uint256 salt', + ].join(','), + ')', + ].join(''), + ), + ), + ); + + public takerTokenFeeAmount: BigNumber; + public taker: string; + public sender: string; + public feeRecipient: string; + + constructor(fields: Partial = {}) { + const _fields = { ...LIMIT_ORDER_DEFAULT_VALUES, ...fields }; + super(_fields); + this.takerTokenFeeAmount = _fields.takerTokenFeeAmount; + this.taker = _fields.taker; + this.sender = _fields.sender; + this.feeRecipient = _fields.feeRecipient; + } + + public clone(fields: Partial = {}): LimitOrder { + return new LimitOrder({ + makerToken: this.makerToken, + takerToken: this.takerToken, + makerAmount: this.makerAmount, + takerAmount: this.takerAmount, + takerTokenFeeAmount: this.takerTokenFeeAmount, + maker: this.maker, + taker: this.taker, + sender: this.sender, + feeRecipient: this.feeRecipient, + pool: this.pool, + expiry: this.expiry, + salt: this.salt, + chainId: this.chainId, + verifyingContract: this.verifyingContract, + ...fields, + }); + } + + public getStructHash(): string { + return hexUtils.hash( + hexUtils.concat( + hexUtils.leftPad(LimitOrder.TYPE_HASH), + hexUtils.leftPad(this.makerToken), + hexUtils.leftPad(this.takerToken), + hexUtils.leftPad(this.makerAmount), + hexUtils.leftPad(this.takerAmount), + hexUtils.leftPad(this.takerTokenFeeAmount), + hexUtils.leftPad(this.maker), + hexUtils.leftPad(this.taker), + hexUtils.leftPad(this.sender), + hexUtils.leftPad(this.feeRecipient), + hexUtils.leftPad(this.pool), + hexUtils.leftPad(this.expiry), + hexUtils.leftPad(this.salt), + ), + ); + } + + public getEIP712TypedData(): EIP712TypedData { + return { + types: { + EIP712Domain: EIP712_DOMAIN_PARAMETERS, + LimitOrder: [ + { type: 'address', name: 'makerToken' }, + { type: 'address', name: 'takerToken' }, + { type: 'uint128', name: 'makerAmount' }, + { type: 'uint128', name: 'takerAmount' }, + { type: 'uint128', name: 'takerTokenFeeAmount' }, + { type: 'address', name: 'maker' }, + { type: 'address', name: 'taker' }, + { type: 'address', name: 'sender' }, + { type: 'address', name: 'feeRecipient' }, + { type: 'bytes32', name: 'pool' }, + { type: 'uint64', name: 'expiry' }, + { type: 'uint256', name: 'salt' }, + ], + }, + domain: createExchangeProxyEIP712Domain(this.chainId, this.verifyingContract) as any, + primaryType: 'LimitOrder', + message: { + makerToken: this.makerToken, + takerToken: this.takerToken, + makerAmount: this.makerAmount.toString(10), + takerAmount: this.takerAmount.toString(10), + takerTokenFeeAmount: this.takerTokenFeeAmount.toString(10), + maker: this.maker, + taker: this.taker, + sender: this.sender, + feeRecipient: this.feeRecipient, + pool: this.pool, + expiry: this.expiry.toString(10), + salt: this.salt.toString(10), + }, + }; + } +} + +export class RfqOrder extends OrderBase { + public static readonly TYPE_HASH = hexUtils.hash( + hexUtils.toHex( + Buffer.from( + [ + 'RfqOrder(', + [ + 'address makerToken', + 'address takerToken', + 'uint128 makerAmount', + 'uint128 takerAmount', + 'address maker', + 'address txOrigin', + 'bytes32 pool', + 'uint64 expiry', + 'uint256 salt', + ].join(','), + ')', + ].join(''), + ), + ), + ); + + public txOrigin: string; + + constructor(fields: Partial = {}) { + const _fields = { ...RFQ_ORDER_DEFAULT_VALUES, ...fields }; + super(_fields); + this.txOrigin = _fields.txOrigin; + } + + public clone(fields: Partial = {}): RfqOrder { + return new RfqOrder({ + makerToken: this.makerToken, + takerToken: this.takerToken, + makerAmount: this.makerAmount, + takerAmount: this.takerAmount, + maker: this.maker, + txOrigin: this.txOrigin, + pool: this.pool, + expiry: this.expiry, + salt: this.salt, + chainId: this.chainId, + verifyingContract: this.verifyingContract, + ...fields, + }); + } + + public getStructHash(): string { + return hexUtils.hash( + hexUtils.concat( + hexUtils.leftPad(RfqOrder.TYPE_HASH), + hexUtils.leftPad(this.makerToken), + hexUtils.leftPad(this.takerToken), + hexUtils.leftPad(this.makerAmount), + hexUtils.leftPad(this.takerAmount), + hexUtils.leftPad(this.maker), + hexUtils.leftPad(this.txOrigin), + hexUtils.leftPad(this.pool), + hexUtils.leftPad(this.expiry), + hexUtils.leftPad(this.salt), + ), + ); + } + + public getEIP712TypedData(): EIP712TypedData { + return { + types: { + EIP712Domain: EIP712_DOMAIN_PARAMETERS, + RfqOrder: [ + { type: 'address', name: 'makerToken' }, + { type: 'address', name: 'takerToken' }, + { type: 'uint128', name: 'makerAmount' }, + { type: 'uint128', name: 'takerAmount' }, + { type: 'address', name: 'maker' }, + { type: 'address', name: 'txOrigin' }, + { type: 'bytes32', name: 'pool' }, + { type: 'uint64', name: 'expiry' }, + { type: 'uint256', name: 'salt' }, + ], + }, + domain: createExchangeProxyEIP712Domain(this.chainId, this.verifyingContract) as any, + primaryType: 'RfqOrder', + message: { + makerToken: this.makerToken, + takerToken: this.takerToken, + makerAmount: this.makerAmount.toString(10), + takerAmount: this.takerAmount.toString(10), + maker: this.maker, + txOrigin: this.txOrigin, + pool: this.pool, + expiry: this.expiry.toString(10), + salt: this.salt.toString(10), + }, + }; + } +} diff --git a/contracts/zero-ex/src/revert_errors.ts b/contracts/zero-ex/src/revert_errors.ts index 5be6dbff1e..72ec23afbb 100644 --- a/contracts/zero-ex/src/revert_errors.ts +++ b/contracts/zero-ex/src/revert_errors.ts @@ -1,7 +1,9 @@ // TODO(dorothy-zbornak): Move these into `@0x/protocol-utils` whenever that // becomes a thing. // tslint:disable:max-classes-per-file -import { RevertError } from '@0x/utils'; +import { Numberish, RevertError } from '@0x/utils'; + +import { OrderStatus } from './orders'; export enum SignatureValidationErrorCodes { AlwaysInvalid = 0, @@ -12,7 +14,6 @@ export enum SignatureValidationErrorCodes { BadSignatureData = 5, } -// tslint:disable:max-classes-per-file export class SignatureValidationError extends RevertError { constructor(code?: SignatureValidationErrorCodes, hash?: string) { super('SignatureValidationError', 'SignatureValidationError(uint8 code, bytes32 hash)', { @@ -22,7 +23,125 @@ export class SignatureValidationError extends RevertError { } } -const types = [SignatureValidationError]; +export class ProtocolFeeRefundFailed extends RevertError { + constructor(receiver?: string, refundAmount?: Numberish) { + super('ProtocolFeeRefundFailed', 'ProtocolFeeRefundFailed(address receiver, uint256 refundAmount)', { + receiver, + refundAmount, + }); + } +} + +export class OrderNotFillableByOriginError extends RevertError { + constructor(orderHash?: string, txOrigin?: string, orderTxOrigin?: string) { + super( + 'OrderNotFillableByOriginError', + 'OrderNotFillableByOriginError(bytes32 orderHash, address txOrigin, address orderTxOrigin)', + { + orderHash, + txOrigin, + orderTxOrigin, + }, + ); + } +} + +export class OrderNotFillableError extends RevertError { + constructor(orderHash?: string, orderStatus?: OrderStatus) { + super('OrderNotFillableError', 'OrderNotFillableError(bytes32 orderHash, uint8 orderStatus)', { + orderHash, + orderStatus, + }); + } +} + +export class OrderNotSignedByMakerError extends RevertError { + constructor(orderHash?: string, signer?: string, maker?: string) { + super( + 'OrderNotSignedByMakerError', + 'OrderNotSignedByMakerError(bytes32 orderHash, address signer, address maker)', + { + orderHash, + signer, + maker, + }, + ); + } +} + +export class OrderNotFillableBySenderError extends RevertError { + constructor(orderHash?: string, sender?: string, orderSender?: string) { + super( + 'OrderNotFillableBySenderError', + 'OrderNotFillableBySenderError(bytes32 orderHash, address sender, address orderSender)', + { + orderHash, + sender, + orderSender, + }, + ); + } +} + +export class OrderNotFillableByTakerError extends RevertError { + constructor(orderHash?: string, taker?: string, orderTaker?: string) { + super( + 'OrderNotFillableByTakerError', + 'OrderNotFillableByTakerError(bytes32 orderHash, address taker, address orderTaker)', + { + orderHash, + taker, + orderTaker, + }, + ); + } +} + +export class CancelSaltTooLowError extends RevertError { + constructor(minValidSalt?: Numberish, oldMinValidSalt?: Numberish) { + super('CancelSaltTooLowError', 'CancelSaltTooLowError(uint256 minValidSalt, uint256 oldMinValidSalt)', { + minValidSalt, + oldMinValidSalt, + }); + } +} + +export class FillOrKillFailedError extends RevertError { + constructor(orderHash?: string, takerTokenFilledAmount?: Numberish, takerTokenFillAmount?: Numberish) { + super( + 'FillOrKillFailedError', + 'FillOrKillFailedError(bytes32 orderHash, uint256 takerTokenFilledAmount, uint256 takerTokenFillAmount)', + { + orderHash, + takerTokenFilledAmount, + takerTokenFillAmount, + }, + ); + } +} + +export class OnlyOrderMakerAllowed extends RevertError { + constructor(orderHash?: string, sender?: string, maker?: string) { + super('OnlyOrderMakerAllowed', 'OnlyOrderMakerAllowed(bytes32 orderHash, address sender, address maker)', { + orderHash, + sender, + maker, + }); + } +} + +const types = [ + SignatureValidationError, + ProtocolFeeRefundFailed, + OrderNotFillableByOriginError, + OrderNotFillableError, + OrderNotSignedByMakerError, + OrderNotFillableBySenderError, + OrderNotFillableByTakerError, + CancelSaltTooLowError, + FillOrKillFailedError, + OnlyOrderMakerAllowed, +]; // Register the types we've defined. for (const type of types) { diff --git a/contracts/zero-ex/src/signature_utils.ts b/contracts/zero-ex/src/signature_utils.ts index 1d5fe3ef30..879f54fa06 100644 --- a/contracts/zero-ex/src/signature_utils.ts +++ b/contracts/zero-ex/src/signature_utils.ts @@ -1,7 +1,7 @@ -import { signatureUtils } from '@0x/order-utils'; import { SupportedProvider } from '@0x/subproviders'; import { EIP712TypedData } from '@0x/types'; -import { hexUtils, signTypedDataUtils } from '@0x/utils'; +import { hexUtils, providerUtils, signTypedDataUtils } from '@0x/utils'; +import { Web3Wrapper } from '@0x/web3-wrapper'; import * as ethjs from 'ethereumjs-util'; /** @@ -33,15 +33,17 @@ export interface Signature extends ECSignature { /** * Sign a hash with the EthSign signature type on a provider. */ -export async function ethSignHashFromProviderAsync( - signer: string, +export async function ethSignHashWithProviderAsync( hash: string, + signer: string, provider: SupportedProvider, ): Promise { - const signatureBytes = await signatureUtils.ecSignHashAsync(provider, hash, signer); - const parsed = parsePackedSignatureBytes(signatureBytes); - assertSignatureType(parsed, SignatureType.EthSign); - return parsed; + const w3w = new Web3Wrapper(providerUtils.standardizeOrThrow(provider)); + const rpcSig = await w3w.signMessageAsync(signer, hash); + return { + ...parseRpcSignature(rpcSig), + signatureType: SignatureType.EthSign, + }; } /** @@ -57,6 +59,22 @@ export function ethSignHashWithKey(hash: string, key: string): Signature { }; } +/** + * Sign a typed data object with the EIP712 signature type on a provider. + */ +export async function eip712SignTypedDataWithProviderAsync( + data: EIP712TypedData, + signer: string, + provider: SupportedProvider, +): Promise { + const w3w = new Web3Wrapper(providerUtils.standardizeOrThrow(provider)); + const rpcSig = await w3w.signTypedDataAsync(signer, data); + return { + ...parseRpcSignature(rpcSig), + signatureType: SignatureType.EIP712, + }; +} + /** * Sign a typed data object with the EIP712 signature type, given a private key. */ @@ -90,24 +108,34 @@ export function ecSignHashWithKey(hash: string, key: string): ECSignature { }; } -function assertSignatureType(signature: Signature, expectedType: SignatureType): void { - if (signature.signatureType !== expectedType) { - throw new Error(`Expected signature type to be ${expectedType} but received ${signature.signatureType}.`); +// Parse a hex signature returned by an RPC call into an `ECSignature`. +function parseRpcSignature(rpcSig: string): ECSignature { + if (hexUtils.size(rpcSig) !== 65) { + throw new Error(`Invalid RPC signature length: "${rpcSig}"`); } -} - -function parsePackedSignatureBytes(signatureBytes: string): Signature { - if (hexUtils.size(signatureBytes) !== 66) { - throw new Error(`Expected packed signatureBytes to be 66 bytes long: ${signatureBytes}`); + // Some providers encode V as 0,1 instead of 27,28. + const VALID_V_VALUES = [0, 1, 27, 28]; + // Some providers return the signature packed as V,R,S and others R,S,V. + // Try to guess which encoding it is (with a slight preference for R,S,V). + let v = parseInt(rpcSig.slice(-2), 16); + if (VALID_V_VALUES.includes(v)) { + // Format is R,S,V + v = v >= 27 ? v : v + 27; + return { + r: hexUtils.slice(rpcSig, 0, 32), + s: hexUtils.slice(rpcSig, 32, 64), + v, + }; } - const typeId = parseInt(signatureBytes.slice(-2), 16) as SignatureType; - if (!Object.values(SignatureType).includes(typeId)) { - throw new Error(`Invalid signatureBytes type ID detected: ${typeId}`); + // Format should be V,R,S + v = parseInt(rpcSig.slice(2, 4), 16); + if (!VALID_V_VALUES.includes(v)) { + throw new Error(`Cannot determine RPC signature layout from V value: "${rpcSig}"`); } + v = v >= 27 ? v : v + 27; return { - signatureType: typeId, - v: parseInt(signatureBytes.slice(2, 4), 16), - r: hexUtils.slice(signatureBytes, 1, 33), - s: hexUtils.slice(signatureBytes, 33), + v, + r: hexUtils.slice(rpcSig, 1, 33), + s: hexUtils.slice(rpcSig, 33, 65), }; } diff --git a/contracts/zero-ex/src/wrappers.ts b/contracts/zero-ex/src/wrappers.ts index 618b271096..114a7455d5 100644 --- a/contracts/zero-ex/src/wrappers.ts +++ b/contracts/zero-ex/src/wrappers.ts @@ -10,7 +10,8 @@ export * from '../generated-wrappers/full_migration'; export * from '../generated-wrappers/i_allowance_target'; export * from '../generated-wrappers/i_erc20_transformer'; export * from '../generated-wrappers/i_flash_wallet'; -export * from '../generated-wrappers/i_liquidity_provider'; +export * from '../generated-wrappers/i_liquidity_provider_feature'; +export * from '../generated-wrappers/i_native_orders_feature'; export * from '../generated-wrappers/i_ownable_feature'; export * from '../generated-wrappers/i_simple_function_registry_feature'; export * from '../generated-wrappers/i_token_spender_feature'; @@ -20,6 +21,7 @@ export * from '../generated-wrappers/initial_migration'; export * from '../generated-wrappers/liquidity_provider_feature'; export * from '../generated-wrappers/log_metadata_transformer'; export * from '../generated-wrappers/meta_transactions_feature'; +export * from '../generated-wrappers/native_orders_feature'; export * from '../generated-wrappers/ownable_feature'; export * from '../generated-wrappers/pay_taker_transformer'; export * from '../generated-wrappers/signature_validator_feature'; diff --git a/contracts/zero-ex/test/artifacts.ts b/contracts/zero-ex/test/artifacts.ts index 59a6ee3831..b34cdf9d86 100644 --- a/contracts/zero-ex/test/artifacts.ts +++ b/contracts/zero-ex/test/artifacts.ts @@ -30,6 +30,7 @@ import * as ILiquidityProvider from '../test/generated-artifacts/ILiquidityProvi import * as ILiquidityProviderFeature from '../test/generated-artifacts/ILiquidityProviderFeature.json'; import * as ILiquidityProviderSandbox from '../test/generated-artifacts/ILiquidityProviderSandbox.json'; import * as IMetaTransactionsFeature from '../test/generated-artifacts/IMetaTransactionsFeature.json'; +import * as INativeOrdersFeature from '../test/generated-artifacts/INativeOrdersFeature.json'; import * as InitialMigration from '../test/generated-artifacts/InitialMigration.json'; import * as IOwnableFeature from '../test/generated-artifacts/IOwnableFeature.json'; import * as ISignatureValidatorFeature from '../test/generated-artifacts/ISignatureValidatorFeature.json'; @@ -47,6 +48,9 @@ import * as LibLiquidityProviderRichErrors from '../test/generated-artifacts/Lib import * as LibMetaTransactionsRichErrors from '../test/generated-artifacts/LibMetaTransactionsRichErrors.json'; import * as LibMetaTransactionsStorage from '../test/generated-artifacts/LibMetaTransactionsStorage.json'; import * as LibMigrate from '../test/generated-artifacts/LibMigrate.json'; +import * as LibNativeOrder from '../test/generated-artifacts/LibNativeOrder.json'; +import * as LibNativeOrdersRichErrors from '../test/generated-artifacts/LibNativeOrdersRichErrors.json'; +import * as LibNativeOrdersStorage from '../test/generated-artifacts/LibNativeOrdersStorage.json'; import * as LibOrderHash from '../test/generated-artifacts/LibOrderHash.json'; import * as LibOwnableRichErrors from '../test/generated-artifacts/LibOwnableRichErrors.json'; import * as LibOwnableStorage from '../test/generated-artifacts/LibOwnableStorage.json'; @@ -82,6 +86,7 @@ import * as MixinSushiswap from '../test/generated-artifacts/MixinSushiswap.json import * as MixinUniswap from '../test/generated-artifacts/MixinUniswap.json'; import * as MixinUniswapV2 from '../test/generated-artifacts/MixinUniswapV2.json'; import * as MixinZeroExBridge from '../test/generated-artifacts/MixinZeroExBridge.json'; +import * as NativeOrdersFeature from '../test/generated-artifacts/NativeOrdersFeature.json'; import * as OwnableFeature from '../test/generated-artifacts/OwnableFeature.json'; import * as PayTakerTransformer from '../test/generated-artifacts/PayTakerTransformer.json'; import * as SignatureValidatorFeature from '../test/generated-artifacts/SignatureValidatorFeature.json'; @@ -92,8 +97,10 @@ import * as TestDelegateCaller from '../test/generated-artifacts/TestDelegateCal import * as TestFillQuoteTransformerBridge from '../test/generated-artifacts/TestFillQuoteTransformerBridge.json'; import * as TestFillQuoteTransformerExchange from '../test/generated-artifacts/TestFillQuoteTransformerExchange.json'; import * as TestFillQuoteTransformerHost from '../test/generated-artifacts/TestFillQuoteTransformerHost.json'; +import * as TestFixinProtocolFees from '../test/generated-artifacts/TestFixinProtocolFees.json'; import * as TestFullMigration from '../test/generated-artifacts/TestFullMigration.json'; import * as TestInitialMigration from '../test/generated-artifacts/TestInitialMigration.json'; +import * as TestLibNativeOrder from '../test/generated-artifacts/TestLibNativeOrder.json'; import * as TestLibSignature from '../test/generated-artifacts/TestLibSignature.json'; import * as TestLibTokenSpender from '../test/generated-artifacts/TestLibTokenSpender.json'; import * as TestLiquidityProvider from '../test/generated-artifacts/TestLiquidityProvider.json'; @@ -101,7 +108,7 @@ import * as TestMetaTransactionsTransformERC20Feature from '../test/generated-ar import * as TestMigrator from '../test/generated-artifacts/TestMigrator.json'; import * as TestMintableERC20Token from '../test/generated-artifacts/TestMintableERC20Token.json'; import * as TestMintTokenERC20Transformer from '../test/generated-artifacts/TestMintTokenERC20Transformer.json'; -import * as TestProtocolFees from '../test/generated-artifacts/TestProtocolFees.json'; +import * as TestNativeOrdersFeature from '../test/generated-artifacts/TestNativeOrdersFeature.json'; import * as TestSimpleFunctionRegistryFeatureImpl1 from '../test/generated-artifacts/TestSimpleFunctionRegistryFeatureImpl1.json'; import * as TestSimpleFunctionRegistryFeatureImpl2 from '../test/generated-artifacts/TestSimpleFunctionRegistryFeatureImpl2.json'; import * as TestStaking from '../test/generated-artifacts/TestStaking.json'; @@ -127,6 +134,7 @@ export const artifacts = { LibCommonRichErrors: LibCommonRichErrors as ContractArtifact, LibLiquidityProviderRichErrors: LibLiquidityProviderRichErrors as ContractArtifact, LibMetaTransactionsRichErrors: LibMetaTransactionsRichErrors as ContractArtifact, + LibNativeOrdersRichErrors: LibNativeOrdersRichErrors as ContractArtifact, LibOwnableRichErrors: LibOwnableRichErrors as ContractArtifact, LibProxyRichErrors: LibProxyRichErrors as ContractArtifact, LibSignatureRichErrors: LibSignatureRichErrors as ContractArtifact, @@ -147,6 +155,7 @@ export const artifacts = { IFeature: IFeature as ContractArtifact, ILiquidityProviderFeature: ILiquidityProviderFeature as ContractArtifact, IMetaTransactionsFeature: IMetaTransactionsFeature as ContractArtifact, + INativeOrdersFeature: INativeOrdersFeature as ContractArtifact, IOwnableFeature: IOwnableFeature as ContractArtifact, ISignatureValidatorFeature: ISignatureValidatorFeature as ContractArtifact, ISimpleFunctionRegistryFeature: ISimpleFunctionRegistryFeature as ContractArtifact, @@ -155,12 +164,14 @@ export const artifacts = { IUniswapFeature: IUniswapFeature as ContractArtifact, LiquidityProviderFeature: LiquidityProviderFeature as ContractArtifact, MetaTransactionsFeature: MetaTransactionsFeature as ContractArtifact, + NativeOrdersFeature: NativeOrdersFeature as ContractArtifact, OwnableFeature: OwnableFeature as ContractArtifact, SignatureValidatorFeature: SignatureValidatorFeature as ContractArtifact, SimpleFunctionRegistryFeature: SimpleFunctionRegistryFeature as ContractArtifact, TokenSpenderFeature: TokenSpenderFeature as ContractArtifact, TransformERC20Feature: TransformERC20Feature as ContractArtifact, UniswapFeature: UniswapFeature as ContractArtifact, + LibNativeOrder: LibNativeOrder as ContractArtifact, LibSignature: LibSignature as ContractArtifact, LibSignedCallData: LibSignedCallData as ContractArtifact, LibTokenSpender: LibTokenSpender as ContractArtifact, @@ -173,6 +184,7 @@ export const artifacts = { LibBootstrap: LibBootstrap as ContractArtifact, LibMigrate: LibMigrate as ContractArtifact, LibMetaTransactionsStorage: LibMetaTransactionsStorage as ContractArtifact, + LibNativeOrdersStorage: LibNativeOrdersStorage as ContractArtifact, LibOwnableStorage: LibOwnableStorage as ContractArtifact, LibProxyStorage: LibProxyStorage as ContractArtifact, LibReentrancyGuardStorage: LibReentrancyGuardStorage as ContractArtifact, @@ -216,8 +228,10 @@ export const artifacts = { TestFillQuoteTransformerBridge: TestFillQuoteTransformerBridge as ContractArtifact, TestFillQuoteTransformerExchange: TestFillQuoteTransformerExchange as ContractArtifact, TestFillQuoteTransformerHost: TestFillQuoteTransformerHost as ContractArtifact, + TestFixinProtocolFees: TestFixinProtocolFees as ContractArtifact, TestFullMigration: TestFullMigration as ContractArtifact, TestInitialMigration: TestInitialMigration as ContractArtifact, + TestLibNativeOrder: TestLibNativeOrder as ContractArtifact, TestLibSignature: TestLibSignature as ContractArtifact, TestLibTokenSpender: TestLibTokenSpender as ContractArtifact, TestLiquidityProvider: TestLiquidityProvider as ContractArtifact, @@ -225,7 +239,7 @@ export const artifacts = { TestMigrator: TestMigrator as ContractArtifact, TestMintTokenERC20Transformer: TestMintTokenERC20Transformer as ContractArtifact, TestMintableERC20Token: TestMintableERC20Token as ContractArtifact, - TestProtocolFees: TestProtocolFees as ContractArtifact, + TestNativeOrdersFeature: TestNativeOrdersFeature as ContractArtifact, TestSimpleFunctionRegistryFeatureImpl1: TestSimpleFunctionRegistryFeatureImpl1 as ContractArtifact, TestSimpleFunctionRegistryFeatureImpl2: TestSimpleFunctionRegistryFeatureImpl2 as ContractArtifact, TestStaking: TestStaking as ContractArtifact, diff --git a/contracts/zero-ex/test/features/limit_orders_feature_test.ts b/contracts/zero-ex/test/features/limit_orders_feature_test.ts new file mode 100644 index 0000000000..219ec66ec8 --- /dev/null +++ b/contracts/zero-ex/test/features/limit_orders_feature_test.ts @@ -0,0 +1,1234 @@ +import { blockchainTests, constants, describe, expect, verifyEventsFromLogs } from '@0x/contracts-test-utils'; +import { AnyRevertError, BigNumber } from '@0x/utils'; +import { TransactionReceiptWithDecodedLogs } from 'ethereum-types'; + +import { LimitOrder, LimitOrderFields, OrderInfo, OrderStatus, RfqOrder, RfqOrderFields } from '../../src/orders'; +import * as RevertErrors from '../../src/revert_errors'; +import { IZeroExContract, IZeroExEvents } from '../../src/wrappers'; +import { artifacts } from '../artifacts'; +import { fullMigrateAsync } from '../utils/migration'; +import { getRandomLimitOrder, getRandomRfqOrder } from '../utils/orders'; +import { TestMintableERC20TokenContract } from '../wrappers'; + +blockchainTests.resets('LimitOrdersFeature', env => { + const { NULL_ADDRESS, MAX_UINT256, ZERO_AMOUNT } = constants; + const GAS_PRICE = new BigNumber('123e9'); + const PROTOCOL_FEE_MULTIPLIER = 1337e3; + const SINGLE_PROTOCOL_FEE = GAS_PRICE.times(PROTOCOL_FEE_MULTIPLIER); + let maker: string; + let taker: string; + let notMaker: string; + let notTaker: string; + let zeroEx: IZeroExContract; + let verifyingContract: string; + let makerToken: TestMintableERC20TokenContract; + let takerToken: TestMintableERC20TokenContract; + let wethToken: TestMintableERC20TokenContract; + + before(async () => { + let owner; + [owner, maker, taker, notMaker, notTaker] = await env.getAccountAddressesAsync(); + [makerToken, takerToken, wethToken] = await Promise.all( + [...new Array(3)].map(async () => + TestMintableERC20TokenContract.deployFrom0xArtifactAsync( + artifacts.TestMintableERC20Token, + env.provider, + env.txDefaults, + artifacts, + ), + ), + ); + zeroEx = await fullMigrateAsync( + owner, + env.provider, + { ...env.txDefaults, gasPrice: GAS_PRICE }, + {}, + { wethAddress: wethToken.address, protocolFeeMultiplier: PROTOCOL_FEE_MULTIPLIER }, + { nativeOrders: artifacts.TestNativeOrdersFeature }, + ); + verifyingContract = zeroEx.address; + await Promise.all( + [maker, notMaker].map(a => + makerToken.approve(zeroEx.address, MAX_UINT256).awaitTransactionSuccessAsync({ from: a }), + ), + ); + await Promise.all( + [taker, notTaker].map(a => + takerToken.approve(zeroEx.address, MAX_UINT256).awaitTransactionSuccessAsync({ from: a }), + ), + ); + }); + + function getTestLimitOrder(fields: Partial = {}): LimitOrder { + return getRandomLimitOrder({ + maker, + verifyingContract, + takerToken: takerToken.address, + makerToken: makerToken.address, + taker: NULL_ADDRESS, + sender: NULL_ADDRESS, + ...fields, + }); + } + + function getTestRfqOrder(fields: Partial = {}): RfqOrder { + return getRandomRfqOrder({ + maker, + verifyingContract, + takerToken: takerToken.address, + makerToken: makerToken.address, + txOrigin: NULL_ADDRESS, + ...fields, + }); + } + + async function prepareBalancesForOrderAsync(order: LimitOrder | RfqOrder, _taker: string = taker): Promise { + await makerToken.mint(maker, order.makerAmount).awaitTransactionSuccessAsync(); + if ('takerTokenFeeAmount' in order) { + await takerToken + .mint(taker, order.takerAmount.plus(order.takerTokenFeeAmount)) + .awaitTransactionSuccessAsync(); + } else { + await takerToken.mint(taker, order.takerAmount).awaitTransactionSuccessAsync(); + } + } + + function assertOrderInfoEquals(actual: OrderInfo, expected: OrderInfo): void { + expect(actual.status).to.eq(expected.status); + expect(actual.orderHash).to.eq(expected.orderHash); + expect(actual.takerTokenFilledAmount).to.bignumber.eq(expected.takerTokenFilledAmount); + } + + function createExpiry(deltaSeconds: number = 60): BigNumber { + return new BigNumber(Math.floor(Date.now() / 1000) + deltaSeconds); + } + + describe('getProtocolFeeMultiplier()', () => { + it('returns the protocol fee multiplier', async () => { + const r = await zeroEx.getProtocolFeeMultiplier().callAsync(); + expect(r).to.bignumber.eq(PROTOCOL_FEE_MULTIPLIER); + }); + }); + + describe('getLimitOrderHash()', () => { + it('returns the correct hash', async () => { + const order = getTestLimitOrder(); + const hash = await zeroEx.getLimitOrderHash(order).callAsync(); + expect(hash).to.eq(order.getHash()); + }); + }); + + describe('getRfqOrderHash()', () => { + it('returns the correct hash', async () => { + const order = getTestRfqOrder(); + const hash = await zeroEx.getRfqOrderHash(order).callAsync(); + expect(hash).to.eq(order.getHash()); + }); + }); + + async function fillLimitOrderAsync( + order: LimitOrder, + fillAmount: BigNumber | number = order.takerAmount, + _taker: string = taker, + ): Promise { + await prepareBalancesForOrderAsync(order, _taker); + return zeroEx + .fillLimitOrder(order, await order.getSignatureWithProviderAsync(env.provider), new BigNumber(fillAmount)) + .awaitTransactionSuccessAsync({ from: _taker, value: SINGLE_PROTOCOL_FEE }); + } + + describe('getLimitOrderInfo()', () => { + it('unfilled order', async () => { + const order = getTestLimitOrder(); + const info = await zeroEx.getLimitOrderInfo(order).callAsync(); + assertOrderInfoEquals(info, { + status: OrderStatus.Fillable, + orderHash: order.getHash(), + takerTokenFilledAmount: ZERO_AMOUNT, + }); + }); + + it('unfilled cancelled order', async () => { + const order = getTestLimitOrder(); + await zeroEx.cancelLimitOrder(order).awaitTransactionSuccessAsync({ from: maker }); + const info = await zeroEx.getLimitOrderInfo(order).callAsync(); + assertOrderInfoEquals(info, { + status: OrderStatus.Cancelled, + orderHash: order.getHash(), + takerTokenFilledAmount: ZERO_AMOUNT, + }); + }); + + it('unfilled expired order', async () => { + const order = getTestLimitOrder({ expiry: createExpiry(-60) }); + const info = await zeroEx.getLimitOrderInfo(order).callAsync(); + assertOrderInfoEquals(info, { + status: OrderStatus.Expired, + orderHash: order.getHash(), + takerTokenFilledAmount: ZERO_AMOUNT, + }); + }); + + it('filled then expired order', async () => { + const expiry = createExpiry(60); + const order = getTestLimitOrder({ expiry }); + // Fill the order first. + await fillLimitOrderAsync(order); + // Advance time to expire the order. + await env.web3Wrapper.increaseTimeAsync(61); + const info = await zeroEx.getLimitOrderInfo(order).callAsync(); + assertOrderInfoEquals(info, { + status: OrderStatus.Filled, // Still reports filled. + orderHash: order.getHash(), + takerTokenFilledAmount: order.takerAmount, + }); + }); + + it('filled order', async () => { + const order = getTestLimitOrder(); + // Fill the order first. + await fillLimitOrderAsync(order); + const info = await zeroEx.getLimitOrderInfo(order).callAsync(); + assertOrderInfoEquals(info, { + status: OrderStatus.Filled, + orderHash: order.getHash(), + takerTokenFilledAmount: order.takerAmount, + }); + }); + + it('partially filled order', async () => { + const order = getTestLimitOrder(); + const fillAmount = order.takerAmount.minus(1); + // Fill the order first. + await fillLimitOrderAsync(order, fillAmount); + const info = await zeroEx.getLimitOrderInfo(order).callAsync(); + assertOrderInfoEquals(info, { + status: OrderStatus.Fillable, + orderHash: order.getHash(), + takerTokenFilledAmount: fillAmount, + }); + }); + + it('filled then cancelled order', async () => { + const order = getTestLimitOrder(); + // Fill the order first. + await fillLimitOrderAsync(order); + await zeroEx.cancelLimitOrder(order).awaitTransactionSuccessAsync({ from: maker }); + const info = await zeroEx.getLimitOrderInfo(order).callAsync(); + assertOrderInfoEquals(info, { + status: OrderStatus.Filled, // Still reports filled. + orderHash: order.getHash(), + takerTokenFilledAmount: order.takerAmount, + }); + }); + + it('partially filled then cancelled order', async () => { + const order = getTestLimitOrder(); + const fillAmount = order.takerAmount.minus(1); + // Fill the order first. + await fillLimitOrderAsync(order, fillAmount); + await zeroEx.cancelLimitOrder(order).awaitTransactionSuccessAsync({ from: maker }); + const info = await zeroEx.getLimitOrderInfo(order).callAsync(); + assertOrderInfoEquals(info, { + status: OrderStatus.Cancelled, + orderHash: order.getHash(), + takerTokenFilledAmount: fillAmount, + }); + }); + }); + + async function fillRfqOrderAsync( + order: RfqOrder, + fillAmount: BigNumber | number = order.takerAmount, + _taker: string = taker, + ): Promise { + await prepareBalancesForOrderAsync(order, _taker); + return zeroEx + .fillRfqOrder(order, await order.getSignatureWithProviderAsync(env.provider), new BigNumber(fillAmount)) + .awaitTransactionSuccessAsync({ from: _taker, value: SINGLE_PROTOCOL_FEE }); + } + + describe('getRfqOrderInfo()', () => { + it('unfilled order', async () => { + const order = getTestRfqOrder(); + const info = await zeroEx.getRfqOrderInfo(order).callAsync(); + assertOrderInfoEquals(info, { + status: OrderStatus.Fillable, + orderHash: order.getHash(), + takerTokenFilledAmount: ZERO_AMOUNT, + }); + }); + + it('unfilled cancelled order', async () => { + const order = getTestRfqOrder(); + await zeroEx.cancelRfqOrder(order).awaitTransactionSuccessAsync({ from: maker }); + const info = await zeroEx.getRfqOrderInfo(order).callAsync(); + assertOrderInfoEquals(info, { + status: OrderStatus.Cancelled, + orderHash: order.getHash(), + takerTokenFilledAmount: ZERO_AMOUNT, + }); + }); + + it('unfilled expired order', async () => { + const expiry = createExpiry(-60); + const order = getTestRfqOrder({ expiry }); + const info = await zeroEx.getRfqOrderInfo(order).callAsync(); + assertOrderInfoEquals(info, { + status: OrderStatus.Expired, + orderHash: order.getHash(), + takerTokenFilledAmount: ZERO_AMOUNT, + }); + }); + + it('filled then expired order', async () => { + const expiry = createExpiry(60); + const order = getTestRfqOrder({ expiry }); + await prepareBalancesForOrderAsync(order); + const sig = await order.getSignatureWithProviderAsync(env.provider); + // Fill the order first. + await zeroEx + .fillRfqOrder(order, sig, order.takerAmount) + .awaitTransactionSuccessAsync({ from: taker, value: SINGLE_PROTOCOL_FEE }); + // Advance time to expire the order. + await env.web3Wrapper.increaseTimeAsync(61); + const info = await zeroEx.getRfqOrderInfo(order).callAsync(); + assertOrderInfoEquals(info, { + status: OrderStatus.Filled, // Still reports filled. + orderHash: order.getHash(), + takerTokenFilledAmount: order.takerAmount, + }); + }); + + it('filled order', async () => { + const order = getTestRfqOrder(); + // Fill the order first. + await fillRfqOrderAsync(order); + const info = await zeroEx.getRfqOrderInfo(order).callAsync(); + assertOrderInfoEquals(info, { + status: OrderStatus.Filled, + orderHash: order.getHash(), + takerTokenFilledAmount: order.takerAmount, + }); + }); + + it('partially filled order', async () => { + const order = getTestRfqOrder(); + const fillAmount = order.takerAmount.minus(1); + // Fill the order first. + await fillRfqOrderAsync(order, fillAmount); + const info = await zeroEx.getRfqOrderInfo(order).callAsync(); + assertOrderInfoEquals(info, { + status: OrderStatus.Fillable, + orderHash: order.getHash(), + takerTokenFilledAmount: fillAmount, + }); + }); + + it('filled then cancelled order', async () => { + const order = getTestRfqOrder(); + // Fill the order first. + await fillRfqOrderAsync(order); + await zeroEx.cancelRfqOrder(order).awaitTransactionSuccessAsync({ from: maker }); + const info = await zeroEx.getRfqOrderInfo(order).callAsync(); + assertOrderInfoEquals(info, { + status: OrderStatus.Filled, // Still reports filled. + orderHash: order.getHash(), + takerTokenFilledAmount: order.takerAmount, + }); + }); + + it('partially filled then cancelled order', async () => { + const order = getTestRfqOrder(); + const fillAmount = order.takerAmount.minus(1); + // Fill the order first. + await fillRfqOrderAsync(order, fillAmount); + await zeroEx.cancelRfqOrder(order).awaitTransactionSuccessAsync({ from: maker }); + const info = await zeroEx.getRfqOrderInfo(order).callAsync(); + assertOrderInfoEquals(info, { + status: OrderStatus.Cancelled, + orderHash: order.getHash(), + takerTokenFilledAmount: fillAmount, + }); + }); + }); + + describe('cancelLimitOrder()', async () => { + it('can cancel an unfilled order', async () => { + const order = getTestLimitOrder(); + const receipt = await zeroEx.cancelLimitOrder(order).awaitTransactionSuccessAsync({ from: maker }); + verifyEventsFromLogs( + receipt.logs, + [{ maker: order.maker, orderHash: order.getHash() }], + IZeroExEvents.OrderCancelled, + ); + const { status } = await zeroEx.getLimitOrderInfo(order).callAsync(); + expect(status).to.eq(OrderStatus.Cancelled); + }); + + it('can cancel a fully filled order', async () => { + const order = getTestLimitOrder(); + await fillLimitOrderAsync(order); + const receipt = await zeroEx.cancelLimitOrder(order).awaitTransactionSuccessAsync({ from: maker }); + verifyEventsFromLogs( + receipt.logs, + [{ maker: order.maker, orderHash: order.getHash() }], + IZeroExEvents.OrderCancelled, + ); + const { status } = await zeroEx.getLimitOrderInfo(order).callAsync(); + expect(status).to.eq(OrderStatus.Filled); // Still reports filled. + }); + + it('can cancel a partially filled order', async () => { + const order = getTestLimitOrder(); + await fillLimitOrderAsync(order, order.takerAmount.minus(1)); + const receipt = await zeroEx.cancelLimitOrder(order).awaitTransactionSuccessAsync({ from: maker }); + verifyEventsFromLogs( + receipt.logs, + [{ maker: order.maker, orderHash: order.getHash() }], + IZeroExEvents.OrderCancelled, + ); + const { status } = await zeroEx.getLimitOrderInfo(order).callAsync(); + expect(status).to.eq(OrderStatus.Cancelled); + }); + + it('can cancel an expired order', async () => { + const expiry = createExpiry(-60); + const order = getTestLimitOrder({ expiry }); + const receipt = await zeroEx.cancelLimitOrder(order).awaitTransactionSuccessAsync({ from: maker }); + verifyEventsFromLogs( + receipt.logs, + [{ maker: order.maker, orderHash: order.getHash() }], + IZeroExEvents.OrderCancelled, + ); + const { status } = await zeroEx.getLimitOrderInfo(order).callAsync(); + expect(status).to.eq(OrderStatus.Cancelled); + }); + + it('can cancel a cancelled order', async () => { + const order = getTestLimitOrder(); + await zeroEx.cancelLimitOrder(order).awaitTransactionSuccessAsync({ from: maker }); + const receipt = await zeroEx.cancelLimitOrder(order).awaitTransactionSuccessAsync({ from: maker }); + verifyEventsFromLogs( + receipt.logs, + [{ maker: order.maker, orderHash: order.getHash() }], + IZeroExEvents.OrderCancelled, + ); + const { status } = await zeroEx.getLimitOrderInfo(order).callAsync(); + expect(status).to.eq(OrderStatus.Cancelled); + }); + + it("cannot cancel someone else's order", async () => { + const order = getTestLimitOrder(); + const tx = zeroEx.cancelLimitOrder(order).awaitTransactionSuccessAsync({ from: notMaker }); + return expect(tx).to.revertWith( + new RevertErrors.OnlyOrderMakerAllowed(order.getHash(), notMaker, order.maker), + ); + }); + }); + + describe('cancelRfqOrder()', async () => { + it('can cancel an unfilled order', async () => { + const order = getTestRfqOrder(); + const receipt = await zeroEx.cancelRfqOrder(order).awaitTransactionSuccessAsync({ from: maker }); + verifyEventsFromLogs( + receipt.logs, + [{ maker: order.maker, orderHash: order.getHash() }], + IZeroExEvents.OrderCancelled, + ); + const { status } = await zeroEx.getRfqOrderInfo(order).callAsync(); + expect(status).to.eq(OrderStatus.Cancelled); + }); + + it('can cancel a fully filled order', async () => { + const order = getTestRfqOrder(); + await fillRfqOrderAsync(order); + const receipt = await zeroEx.cancelRfqOrder(order).awaitTransactionSuccessAsync({ from: maker }); + verifyEventsFromLogs( + receipt.logs, + [{ maker: order.maker, orderHash: order.getHash() }], + IZeroExEvents.OrderCancelled, + ); + const { status } = await zeroEx.getRfqOrderInfo(order).callAsync(); + expect(status).to.eq(OrderStatus.Filled); // Still reports filled. + }); + + it('can cancel a partially filled order', async () => { + const order = getTestRfqOrder(); + await fillRfqOrderAsync(order, order.takerAmount.minus(1)); + const receipt = await zeroEx.cancelRfqOrder(order).awaitTransactionSuccessAsync({ from: maker }); + verifyEventsFromLogs( + receipt.logs, + [{ maker: order.maker, orderHash: order.getHash() }], + IZeroExEvents.OrderCancelled, + ); + const { status } = await zeroEx.getRfqOrderInfo(order).callAsync(); + expect(status).to.eq(OrderStatus.Cancelled); // Still reports filled. + }); + + it('can cancel an expired order', async () => { + const expiry = createExpiry(-60); + const order = getTestRfqOrder({ expiry }); + const receipt = await zeroEx.cancelRfqOrder(order).awaitTransactionSuccessAsync({ from: maker }); + verifyEventsFromLogs( + receipt.logs, + [{ maker: order.maker, orderHash: order.getHash() }], + IZeroExEvents.OrderCancelled, + ); + const { status } = await zeroEx.getRfqOrderInfo(order).callAsync(); + expect(status).to.eq(OrderStatus.Cancelled); + }); + + it('can cancel a cancelled order', async () => { + const order = getTestRfqOrder(); + await zeroEx.cancelRfqOrder(order).awaitTransactionSuccessAsync({ from: maker }); + const receipt = await zeroEx.cancelRfqOrder(order).awaitTransactionSuccessAsync({ from: maker }); + verifyEventsFromLogs( + receipt.logs, + [{ maker: order.maker, orderHash: order.getHash() }], + IZeroExEvents.OrderCancelled, + ); + const { status } = await zeroEx.getRfqOrderInfo(order).callAsync(); + expect(status).to.eq(OrderStatus.Cancelled); + }); + + it("cannot cancel someone else's order", async () => { + const order = getTestRfqOrder(); + const tx = zeroEx.cancelRfqOrder(order).awaitTransactionSuccessAsync({ from: notMaker }); + return expect(tx).to.revertWith( + new RevertErrors.OnlyOrderMakerAllowed(order.getHash(), notMaker, order.maker), + ); + }); + }); + + describe('batchCancelLimitOrders()', async () => { + it('can cancel multiple orders', async () => { + const orders = [...new Array(3)].map(() => getTestLimitOrder()); + const receipt = await zeroEx.batchCancelLimitOrders(orders).awaitTransactionSuccessAsync({ from: maker }); + verifyEventsFromLogs( + receipt.logs, + orders.map(o => ({ maker: o.maker, orderHash: o.getHash() })), + IZeroExEvents.OrderCancelled, + ); + const infos = await Promise.all(orders.map(o => zeroEx.getLimitOrderInfo(o).callAsync())); + expect(infos.map(i => i.status)).to.deep.eq(infos.map(() => OrderStatus.Cancelled)); + }); + + it("cannot cancel someone else's orders", async () => { + const orders = [...new Array(3)].map(() => getTestLimitOrder()); + const tx = zeroEx.batchCancelLimitOrders(orders).awaitTransactionSuccessAsync({ from: notMaker }); + return expect(tx).to.revertWith( + new RevertErrors.OnlyOrderMakerAllowed(orders[0].getHash(), notMaker, orders[0].maker), + ); + }); + }); + + describe('batchCancelRfqOrders()', async () => { + it('can cancel multiple orders', async () => { + const orders = [...new Array(3)].map(() => getTestRfqOrder()); + const receipt = await zeroEx.batchCancelRfqOrders(orders).awaitTransactionSuccessAsync({ from: maker }); + verifyEventsFromLogs( + receipt.logs, + orders.map(o => ({ maker: o.maker, orderHash: o.getHash() })), + IZeroExEvents.OrderCancelled, + ); + const infos = await Promise.all(orders.map(o => zeroEx.getRfqOrderInfo(o).callAsync())); + expect(infos.map(i => i.status)).to.deep.eq(infos.map(() => OrderStatus.Cancelled)); + }); + + it("cannot cancel someone else's orders", async () => { + const orders = [...new Array(3)].map(() => getTestRfqOrder()); + const tx = zeroEx.batchCancelRfqOrders(orders).awaitTransactionSuccessAsync({ from: notMaker }); + return expect(tx).to.revertWith( + new RevertErrors.OnlyOrderMakerAllowed(orders[0].getHash(), notMaker, orders[0].maker), + ); + }); + }); + + describe('cancelPairOrders()', async () => { + it('can cancel multiple limit orders of the same pair with salt < minValidSalt', async () => { + const orders = [...new Array(3)].map((_v, i) => getTestLimitOrder().clone({ salt: new BigNumber(i) })); + // Cancel the first two orders. + const minValidSalt = orders[2].salt; + const receipt = await zeroEx + .cancelPairLimitOrders(makerToken.address, takerToken.address, minValidSalt) + .awaitTransactionSuccessAsync({ from: maker }); + verifyEventsFromLogs( + receipt.logs, + [ + { + maker, + makerToken: makerToken.address, + takerToken: takerToken.address, + minValidSalt, + }, + ], + IZeroExEvents.PairOrdersCancelled, + ); + const statuses = (await Promise.all(orders.map(o => zeroEx.getLimitOrderInfo(o).callAsync()))).map( + oi => oi.status, + ); + expect(statuses).to.deep.eq([OrderStatus.Cancelled, OrderStatus.Cancelled, OrderStatus.Fillable]); + }); + + it('does not cancel limit orders of a different pair', async () => { + const order = getRandomLimitOrder({ salt: new BigNumber(1) }); + // Cancel salts <= the order's, but flip the tokens to be a different + // pair. + const minValidSalt = order.salt.plus(1); + await zeroEx + .cancelPairLimitOrders(takerToken.address, makerToken.address, minValidSalt) + .awaitTransactionSuccessAsync({ from: maker }); + const { status } = await zeroEx.getLimitOrderInfo(order).callAsync(); + expect(status).to.eq(OrderStatus.Fillable); + }); + + it('can cancel multiple RFQ orders of the same pair with salt < minValidSalt', async () => { + const orders = [...new Array(3)].map((_v, i) => getTestRfqOrder().clone({ salt: new BigNumber(i) })); + // Cancel the first two orders. + const minValidSalt = orders[2].salt; + const receipt = await zeroEx + .cancelPairRfqOrders(makerToken.address, takerToken.address, minValidSalt) + .awaitTransactionSuccessAsync({ from: maker }); + verifyEventsFromLogs( + receipt.logs, + [ + { + maker, + makerToken: makerToken.address, + takerToken: takerToken.address, + minValidSalt, + }, + ], + IZeroExEvents.PairOrdersCancelled, + ); + const statuses = (await Promise.all(orders.map(o => zeroEx.getRfqOrderInfo(o).callAsync()))).map( + oi => oi.status, + ); + expect(statuses).to.deep.eq([OrderStatus.Cancelled, OrderStatus.Cancelled, OrderStatus.Fillable]); + }); + + it('does not cancel RFQ orders of a different pair', async () => { + const order = getRandomRfqOrder({ salt: new BigNumber(1) }); + // Cancel salts <= the order's, but flip the tokens to be a different + // pair. + const minValidSalt = order.salt.plus(1); + await zeroEx + .cancelPairRfqOrders(takerToken.address, makerToken.address, minValidSalt) + .awaitTransactionSuccessAsync({ from: maker }); + const { status } = await zeroEx.getRfqOrderInfo(order).callAsync(); + expect(status).to.eq(OrderStatus.Fillable); + }); + }); + + describe('batchCancelPairOrders()', async () => { + it('can cancel multiple limit order pairs', async () => { + const orders = [ + getTestLimitOrder({ salt: new BigNumber(1) }), + // Flip the tokens for the other order. + getTestLimitOrder({ + makerToken: takerToken.address, + takerToken: makerToken.address, + salt: new BigNumber(1), + }), + ]; + const minValidSalt = new BigNumber(2); + const receipt = await zeroEx + .batchCancelPairLimitOrders( + [makerToken.address, takerToken.address], + [takerToken.address, makerToken.address], + [minValidSalt, minValidSalt], + ) + .awaitTransactionSuccessAsync({ from: maker }); + verifyEventsFromLogs( + receipt.logs, + [ + { + maker, + makerToken: makerToken.address, + takerToken: takerToken.address, + minValidSalt, + }, + { + maker, + makerToken: takerToken.address, + takerToken: makerToken.address, + minValidSalt, + }, + ], + IZeroExEvents.PairOrdersCancelled, + ); + const statuses = (await Promise.all(orders.map(o => zeroEx.getLimitOrderInfo(o).callAsync()))).map( + oi => oi.status, + ); + expect(statuses).to.deep.eq([OrderStatus.Cancelled, OrderStatus.Cancelled]); + }); + + it('can cancel multiple RFQ order pairs', async () => { + const orders = [ + getTestRfqOrder({ salt: new BigNumber(1) }), + // Flip the tokens for the other order. + getTestRfqOrder({ + makerToken: takerToken.address, + takerToken: makerToken.address, + salt: new BigNumber(1), + }), + ]; + const minValidSalt = new BigNumber(2); + const receipt = await zeroEx + .batchCancelPairRfqOrders( + [makerToken.address, takerToken.address], + [takerToken.address, makerToken.address], + [minValidSalt, minValidSalt], + ) + .awaitTransactionSuccessAsync({ from: maker }); + verifyEventsFromLogs( + receipt.logs, + [ + { + maker, + makerToken: makerToken.address, + takerToken: takerToken.address, + minValidSalt, + }, + { + maker, + makerToken: takerToken.address, + takerToken: makerToken.address, + minValidSalt, + }, + ], + IZeroExEvents.PairOrdersCancelled, + ); + const statuses = (await Promise.all(orders.map(o => zeroEx.getRfqOrderInfo(o).callAsync()))).map( + oi => oi.status, + ); + expect(statuses).to.deep.eq([OrderStatus.Cancelled, OrderStatus.Cancelled]); + }); + }); + + interface LimitOrderFilledAmounts { + makerTokenFilledAmount: BigNumber; + takerTokenFilledAmount: BigNumber; + takerTokenFeeFilledAmount: BigNumber; + } + + function computeLimitOrderFilledAmounts( + order: LimitOrder, + takerTokenFillAmount: BigNumber = order.takerAmount, + takerTokenAlreadyFilledAmount: BigNumber = ZERO_AMOUNT, + ): LimitOrderFilledAmounts { + const fillAmount = BigNumber.min( + order.takerAmount, + takerTokenFillAmount, + order.takerAmount.minus(takerTokenAlreadyFilledAmount), + ); + const makerTokenFilledAmount = fillAmount + .times(order.makerAmount) + .div(order.takerAmount) + .integerValue(BigNumber.ROUND_DOWN); + const takerTokenFeeFilledAmount = fillAmount + .times(order.takerTokenFeeAmount) + .div(order.takerAmount) + .integerValue(BigNumber.ROUND_DOWN); + return { + makerTokenFilledAmount, + takerTokenFilledAmount: fillAmount, + takerTokenFeeFilledAmount, + }; + } + + function createLimitOrderFilledEventArgs( + order: LimitOrder, + takerTokenFillAmount: BigNumber = order.takerAmount, + takerTokenAlreadyFilledAmount: BigNumber = ZERO_AMOUNT, + ): object { + const { + makerTokenFilledAmount, + takerTokenFilledAmount, + takerTokenFeeFilledAmount, + } = computeLimitOrderFilledAmounts(order, takerTokenFillAmount, takerTokenAlreadyFilledAmount); + return { + taker, + takerTokenFilledAmount, + makerTokenFilledAmount, + takerTokenFeeFilledAmount, + orderHash: order.getHash(), + maker: order.maker, + feeRecipient: order.feeRecipient, + makerToken: order.makerToken, + takerToken: order.takerToken, + protocolFeePaid: SINGLE_PROTOCOL_FEE, + pool: order.pool, + }; + } + + async function assertExpectedFinalBalancesFromLimitOrderFillAsync( + order: LimitOrder, + takerTokenFillAmount: BigNumber = order.takerAmount, + takerTokenAlreadyFilledAmount: BigNumber = ZERO_AMOUNT, + ): Promise { + const { + makerTokenFilledAmount, + takerTokenFilledAmount, + takerTokenFeeFilledAmount, + } = computeLimitOrderFilledAmounts(order, takerTokenFillAmount, takerTokenAlreadyFilledAmount); + const makerBalance = await takerToken.balanceOf(maker).callAsync(); + const takerBalance = await makerToken.balanceOf(taker).callAsync(); + const feeRecipientBalance = await takerToken.balanceOf(order.feeRecipient).callAsync(); + expect(makerBalance).to.bignumber.eq(takerTokenFilledAmount); + expect(takerBalance).to.bignumber.eq(makerTokenFilledAmount); + expect(feeRecipientBalance).to.bignumber.eq(takerTokenFeeFilledAmount); + } + + describe('fillLimitOrder()', () => { + it('can fully fill an order', async () => { + const order = getTestLimitOrder(); + const receipt = await fillLimitOrderAsync(order); + verifyEventsFromLogs( + receipt.logs, + [createLimitOrderFilledEventArgs(order)], + IZeroExEvents.LimitOrderFilled, + ); + assertOrderInfoEquals(await zeroEx.getLimitOrderInfo(order).callAsync(), { + orderHash: order.getHash(), + status: OrderStatus.Filled, + takerTokenFilledAmount: order.takerAmount, + }); + await assertExpectedFinalBalancesFromLimitOrderFillAsync(order); + }); + + it('can partially fill an order', async () => { + const order = getTestLimitOrder(); + const fillAmount = order.takerAmount.minus(1); + const receipt = await fillLimitOrderAsync(order, fillAmount); + verifyEventsFromLogs( + receipt.logs, + [createLimitOrderFilledEventArgs(order, fillAmount)], + IZeroExEvents.LimitOrderFilled, + ); + assertOrderInfoEquals(await zeroEx.getLimitOrderInfo(order).callAsync(), { + orderHash: order.getHash(), + status: OrderStatus.Fillable, + takerTokenFilledAmount: fillAmount, + }); + await assertExpectedFinalBalancesFromLimitOrderFillAsync(order, fillAmount); + }); + + it('can fully fill an order in two steps', async () => { + const order = getTestLimitOrder(); + let fillAmount = order.takerAmount.dividedToIntegerBy(2); + let receipt = await fillLimitOrderAsync(order, fillAmount); + verifyEventsFromLogs( + receipt.logs, + [createLimitOrderFilledEventArgs(order, fillAmount)], + IZeroExEvents.LimitOrderFilled, + ); + const alreadyFilledAmount = fillAmount; + fillAmount = order.takerAmount.minus(fillAmount); + receipt = await fillLimitOrderAsync(order, fillAmount); + verifyEventsFromLogs( + receipt.logs, + [createLimitOrderFilledEventArgs(order, fillAmount, alreadyFilledAmount)], + IZeroExEvents.LimitOrderFilled, + ); + assertOrderInfoEquals(await zeroEx.getLimitOrderInfo(order).callAsync(), { + orderHash: order.getHash(), + status: OrderStatus.Filled, + takerTokenFilledAmount: order.takerAmount, + }); + }); + + it('clamps fill amount to remaining available', async () => { + const order = getTestLimitOrder(); + const fillAmount = order.takerAmount.plus(1); + const receipt = await fillLimitOrderAsync(order, fillAmount); + verifyEventsFromLogs( + receipt.logs, + [createLimitOrderFilledEventArgs(order, fillAmount)], + IZeroExEvents.LimitOrderFilled, + ); + assertOrderInfoEquals(await zeroEx.getLimitOrderInfo(order).callAsync(), { + orderHash: order.getHash(), + status: OrderStatus.Filled, + takerTokenFilledAmount: order.takerAmount, + }); + await assertExpectedFinalBalancesFromLimitOrderFillAsync(order, fillAmount); + }); + + it('clamps fill amount to remaining available in partial filled order', async () => { + const order = getTestLimitOrder(); + let fillAmount = order.takerAmount.dividedToIntegerBy(2); + let receipt = await fillLimitOrderAsync(order, fillAmount); + verifyEventsFromLogs( + receipt.logs, + [createLimitOrderFilledEventArgs(order, fillAmount)], + IZeroExEvents.LimitOrderFilled, + ); + const alreadyFilledAmount = fillAmount; + fillAmount = order.takerAmount.minus(fillAmount).plus(1); + receipt = await fillLimitOrderAsync(order, fillAmount); + verifyEventsFromLogs( + receipt.logs, + [createLimitOrderFilledEventArgs(order, fillAmount, alreadyFilledAmount)], + IZeroExEvents.LimitOrderFilled, + ); + assertOrderInfoEquals(await zeroEx.getLimitOrderInfo(order).callAsync(), { + orderHash: order.getHash(), + status: OrderStatus.Filled, + takerTokenFilledAmount: order.takerAmount, + }); + }); + + it('cannot fill an expired order', async () => { + const order = getTestLimitOrder({ expiry: createExpiry(-60) }); + const tx = fillLimitOrderAsync(order); + return expect(tx).to.revertWith( + new RevertErrors.OrderNotFillableError(order.getHash(), OrderStatus.Expired), + ); + }); + + it('cannot fill a cancelled order', async () => { + const order = getTestLimitOrder(); + await zeroEx.cancelLimitOrder(order).awaitTransactionSuccessAsync({ from: maker }); + const tx = fillLimitOrderAsync(order); + return expect(tx).to.revertWith( + new RevertErrors.OrderNotFillableError(order.getHash(), OrderStatus.Cancelled), + ); + }); + + it('cannot fill a salt/pair cancelled order', async () => { + const order = getTestLimitOrder(); + await zeroEx + .cancelPairLimitOrders(makerToken.address, takerToken.address, order.salt.plus(1)) + .awaitTransactionSuccessAsync({ from: maker }); + const tx = fillLimitOrderAsync(order); + return expect(tx).to.revertWith( + new RevertErrors.OrderNotFillableError(order.getHash(), OrderStatus.Cancelled), + ); + }); + + it('non-taker cannot fill order', async () => { + const order = getTestLimitOrder({ taker }); + const tx = fillLimitOrderAsync(order, order.takerAmount, notTaker); + return expect(tx).to.revertWith( + new RevertErrors.OrderNotFillableByTakerError(order.getHash(), notTaker, order.taker), + ); + }); + + it('non-sender cannot fill order', async () => { + const order = getTestLimitOrder({ sender: taker }); + const tx = fillLimitOrderAsync(order, order.takerAmount, notTaker); + return expect(tx).to.revertWith( + new RevertErrors.OrderNotFillableBySenderError(order.getHash(), notTaker, order.sender), + ); + }); + + it('cannot fill order with bad signature', async () => { + const order = getTestLimitOrder(); + // Overwrite chainId to result in a different hash and therefore different + // signature. + const tx = fillLimitOrderAsync(order.clone({ chainId: 1234 })); + return expect(tx).to.revertWith( + new RevertErrors.OrderNotSignedByMakerError(order.getHash(), undefined, order.maker), + ); + }); + + it('fails if no protocol fee attached (and no weth allowance)', async () => { + const order = getTestLimitOrder(); + await prepareBalancesForOrderAsync(order); + const tx = zeroEx + .fillLimitOrder( + order, + await order.getSignatureWithProviderAsync(env.provider), + new BigNumber(order.takerAmount), + ) + .awaitTransactionSuccessAsync({ from: taker, value: ZERO_AMOUNT }); + // The exact revert error depends on whether we are still doing a + // token spender fallthroigh, so we won't get too specific. + return expect(tx).to.revertWith(new AnyRevertError()); + }); + }); + + interface RfqOrderFilledAmounts { + makerTokenFilledAmount: BigNumber; + takerTokenFilledAmount: BigNumber; + } + + function computeRfqOrderFilledAmounts( + order: RfqOrder, + takerTokenFillAmount: BigNumber = order.takerAmount, + takerTokenAlreadyFilledAmount: BigNumber = ZERO_AMOUNT, + ): RfqOrderFilledAmounts { + const fillAmount = BigNumber.min( + order.takerAmount, + takerTokenFillAmount, + order.takerAmount.minus(takerTokenAlreadyFilledAmount), + ); + const makerTokenFilledAmount = fillAmount + .times(order.makerAmount) + .div(order.takerAmount) + .integerValue(BigNumber.ROUND_DOWN); + return { + makerTokenFilledAmount, + takerTokenFilledAmount: fillAmount, + }; + } + + function createRfqOrderFilledEventArgs( + order: RfqOrder, + takerTokenFillAmount: BigNumber = order.takerAmount, + takerTokenAlreadyFilledAmount: BigNumber = ZERO_AMOUNT, + ): object { + const { makerTokenFilledAmount, takerTokenFilledAmount } = computeRfqOrderFilledAmounts( + order, + takerTokenFillAmount, + takerTokenAlreadyFilledAmount, + ); + return { + taker, + takerTokenFilledAmount, + makerTokenFilledAmount, + orderHash: order.getHash(), + maker: order.maker, + makerToken: order.makerToken, + takerToken: order.takerToken, + protocolFeePaid: SINGLE_PROTOCOL_FEE, + pool: order.pool, + }; + } + + async function assertExpectedFinalBalancesFromRfqOrderFillAsync( + order: RfqOrder, + takerTokenFillAmount: BigNumber = order.takerAmount, + takerTokenAlreadyFilledAmount: BigNumber = ZERO_AMOUNT, + ): Promise { + const { makerTokenFilledAmount, takerTokenFilledAmount } = computeRfqOrderFilledAmounts( + order, + takerTokenFillAmount, + takerTokenAlreadyFilledAmount, + ); + const makerBalance = await takerToken.balanceOf(maker).callAsync(); + const takerBalance = await makerToken.balanceOf(taker).callAsync(); + expect(makerBalance).to.bignumber.eq(takerTokenFilledAmount); + expect(takerBalance).to.bignumber.eq(makerTokenFilledAmount); + } + + describe('fillRfqOrder()', () => { + it('can fully fill an order', async () => { + const order = getTestRfqOrder(); + const receipt = await fillRfqOrderAsync(order); + verifyEventsFromLogs(receipt.logs, [createRfqOrderFilledEventArgs(order)], IZeroExEvents.RfqOrderFilled); + assertOrderInfoEquals(await zeroEx.getRfqOrderInfo(order).callAsync(), { + orderHash: order.getHash(), + status: OrderStatus.Filled, + takerTokenFilledAmount: order.takerAmount, + }); + await assertExpectedFinalBalancesFromRfqOrderFillAsync(order); + }); + + it('can partially fill an order', async () => { + const order = getTestRfqOrder(); + const fillAmount = order.takerAmount.minus(1); + const receipt = await fillRfqOrderAsync(order, fillAmount); + verifyEventsFromLogs( + receipt.logs, + [createRfqOrderFilledEventArgs(order, fillAmount)], + IZeroExEvents.RfqOrderFilled, + ); + assertOrderInfoEquals(await zeroEx.getRfqOrderInfo(order).callAsync(), { + orderHash: order.getHash(), + status: OrderStatus.Fillable, + takerTokenFilledAmount: fillAmount, + }); + await assertExpectedFinalBalancesFromRfqOrderFillAsync(order, fillAmount); + }); + + it('can fully fill an order in two steps', async () => { + const order = getTestRfqOrder(); + let fillAmount = order.takerAmount.dividedToIntegerBy(2); + let receipt = await fillRfqOrderAsync(order, fillAmount); + verifyEventsFromLogs( + receipt.logs, + [createRfqOrderFilledEventArgs(order, fillAmount)], + IZeroExEvents.RfqOrderFilled, + ); + const alreadyFilledAmount = fillAmount; + fillAmount = order.takerAmount.minus(fillAmount); + receipt = await fillRfqOrderAsync(order, fillAmount); + verifyEventsFromLogs( + receipt.logs, + [createRfqOrderFilledEventArgs(order, fillAmount, alreadyFilledAmount)], + IZeroExEvents.RfqOrderFilled, + ); + assertOrderInfoEquals(await zeroEx.getRfqOrderInfo(order).callAsync(), { + orderHash: order.getHash(), + status: OrderStatus.Filled, + takerTokenFilledAmount: order.takerAmount, + }); + }); + + it('clamps fill amount to remaining available', async () => { + const order = getTestRfqOrder(); + const fillAmount = order.takerAmount.plus(1); + const receipt = await fillRfqOrderAsync(order, fillAmount); + verifyEventsFromLogs( + receipt.logs, + [createRfqOrderFilledEventArgs(order, fillAmount)], + IZeroExEvents.RfqOrderFilled, + ); + assertOrderInfoEquals(await zeroEx.getRfqOrderInfo(order).callAsync(), { + orderHash: order.getHash(), + status: OrderStatus.Filled, + takerTokenFilledAmount: order.takerAmount, + }); + await assertExpectedFinalBalancesFromRfqOrderFillAsync(order, fillAmount); + }); + + it('clamps fill amount to remaining available in partial filled order', async () => { + const order = getTestRfqOrder(); + let fillAmount = order.takerAmount.dividedToIntegerBy(2); + let receipt = await fillRfqOrderAsync(order, fillAmount); + verifyEventsFromLogs( + receipt.logs, + [createRfqOrderFilledEventArgs(order, fillAmount)], + IZeroExEvents.RfqOrderFilled, + ); + const alreadyFilledAmount = fillAmount; + fillAmount = order.takerAmount.minus(fillAmount).plus(1); + receipt = await fillRfqOrderAsync(order, fillAmount); + verifyEventsFromLogs( + receipt.logs, + [createRfqOrderFilledEventArgs(order, fillAmount, alreadyFilledAmount)], + IZeroExEvents.RfqOrderFilled, + ); + assertOrderInfoEquals(await zeroEx.getRfqOrderInfo(order).callAsync(), { + orderHash: order.getHash(), + status: OrderStatus.Filled, + takerTokenFilledAmount: order.takerAmount, + }); + }); + + it('cannot fill an order with wrong tx.origin', async () => { + const order = getTestRfqOrder({ txOrigin: taker }); + const tx = fillRfqOrderAsync(order, order.takerAmount, notTaker); + return expect(tx).to.revertWith( + new RevertErrors.OrderNotFillableByOriginError(order.getHash(), notTaker, taker), + ); + }); + + it('cannot fill an expired order', async () => { + const order = getTestRfqOrder({ expiry: createExpiry(-60) }); + const tx = fillRfqOrderAsync(order); + return expect(tx).to.revertWith( + new RevertErrors.OrderNotFillableError(order.getHash(), OrderStatus.Expired), + ); + }); + + it('cannot fill a cancelled order', async () => { + const order = getTestRfqOrder(); + await zeroEx.cancelRfqOrder(order).awaitTransactionSuccessAsync({ from: maker }); + const tx = fillRfqOrderAsync(order); + return expect(tx).to.revertWith( + new RevertErrors.OrderNotFillableError(order.getHash(), OrderStatus.Cancelled), + ); + }); + + it('cannot fill a salt/pair cancelled order', async () => { + const order = getTestRfqOrder(); + await zeroEx + .cancelPairRfqOrders(makerToken.address, takerToken.address, order.salt.plus(1)) + .awaitTransactionSuccessAsync({ from: maker }); + const tx = fillRfqOrderAsync(order); + return expect(tx).to.revertWith( + new RevertErrors.OrderNotFillableError(order.getHash(), OrderStatus.Cancelled), + ); + }); + + it('cannot fill order with bad signature', async () => { + const order = getTestRfqOrder(); + // Overwrite chainId to result in a different hash and therefore different + // signature. + const tx = fillRfqOrderAsync(order.clone({ chainId: 1234 })); + return expect(tx).to.revertWith( + new RevertErrors.OrderNotSignedByMakerError(order.getHash(), undefined, order.maker), + ); + }); + + it('fails if no protocol fee attached (and no weth allowance)', async () => { + const order = getTestRfqOrder(); + await prepareBalancesForOrderAsync(order); + const tx = zeroEx + .fillRfqOrder( + order, + await order.getSignatureWithProviderAsync(env.provider), + new BigNumber(order.takerAmount), + ) + .awaitTransactionSuccessAsync({ from: taker, value: ZERO_AMOUNT }); + // The exact revert error depends on whether we are still doing a + // token spender fallthroigh, so we won't get too specific. + return expect(tx).to.revertWith(new AnyRevertError()); + }); + }); + + describe('fillOrKillLimitOrder()', () => { + it('can fully fill an order', async () => { + const order = getTestLimitOrder(); + await prepareBalancesForOrderAsync(order); + const receipt = await zeroEx + .fillOrKillLimitOrder(order, await order.getSignatureWithProviderAsync(env.provider), order.takerAmount) + .awaitTransactionSuccessAsync({ from: taker, value: SINGLE_PROTOCOL_FEE }); + verifyEventsFromLogs( + receipt.logs, + [createLimitOrderFilledEventArgs(order)], + IZeroExEvents.LimitOrderFilled, + ); + }); + + it('reverts if cannot fill the exact amount', async () => { + const order = getTestLimitOrder(); + await prepareBalancesForOrderAsync(order); + const fillAmount = order.takerAmount.plus(1); + const tx = zeroEx + .fillOrKillLimitOrder(order, await order.getSignatureWithProviderAsync(env.provider), fillAmount) + .awaitTransactionSuccessAsync({ from: taker, value: SINGLE_PROTOCOL_FEE }); + return expect(tx).to.revertWith( + new RevertErrors.FillOrKillFailedError(order.getHash(), order.takerAmount, fillAmount), + ); + }); + }); + + describe('fillOrKillRfqOrder()', () => { + it('can fully fill an order', async () => { + const order = getTestRfqOrder(); + await prepareBalancesForOrderAsync(order); + const receipt = await zeroEx + .fillOrKillRfqOrder(order, await order.getSignatureWithProviderAsync(env.provider), order.takerAmount) + .awaitTransactionSuccessAsync({ from: taker, value: SINGLE_PROTOCOL_FEE }); + verifyEventsFromLogs(receipt.logs, [createRfqOrderFilledEventArgs(order)], IZeroExEvents.RfqOrderFilled); + }); + + it('reverts if cannot fill the exact amount', async () => { + const order = getTestRfqOrder(); + await prepareBalancesForOrderAsync(order); + const fillAmount = order.takerAmount.plus(1); + const tx = zeroEx + .fillOrKillRfqOrder(order, await order.getSignatureWithProviderAsync(env.provider), fillAmount) + .awaitTransactionSuccessAsync({ from: taker, value: SINGLE_PROTOCOL_FEE }); + return expect(tx).to.revertWith( + new RevertErrors.FillOrKillFailedError(order.getHash(), order.takerAmount, fillAmount), + ); + }); + }); + + it.skip('RFQ gas benchmark', async () => { + const orders = [...new Array(2)].map(() => + getTestRfqOrder({ pool: '0x0000000000000000000000000000000000000000000000000000000000000000' }), + ); + // Fill one to warm up the fee pool. + await fillRfqOrderAsync(orders[0]); + const receipt = await fillRfqOrderAsync(orders[1]); + // tslint:disable-next-line: no-console + console.log(receipt.gasUsed); + }); +}); diff --git a/contracts/zero-ex/test/full_migration_test.ts b/contracts/zero-ex/test/full_migration_test.ts index 993472e026..1461280a5f 100644 --- a/contracts/zero-ex/test/full_migration_test.ts +++ b/contracts/zero-ex/test/full_migration_test.ts @@ -10,6 +10,7 @@ import { deployFullFeaturesAsync, FullFeatures } from './utils/migration'; import { AllowanceTargetContract, IMetaTransactionsFeatureContract, + INativeOrdersFeatureContract, IOwnableFeatureContract, ISignatureValidatorFeatureContract, ISimpleFunctionRegistryFeatureContract, @@ -45,9 +46,9 @@ blockchainTests.resets('Full migration', env => { artifacts, await migrator.getBootstrapper().callAsync(), ); - features = await deployFullFeaturesAsync(env.provider, env.txDefaults, zeroEx.address); + features = await deployFullFeaturesAsync(env.provider, env.txDefaults, { zeroExAddress: zeroEx.address }); await migrator - .initializeZeroEx(owner, zeroEx.address, features, { transformerDeployer }) + .migrateZeroEx(owner, zeroEx.address, features, { transformerDeployer }) .awaitTransactionSuccessAsync(); registry = new ISimpleFunctionRegistryFeatureContract(zeroEx.address, env.provider, env.txDefaults); }); @@ -63,10 +64,10 @@ blockchainTests.resets('Full migration', env => { expect(dieRecipient).to.eq(owner); }); - it('Non-deployer cannot call initializeZeroEx()', async () => { + it('Non-deployer cannot call migrateZeroEx()', async () => { const notDeployer = randomAddress(); const tx = migrator - .initializeZeroEx(owner, zeroEx.address, features, { transformerDeployer }) + .migrateZeroEx(owner, zeroEx.address, features, { transformerDeployer }) .callAsync({ from: notDeployer }); return expect(tx).to.revertWith('FullMigration/INVALID_SENDER'); }); @@ -103,6 +104,31 @@ blockchainTests.resets('Full migration', env => { 'getMetaTransactionHash', ], }, + LimitOrdersFeature: { + contractType: INativeOrdersFeatureContract, + fns: [ + 'transferProtocolFeesForPools', + 'fillLimitOrder', + 'fillRfqOrder', + 'fillOrKillLimitOrder', + 'fillOrKillRfqOrder', + '_fillLimitOrder', + '_fillRfqOrder', + 'cancelLimitOrder', + 'cancelRfqOrder', + 'batchCancelLimitOrders', + 'batchCancelRfqOrders', + 'cancelPairLimitOrders', + 'batchCancelPairLimitOrders', + 'cancelPairRfqOrders', + 'batchCancelPairRfqOrders', + 'getLimitOrderInfo', + 'getRfqOrderInfo', + 'getLimitOrderHash', + 'getRfqOrderHash', + 'getProtocolFeeMultiplier', + ], + }, }; function createFakeInputs(inputs: DataItem[] | DataItem): any | any[] { @@ -139,6 +165,11 @@ blockchainTests.resets('Full migration', env => { return hexUtils.random(parseInt(/\d+$/.exec(item.type)![0], 10)); } if (/^uint\d+$/.test(item.type)) { + if (item.type === 'uint8') { + // Solidity will revert if enum values are out of range, so + // play it safe and pick zero. + return 0; + } return new BigNumber(hexUtils.random(parseInt(/\d+$/.exec(item.type)![0], 10) / 8)); } if (/^int\d+$/.test(item.type)) { diff --git a/contracts/zero-ex/test/lib_limit_orders_test.ts b/contracts/zero-ex/test/lib_limit_orders_test.ts new file mode 100644 index 0000000000..4ff217a8c4 --- /dev/null +++ b/contracts/zero-ex/test/lib_limit_orders_test.ts @@ -0,0 +1,34 @@ +import { blockchainTests, describe, expect } from '@0x/contracts-test-utils'; + +import { artifacts } from './artifacts'; +import { getRandomLimitOrder, getRandomRfqOrder } from './utils/orders'; +import { TestLibNativeOrderContract } from './wrappers'; + +blockchainTests('LibLimitOrder tests', env => { + let testContract: TestLibNativeOrderContract; + + before(async () => { + testContract = await TestLibNativeOrderContract.deployFrom0xArtifactAsync( + artifacts.TestLibNativeOrder, + env.provider, + env.txDefaults, + artifacts, + ); + }); + + describe('getLimitOrderStructHash()', () => { + it('returns the correct hash', async () => { + const order = getRandomLimitOrder(); + const structHash = await testContract.getLimitOrderStructHash(order).callAsync(); + expect(structHash).to.eq(order.getStructHash()); + }); + }); + + describe('getRfqOrderStructHash()', () => { + it('returns the correct hash', async () => { + const order = getRandomRfqOrder(); + const structHash = await testContract.getRfqOrderStructHash(order).callAsync(); + expect(structHash).to.eq(order.getStructHash()); + }); + }); +}); diff --git a/contracts/zero-ex/test/protocol_fees_test.ts b/contracts/zero-ex/test/protocol_fees_test.ts index 1b2582dd93..5acb63bdb7 100644 --- a/contracts/zero-ex/test/protocol_fees_test.ts +++ b/contracts/zero-ex/test/protocol_fees_test.ts @@ -1,25 +1,20 @@ -import { blockchainTests, constants, expect } from '@0x/contracts-test-utils'; -import { AuthorizableRevertErrors, BigNumber, hexUtils, ZeroExRevertErrors } from '@0x/utils'; -import { TransactionReceiptWithDecodedLogs } from 'ethereum-types'; +import { blockchainTests, expect } from '@0x/contracts-test-utils'; +import { AuthorizableRevertErrors, BigNumber, hexUtils } from '@0x/utils'; import { artifacts } from './artifacts'; -import { FeeCollectorContract, TestProtocolFeesContract, TestStakingContract, TestWethContract } from './wrappers'; +import { FeeCollectorContract, TestFixinProtocolFeesContract, TestStakingContract, TestWethContract } from './wrappers'; blockchainTests.resets('ProtocolFees', env => { - let payer: string; + const FEE_MULTIPLIER = 70e3; + let taker: string; let unauthorized: string; - let protocolFees: TestProtocolFeesContract; + let protocolFees: TestFixinProtocolFeesContract; let staking: TestStakingContract; let weth: TestWethContract; + let singleFeeAmount: BigNumber; before(async () => { - [payer, unauthorized] = await env.getAccountAddressesAsync(); - protocolFees = await TestProtocolFeesContract.deployFrom0xArtifactAsync( - artifacts.TestProtocolFees, - env.provider, - env.txDefaults, - artifacts, - ); + [taker, unauthorized] = await env.getAccountAddressesAsync(); weth = await TestWethContract.deployFrom0xArtifactAsync( artifacts.TestWeth, env.provider, @@ -33,30 +28,26 @@ blockchainTests.resets('ProtocolFees', env => { artifacts, weth.address, ); - await weth.mint(payer, constants.ONE_ETHER).awaitTransactionSuccessAsync(); - await weth.approve(protocolFees.address, constants.ONE_ETHER).awaitTransactionSuccessAsync({ from: payer }); + protocolFees = await TestFixinProtocolFeesContract.deployFrom0xArtifactAsync( + artifacts.TestFixinProtocolFees, + env.provider, + { ...env.txDefaults, from: taker }, + artifacts, + weth.address, + staking.address, + FEE_MULTIPLIER, + ); + singleFeeAmount = await protocolFees.getSingleProtocolFee().callAsync(); + await weth.mint(taker, singleFeeAmount).awaitTransactionSuccessAsync(); + await weth.approve(protocolFees.address, singleFeeAmount).awaitTransactionSuccessAsync({ from: taker }); }); - async function collectAsync( - poolId: string, - amount: BigNumber, - etherValue: BigNumber, - ): Promise { - return protocolFees - .collectProtocolFee(poolId, amount, weth.address) - .awaitTransactionSuccessAsync({ from: payer, value: etherValue }); - } - - async function transferFeesAsync(poolId: string): Promise { - return protocolFees.transferFeesForPool(poolId, staking.address, weth.address).awaitTransactionSuccessAsync(); - } - describe('FeeCollector', () => { it('should disallow unauthorized initialization', async () => { const pool = hexUtils.random(); - await collectAsync(pool, constants.ONE_ETHER, constants.ZERO_AMOUNT); - await transferFeesAsync(pool); + await protocolFees.collectProtocolFee(pool).awaitTransactionSuccessAsync({ value: singleFeeAmount }); + await protocolFees.transferFeesForPool(pool).awaitTransactionSuccessAsync(); const feeCollector = new FeeCollectorContract( await protocolFees.getFeeCollector(pool).callAsync(), @@ -74,91 +65,70 @@ blockchainTests.resets('ProtocolFees', env => { describe('_collectProtocolFee()', () => { const pool1 = hexUtils.random(); const pool2 = hexUtils.random(); + let feeCollector1Address: string; + let feeCollector2Address: string; - it('should revert if WETH transfer fails', async () => { - const tooMuch = constants.ONE_ETHER.plus(1); - const tx = collectAsync(pool1, constants.ONE_ETHER.plus(1), constants.ZERO_AMOUNT); - return expect(tx).to.revertWith( - new ZeroExRevertErrors.Spender.SpenderERC20TransferFromFailedError( - weth.address, - payer, - undefined, - tooMuch, - undefined, - ), - ); + before(async () => { + feeCollector1Address = await protocolFees.getFeeCollector(pool1).callAsync(); + feeCollector2Address = await protocolFees.getFeeCollector(pool2).callAsync(); }); it('should revert if insufficient ETH transferred', async () => { - const tooLittle = constants.ONE_ETHER.minus(1); - const tx = collectAsync(pool1, constants.ONE_ETHER, tooLittle); + const tooLittle = singleFeeAmount.minus(1); + const tx = protocolFees.collectProtocolFee(pool1).awaitTransactionSuccessAsync({ value: tooLittle }); return expect(tx).to.revertWith('FixinProtocolFees/ETHER_TRANSFER_FALIED'); }); - it('should accept WETH fee', async () => { - const beforeWETH = await weth.balanceOf(payer).callAsync(); - await collectAsync(pool1, constants.ONE_ETHER, constants.ZERO_AMOUNT); - const afterWETH = await weth.balanceOf(payer).callAsync(); - - return expect(beforeWETH.minus(afterWETH)).to.bignumber.eq(constants.ONE_ETHER); - }); - it('should accept ETH fee', async () => { - const beforeWETH = await weth.balanceOf(payer).callAsync(); - const beforeETH = await env.web3Wrapper.getBalanceInWeiAsync(payer); - await collectAsync(pool1, constants.ONE_ETHER, constants.ONE_ETHER); - const afterWETH = await weth.balanceOf(payer).callAsync(); - const afterETH = await env.web3Wrapper.getBalanceInWeiAsync(payer); + const beforeETH = await env.web3Wrapper.getBalanceInWeiAsync(taker); + await protocolFees.collectProtocolFee(pool1).awaitTransactionSuccessAsync({ value: singleFeeAmount }); + const afterETH = await env.web3Wrapper.getBalanceInWeiAsync(taker); - // We check for greater than 1 ether spent to allow for spending on gas. - await expect(beforeETH.minus(afterETH)).to.bignumber.gt(constants.ONE_ETHER); - return expect(beforeWETH).to.bignumber.eq(afterWETH); - }); + // We check for greater than fee spent to allow for spending on gas. + await expect(beforeETH.minus(afterETH)).to.bignumber.gt(singleFeeAmount); - it('should transfer both ETH and WETH', async () => { - await collectAsync(pool1, constants.ONE_ETHER, constants.ZERO_AMOUNT); - await collectAsync(pool1, constants.ONE_ETHER, constants.ONE_ETHER); - await transferFeesAsync(pool1); - - const balanceWETH = await weth.balanceOf(staking.address).callAsync(); - - // We leave 1 wei behind of both ETH and WETH. - return expect(balanceWETH).to.bignumber.eq(constants.ONE_ETHER.times(2).minus(2)); + await expect(await env.web3Wrapper.getBalanceInWeiAsync(feeCollector1Address)).to.bignumber.eq( + singleFeeAmount, + ); }); it('should accept ETH after first transfer', async () => { - await collectAsync(pool1, constants.ONE_ETHER, constants.ONE_ETHER); - await transferFeesAsync(pool1); - await collectAsync(pool1, constants.ONE_ETHER, constants.ONE_ETHER); - await transferFeesAsync(pool1); + await protocolFees.collectProtocolFee(pool1).awaitTransactionSuccessAsync({ value: singleFeeAmount }); + await protocolFees.transferFeesForPool(pool1).awaitTransactionSuccessAsync(); + await protocolFees.collectProtocolFee(pool1).awaitTransactionSuccessAsync({ value: singleFeeAmount }); + await protocolFees.transferFeesForPool(pool1).awaitTransactionSuccessAsync(); const balanceWETH = await weth.balanceOf(staking.address).callAsync(); - // We leave 1 wei behind of both ETH and WETH - return expect(balanceWETH).to.bignumber.eq(constants.ONE_ETHER.times(2).minus(2)); + // We leave 1 wei of WETH behind. + await expect(balanceWETH).to.bignumber.eq(singleFeeAmount.times(2).minus(1)); + await expect(await weth.balanceOf(feeCollector1Address).callAsync()).to.bignumber.equal(1); + // And no ETH. + await expect(await env.web3Wrapper.getBalanceInWeiAsync(feeCollector1Address)).to.bignumber.eq(0); }); it('should attribute fees correctly', async () => { - const pool1Amount = new BigNumber(12345); - const pool2Amount = new BigNumber(45678); - - await collectAsync(pool1, pool1Amount, pool1Amount); // ETH - await transferFeesAsync(pool1); - await collectAsync(pool2, pool2Amount, constants.ZERO_AMOUNT); // WETH - await transferFeesAsync(pool2); + await protocolFees.collectProtocolFee(pool1).awaitTransactionSuccessAsync({ value: singleFeeAmount }); + await protocolFees.transferFeesForPool(pool1).awaitTransactionSuccessAsync(); + await protocolFees.collectProtocolFee(pool2).awaitTransactionSuccessAsync({ value: singleFeeAmount }); + await protocolFees.transferFeesForPool(pool2).awaitTransactionSuccessAsync(); const pool1Balance = await staking.balanceForPool(pool1).callAsync(); const pool2Balance = await staking.balanceForPool(pool2).callAsync(); const balanceWETH = await weth.balanceOf(staking.address).callAsync(); - await expect(balanceWETH).to.bignumber.equal(pool1Balance.plus(pool2Balance)); + await expect(balanceWETH).to.bignumber.equal(singleFeeAmount.times(2).minus(2)); - // We leave 1 wei behind of both ETH and WETH. - await expect(pool1Balance).to.bignumber.equal(pool1Amount.minus(2)); - - // Here we paid in WETH, so there's just 1 wei of WETH held back. - return expect(pool2Balance).to.bignumber.equal(pool2Amount.minus(1)); + // We leave 1 wei of WETH behind. + await expect(pool1Balance).to.bignumber.equal(singleFeeAmount.minus(1)); + await expect(pool2Balance).to.bignumber.equal(singleFeeAmount.minus(1)); + await expect(await weth.balanceOf(feeCollector1Address).callAsync()).to.bignumber.equal(1); + await expect(await weth.balanceOf(feeCollector2Address).callAsync()).to.bignumber.equal(1); + await expect(pool2Balance).to.bignumber.equal(singleFeeAmount.minus(1)); + // And no ETH. + await expect(await env.web3Wrapper.getBalanceInWeiAsync(feeCollector1Address)).to.bignumber.eq(0); + await expect(await env.web3Wrapper.getBalanceInWeiAsync(feeCollector2Address)).to.bignumber.eq(0); }); }); }); diff --git a/contracts/zero-ex/test/utils/migration.ts b/contracts/zero-ex/test/utils/migration.ts index 566a32db72..dc75ef5300 100644 --- a/contracts/zero-ex/test/utils/migration.ts +++ b/contracts/zero-ex/test/utils/migration.ts @@ -4,6 +4,7 @@ export { deployFullFeaturesAsync, initialMigrateAsync, fullMigrateAsync, - FullMigrationOpts, + FullMigrationConfig, + FullFeaturesDeployConfig, FullFeatures, } from '../../src/migration'; diff --git a/contracts/zero-ex/test/utils/orders.ts b/contracts/zero-ex/test/utils/orders.ts new file mode 100644 index 0000000000..3b9cc5a009 --- /dev/null +++ b/contracts/zero-ex/test/utils/orders.ts @@ -0,0 +1,43 @@ +import { getRandomInteger, randomAddress } from '@0x/contracts-test-utils'; +import { BigNumber, hexUtils } from '@0x/utils'; + +import { LimitOrder, LimitOrderFields, RfqOrder, RfqOrderFields } from '../../src/orders'; + +/** + * Generate a random limit order. + */ +export function getRandomLimitOrder(fields: Partial = {}): LimitOrder { + return new LimitOrder({ + makerToken: randomAddress(), + takerToken: randomAddress(), + makerAmount: getRandomInteger('1e18', '100e18'), + takerAmount: getRandomInteger('1e6', '100e6'), + takerTokenFeeAmount: getRandomInteger('0.01e18', '1e18'), + maker: randomAddress(), + taker: randomAddress(), + sender: randomAddress(), + feeRecipient: randomAddress(), + pool: hexUtils.random(), + expiry: new BigNumber(Math.floor(Date.now() / 1000 + 60)), + salt: new BigNumber(hexUtils.random()), + ...fields, + }); +} + +/** + * Generate a random RFQ order. + */ +export function getRandomRfqOrder(fields: Partial = {}): RfqOrder { + return new RfqOrder({ + makerToken: randomAddress(), + takerToken: randomAddress(), + makerAmount: getRandomInteger('1e18', '100e18'), + takerAmount: getRandomInteger('1e6', '100e6'), + maker: randomAddress(), + txOrigin: randomAddress(), + pool: hexUtils.random(), + expiry: new BigNumber(Math.floor(Date.now() / 1000 + 60)), + salt: new BigNumber(hexUtils.random()), + ...fields, + }); +} diff --git a/contracts/zero-ex/test/wrappers.ts b/contracts/zero-ex/test/wrappers.ts index 5dc09b723b..14b14f3cf3 100644 --- a/contracts/zero-ex/test/wrappers.ts +++ b/contracts/zero-ex/test/wrappers.ts @@ -28,6 +28,7 @@ export * from '../test/generated-wrappers/i_liquidity_provider'; export * from '../test/generated-wrappers/i_liquidity_provider_feature'; export * from '../test/generated-wrappers/i_liquidity_provider_sandbox'; export * from '../test/generated-wrappers/i_meta_transactions_feature'; +export * from '../test/generated-wrappers/i_native_orders_feature'; export * from '../test/generated-wrappers/i_ownable_feature'; export * from '../test/generated-wrappers/i_signature_validator_feature'; export * from '../test/generated-wrappers/i_simple_function_registry_feature'; @@ -45,6 +46,9 @@ export * from '../test/generated-wrappers/lib_liquidity_provider_rich_errors'; export * from '../test/generated-wrappers/lib_meta_transactions_rich_errors'; export * from '../test/generated-wrappers/lib_meta_transactions_storage'; export * from '../test/generated-wrappers/lib_migrate'; +export * from '../test/generated-wrappers/lib_native_order'; +export * from '../test/generated-wrappers/lib_native_orders_rich_errors'; +export * from '../test/generated-wrappers/lib_native_orders_storage'; export * from '../test/generated-wrappers/lib_order_hash'; export * from '../test/generated-wrappers/lib_ownable_rich_errors'; export * from '../test/generated-wrappers/lib_ownable_storage'; @@ -80,6 +84,7 @@ export * from '../test/generated-wrappers/mixin_sushiswap'; export * from '../test/generated-wrappers/mixin_uniswap'; export * from '../test/generated-wrappers/mixin_uniswap_v2'; export * from '../test/generated-wrappers/mixin_zero_ex_bridge'; +export * from '../test/generated-wrappers/native_orders_feature'; export * from '../test/generated-wrappers/ownable_feature'; export * from '../test/generated-wrappers/pay_taker_transformer'; export * from '../test/generated-wrappers/signature_validator_feature'; @@ -90,8 +95,10 @@ export * from '../test/generated-wrappers/test_delegate_caller'; export * from '../test/generated-wrappers/test_fill_quote_transformer_bridge'; export * from '../test/generated-wrappers/test_fill_quote_transformer_exchange'; export * from '../test/generated-wrappers/test_fill_quote_transformer_host'; +export * from '../test/generated-wrappers/test_fixin_protocol_fees'; export * from '../test/generated-wrappers/test_full_migration'; export * from '../test/generated-wrappers/test_initial_migration'; +export * from '../test/generated-wrappers/test_lib_native_order'; export * from '../test/generated-wrappers/test_lib_signature'; export * from '../test/generated-wrappers/test_lib_token_spender'; export * from '../test/generated-wrappers/test_liquidity_provider'; @@ -99,7 +106,7 @@ export * from '../test/generated-wrappers/test_meta_transactions_transform_erc20 export * from '../test/generated-wrappers/test_migrator'; export * from '../test/generated-wrappers/test_mint_token_erc20_transformer'; export * from '../test/generated-wrappers/test_mintable_erc20_token'; -export * from '../test/generated-wrappers/test_protocol_fees'; +export * from '../test/generated-wrappers/test_native_orders_feature'; export * from '../test/generated-wrappers/test_simple_function_registry_feature_impl1'; export * from '../test/generated-wrappers/test_simple_function_registry_feature_impl2'; export * from '../test/generated-wrappers/test_staking'; diff --git a/contracts/zero-ex/tsconfig.json b/contracts/zero-ex/tsconfig.json index 78e2cc5832..da1dce5609 100644 --- a/contracts/zero-ex/tsconfig.json +++ b/contracts/zero-ex/tsconfig.json @@ -10,7 +10,8 @@ "generated-artifacts/IAllowanceTarget.json", "generated-artifacts/IERC20Transformer.json", "generated-artifacts/IFlashWallet.json", - "generated-artifacts/ILiquidityProvider.json", + "generated-artifacts/ILiquidityProviderFeature.json", + "generated-artifacts/INativeOrdersFeature.json", "generated-artifacts/IOwnableFeature.json", "generated-artifacts/ISimpleFunctionRegistryFeature.json", "generated-artifacts/ITokenSpenderFeature.json", @@ -20,6 +21,7 @@ "generated-artifacts/LiquidityProviderFeature.json", "generated-artifacts/LogMetadataTransformer.json", "generated-artifacts/MetaTransactionsFeature.json", + "generated-artifacts/NativeOrdersFeature.json", "generated-artifacts/OwnableFeature.json", "generated-artifacts/PayTakerTransformer.json", "generated-artifacts/SignatureValidatorFeature.json", @@ -53,6 +55,7 @@ "test/generated-artifacts/ILiquidityProviderFeature.json", "test/generated-artifacts/ILiquidityProviderSandbox.json", "test/generated-artifacts/IMetaTransactionsFeature.json", + "test/generated-artifacts/INativeOrdersFeature.json", "test/generated-artifacts/IOwnableFeature.json", "test/generated-artifacts/ISignatureValidatorFeature.json", "test/generated-artifacts/ISimpleFunctionRegistryFeature.json", @@ -70,6 +73,9 @@ "test/generated-artifacts/LibMetaTransactionsRichErrors.json", "test/generated-artifacts/LibMetaTransactionsStorage.json", "test/generated-artifacts/LibMigrate.json", + "test/generated-artifacts/LibNativeOrder.json", + "test/generated-artifacts/LibNativeOrdersRichErrors.json", + "test/generated-artifacts/LibNativeOrdersStorage.json", "test/generated-artifacts/LibOrderHash.json", "test/generated-artifacts/LibOwnableRichErrors.json", "test/generated-artifacts/LibOwnableStorage.json", @@ -105,6 +111,7 @@ "test/generated-artifacts/MixinUniswap.json", "test/generated-artifacts/MixinUniswapV2.json", "test/generated-artifacts/MixinZeroExBridge.json", + "test/generated-artifacts/NativeOrdersFeature.json", "test/generated-artifacts/OwnableFeature.json", "test/generated-artifacts/PayTakerTransformer.json", "test/generated-artifacts/SignatureValidatorFeature.json", @@ -115,8 +122,10 @@ "test/generated-artifacts/TestFillQuoteTransformerBridge.json", "test/generated-artifacts/TestFillQuoteTransformerExchange.json", "test/generated-artifacts/TestFillQuoteTransformerHost.json", + "test/generated-artifacts/TestFixinProtocolFees.json", "test/generated-artifacts/TestFullMigration.json", "test/generated-artifacts/TestInitialMigration.json", + "test/generated-artifacts/TestLibNativeOrder.json", "test/generated-artifacts/TestLibSignature.json", "test/generated-artifacts/TestLibTokenSpender.json", "test/generated-artifacts/TestLiquidityProvider.json", @@ -124,7 +133,7 @@ "test/generated-artifacts/TestMigrator.json", "test/generated-artifacts/TestMintTokenERC20Transformer.json", "test/generated-artifacts/TestMintableERC20Token.json", - "test/generated-artifacts/TestProtocolFees.json", + "test/generated-artifacts/TestNativeOrdersFeature.json", "test/generated-artifacts/TestSimpleFunctionRegistryFeatureImpl1.json", "test/generated-artifacts/TestSimpleFunctionRegistryFeatureImpl2.json", "test/generated-artifacts/TestStaking.json", diff --git a/docs/basics/orders.rst b/docs/basics/orders.rst index 5c1d3535b3..5f92dfe200 100644 --- a/docs/basics/orders.rst +++ b/docs/basics/orders.rst @@ -2,11 +2,7 @@ Orders ###### -An order is a message passed into the 0x Protocol to facilitate an ERC20->ERC20 trade. There are currently two types of orders in 0x V4: Limit, RFQ. - - -.. note:: - As of v4 of the protocol, the maker address is no longer explicitly defined in limit orders. The maker is instead recovered from the signature of the order's EIP712 hash. +An order is a message passed into the 0x Protocol to facilitate an ERC20->ERC20 trade. There are currently two types of orders in 0x V4: **Limit** and **RFQ**. .. note:: 0x Orders currently support the exchange of ERC20 Tokens. Other asset classes, like ERC721, @@ -22,48 +18,45 @@ Structure The ``LimitOrder`` struct has the following fields: -+------------------+-------------+-----------------------------------------------------------------------------+ -| Field | Type | Description | -+==================+=============+=============================================================================+ -| ``makerToken`` | ``address`` | The ERC20 token the maker is selling and the maker is selling to the taker. | -+------------------+-------------+-----------------------------------------------------------------------------+ -| ``takerToken`` | ``address`` | The ERC20 token the taker is selling and the taker is selling to the maker. | -+------------------+-------------+-----------------------------------------------------------------------------+ -| ``makerAmount`` | ``uint128`` | The amount of makerToken being sold by the maker. | -+------------------+-------------+-----------------------------------------------------------------------------+ -| ``takerAmount`` | ``uint128`` | The amount of takerToken being sold by the taker. | -+------------------+-------------+-----------------------------------------------------------------------------+ -| ``feeRecipient`` | ``address`` | Recipient of maker token or taker token fees (if non-zero). | -+------------------+-------------+-----------------------------------------------------------------------------+ -| ``feeAmount`` | ``uint128`` | Amount of takerToken paid by the taker to the feeRecipient. | -+------------------+-------------+-----------------------------------------------------------------------------+ -| ``taker`` | ``address`` | Allowed taker address. Set to zero to allow any taker. | -+------------------+-------------+-----------------------------------------------------------------------------+ -| ``sender`` | ``address`` | Allowed address to directly call ``fillLimitOrder()`` (``msg.sender``). | -| | | This is distinct from ``taker`` in meta-transactions. | -| | | Set to zero to allow any caller. | -+------------------+-------------+-----------------------------------------------------------------------------+ -| ``pool`` | ``uint256`` | The staking pool to attribute the 0x protocol fee from this order. | -| | | Set to zero to attribute to the default pool, not owned by anyone. | -+------------------+-------------+-----------------------------------------------------------------------------+ -| ``expiry`` | ``uint64`` | The Unix timestamp in seconds when this order expires. | -+------------------+-------------+-----------------------------------------------------------------------------+ -| ``salt`` | ``uint256`` | Arbitrary number to enforce uniqueness of the order's hash. | -+------------------+-------------+-----------------------------------------------------------------------------+ ++--------------------------+-------------+-----------------------------------------------------------------------------+ +| Field | Type | Description | ++==========================+=============+=============================================================================+ +| ``makerToken`` | ``address`` | The ERC20 token the maker is selling and the maker is selling to the taker. | ++--------------------------+-------------+-----------------------------------------------------------------------------+ +| ``takerToken`` | ``address`` | The ERC20 token the taker is selling and the taker is selling to the maker. | ++--------------------------+-------------+-----------------------------------------------------------------------------+ +| ``makerAmount`` | ``uint128`` | The amount of makerToken being sold by the maker. | ++--------------------------+-------------+-----------------------------------------------------------------------------+ +| ``takerAmount`` | ``uint128`` | The amount of takerToken being sold by the taker. | ++--------------------------+-------------+-----------------------------------------------------------------------------+ +| ``takerTokenFeeAmount`` | ``uint128`` | Amount of takerToken paid by the taker to the feeRecipient. | ++--------------------------+-------------+-----------------------------------------------------------------------------+ +| ``maker`` | ``address`` | The address of the maker, and signer, of this order. | ++--------------------------+-------------+-----------------------------------------------------------------------------+ +| ``taker`` | ``address`` | Allowed taker address. Set to zero to allow any taker. | ++--------------------------+-------------+-----------------------------------------------------------------------------+ +| ``sender`` | ``address`` | Allowed address to directly call ``fillLimitOrder()`` (``msg.sender``). | +| | | This is distinct from ``taker`` in meta-transactions. | +| | | Set to zero to allow any caller. | ++--------------------------+-------------+-----------------------------------------------------------------------------+ +| ``feeRecipient`` | ``address`` | Recipient of maker token or taker token fees (if non-zero). | ++--------------------------+-------------+-----------------------------------------------------------------------------+ +| ``pool`` | ``bytes32`` | The staking pool to attribute the 0x protocol fee from this order. | +| | | Set to zero to attribute to the default pool, not owned by anyone. | ++--------------------------+-------------+-----------------------------------------------------------------------------+ +| ``expiry`` | ``uint64`` | The Unix timestamp in seconds when this order expires. | ++--------------------------+-------------+-----------------------------------------------------------------------------+ +| ``salt`` | ``uint256`` | Arbitrary number to enforce uniqueness of the order's hash. | ++--------------------------+-------------+-----------------------------------------------------------------------------+ Hashing limit orders -------------------- -There are two hashes associated with limit orders: the signature hash and the fill hash. The signature hash is what gets signed during the signing step. The fill hash is the hash used to uniquely identify an order inside the protocol and can be considered the "canonical" hash of the order. - -Computing the signature hash -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The signature hash is the hash of the order struct, following the `EIP712 spec `_. In solidity, the signature hash is computed as: +The hash of the order is used to uniquely identify an order inside the protocol. It is computed following the `EIP712 spec `_ standard. In solidity, the hash is computed as: .. code-block:: solidity - bytes32 signatureHash = keccak256(abi.encodePacked( + bytes32 orderHash = keccak256(abi.encodePacked( '\x19\x01', // The domain separator. keccak256(abi.encode( @@ -90,11 +83,12 @@ The signature hash is the hash of the order struct, following the `EIP712 spec < 'address takerToken,', 'uint128 makerAmount,', 'uint128 takerAmount,', - 'address feeRecipient,', - 'uint128 feeAmount,', + 'uint128 takerTokenFeeAmount,', 'address taker,', + 'address maker,', 'address sender,', - 'uint256 pool,', + 'address feeRecipient,', + 'bytes32 pool,', 'uint64 expiry,', 'uint256 salt)' )), @@ -103,34 +97,22 @@ The signature hash is the hash of the order struct, following the `EIP712 spec < order.takerToken, order.makerAmount, order.takerAmount, - order.feeRecipient, - order.feeAmount, + order.takerTokenFeeAmount, + order.maker, order.taker, order.sender, + order.feeRecipient, order.pool, order.expiry, order.salt )) )); -Computing the fill hash -^^^^^^^^^^^^^^^^^^^^^^^^^ - -The fill hash simply hashes the previous signature hash with the maker's address, which can be recovered from the order's signature if not already known. +Alternatively, the Exchange Proxy contract can be used to retrieve the hash given an order. .. code-block:: solidity - // For EthSign signatures, the signatureHash would need to be replaced with - // keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", signatureHash)) - address makerAddress = ecrecover(signatureHash, signature.v, signature.r, signature.s); - bytes32 fillHash = keccak256(abi.encode(signatureHash, makerAddress)); - -Alternatively, the Exchange Proxy contract can be used to retrieve these hashes given an order and signature. - -.. code-block:: solidity - - bytes32 signatureHash = IZeroEx(0xDef1C0ded9bec7F1a1670819833240f027b25EfF).getLimitOrderSignatureHash(order); - bytes32 fillHash = IZeroEx(0xDef1C0ded9bec7F1a1670819833240f027b25EfF).getLimitOrderFillHash(order, signature); + bytes32 orderHash = IZeroEx(0xDef1C0ded9bec7F1a1670819833240f027b25EfF).getLimitOrderHash(order); Signing limit orders -------------------- @@ -153,21 +135,26 @@ There are two types of signatures supported: ``EIP712`` and ``EthSign``. * The ``EIP712`` signature type is best for web frontends that present an order to be signed through Metamask in a human-readable format. It relies on the `eth_signTypedData `_ JSON-RPC method exposed by MetaMask. This signature has the ``signatureType`` of ``2``. * The ``EthSign`` signature is best for use with headless providers, such as when using a geth node. This relies on the ``eth_sign`` JSON-RPC method common to all nodes. This signature has the ``signatureType`` of ``3``. -In both cases, the ``@0x/order-utils`` package simplifies generating these signatures. +In both cases, the ``@0x/protocol-utils`` package simplifies generating these signatures. .. code-block:: javascript - :linenos: - const orderUtils = require('@0x/order-utils'); - const order = new orderUtils.LimitOrder({ + const utils = require('@0x/protocol-utils'); + const order = new utils.LimitOrder({ makerToken: '0x6B175474E89094C44Da98b954EedeAC495271d0F', // DAI takerToken: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', // WETH ... // Other fields }); // Generate an EIP712 signature - const signature = await order.signTypedDataAsync(web3.currentProvider, makerAddress); + const signature = await order.eip712SignTypedDataWithProviderAsync( + web3.currentProvider, + makerAddress, + ); // Generate an EthSign signature - const signature = await order.sign(web3.currentProvider, makerAddress); + const signature = await order.ethSignHashWithProviderAsync( + web3.currentProvider, + makerAddress, + ); Filling limit orders -------------------- @@ -185,12 +172,12 @@ Limit orders can be filled with the ``fillLimitOrder()`` or ``fillOrKillLimitOrd // The signature Signature calldata signature, // How much taker token to fill the order with - uint256 takerTokenFillAmount + uint128 takerTokenFillAmount ) external payable // How much maker token from the order the taker received. - returns (uint256 makerTokenFillAmount); + returns (uint128 takerTokenFillAmount, uint128 makerTokenFillAmount); ``fillOrKillLimitOrder()`` fills a single limit order for **exactly** ``takerTokenFillAmount``: @@ -202,17 +189,17 @@ Limit orders can be filled with the ``fillLimitOrder()`` or ``fillOrKillLimitOrd // The signature Signature calldata signature, // How much taker token to fill the order with - uint256 takerTokenFillAmount + uint128 takerTokenFillAmount ) external payable // How much maker token from the order the taker received. - returns (uint256 makerTokenFillAmount); + returns (uint128 makerTokenFillAmount); Cancelling a limit order ------------------------ -Because there is no way to un-sign an order that has been distributed, limit orders must be cancelled on-chain through ``cancelLimitOrder()``, ``batchCancelLimitOrders()`` or ``cancelLimitOrdersUpTo()`` functions. They can only be called by the order's maker. +Because there is no way to un-sign an order that has been distributed, limit orders must be cancelled on-chain through one of several functions. They can only be called by the order's maker. ``cancelLimitOrder()`` cancels a single limit order created by the caller: @@ -234,15 +221,28 @@ Because there is no way to un-sign an order that has been distributed, limit ord ) external; -``cancelLimitOrdersUpTo()`` will cancel limit orders created by the caller with a ``salt`` field <= the value provided. Subsequent calls to this function must provide a ``salt`` >= the last call to succeed. +``cancelLimitPairOrders()`` will cancel all limit orders created by the caller with with a maker and taker token pair and a ``salt`` field < the ``salt`` provided. Subsequent calls to this function with the same tokens must provide a ``salt`` >= the last call to succeed. .. code-block:: solidity - function cancelLimitOrdersUpTo( + function cancelLimitPairLimitOrders( + address makerToken, + address takerToken, uint256 salt; ) external; +``batchCancelLimitPairOrders()`` performs multiple ``cancelLimitPairOrders()`` at once. Each respective index across arrays is equivalent to a single call. + +.. code-block:: solidity + + function batchCancelLimitPairOrders( + address[] makerTokens, + address[] takerTokens, + uint256[] salts; + ) + external; + Getting the status of a limit order ----------------------------------- @@ -250,31 +250,30 @@ The Exchange Proxy exposes a function ``getLimitOrderInfo()`` to query informati .. code-block:: solidity - enum OrderState { + enum OrderStatus { INVALID, - CANCELLED, FILLABLE, - FILLED + FILLED, + CANCELLED, + EXPIRED } struct OrderInfo { - // The fill hash. - bytes32 fillHash; + // The order hash. + bytes32 orderHash; // Current state of the order. - OrderState state; + OrderStatus status; // How much taker token has been filled in the order. - uint256 takerTokenFilledAmount; + uint128 takerTokenFilledAmount; } function getLimitOrderInfo( // The order - LimitOrder calldata order, - // The signature - Signature calldata signature + LimitOrder calldata order ) external view - returns (OrderInfo memory status); + returns (OrderInfo memory orderInfo); RFQ Orders ========== @@ -303,9 +302,11 @@ The ``RFQOrder`` struct has the following fields: +-----------------+-------------+-----------------------------------------------------------------------------+ | ``takerAmount`` | ``uint128`` | The amount of takerToken being sold by the taker. | +-----------------+-------------+-----------------------------------------------------------------------------+ +| ``maker`` | ``address`` | The address of the maker, and signer, of this order. | ++-----------------+-------------+-----------------------------------------------------------------------------+ | ``txOrigin`` | ``address`` | The allowed address of the EOA that submitted the Ethereum transaction. | +-----------------+-------------+-----------------------------------------------------------------------------+ -| ``pool`` | ``uint256`` | The staking pool to attribute the 0x protocol fee from this order. | +| ``pool`` | ``bytes32`` | The staking pool to attribute the 0x protocol fee from this order. | | | | Set to zero to attribute to the default pool, not owned by anyone. | +-----------------+-------------+-----------------------------------------------------------------------------+ | ``expiry`` | ``uint64`` | The Unix timestamp in seconds when this order expires. | @@ -316,16 +317,11 @@ The ``RFQOrder`` struct has the following fields: Hashing RFQ orders ------------------ -There are two hashes associated with RFQ orders: the signature hash and the fill hash. The signature hash is what gets signed during the signing step. The fill hash is the hash used to uniquely identify an order inside the protocol and can be considered the "canonical" hash of the order. - -Computing the signature hash -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The signature hash is the hash of the order struct, following the `EIP712 spec `_. In solidity, the signature hash is computed as: +The hash of the order is used to uniquely identify an order inside the protocol. It is computed following the `EIP712 spec `_ standard. In solidity, the hash is computed as: .. code-block:: solidity - bytes32 signatureHash = keccak256(abi.encodePacked( + bytes32 orderHash = keccak256(abi.encodePacked( '\x19\x01', // The domain separator. keccak256(abi.encode( @@ -352,8 +348,9 @@ The signature hash is the hash of the order struct, following the `EIP712 spec < 'address takerToken,', 'uint128 makerAmount,', 'uint128 takerAmount,', + 'address maker,' 'address txOrigin,' - 'uint256 pool,', + 'bytes32 pool,', 'uint64 expiry,', 'uint256 salt)' )), @@ -362,6 +359,7 @@ The signature hash is the hash of the order struct, following the `EIP712 spec < order.takerToken, order.makerAmount, order.takerAmount, + order.maker, order.txOrigin, order.pool, order.expiry, @@ -369,32 +367,11 @@ The signature hash is the hash of the order struct, following the `EIP712 spec < )) )); -Computing the fill hash -^^^^^^^^^^^^^^^^^^^^^^^ - -The fill hash simply hashes the previous signature hash with the maker's address, which can be recovered from the order's signature if not already known. +Alternatively, the Exchange Proxy contract can be used to retrieve the hash given an order. .. code-block:: solidity - // For EthSign signatures, the signatureHash would need to be replaced with - // keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", signatureHash)) - address makerAddress = ecrecover( - keccak256(abi.encodePacked( - '\x19Ethereum Signed Message:\n32', - signatureHash - )), - signature.v, - signature.r, - signature.s - ); - bytes32 fillHash = keccak256(abi.encode(signatureHash, makerAddress)); - -Alternatively, the Exchange Proxy contract can be used to retrieve these hashes given an order and signature. - -.. code-block:: solidity - - bytes32 signatureHash = IZeroEx(0xDef1C0ded9bec7F1a1670819833240f027b25EfF).getRfqOrderSignatureHash(order); - bytes32 fillHash = IZeroEx(0xDef1C0ded9bec7F1a1670819833240f027b25EfF).getRfqOrderFillHash(order, signature); + bytes32 orderHash = IZeroEx(0xDef1C0ded9bec7F1a1670819833240f027b25EfF).getLimitOrderHash(order); Signing RFQ orders ------------------ @@ -411,18 +388,21 @@ The protocol accepts signatures defined by the following struct: bytes32 s; // Signature data. } -The ``@0x/order-utils`` node package simplifies the process of creating a valid signature object. +The ``@0x/protocol-utils`` node package simplifies the process of creating a valid signature object. .. code-block:: javascript - :linenos: - const orderUtils = require('@0x/order-utils'); - const order = new orderUtils.RfqOrder({ + const utils = require('@0x/protocol-utils'); + const order = new utils.RfqOrder({ makerToken: '0x6B175474E89094C44Da98b954EedeAC495271d0F', // DAI takerToken: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', // WETH ... // Other fields }); - const signature = await order.sign(web3.currentProvider, makerAddress); + // Generate an EthSign signature + const signature = await order.ethSignHashWithProviderAsync( + web3.currentProvider, + makerAddress, + ); Filling RFQ Orders ------------------ @@ -439,12 +419,12 @@ RFQ orders can be filled with the ``fillRfqOrder()`` or ``fillOrKillRfqOrder()`` // The signature Signature calldata signature, // How much taker token to fill the order with - uint256 takerTokenFillAmount + uint128 takerTokenFillAmount ) external payable // How much maker token from the order the taker received. - returns (uint256 makerTokenFillAmount); + returns (uint128 takerTokenFillAmount, uint128 makerTokenFillAmount); ``fillOrKillRfqOrder()`` fills a single RFQ order for **exactly** ``takerTokenFillAmount``: @@ -456,17 +436,17 @@ RFQ orders can be filled with the ``fillRfqOrder()`` or ``fillOrKillRfqOrder()`` // The signature Signature calldata signature, // How much taker token to fill the order with - uint256 takerTokenFillAmount + uint128 takerTokenFillAmount ) external payable // How much maker token from the order the taker received. - returns (uint256 makerTokenFillAmount); + returns (uint128 makerTokenFillAmount); Cancelling an RFQ order ----------------------- -Similar to limit orders, RFQ orders can be cancelled on-chain through ``cancelRfqOrder()`` or ``batchCancelRfqOrders()`` (but there is no ``...UpTo()`` variant). Both can only be called by the order's maker. +Similar to limit orders, RFQ orders can be cancelled on-chain through a variety of functions, which can only be called by the order's maker. ``cancelRfqOrder()`` cancels a single RFQ order created by the caller: @@ -488,6 +468,28 @@ Similar to limit orders, RFQ orders can be cancelled on-chain through ``cancelRf ) external; +``cancelPairRfqOrders()`` will cancel all RFQ orders created by the caller with with a maker and taker token pair and a ``salt`` field < the ``salt`` provided. Subsequent calls to this function with the same tokens must provide a ``salt`` >= the last call to succeed. + +.. code-block:: solidity + + function cancelPairRfqOrders( + address makerToken, + address takerToken, + uint256 salt; + ) + external; + +``batchCancelPairRfqOrders()`` performs multiple ``cancelPairRfqOrders()`` at once. Each respective index across arrays is equivalent to a single call. + +.. code-block:: solidity + + function batchCancelPairRfqOrders( + address[] makerTokens, + address[] takerTokens, + uint256[] salts; + ) + external; + Getting the status of an RFQ order ---------------------------------- @@ -495,28 +497,27 @@ The Exchange Proxy exposes a function ``getRfqOrderInfo()`` to query information .. code-block:: solidity - enum OrderState { + enum OrderStatus { INVALID, - CANCELLED, FILLABLE, - FILLED + FILLED, + CANCELLED, + EXPIRED } struct OrderInfo { - // The fill hash. - bytes32 fillHash; + // The order hash. + bytes32 orderHash; // Current state of the order. - OrderState state; + OrderStatus status; // How much taker token has been filled in the order. - uint256 takerTokenFilledAmount; + uint128 takerTokenFilledAmount; } function getRfqOrderInfo( // The order - RfqOrder calldata order, - // The signature - Signature calldata signature + RfqOrder calldata order ) external view - returns (OrderInfo memory status); + returns (OrderInfo memory orderInfo); diff --git a/packages/contract-addresses/CHANGELOG.json b/packages/contract-addresses/CHANGELOG.json index a65051f274..f3b788a088 100644 --- a/packages/contract-addresses/CHANGELOG.json +++ b/packages/contract-addresses/CHANGELOG.json @@ -1,4 +1,13 @@ [ + { + "version": "5.4.0", + "changes": [ + { + "note": "Update ganache snapshot addresses", + "pr": 27 + } + ] + }, { "version": "5.3.0", "changes": [ diff --git a/packages/contract-addresses/addresses.json b/packages/contract-addresses/addresses.json index 2dd4fd94f9..2a67136840 100644 --- a/packages/contract-addresses/addresses.json +++ b/packages/contract-addresses/addresses.json @@ -198,10 +198,10 @@ "exchangeProxyFlashWallet": "0xb9682a8e7920b431f1d412b8510f0077410c8faa", "exchangeProxyLiquidityProviderSandbox": "0x0000000000000000000000000000000000000000", "transformers": { - "wethTransformer": "0xc6b0d3c45a6b5092808196cb00df5c357d55e1d5", - "payTakerTransformer": "0x7209185959d7227fb77274e1e88151d7c4c368d3", - "affiliateFeeTransformer": "0x3f16ca81691dab9184cb4606c361d73c4fd2510a", - "fillQuoteTransformer": "0x99356167edba8fbdc36959e3f5d0c43d1ba9c6db" + "wethTransformer": "0x7209185959d7227fb77274e1e88151d7c4c368d3", + "payTakerTransformer": "0x3f16ca81691dab9184cb4606c361d73c4fd2510a", + "affiliateFeeTransformer": "0x99356167edba8fbdc36959e3f5d0c43d1ba9c6db", + "fillQuoteTransformer": "0x45b3a72221e571017c0f0ec42189e11d149d0ace" } } }