From 3cc639c8d0e10cd9b11d744c3b704497cccb6b60 Mon Sep 17 00:00:00 2001 From: mzhu25 Date: Mon, 8 Mar 2021 15:45:49 -0800 Subject: [PATCH] MultiplexFeature and BatchFillNativeOrdersFeature (#140) * WrappedFillFeature * Address internal feedback * create features/interfaces/ directory * Split NativeOrdersFeature into mixins * Rename mixins to use NativeOrders namespace * Add BatchFillNativeOrdersFeature * Rename WrapperFillFeature => MultiplexFeature and add natspec comments * Emit LiquidityProviderSwap event * post-rebase fixes * Multiplex mainnet fork tests * lint * Add tests for batch fill functions * Remove market functions * Addres PR feedback * Remove nested _batchFill calls from _multiHopFill * Add BatchFillIncompleteRevertError type * Use call{value: amount}() instead of transfer(amount) * Remove outdated comment * Update some comments * Add events * Address spot-check recommendations * Remove-top level events, add ExpiredRfqOrder event * Update changelog * Change ExpiredRfqOrder event * Update IZeroEx artifact and contract wrapper --- contracts/erc20/src/index.ts | 1 + contracts/zero-ex/CHANGELOG.json | 4 + contracts/zero-ex/contracts/src/IZeroEx.sol | 22 +- .../src/errors/LibNativeOrdersRichErrors.sol | 17 + .../features/BatchFillNativeOrdersFeature.sol | 198 +++ .../src/features/BootstrapFeature.sol | 2 +- .../src/features/LiquidityProviderFeature.sol | 4 +- .../src/features/MetaTransactionsFeature.sol | 8 +- .../src/features/MultiplexFeature.sol | 805 +++++++++++ .../src/features/NativeOrdersFeature.sol | 1177 +---------------- .../contracts/src/features/OwnableFeature.sol | 4 +- .../SimpleFunctionRegistryFeature.sol | 4 +- .../src/features/TokenSpenderFeature.sol | 4 +- .../src/features/TransformERC20Feature.sol | 6 +- .../contracts/src/features/UniswapFeature.sol | 6 +- .../IBatchFillNativeOrdersFeature.sol | 70 + .../{ => interfaces}/IBootstrapFeature.sol | 0 .../features/{ => interfaces}/IFeature.sol | 0 .../ILiquidityProviderFeature.sol | 2 +- .../IMetaTransactionsFeature.sol | 2 +- .../features/interfaces/IMultiplexFeature.sol | 117 ++ .../interfaces/INativeOrdersEvents.sol | 116 ++ .../{ => interfaces}/INativeOrdersFeature.sol | 95 +- .../{ => interfaces}/IOwnableFeature.sol | 0 .../ISimpleFunctionRegistryFeature.sol | 0 .../{ => interfaces}/ITokenSpenderFeature.sol | 0 .../ITransformERC20Feature.sol | 4 +- .../{ => interfaces}/IUniswapFeature.sol | 0 .../src/features/libs/LibNativeOrder.sol | 24 + .../NativeOrdersCancellation.sol | 266 ++++ .../native_orders/NativeOrdersInfo.sol | 394 ++++++ .../NativeOrdersProtocolFees.sol | 71 + .../native_orders/NativeOrdersSettlement.sol | 569 ++++++++ .../contracts/src/fixins/FixinCommon.sol | 4 +- .../src/fixins/FixinTokenSpender.sol | 2 +- .../src/migrations/FullMigration.sol | 2 +- .../src/migrations/InitialMigration.sol | 2 +- .../src/transformers/FillQuoteTransformer.sol | 4 +- .../contracts/src/vendor/IUniswapV2Pair.sol | 48 + .../contracts/test/TestInitialMigration.sol | 2 +- ...estMetaTransactionsNativeOrdersFeature.sol | 2 +- ...tMetaTransactionsTransformERC20Feature.sol | 2 +- .../zero-ex/contracts/test/TestMigrator.sol | 2 +- .../test/TestRfqOriginRegistration.sol | 2 +- contracts/zero-ex/package.json | 4 +- contracts/zero-ex/src/artifacts.ts | 8 + contracts/zero-ex/src/wrappers.ts | 4 + contracts/zero-ex/test/artifacts.ts | 40 +- .../features/batch_fill_native_orders_test.ts | 479 +++++++ .../zero-ex/test/features/multiplex_test.ts | 764 +++++++++++ .../features/native_orders_feature_test.ts | 408 ++---- contracts/zero-ex/test/utils/orders.ts | 281 +++- contracts/zero-ex/test/wrappers.ts | 10 + contracts/zero-ex/tsconfig.json | 14 + packages/contract-artifacts/CHANGELOG.json | 9 + .../contract-artifacts/artifacts/IZeroEx.json | 221 +++- packages/contract-wrappers/CHANGELOG.json | 9 + .../src/generated-wrappers/i_zero_ex.ts | 618 +++++++++ packages/contract-wrappers/src/index.ts | 1 + .../src/revert-errors/native_orders.ts | 15 + 60 files changed, 5337 insertions(+), 1612 deletions(-) create mode 100644 contracts/zero-ex/contracts/src/features/BatchFillNativeOrdersFeature.sol create mode 100644 contracts/zero-ex/contracts/src/features/MultiplexFeature.sol create mode 100644 contracts/zero-ex/contracts/src/features/interfaces/IBatchFillNativeOrdersFeature.sol rename contracts/zero-ex/contracts/src/features/{ => interfaces}/IBootstrapFeature.sol (100%) rename contracts/zero-ex/contracts/src/features/{ => interfaces}/IFeature.sol (100%) rename contracts/zero-ex/contracts/src/features/{ => interfaces}/ILiquidityProviderFeature.sol (98%) rename contracts/zero-ex/contracts/src/features/{ => interfaces}/IMetaTransactionsFeature.sol (99%) create mode 100644 contracts/zero-ex/contracts/src/features/interfaces/IMultiplexFeature.sol create mode 100644 contracts/zero-ex/contracts/src/features/interfaces/INativeOrdersEvents.sol rename contracts/zero-ex/contracts/src/features/{ => interfaces}/INativeOrdersFeature.sol (80%) rename contracts/zero-ex/contracts/src/features/{ => interfaces}/IOwnableFeature.sol (100%) rename contracts/zero-ex/contracts/src/features/{ => interfaces}/ISimpleFunctionRegistryFeature.sol (100%) rename contracts/zero-ex/contracts/src/features/{ => interfaces}/ITokenSpenderFeature.sol (100%) rename contracts/zero-ex/contracts/src/features/{ => interfaces}/ITransformERC20Feature.sol (98%) rename contracts/zero-ex/contracts/src/features/{ => interfaces}/IUniswapFeature.sol (100%) create mode 100644 contracts/zero-ex/contracts/src/features/native_orders/NativeOrdersCancellation.sol create mode 100644 contracts/zero-ex/contracts/src/features/native_orders/NativeOrdersInfo.sol create mode 100644 contracts/zero-ex/contracts/src/features/native_orders/NativeOrdersProtocolFees.sol create mode 100644 contracts/zero-ex/contracts/src/features/native_orders/NativeOrdersSettlement.sol create mode 100644 contracts/zero-ex/contracts/src/vendor/IUniswapV2Pair.sol create mode 100644 contracts/zero-ex/test/features/batch_fill_native_orders_test.ts create mode 100644 contracts/zero-ex/test/features/multiplex_test.ts diff --git a/contracts/erc20/src/index.ts b/contracts/erc20/src/index.ts index 9b6092d32b..53b045165a 100644 --- a/contracts/erc20/src/index.ts +++ b/contracts/erc20/src/index.ts @@ -6,6 +6,7 @@ export { WETH9Events, WETH9DepositEventArgs, WETH9TransferEventArgs, + WETH9WithdrawalEventArgs, ZRXTokenContract, DummyERC20TokenTransferEventArgs, ERC20TokenEventArgs, diff --git a/contracts/zero-ex/CHANGELOG.json b/contracts/zero-ex/CHANGELOG.json index eaef69b1b6..aa595a0588 100644 --- a/contracts/zero-ex/CHANGELOG.json +++ b/contracts/zero-ex/CHANGELOG.json @@ -9,6 +9,10 @@ { "note": "Emit `LiquidityProviderFill` event in `CurveLiquidityProvider`", "pr": 143 + }, + { + "note": "Add BatchFillNativeOrdersFeature and MultiplexFeature", + "pr": 140 } ] }, diff --git a/contracts/zero-ex/contracts/src/IZeroEx.sol b/contracts/zero-ex/contracts/src/IZeroEx.sol index b2f3062c70..7421d070dd 100644 --- a/contracts/zero-ex/contracts/src/IZeroEx.sol +++ b/contracts/zero-ex/contracts/src/IZeroEx.sol @@ -20,14 +20,16 @@ pragma solidity ^0.6.5; pragma experimental ABIEncoderV2; -import "./features/IOwnableFeature.sol"; -import "./features/ISimpleFunctionRegistryFeature.sol"; -import "./features/ITokenSpenderFeature.sol"; -import "./features/ITransformERC20Feature.sol"; -import "./features/IMetaTransactionsFeature.sol"; -import "./features/IUniswapFeature.sol"; -import "./features/ILiquidityProviderFeature.sol"; -import "./features/INativeOrdersFeature.sol"; +import "./features/interfaces/IOwnableFeature.sol"; +import "./features/interfaces/ISimpleFunctionRegistryFeature.sol"; +import "./features/interfaces/ITokenSpenderFeature.sol"; +import "./features/interfaces/ITransformERC20Feature.sol"; +import "./features/interfaces/IMetaTransactionsFeature.sol"; +import "./features/interfaces/IUniswapFeature.sol"; +import "./features/interfaces/ILiquidityProviderFeature.sol"; +import "./features/interfaces/INativeOrdersFeature.sol"; +import "./features/interfaces/IBatchFillNativeOrdersFeature.sol"; +import "./features/interfaces/IMultiplexFeature.sol"; /// @dev Interface for a fully featured Exchange Proxy. @@ -39,7 +41,9 @@ interface IZeroEx is IMetaTransactionsFeature, IUniswapFeature, ILiquidityProviderFeature, - INativeOrdersFeature + INativeOrdersFeature, + IBatchFillNativeOrdersFeature, + IMultiplexFeature { // solhint-disable state-visibility diff --git a/contracts/zero-ex/contracts/src/errors/LibNativeOrdersRichErrors.sol b/contracts/zero-ex/contracts/src/errors/LibNativeOrdersRichErrors.sol index 41d1d5cb5a..e339caac44 100644 --- a/contracts/zero-ex/contracts/src/errors/LibNativeOrdersRichErrors.sol +++ b/contracts/zero-ex/contracts/src/errors/LibNativeOrdersRichErrors.sol @@ -170,4 +170,21 @@ library LibNativeOrdersRichErrors { maker ); } + + function BatchFillIncompleteError( + bytes32 orderHash, + uint256 takerTokenFilledAmount, + uint256 takerTokenFillAmount + ) + internal + pure + returns (bytes memory) + { + return abi.encodeWithSelector( + bytes4(keccak256("BatchFillIncompleteError(bytes32,uint256,uint256)")), + orderHash, + takerTokenFilledAmount, + takerTokenFillAmount + ); + } } diff --git a/contracts/zero-ex/contracts/src/features/BatchFillNativeOrdersFeature.sol b/contracts/zero-ex/contracts/src/features/BatchFillNativeOrdersFeature.sol new file mode 100644 index 0000000000..1b67b33af4 --- /dev/null +++ b/contracts/zero-ex/contracts/src/features/BatchFillNativeOrdersFeature.sol @@ -0,0 +1,198 @@ +// SPDX-License-Identifier: Apache-2.0 +/* + + Copyright 2021 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-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 "../errors/LibNativeOrdersRichErrors.sol"; +import "../fixins/FixinCommon.sol"; +import "../fixins/FixinEIP712.sol"; +import "../migrations/LibMigrate.sol"; +import "./interfaces/IFeature.sol"; +import "./interfaces/IBatchFillNativeOrdersFeature.sol"; +import "./interfaces/INativeOrdersFeature.sol"; +import "./libs/LibNativeOrder.sol"; +import "./libs/LibSignature.sol"; + + +/// @dev Feature for batch/market filling limit and RFQ orders. +contract BatchFillNativeOrdersFeature is + IFeature, + IBatchFillNativeOrdersFeature, + FixinCommon, + FixinEIP712 +{ + using LibSafeMathV06 for uint128; + using LibSafeMathV06 for uint256; + using LibRichErrorsV06 for bytes; + + /// @dev Name of this feature. + string public constant override FEATURE_NAME = "BatchFill"; + /// @dev Version of this feature. + uint256 public immutable override FEATURE_VERSION = _encodeVersion(1, 0, 0); + + constructor(address zeroExAddress) + public + FixinEIP712(zeroExAddress) + { + // 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.batchFillLimitOrders.selector); + _registerFeatureFunction(this.batchFillRfqOrders.selector); + return LibMigrate.MIGRATE_SUCCESS; + } + + /// @dev Fills multiple limit orders. + /// @param orders Array of limit orders. + /// @param signatures Array of signatures corresponding to each order. + /// @param takerTokenFillAmounts Array of desired amounts to fill each order. + /// @param revertIfIncomplete If true, reverts if this function fails to + /// fill the full fill amount for any individual order. + /// @return takerTokenFilledAmounts Array of amounts filled, in taker token. + /// @return makerTokenFilledAmounts Array of amounts filled, in maker token. + function batchFillLimitOrders( + LibNativeOrder.LimitOrder[] calldata orders, + LibSignature.Signature[] calldata signatures, + uint128[] calldata takerTokenFillAmounts, + bool revertIfIncomplete + ) + external + payable + override + returns ( + uint128[] memory takerTokenFilledAmounts, + uint128[] memory makerTokenFilledAmounts + ) + { + require( + orders.length == signatures.length && orders.length == takerTokenFillAmounts.length, + 'BatchFillNativeOrdersFeature::batchFillLimitOrders/MISMATCHED_ARRAY_LENGTHS' + ); + takerTokenFilledAmounts = new uint128[](orders.length); + makerTokenFilledAmounts = new uint128[](orders.length); + uint256 protocolFee = uint256(INativeOrdersFeature(address(this)).getProtocolFeeMultiplier()) + .safeMul(tx.gasprice); + uint256 ethProtocolFeePaid; + for (uint256 i = 0; i != orders.length; i++) { + try + INativeOrdersFeature(address(this))._fillLimitOrder + ( + orders[i], + signatures[i], + takerTokenFillAmounts[i], + msg.sender, + msg.sender + ) + returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount) + { + // Update amounts filled. + (takerTokenFilledAmounts[i], makerTokenFilledAmounts[i]) = + (takerTokenFilledAmount, makerTokenFilledAmount); + ethProtocolFeePaid = ethProtocolFeePaid.safeAdd(protocolFee); + } catch {} + + if ( + revertIfIncomplete && + takerTokenFilledAmounts[i] < takerTokenFillAmounts[i] + ) { + bytes32 orderHash = _getEIP712Hash( + LibNativeOrder.getLimitOrderStructHash(orders[i]) + ); + // Did not fill the amount requested. + LibNativeOrdersRichErrors.BatchFillIncompleteError( + orderHash, + takerTokenFilledAmounts[i], + takerTokenFillAmounts[i] + ).rrevert(); + } + } + LibNativeOrder.refundExcessProtocolFeeToSender(ethProtocolFeePaid); + } + + /// @dev Fills multiple RFQ orders. + /// @param orders Array of RFQ orders. + /// @param signatures Array of signatures corresponding to each order. + /// @param takerTokenFillAmounts Array of desired amounts to fill each order. + /// @param revertIfIncomplete If true, reverts if this function fails to + /// fill the full fill amount for any individual order. + /// @return takerTokenFilledAmounts Array of amounts filled, in taker token. + /// @return makerTokenFilledAmounts Array of amounts filled, in maker token. + function batchFillRfqOrders( + LibNativeOrder.RfqOrder[] calldata orders, + LibSignature.Signature[] calldata signatures, + uint128[] calldata takerTokenFillAmounts, + bool revertIfIncomplete + ) + external + override + returns ( + uint128[] memory takerTokenFilledAmounts, + uint128[] memory makerTokenFilledAmounts + ) + { + require( + orders.length == signatures.length && orders.length == takerTokenFillAmounts.length, + 'BatchFillNativeOrdersFeature::batchFillRfqOrders/MISMATCHED_ARRAY_LENGTHS' + ); + takerTokenFilledAmounts = new uint128[](orders.length); + makerTokenFilledAmounts = new uint128[](orders.length); + for (uint256 i = 0; i != orders.length; i++) { + try + INativeOrdersFeature(address(this))._fillRfqOrder + ( + orders[i], + signatures[i], + takerTokenFillAmounts[i], + msg.sender + ) + returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount) + { + // Update amounts filled. + (takerTokenFilledAmounts[i], makerTokenFilledAmounts[i]) = + (takerTokenFilledAmount, makerTokenFilledAmount); + } catch {} + + if ( + revertIfIncomplete && + takerTokenFilledAmounts[i] < takerTokenFillAmounts[i] + ) { + // Did not fill the amount requested. + bytes32 orderHash = _getEIP712Hash( + LibNativeOrder.getRfqOrderStructHash(orders[i]) + ); + LibNativeOrdersRichErrors.BatchFillIncompleteError( + orderHash, + takerTokenFilledAmounts[i], + takerTokenFillAmounts[i] + ).rrevert(); + } + } + } +} diff --git a/contracts/zero-ex/contracts/src/features/BootstrapFeature.sol b/contracts/zero-ex/contracts/src/features/BootstrapFeature.sol index 09998c191b..b697ead362 100644 --- a/contracts/zero-ex/contracts/src/features/BootstrapFeature.sol +++ b/contracts/zero-ex/contracts/src/features/BootstrapFeature.sol @@ -23,7 +23,7 @@ pragma experimental ABIEncoderV2; import "@0x/contracts-utils/contracts/src/v06/errors/LibRichErrorsV06.sol"; import "../migrations/LibBootstrap.sol"; import "../storage/LibProxyStorage.sol"; -import "./IBootstrapFeature.sol"; +import "./interfaces/IBootstrapFeature.sol"; /// @dev Detachable `bootstrap()` feature. diff --git a/contracts/zero-ex/contracts/src/features/LiquidityProviderFeature.sol b/contracts/zero-ex/contracts/src/features/LiquidityProviderFeature.sol index 2e647e770f..8d6ba51312 100644 --- a/contracts/zero-ex/contracts/src/features/LiquidityProviderFeature.sol +++ b/contracts/zero-ex/contracts/src/features/LiquidityProviderFeature.sol @@ -31,8 +31,8 @@ import "../fixins/FixinCommon.sol"; import "../fixins/FixinTokenSpender.sol"; import "../migrations/LibMigrate.sol"; import "../transformers/LibERC20Transformer.sol"; -import "./IFeature.sol"; -import "./ILiquidityProviderFeature.sol"; +import "./interfaces/IFeature.sol"; +import "./interfaces/ILiquidityProviderFeature.sol"; contract LiquidityProviderFeature is diff --git a/contracts/zero-ex/contracts/src/features/MetaTransactionsFeature.sol b/contracts/zero-ex/contracts/src/features/MetaTransactionsFeature.sol index 937b535324..d57a8f88d7 100644 --- a/contracts/zero-ex/contracts/src/features/MetaTransactionsFeature.sol +++ b/contracts/zero-ex/contracts/src/features/MetaTransactionsFeature.sol @@ -30,11 +30,11 @@ import "../fixins/FixinTokenSpender.sol"; import "../fixins/FixinEIP712.sol"; import "../migrations/LibMigrate.sol"; import "../storage/LibMetaTransactionsStorage.sol"; -import "./IMetaTransactionsFeature.sol"; -import "./ITransformERC20Feature.sol"; +import "./interfaces/IFeature.sol"; +import "./interfaces/IMetaTransactionsFeature.sol"; +import "./interfaces/INativeOrdersFeature.sol"; +import "./interfaces/ITransformERC20Feature.sol"; import "./libs/LibSignature.sol"; -import "./IFeature.sol"; -import "./INativeOrdersFeature.sol"; /// @dev MetaTransactions feature. contract MetaTransactionsFeature is diff --git a/contracts/zero-ex/contracts/src/features/MultiplexFeature.sol b/contracts/zero-ex/contracts/src/features/MultiplexFeature.sol new file mode 100644 index 0000000000..e02df5f7ac --- /dev/null +++ b/contracts/zero-ex/contracts/src/features/MultiplexFeature.sol @@ -0,0 +1,805 @@ +// SPDX-License-Identifier: Apache-2.0 +/* + + Copyright 2021 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-utils/contracts/src/v06/LibMathV06.sol"; +import "@0x/contracts-utils/contracts/src/v06/LibSafeMathV06.sol"; +import "../external/ILiquidityProviderSandbox.sol"; +import "../fixins/FixinCommon.sol"; +import "../fixins/FixinEIP712.sol"; +import "../fixins/FixinTokenSpender.sol"; +import "../migrations/LibMigrate.sol"; +import "../transformers/LibERC20Transformer.sol"; +import "../vendor/ILiquidityProvider.sol"; +import "../vendor/IUniswapV2Pair.sol"; +import "./interfaces/IFeature.sol"; +import "./interfaces/IMultiplexFeature.sol"; +import "./interfaces/INativeOrdersFeature.sol"; +import "./interfaces/ITransformERC20Feature.sol"; +import "./libs/LibNativeOrder.sol"; + + +/// @dev This feature enables efficient batch and multi-hop trades +/// using different liquidity sources. +contract MultiplexFeature is + IFeature, + IMultiplexFeature, + FixinCommon, + FixinEIP712, + FixinTokenSpender +{ + using LibERC20Transformer for IERC20TokenV06; + using LibSafeMathV06 for uint128; + using LibSafeMathV06 for uint256; + + /// @dev Name of this feature. + string public constant override FEATURE_NAME = "MultiplexFeature"; + /// @dev Version of this feature. + uint256 public immutable override FEATURE_VERSION = _encodeVersion(1, 0, 0); + + /// @dev The WETH token contract. + IEtherTokenV06 private immutable weth; + /// @dev The sandbox contract address. + ILiquidityProviderSandbox public immutable sandbox; + // address of the UniswapV2Factory contract. + address private constant UNISWAP_FACTORY = 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f; + // address of the (Sushiswap) UniswapV2Factory contract. + address private constant SUSHISWAP_FACTORY = 0xC0AEe478e3658e2610c5F7A4A2E1777cE9e4f2Ac; + // Init code hash of the UniswapV2Pair contract. + uint256 private constant UNISWAP_PAIR_INIT_CODE_HASH = 0x96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f; + // Init code hash of the (Sushiswap) UniswapV2Pair contract. + uint256 private constant SUSHISWAP_PAIR_INIT_CODE_HASH = 0xe18a34eb0e04b04f7a0ac29a6e80748dca96319b42c54d679cb821dca90c6303; + + constructor( + address zeroExAddress, + IEtherTokenV06 weth_, + ILiquidityProviderSandbox sandbox_, + bytes32 greedyTokensBloomFilter + ) + public + FixinEIP712(zeroExAddress) + FixinTokenSpender(greedyTokensBloomFilter) + { + weth = weth_; + sandbox = sandbox_; + } + + /// @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.batchFill.selector); + _registerFeatureFunction(this.multiHopFill.selector); + return LibMigrate.MIGRATE_SUCCESS; + } + + /// @dev Executes a batch of fills selling `fillData.inputToken` + /// for `fillData.outputToken` in sequence. Refer to the + /// internal variant `_batchFill` for the allowed nested + /// operations. + /// @param fillData Encodes the input/output tokens, the sell + /// amount, and the nested operations for this batch fill. + /// @param minBuyAmount The minimum amount of `fillData.outputToken` + /// to buy. Reverts if this amount is not met. + /// @return outputTokenAmount The amount of the output token bought. + function batchFill( + BatchFillData memory fillData, + uint256 minBuyAmount + ) + public + payable + override + returns (uint256 outputTokenAmount) + { + // Cache the sender's balance of the output token. + outputTokenAmount = fillData.outputToken.getTokenBalanceOf(msg.sender); + // Cache the contract's ETH balance prior to this call. + uint256 ethBalanceBefore = address(this).balance.safeSub(msg.value); + + // Perform the batch fill. + _batchFill(fillData); + + // The `outputTokenAmount` returned by `_batchFill` may not + // be fully accurate (e.g. due to some janky token). + outputTokenAmount = fillData.outputToken.getTokenBalanceOf(msg.sender) + .safeSub(outputTokenAmount); + require( + outputTokenAmount >= minBuyAmount, + "MultiplexFeature::batchFill/UNDERBOUGHT" + ); + + uint256 ethBalanceAfter = address(this).balance; + require( + ethBalanceAfter >= ethBalanceBefore, + "MultiplexFeature::batchFill/OVERSPENT_ETH" + ); + // Refund ETH + if (ethBalanceAfter > ethBalanceBefore) { + _transferEth(msg.sender, ethBalanceAfter - ethBalanceBefore); + } + } + + /// @dev Executes a sequence of fills "hopping" through the + /// path of tokens given by `fillData.tokens`. Refer to the + /// internal variant `_multiHopFill` for the allowed nested + /// operations. + /// @param fillData Encodes the path of tokens, the sell amount, + /// and the nested operations for this multi-hop fill. + /// @param minBuyAmount The minimum amount of the output token + /// to buy. Reverts if this amount is not met. + /// @return outputTokenAmount The amount of the output token bought. + function multiHopFill( + MultiHopFillData memory fillData, + uint256 minBuyAmount + ) + public + payable + override + returns (uint256 outputTokenAmount) + { + IERC20TokenV06 outputToken = IERC20TokenV06(fillData.tokens[fillData.tokens.length - 1]); + // Cache the sender's balance of the output token. + outputTokenAmount = outputToken.getTokenBalanceOf(msg.sender); + // Cache the contract's ETH balance prior to this call. + uint256 ethBalanceBefore = address(this).balance.safeSub(msg.value); + + // Perform the multi-hop fill. Pass in `msg.value` as the maximum + // allowable amount of ETH for the wrapped calls to consume. + _multiHopFill(fillData, msg.value); + + // The `outputTokenAmount` returned by `_multiHopFill` may not + // be fully accurate (e.g. due to some janky token). + outputTokenAmount = outputToken.getTokenBalanceOf(msg.sender) + .safeSub(outputTokenAmount); + require( + outputTokenAmount >= minBuyAmount, + "MultiplexFeature::multiHopFill/UNDERBOUGHT" + ); + + uint256 ethBalanceAfter = address(this).balance; + require( + ethBalanceAfter >= ethBalanceBefore, + "MultiplexFeature::multiHopFill/OVERSPENT_ETH" + ); + // Refund ETH + if (ethBalanceAfter > ethBalanceBefore) { + _transferEth(msg.sender, ethBalanceAfter - ethBalanceBefore); + } + } + + // Similar to FQT. If `fillData.sellAmount` is set to `type(uint256).max`, + // this is effectively a batch fill. Otherwise it can be set to perform a + // market sell of some amount. Note that the `outputTokenAmount` returned + // by this function could theoretically be inaccurate if `msg.sender` has + // set a token allowance on an external contract that gets called during + // the execution of this function. + function _batchFill(BatchFillData memory fillData) + internal + returns (uint256 outputTokenAmount, uint256 remainingEth) + { + // Track the remaining ETH allocated to this call. + remainingEth = msg.value; + // Track the amount of input token sold. + uint256 soldAmount; + for (uint256 i = 0; i != fillData.calls.length; i++) { + // Check if we've hit our target. + if (soldAmount >= fillData.sellAmount) { break; } + WrappedBatchCall memory wrappedCall = fillData.calls[i]; + // Compute the fill amount. + uint256 inputTokenAmount = LibSafeMathV06.min256( + wrappedCall.sellAmount, + fillData.sellAmount.safeSub(soldAmount) + ); + if (wrappedCall.selector == INativeOrdersFeature._fillRfqOrder.selector) { + // Decode the RFQ order and signature. + ( + LibNativeOrder.RfqOrder memory order, + LibSignature.Signature memory signature + ) = abi.decode( + wrappedCall.data, + (LibNativeOrder.RfqOrder, LibSignature.Signature) + ); + if (order.expiry <= uint64(block.timestamp)) { + bytes32 orderHash = _getEIP712Hash( + LibNativeOrder.getRfqOrderStructHash(order) + ); + emit ExpiredRfqOrder( + orderHash, + order.maker, + order.expiry + ); + continue; + } + require( + order.takerToken == fillData.inputToken && + order.makerToken == fillData.outputToken, + "MultiplexFeature::_batchFill/RFQ_ORDER_INVALID_TOKENS" + ); + // Try filling the RFQ order. Swallows reverts. + try + INativeOrdersFeature(address(this))._fillRfqOrder + ( + order, + signature, + inputTokenAmount.safeDowncastToUint128(), + msg.sender + ) + returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount) + { + // Increment the sold and bought amounts. + soldAmount = soldAmount.safeAdd(takerTokenFilledAmount); + outputTokenAmount = outputTokenAmount.safeAdd(makerTokenFilledAmount); + } catch {} + } else if (wrappedCall.selector == this._sellToUniswap.selector) { + (address[] memory tokens, bool isSushi) = abi.decode( + wrappedCall.data, + (address[], bool) + ); + require( + tokens.length >= 2 && + tokens[0] == address(fillData.inputToken) && + tokens[tokens.length - 1] == address(fillData.outputToken), + "MultiplexFeature::_batchFill/UNISWAP_INVALID_TOKENS" + ); + // Perform the Uniswap/Sushiswap trade. + uint256 outputTokenAmount_ = _sellToUniswap( + tokens, + inputTokenAmount, + isSushi, + address(0), + msg.sender + ); + // Increment the sold and bought amounts. + soldAmount = soldAmount.safeAdd(inputTokenAmount); + outputTokenAmount = outputTokenAmount.safeAdd(outputTokenAmount_); + } else if (wrappedCall.selector == this._sellToLiquidityProvider.selector) { + (address provider, bytes memory auxiliaryData) = abi.decode( + wrappedCall.data, + (address, bytes) + ); + if (fillData.inputToken.isTokenETH()) { + inputTokenAmount = LibSafeMathV06.min256( + inputTokenAmount, + remainingEth + ); + // Transfer the input ETH to the provider. + _transferEth(payable(provider), inputTokenAmount); + // Count that ETH as spent. + remainingEth -= inputTokenAmount; + } else { + // Transfer input ERC20 tokens to the provider. + _transferERC20Tokens( + fillData.inputToken, + msg.sender, + provider, + inputTokenAmount + ); + } + // Perform the PLP trade. + uint256 outputTokenAmount_ = _sellToLiquidityProvider( + fillData.inputToken, + fillData.outputToken, + inputTokenAmount, + ILiquidityProvider(provider), + msg.sender, + auxiliaryData + ); + // Increment the sold and bought amounts. + soldAmount = soldAmount.safeAdd(inputTokenAmount); + outputTokenAmount = outputTokenAmount.safeAdd(outputTokenAmount_); + } else if (wrappedCall.selector == ITransformERC20Feature._transformERC20.selector) { + ITransformERC20Feature.TransformERC20Args memory args; + args.taker = msg.sender; + args.inputToken = fillData.inputToken; + args.outputToken = fillData.outputToken; + args.inputTokenAmount = inputTokenAmount; + args.minOutputTokenAmount = 0; + uint256 ethValue; + (args.transformations, ethValue) = abi.decode( + wrappedCall.data, + (ITransformERC20Feature.Transformation[], uint256) + ); + // Do not spend more than the remaining ETH. + ethValue = LibSafeMathV06.min256( + ethValue, + remainingEth + ); + if (ethValue > 0) { + require( + args.inputToken.isTokenETH(), + "MultiplexFeature::_batchFill/ETH_TRANSFORM_ONLY" + ); + } + try ITransformERC20Feature(address(this))._transformERC20 + {value: ethValue} + (args) + returns (uint256 outputTokenAmount_) + { + remainingEth -= ethValue; + soldAmount = soldAmount.safeAdd(inputTokenAmount); + outputTokenAmount = outputTokenAmount.safeAdd(outputTokenAmount_); + } catch {} + } else if (wrappedCall.selector == this._multiHopFill.selector) { + MultiHopFillData memory multiHopFillData; + uint256 ethValue; + ( + multiHopFillData.tokens, + multiHopFillData.calls, + ethValue + ) = abi.decode( + wrappedCall.data, + (address[], WrappedMultiHopCall[], uint256) + ); + multiHopFillData.sellAmount = inputTokenAmount; + // Do not spend more than the remaining ETH. + ethValue = LibSafeMathV06.min256( + ethValue, + remainingEth + ); + // Subtract the ethValue allocated to the nested multi-hop fill. + remainingEth -= ethValue; + (uint256 outputTokenAmount_, uint256 leftoverEth) = + _multiHopFill(multiHopFillData, ethValue); + // Increment the sold and bought amounts. + soldAmount = soldAmount.safeAdd(inputTokenAmount); + outputTokenAmount = outputTokenAmount.safeAdd(outputTokenAmount_); + // Add back any ETH that wasn't used by the nested multi-hop fill. + remainingEth += leftoverEth; + } else { + revert("MultiplexFeature::_batchFill/UNRECOGNIZED_SELECTOR"); + } + } + } + + // Internal variant of `multiHopFill`. This function can be nested within + // a `_batchFill`. + // This function executes a sequence of fills "hopping" through the + // path of tokens given by `fillData.tokens`. The nested operations that + // can be used as "hops" are: + // - WETH.deposit (wraps ETH) + // - WETH.withdraw (unwraps WETH) + // - _sellToUniswap (executes a Uniswap/Sushiswap swap) + // - _sellToLiquidityProvider (executes a PLP swap) + // - _transformERC20 (executes arbitrary ERC20 Transformations) + // This function optimizes the number of ERC20 transfers performed + // by having each hop transfer its output tokens directly to the + // target address of the next hop. Note that the `outputTokenAmount` returned + // by this function could theoretically be inaccurate if `msg.sender` has + // set a token allowance on an external contract that gets called during + // the execution of this function. + function _multiHopFill(MultiHopFillData memory fillData, uint256 totalEth) + public + returns (uint256 outputTokenAmount, uint256 remainingEth) + { + // There should be one call/hop between every two tokens + // in the path. + // tokens[0]––calls[0]––>tokens[1]––...––calls[n-1]––>tokens[n] + require( + fillData.tokens.length == fillData.calls.length + 1, + "MultiplexFeature::_multiHopFill/MISMATCHED_ARRAY_LENGTHS" + ); + // Track the remaining ETH allocated to this call. + remainingEth = totalEth; + // This variable is used as the input and output amounts of + // each hop. After the final hop, this will contain the output + // amount of the multi-hop fill. + outputTokenAmount = fillData.sellAmount; + // This variable is used to cache the address to target in the + // next hop. See `_computeHopRecipient` for details. + address nextTarget; + for (uint256 i = 0; i != fillData.calls.length; i++) { + WrappedMultiHopCall memory wrappedCall = fillData.calls[i]; + if (wrappedCall.selector == this._sellToUniswap.selector) { + // If the next hop supports a "transfer then execute" pattern, + // the recipient will not be `msg.sender`. See `_computeHopRecipient` + // for details. + address recipient = _computeHopRecipient(fillData.calls, i); + (address[] memory tokens, bool isSushi) = abi.decode( + wrappedCall.data, + (address[], bool) + ); + // Perform the Uniswap/Sushiswap trade. + outputTokenAmount = _sellToUniswap( + tokens, + outputTokenAmount, + isSushi, + nextTarget, + recipient + ); + // If the recipient was not `msg.sender`, it must be the target + // contract for the next hop. + nextTarget = recipient == msg.sender ? address(0) : recipient; + } else if (wrappedCall.selector == this._sellToLiquidityProvider.selector) { + // If the next hop supports a "transfer then execute" pattern, + // the recipient will not be `msg.sender`. See `_computeHopRecipient` + // for details. + address recipient = _computeHopRecipient(fillData.calls, i); + // If `nextTarget` was not set in the previous hop, then we + // need to send in the input ETH/tokens to the liquidity provider + // contract before executing the trade. + if (nextTarget == address(0)) { + (address provider, bytes memory auxiliaryData) = abi.decode( + wrappedCall.data, + (address, bytes) + ); + // Transfer input ETH or ERC20 tokens to the liquidity + // provider contract. + if (IERC20TokenV06(fillData.tokens[i]).isTokenETH()) { + outputTokenAmount = LibSafeMathV06.min256( + outputTokenAmount, + remainingEth + ); + _transferEth(payable(provider), outputTokenAmount); + remainingEth -= outputTokenAmount; + } else { + _transferERC20Tokens( + IERC20TokenV06(fillData.tokens[i]), + msg.sender, + provider, + outputTokenAmount + ); + } + outputTokenAmount = _sellToLiquidityProvider( + IERC20TokenV06(fillData.tokens[i]), + IERC20TokenV06(fillData.tokens[i + 1]), + outputTokenAmount, + ILiquidityProvider(provider), + recipient, + auxiliaryData + ); + } else { + (, bytes memory auxiliaryData) = abi.decode( + wrappedCall.data, + (address, bytes) + ); + // Tokens and ETH have already been transferred to + // the liquidity provider contract in the previous hop. + outputTokenAmount = _sellToLiquidityProvider( + IERC20TokenV06(fillData.tokens[i]), + IERC20TokenV06(fillData.tokens[i + 1]), + outputTokenAmount, + ILiquidityProvider(nextTarget), + recipient, + auxiliaryData + ); + } + // If the recipient was not `msg.sender`, it must be the target + // contract for the next hop. + nextTarget = recipient == msg.sender ? address(0) : recipient; + } else if (wrappedCall.selector == ITransformERC20Feature._transformERC20.selector) { + ITransformERC20Feature.TransformERC20Args memory args; + args.inputToken = IERC20TokenV06(fillData.tokens[i]); + args.outputToken = IERC20TokenV06(fillData.tokens[i + 1]); + args.minOutputTokenAmount = 0; + args.taker = payable(_computeHopRecipient(fillData.calls, i)); + if (nextTarget != address(0)) { + // If `nextTarget` was set in the previous hop, then the input + // token was already sent to the FlashWallet. Setting + // `inputTokenAmount` to 0 indicates that no tokens need to + // be pulled into the FlashWallet before executing the + // transformations. + args.inputTokenAmount = 0; + } else if ( + args.taker != msg.sender && + !args.inputToken.isTokenETH() + ) { + address flashWallet = address( + ITransformERC20Feature(address(this)).getTransformWallet() + ); + // The input token has _not_ already been sent to the + // FlashWallet. We also want PayTakerTransformer to + // send the output token to some address other than + // msg.sender, so we must transfer the input token + // to the FlashWallet here. + _transferERC20Tokens( + args.inputToken, + msg.sender, + flashWallet, + outputTokenAmount + ); + args.inputTokenAmount = 0; + } else { + // Otherwise, either: + // (1) args.taker == msg.sender, in which case + // `_transformERC20` will pull the input token + // into the FlashWallet, or + // (2) args.inputToken == ETH_TOKEN_ADDRESS, in which + // case ETH is attached to the call and no token + // transfer occurs. + args.inputTokenAmount = outputTokenAmount; + } + uint256 ethValue; + (args.transformations, ethValue) = abi.decode( + wrappedCall.data, + (ITransformERC20Feature.Transformation[], uint256) + ); + // Do not spend more than the remaining ETH. + ethValue = LibSafeMathV06.min256(ethValue, remainingEth); + if (ethValue > 0) { + require( + args.inputToken.isTokenETH(), + "MultiplexFeature::_multiHopFill/ETH_TRANSFORM_ONLY" + ); + } + // Call `_transformERC20`. + outputTokenAmount = ITransformERC20Feature(address(this)) + ._transformERC20{value: ethValue}(args); + // Decrement the remaining ETH. + remainingEth -= ethValue; + // If the recipient was not `msg.sender`, it must be the target + // contract for the next hop. + nextTarget = args.taker == msg.sender ? address(0) : args.taker; + } else if (wrappedCall.selector == IEtherTokenV06.deposit.selector) { + require( + i == 0, + "MultiplexFeature::_multiHopFill/DEPOSIT_FIRST_HOP_ONLY" + ); + uint256 ethValue = LibSafeMathV06.min256(outputTokenAmount, remainingEth); + // Wrap ETH. + weth.deposit{value: ethValue}(); + nextTarget = _computeHopRecipient(fillData.calls, i); + weth.transfer(nextTarget, ethValue); + remainingEth -= ethValue; + } else if (wrappedCall.selector == IEtherTokenV06.withdraw.selector) { + require( + i == fillData.calls.length - 1, + "MultiplexFeature::_multiHopFill/WITHDRAW_LAST_HOP_ONLY" + ); + // Unwrap WETH and send to `msg.sender`. + weth.withdraw(outputTokenAmount); + _transferEth(msg.sender, outputTokenAmount); + nextTarget = address(0); + } else { + revert("MultiplexFeature::_multiHopFill/UNRECOGNIZED_SELECTOR"); + } + } + } + + // Similar to the UniswapFeature, but with a couple of differences: + // - Does not perform the transfer in if `pairAddress` is given, + // which indicates that the transfer in was already performed + // in the previous hop of a multi-hop fill. + // - Does not include a minBuyAmount check (which is performed in + // either `batchFill` or `multiHopFill`). + // - Takes a `recipient` address parameter, so the output of the + // final `swap` call can be sent to an address other than `msg.sender`. + function _sellToUniswap( + address[] memory tokens, + uint256 sellAmount, + bool isSushi, + address pairAddress, + address recipient + ) + public + returns (uint256 outputTokenAmount) + { + require(tokens.length > 1, "MultiplexFeature::_sellToUniswap/InvalidTokensLength"); + + if (pairAddress == address(0)) { + pairAddress = _computeUniswapPairAddress(tokens[0], tokens[1], isSushi); + _transferERC20Tokens( + IERC20TokenV06(tokens[0]), + msg.sender, + pairAddress, + sellAmount + ); + } + + for (uint256 i = 0; i < tokens.length - 1; i++) { + (address inputToken, address outputToken) = (tokens[i], tokens[i + 1]); + outputTokenAmount = _computeUniswapOutputAmount( + pairAddress, + inputToken, + outputToken, + sellAmount + ); + (uint256 amount0Out, uint256 amount1Out) = inputToken < outputToken + ? (uint256(0), outputTokenAmount) + : (outputTokenAmount, uint256(0)); + address to = i < tokens.length - 2 + ? _computeUniswapPairAddress(outputToken, tokens[i + 2], isSushi) + : recipient; + IUniswapV2Pair(pairAddress).swap( + amount0Out, + amount1Out, + to, + new bytes(0) + ); + pairAddress = to; + sellAmount = outputTokenAmount; + } + } + + // Same as the LiquidityProviderFeature, but without the transfer in + // (which is potentially done in the previous hop of a multi-hop fill) + // and without the minBuyAmount check (which is performed at the top, i.e. + // in either `batchFill` or `multiHopFill`). + function _sellToLiquidityProvider( + IERC20TokenV06 inputToken, + IERC20TokenV06 outputToken, + uint256 inputTokenAmount, + ILiquidityProvider provider, + address recipient, + bytes memory auxiliaryData + ) + public + returns (uint256 outputTokenAmount) + { + uint256 balanceBefore = IERC20TokenV06(outputToken).getTokenBalanceOf(recipient); + if (IERC20TokenV06(inputToken).isTokenETH()) { + sandbox.executeSellEthForToken( + provider, + outputToken, + recipient, + 0, + auxiliaryData + ); + } else if (IERC20TokenV06(outputToken).isTokenETH()) { + sandbox.executeSellTokenForEth( + provider, + inputToken, + recipient, + 0, + auxiliaryData + ); + } else { + sandbox.executeSellTokenForToken( + provider, + inputToken, + outputToken, + recipient, + 0, + auxiliaryData + ); + } + outputTokenAmount = IERC20TokenV06(outputToken).getTokenBalanceOf(recipient) + .safeSub(balanceBefore); + emit LiquidityProviderSwap( + address(inputToken), + address(outputToken), + inputTokenAmount, + outputTokenAmount, + address(provider), + recipient + ); + return outputTokenAmount; + } + + function _transferEth(address payable recipient, uint256 amount) + private + { + (bool success,) = recipient.call{value: amount}(""); + require(success, "MultiplexFeature::_transferEth/TRANSFER_FALIED"); + } + + // Some liquidity sources (e.g. Uniswap, Sushiswap, and PLP) can be passed + // a `recipient` parameter so the boguht tokens are transferred to the + // `recipient` address rather than `msg.sender`. + // Some liquidity sources (also Uniswap, Sushiswap, and PLP incidentally) + // support a "transfer then execute" pattern, where the token being sold + // can be transferred into the contract before calling a swap function to + // execute the trade. + // If the current hop in a multi-hop fill satisfies the first condition, + // and the next hop satisfies the second condition, the tokens bought + // in the current hop can be directly sent to the target contract of + // the next hop to save a transfer. + function _computeHopRecipient( + WrappedMultiHopCall[] memory calls, + uint256 i + ) + private + view + returns (address recipient) + { + recipient = msg.sender; + if (i < calls.length - 1) { + WrappedMultiHopCall memory nextCall = calls[i + 1]; + if (nextCall.selector == this._sellToUniswap.selector) { + (address[] memory tokens, bool isSushi) = abi.decode( + nextCall.data, + (address[], bool) + ); + recipient = _computeUniswapPairAddress(tokens[0], tokens[1], isSushi); + } else if (nextCall.selector == this._sellToLiquidityProvider.selector) { + (recipient,) = abi.decode( + nextCall.data, + (address, bytes) + ); + } else if (nextCall.selector == IEtherTokenV06.withdraw.selector) { + recipient = address(this); + } else if (nextCall.selector == ITransformERC20Feature._transformERC20.selector) { + recipient = address( + ITransformERC20Feature(address(this)).getTransformWallet() + ); + } + } + require( + recipient != address(0), + "MultiplexFeature::_computeHopRecipient/RECIPIENT_IS_NULL" + ); + } + + // Computes the the amount of output token that would be bought + // from Uniswap/Sushiswap given the input amount. + function _computeUniswapOutputAmount( + address pairAddress, + address inputToken, + address outputToken, + uint256 inputAmount + ) + private + view + returns (uint256 outputAmount) + { + require( + inputAmount > 0, + "MultiplexFeature::_computeUniswapOutputAmount/INSUFFICIENT_INPUT_AMOUNT" + ); + (uint256 reserve0, uint256 reserve1,) = IUniswapV2Pair(pairAddress).getReserves(); + require( + reserve0 > 0 && reserve1 > 0, + 'MultiplexFeature::_computeUniswapOutputAmount/INSUFFICIENT_LIQUIDITY' + ); + (uint256 inputReserve, uint256 outputReserve) = inputToken < outputToken + ? (reserve0, reserve1) + : (reserve1, reserve0); + uint256 inputAmountWithFee = inputAmount.safeMul(997); + uint256 numerator = inputAmountWithFee.safeMul(outputReserve); + uint256 denominator = inputReserve.safeMul(1000).safeAdd(inputAmountWithFee); + return numerator / denominator; + } + + // Computes the Uniswap/Sushiswap pair contract address for the + // given tokens. + function _computeUniswapPairAddress( + address tokenA, + address tokenB, + bool isSushi + ) + private + pure + returns (address pairAddress) + { + (address token0, address token1) = tokenA < tokenB + ? (tokenA, tokenB) + : (tokenB, tokenA); + if (isSushi) { + return address(uint256(keccak256(abi.encodePacked( + hex'ff', + SUSHISWAP_FACTORY, + keccak256(abi.encodePacked(token0, token1)), + SUSHISWAP_PAIR_INIT_CODE_HASH + )))); + } else { + return address(uint256(keccak256(abi.encodePacked( + hex'ff', + UNISWAP_FACTORY, + keccak256(abi.encodePacked(token0, token1)), + UNISWAP_PAIR_INIT_CODE_HASH + )))); + } + } +} diff --git a/contracts/zero-ex/contracts/src/features/NativeOrdersFeature.sol b/contracts/zero-ex/contracts/src/features/NativeOrdersFeature.sol index d4ad86bf47..fd5ddfc715 100644 --- a/contracts/zero-ex/contracts/src/features/NativeOrdersFeature.sol +++ b/contracts/zero-ex/contracts/src/features/NativeOrdersFeature.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 /* - Copyright 2020 ZeroEx Intl. + Copyright 2021 ZeroEx Intl. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -20,100 +20,21 @@ 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 "../fixins/FixinTokenSpender.sol"; -import "../errors/LibNativeOrdersRichErrors.sol"; import "../migrations/LibMigrate.sol"; -import "../storage/LibNativeOrdersStorage.sol"; -import "../vendor/v3/IStaking.sol"; -import "./libs/LibSignature.sol"; -import "./libs/LibNativeOrder.sol"; -import "./INativeOrdersFeature.sol"; -import "./IFeature.sol"; +import "./interfaces/IFeature.sol"; +import "./interfaces/INativeOrdersFeature.sol"; +import "./native_orders/NativeOrdersSettlement.sol"; -/// @dev Feature for interacting with limit orders. +/// @dev Feature for interacting with limit and RFQ orders. contract NativeOrdersFeature is IFeature, - INativeOrdersFeature, - FixinCommon, - FixinProtocolFees, - FixinEIP712, - FixinTokenSpender + NativeOrdersSettlement { - using LibSafeMathV06 for uint256; - using LibSafeMathV06 for uint128; - using LibRichErrorsV06 for bytes; - using LibERC20TokenV06 for IERC20TokenV06; - - /// @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 Params for `_getActualFillableTakerTokenAmount()`. - struct GetActualFillableTakerTokenAmountParams { - address maker; - IERC20TokenV06 makerToken; - uint128 orderMakerAmount; - uint128 orderTakerAmount; - LibNativeOrder.OrderInfo orderInfo; - } - /// @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, 1); - /// @dev Highest bit of a uint256, used to flag cancelled orders. - uint256 private constant HIGH_BIT = 1 << 255; constructor( address zeroExAddress, @@ -124,9 +45,14 @@ contract NativeOrdersFeature is bytes32 greedyTokensBloomFilter ) public - FixinEIP712(zeroExAddress) - FixinProtocolFees(weth, staking, feeCollectorController, protocolFeeMultiplier) - FixinTokenSpender(greedyTokensBloomFilter) + NativeOrdersSettlement( + zeroExAddress, + weth, + staking, + feeCollectorController, + protocolFeeMultiplier, + greedyTokensBloomFilter + ) { // solhint-disable no-empty-blocks } @@ -165,1079 +91,4 @@ contract NativeOrdersFeature is _registerFeatureFunction(this.batchGetRfqOrderRelevantStates.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 - returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount) - { - FillNativeOrderResults memory results = - _fillRfqOrderPrivate( - order, - signature, - takerTokenFillAmount, - msg.sender - ); - (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 - 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(); - } - 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 - virtual - 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 - virtual - override - onlySelf - returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount) - { - FillNativeOrderResults memory results = - _fillRfqOrderPrivate( - order, - signature, - takerTokenFillAmount, - taker - ); - (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 PairCancelledLimitOrders( - 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, - "NativeOrdersFeature/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 PairCancelledRfqOrders( - msg.sender, - address(makerToken), - address(takerToken), - minValidSalt - ); - } - - /// @dev Mark what tx.origin addresses are allowed to fill an order that - /// specifies the message sender as its txOrigin. - /// @param origins An array of origin addresses to update. - /// @param allowed True to register, false to unregister. - function registerAllowedRfqOrigins( - address[] memory origins, - bool allowed - ) - external - override - { - require(msg.sender == tx.origin, - "NativeOrdersFeature/NO_CONTRACT_ORIGINS"); - - LibNativeOrdersStorage.Storage storage stor = - LibNativeOrdersStorage.getStorage(); - - for (uint256 i = 0; i < origins.length; i++) { - stor.originRegistry[msg.sender][origins[i]] = allowed; - } - - emit RfqOrderOriginsAllowed(msg.sender, origins, allowed); - } - - /// @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, - "NativeOrdersFeature/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 - ); - - // Check for missing txOrigin. - if (order.txOrigin == address(0)) { - orderInfo.status = LibNativeOrder.OrderStatus.INVALID; - } - } - - /// @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 order info, fillable amount, and signature validity for a limit order. - /// Fillable amount is determined using balances and allowances of the maker. - /// @param order The limit order. - /// @param signature The order signature. - /// @return orderInfo Info about the order. - /// @return actualFillableTakerTokenAmount How much of the order is fillable - /// based on maker funds, in taker tokens. - /// @return isSignatureValid Whether the signature is valid. - function getLimitOrderRelevantState( - LibNativeOrder.LimitOrder memory order, - LibSignature.Signature calldata signature - ) - public - override - view - returns ( - LibNativeOrder.OrderInfo memory orderInfo, - uint128 actualFillableTakerTokenAmount, - bool isSignatureValid - ) - { - orderInfo = getLimitOrderInfo(order); - actualFillableTakerTokenAmount = _getActualFillableTakerTokenAmount( - GetActualFillableTakerTokenAmountParams({ - maker: order.maker, - makerToken: order.makerToken, - orderMakerAmount: order.makerAmount, - orderTakerAmount: order.takerAmount, - orderInfo: orderInfo - }) - ); - isSignatureValid = order.maker == - LibSignature.getSignerOfHash(orderInfo.orderHash, signature); - } - - /// @dev Get order info, fillable amount, and signature validity for an RFQ order. - /// Fillable amount is determined using balances and allowances of the maker. - /// @param order The RFQ order. - /// @param signature The order signature. - /// @return orderInfo Info about the order. - /// @return actualFillableTakerTokenAmount How much of the order is fillable - /// based on maker funds, in taker tokens. - /// @return isSignatureValid Whether the signature is valid. - function getRfqOrderRelevantState( - LibNativeOrder.RfqOrder memory order, - LibSignature.Signature memory signature - ) - public - override - view - returns ( - LibNativeOrder.OrderInfo memory orderInfo, - uint128 actualFillableTakerTokenAmount, - bool isSignatureValid - ) - { - orderInfo = getRfqOrderInfo(order); - actualFillableTakerTokenAmount = _getActualFillableTakerTokenAmount( - GetActualFillableTakerTokenAmountParams({ - maker: order.maker, - makerToken: order.makerToken, - orderMakerAmount: order.makerAmount, - orderTakerAmount: order.takerAmount, - orderInfo: orderInfo - }) - ); - isSignatureValid = order.maker == - LibSignature.getSignerOfHash(orderInfo.orderHash, signature); - } - - /// @dev Batch version of `getLimitOrderRelevantState()`, without reverting. - /// Orders that would normally cause `getLimitOrderRelevantState()` - /// to revert will have empty results. - /// @param orders The limit orders. - /// @param signatures The order signatures. - /// @return orderInfos Info about the orders. - /// @return actualFillableTakerTokenAmounts How much of each order is fillable - /// based on maker funds, in taker tokens. - /// @return isSignatureValids Whether each signature is valid for the order. - function batchGetLimitOrderRelevantStates( - LibNativeOrder.LimitOrder[] calldata orders, - LibSignature.Signature[] calldata signatures - ) - external - override - view - returns ( - LibNativeOrder.OrderInfo[] memory orderInfos, - uint128[] memory actualFillableTakerTokenAmounts, - bool[] memory isSignatureValids - ) - { - require( - orders.length == signatures.length, - "NativeOrdersFeature/MISMATCHED_ARRAY_LENGTHS" - ); - orderInfos = new LibNativeOrder.OrderInfo[](orders.length); - actualFillableTakerTokenAmounts = new uint128[](orders.length); - isSignatureValids = new bool[](orders.length); - for (uint256 i = 0; i < orders.length; ++i) { - try - this.getLimitOrderRelevantState(orders[i], signatures[i]) - returns ( - LibNativeOrder.OrderInfo memory orderInfo, - uint128 actualFillableTakerTokenAmount, - bool isSignatureValid - ) - { - orderInfos[i] = orderInfo; - actualFillableTakerTokenAmounts[i] = actualFillableTakerTokenAmount; - isSignatureValids[i] = isSignatureValid; - } - catch {} - } - } - - /// @dev Batch version of `getRfqOrderRelevantState()`, without reverting. - /// Orders that would normally cause `getRfqOrderRelevantState()` - /// to revert will have empty results. - /// @param orders The RFQ orders. - /// @param signatures The order signatures. - /// @return orderInfos Info about the orders. - /// @return actualFillableTakerTokenAmounts How much of each order is fillable - /// based on maker funds, in taker tokens. - /// @return isSignatureValids Whether each signature is valid for the order. - function batchGetRfqOrderRelevantStates( - LibNativeOrder.RfqOrder[] calldata orders, - LibSignature.Signature[] calldata signatures - ) - external - override - view - returns ( - LibNativeOrder.OrderInfo[] memory orderInfos, - uint128[] memory actualFillableTakerTokenAmounts, - bool[] memory isSignatureValids - ) - { - require( - orders.length == signatures.length, - "NativeOrdersFeature/MISMATCHED_ARRAY_LENGTHS" - ); - orderInfos = new LibNativeOrder.OrderInfo[](orders.length); - actualFillableTakerTokenAmounts = new uint128[](orders.length); - isSignatureValids = new bool[](orders.length); - for (uint256 i = 0; i < orders.length; ++i) { - try - this.getRfqOrderRelevantState(orders[i], signatures[i]) - returns ( - LibNativeOrder.OrderInfo memory orderInfo, - uint128 actualFillableTakerTokenAmount, - bool isSignatureValid - ) - { - orderInfos[i] = orderInfo; - actualFillableTakerTokenAmounts[i] = actualFillableTakerTokenAmount; - isSignatureValids[i] = isSignatureValid; - } - catch {} - } - } - - /// @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 Calculate the actual fillable taker token amount of an order - /// based on maker allowance and balances. - function _getActualFillableTakerTokenAmount( - GetActualFillableTakerTokenAmountParams memory params - ) - private - view - returns (uint128 actualFillableTakerTokenAmount) - { - if (params.orderMakerAmount == 0 || params.orderTakerAmount == 0) { - // Empty order. - return 0; - } - if (params.orderInfo.status != LibNativeOrder.OrderStatus.FILLABLE) { - // Not fillable. - return 0; - } - - // Get the fillable maker amount based on the order quantities and - // previously filled amount - uint256 fillableMakerTokenAmount = LibMathV06.getPartialAmountFloor( - uint256( - params.orderTakerAmount - - params.orderInfo.takerTokenFilledAmount - ), - uint256(params.orderTakerAmount), - uint256(params.orderMakerAmount) - ); - // Clamp it to the amount of maker tokens we can spend on behalf of the - // maker. - fillableMakerTokenAmount = LibSafeMathV06.min256( - fillableMakerTokenAmount, - _getSpendableERC20BalanceOf(params.makerToken, params.maker) - ); - // Convert to taker token amount. - return LibMathV06.getPartialAmountCeil( - fillableMakerTokenAmount, - uint256(params.orderMakerAmount), - uint256(params.orderTakerAmount) - ).safeDowncastToUint128(); - } - - /// @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, maker); - } - - /// @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 - )); - _transferERC20Tokens( - 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(); - } - - { - LibNativeOrdersStorage.Storage storage stor = - LibNativeOrdersStorage.getStorage(); - - // Must be fillable by the tx.origin. - if (order.txOrigin != tx.origin && !stor.originRegistry[order.txOrigin][tx.origin]) { - LibNativeOrdersRichErrors.OrderNotFillableByOriginError( - orderInfo.orderHash, - tx.origin, - order.txOrigin - ).rrevert(); - } - } - - // Must be fillable by the taker. - if (order.taker != address(0) && order.taker != taker) { - LibNativeOrdersRichErrors.OrderNotFillableByTakerError( - orderInfo.orderHash, - taker, - order.taker - ).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(); - } - } - - // 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, - 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. - _transferERC20Tokens( - settleInfo.takerToken, - settleInfo.taker, - settleInfo.maker, - takerTokenFilledAmount - ); - - // Transfer maker -> taker. - _transferERC20Tokens( - 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/OwnableFeature.sol b/contracts/zero-ex/contracts/src/features/OwnableFeature.sol index 01f9cb4f0e..563dd15596 100644 --- a/contracts/zero-ex/contracts/src/features/OwnableFeature.sol +++ b/contracts/zero-ex/contracts/src/features/OwnableFeature.sol @@ -26,8 +26,8 @@ import "../errors/LibOwnableRichErrors.sol"; import "../storage/LibOwnableStorage.sol"; import "../migrations/LibBootstrap.sol"; import "../migrations/LibMigrate.sol"; -import "./IFeature.sol"; -import "./IOwnableFeature.sol"; +import "./interfaces/IFeature.sol"; +import "./interfaces/IOwnableFeature.sol"; import "./SimpleFunctionRegistryFeature.sol"; diff --git a/contracts/zero-ex/contracts/src/features/SimpleFunctionRegistryFeature.sol b/contracts/zero-ex/contracts/src/features/SimpleFunctionRegistryFeature.sol index 7180ca6775..fa83326055 100644 --- a/contracts/zero-ex/contracts/src/features/SimpleFunctionRegistryFeature.sol +++ b/contracts/zero-ex/contracts/src/features/SimpleFunctionRegistryFeature.sol @@ -26,8 +26,8 @@ import "../storage/LibProxyStorage.sol"; import "../storage/LibSimpleFunctionRegistryStorage.sol"; import "../errors/LibSimpleFunctionRegistryRichErrors.sol"; import "../migrations/LibBootstrap.sol"; -import "./IFeature.sol"; -import "./ISimpleFunctionRegistryFeature.sol"; +import "./interfaces/IFeature.sol"; +import "./interfaces/ISimpleFunctionRegistryFeature.sol"; /// @dev Basic registry management features. diff --git a/contracts/zero-ex/contracts/src/features/TokenSpenderFeature.sol b/contracts/zero-ex/contracts/src/features/TokenSpenderFeature.sol index 9e9a53b75f..b2f58e70b4 100644 --- a/contracts/zero-ex/contracts/src/features/TokenSpenderFeature.sol +++ b/contracts/zero-ex/contracts/src/features/TokenSpenderFeature.sol @@ -29,8 +29,8 @@ import "../fixins/FixinCommon.sol"; import "../migrations/LibMigrate.sol"; import "../external/IAllowanceTarget.sol"; import "../storage/LibTokenSpenderStorage.sol"; -import "./ITokenSpenderFeature.sol"; -import "./IFeature.sol"; +import "./interfaces/IFeature.sol"; +import "./interfaces/ITokenSpenderFeature.sol"; /// @dev Feature that allows spending token allowances. diff --git a/contracts/zero-ex/contracts/src/features/TransformERC20Feature.sol b/contracts/zero-ex/contracts/src/features/TransformERC20Feature.sol index 05eb3c3f7d..5ed06afbe5 100644 --- a/contracts/zero-ex/contracts/src/features/TransformERC20Feature.sol +++ b/contracts/zero-ex/contracts/src/features/TransformERC20Feature.sol @@ -33,8 +33,8 @@ import "../external/FlashWallet.sol"; import "../storage/LibTransformERC20Storage.sol"; import "../transformers/IERC20Transformer.sol"; import "../transformers/LibERC20Transformer.sol"; -import "./ITransformERC20Feature.sol"; -import "./IFeature.sol"; +import "./interfaces/IFeature.sol"; +import "./interfaces/ITransformERC20Feature.sol"; /// @dev Feature to composably transform between ERC20 tokens. @@ -313,7 +313,7 @@ contract TransformERC20Feature is to.transfer(msg.value); } // Transfer input tokens. - if (!LibERC20Transformer.isTokenETH(inputToken)) { + if (!LibERC20Transformer.isTokenETH(inputToken) && amount != 0) { // Token is not ETH, so pull ERC20 tokens. _transferERC20Tokens( inputToken, diff --git a/contracts/zero-ex/contracts/src/features/UniswapFeature.sol b/contracts/zero-ex/contracts/src/features/UniswapFeature.sol index 65c17b721e..b44ce54974 100644 --- a/contracts/zero-ex/contracts/src/features/UniswapFeature.sol +++ b/contracts/zero-ex/contracts/src/features/UniswapFeature.sol @@ -25,8 +25,8 @@ import "@0x/contracts-erc20/contracts/src/v06/IEtherTokenV06.sol"; import "../migrations/LibMigrate.sol"; import "../external/IAllowanceTarget.sol"; import "../fixins/FixinCommon.sol"; -import "./IFeature.sol"; -import "./IUniswapFeature.sol"; +import "./interfaces/IFeature.sol"; +import "./interfaces/IUniswapFeature.sol"; /// @dev VIP uniswap fill functions. @@ -380,7 +380,7 @@ contract UniswapFeature is // will eat all our gas. if isTokenPossiblyGreedy(token) { // Check if we have enough direct allowance by calling - // `token.allowance()`` + // `token.allowance()` mstore(0xB00, ALLOWANCE_CALL_SELECTOR_32) mstore(0xB04, caller()) mstore(0xB24, address()) diff --git a/contracts/zero-ex/contracts/src/features/interfaces/IBatchFillNativeOrdersFeature.sol b/contracts/zero-ex/contracts/src/features/interfaces/IBatchFillNativeOrdersFeature.sol new file mode 100644 index 0000000000..7312bd5f10 --- /dev/null +++ b/contracts/zero-ex/contracts/src/features/interfaces/IBatchFillNativeOrdersFeature.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: Apache-2.0 +/* + + Copyright 2021 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 "../libs/LibNativeOrder.sol"; +import "../libs/LibSignature.sol"; + + +/// @dev Feature for batch/market filling limit and RFQ orders. +interface IBatchFillNativeOrdersFeature { + + /// @dev Fills multiple limit orders. + /// @param orders Array of limit orders. + /// @param signatures Array of signatures corresponding to each order. + /// @param takerTokenFillAmounts Array of desired amounts to fill each order. + /// @param revertIfIncomplete If true, reverts if this function fails to + /// fill the full fill amount for any individual order. + /// @return takerTokenFilledAmounts Array of amounts filled, in taker token. + /// @return makerTokenFilledAmounts Array of amounts filled, in maker token. + function batchFillLimitOrders( + LibNativeOrder.LimitOrder[] calldata orders, + LibSignature.Signature[] calldata signatures, + uint128[] calldata takerTokenFillAmounts, + bool revertIfIncomplete + ) + external + payable + returns ( + uint128[] memory takerTokenFilledAmounts, + uint128[] memory makerTokenFilledAmounts + ); + + /// @dev Fills multiple RFQ orders. + /// @param orders Array of RFQ orders. + /// @param signatures Array of signatures corresponding to each order. + /// @param takerTokenFillAmounts Array of desired amounts to fill each order. + /// @param revertIfIncomplete If true, reverts if this function fails to + /// fill the full fill amount for any individual order. + /// @return takerTokenFilledAmounts Array of amounts filled, in taker token. + /// @return makerTokenFilledAmounts Array of amounts filled, in maker token. + function batchFillRfqOrders( + LibNativeOrder.RfqOrder[] calldata orders, + LibSignature.Signature[] calldata signatures, + uint128[] calldata takerTokenFillAmounts, + bool revertIfIncomplete + ) + external + returns ( + uint128[] memory takerTokenFilledAmounts, + uint128[] memory makerTokenFilledAmounts + ); +} diff --git a/contracts/zero-ex/contracts/src/features/IBootstrapFeature.sol b/contracts/zero-ex/contracts/src/features/interfaces/IBootstrapFeature.sol similarity index 100% rename from contracts/zero-ex/contracts/src/features/IBootstrapFeature.sol rename to contracts/zero-ex/contracts/src/features/interfaces/IBootstrapFeature.sol diff --git a/contracts/zero-ex/contracts/src/features/IFeature.sol b/contracts/zero-ex/contracts/src/features/interfaces/IFeature.sol similarity index 100% rename from contracts/zero-ex/contracts/src/features/IFeature.sol rename to contracts/zero-ex/contracts/src/features/interfaces/IFeature.sol diff --git a/contracts/zero-ex/contracts/src/features/ILiquidityProviderFeature.sol b/contracts/zero-ex/contracts/src/features/interfaces/ILiquidityProviderFeature.sol similarity index 98% rename from contracts/zero-ex/contracts/src/features/ILiquidityProviderFeature.sol rename to contracts/zero-ex/contracts/src/features/interfaces/ILiquidityProviderFeature.sol index 170b5947af..cf2bc12bea 100644 --- a/contracts/zero-ex/contracts/src/features/ILiquidityProviderFeature.sol +++ b/contracts/zero-ex/contracts/src/features/interfaces/ILiquidityProviderFeature.sol @@ -21,7 +21,7 @@ pragma solidity ^0.6.5; pragma experimental ABIEncoderV2; import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol"; -import "../vendor/ILiquidityProvider.sol"; +import "../../vendor/ILiquidityProvider.sol"; /// @dev Feature to swap directly with an on-chain liquidity provider. diff --git a/contracts/zero-ex/contracts/src/features/IMetaTransactionsFeature.sol b/contracts/zero-ex/contracts/src/features/interfaces/IMetaTransactionsFeature.sol similarity index 99% rename from contracts/zero-ex/contracts/src/features/IMetaTransactionsFeature.sol rename to contracts/zero-ex/contracts/src/features/interfaces/IMetaTransactionsFeature.sol index 8dff7acce1..9e50c1e068 100644 --- a/contracts/zero-ex/contracts/src/features/IMetaTransactionsFeature.sol +++ b/contracts/zero-ex/contracts/src/features/interfaces/IMetaTransactionsFeature.sol @@ -21,7 +21,7 @@ pragma solidity ^0.6.5; pragma experimental ABIEncoderV2; import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol"; -import "./libs/LibSignature.sol"; +import "../libs/LibSignature.sol"; /// @dev Meta-transactions feature. interface IMetaTransactionsFeature { diff --git a/contracts/zero-ex/contracts/src/features/interfaces/IMultiplexFeature.sol b/contracts/zero-ex/contracts/src/features/interfaces/IMultiplexFeature.sol new file mode 100644 index 0000000000..e5ca748e03 --- /dev/null +++ b/contracts/zero-ex/contracts/src/features/interfaces/IMultiplexFeature.sol @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: Apache-2.0 +/* + + Copyright 2021 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"; + + +interface IMultiplexFeature { + + // Parameters for `batchFill`. + struct BatchFillData { + // The token being sold. + IERC20TokenV06 inputToken; + // The token being bought. + IERC20TokenV06 outputToken; + // The amount of `inputToken` to sell. + uint256 sellAmount; + // The nested calls to perform. + WrappedBatchCall[] calls; + } + + // Represents a call nested within a `batchFill`. + struct WrappedBatchCall { + // The selector of the function to call. + bytes4 selector; + // Amount of `inputToken` to sell. + uint256 sellAmount; + // ABI-encoded parameters needed to perform the call. + bytes data; + } + + // Parameters for `multiHopFill`. + struct MultiHopFillData { + // The sell path, i.e. + // tokens = [inputToken, hopToken1, ..., hopTokenN, outputToken] + address[] tokens; + // The amount of `tokens[0]` to sell. + uint256 sellAmount; + // The nested calls to perform. + WrappedMultiHopCall[] calls; + } + + // Represents a call nested within a `multiHopFill`. + struct WrappedMultiHopCall { + // The selector of the function to call. + bytes4 selector; + // ABI-encoded parameters needed to perform the call. + bytes data; + } + + event LiquidityProviderSwap( + address inputToken, + address outputToken, + uint256 inputTokenAmount, + uint256 outputTokenAmount, + address provider, + address recipient + ); + + event ExpiredRfqOrder( + bytes32 orderHash, + address maker, + uint64 expiry + ); + + /// @dev Executes a batch of fills selling `fillData.inputToken` + /// for `fillData.outputToken` in sequence. Refer to the + /// internal variant `_batchFill` for the allowed nested + /// operations. + /// @param fillData Encodes the input/output tokens, the sell + /// amount, and the nested operations for this batch fill. + /// @param minBuyAmount The minimum amount of `fillData.outputToken` + /// to buy. Reverts if this amount is not met. + /// @return outputTokenAmount The amount of the output token bought. + function batchFill( + BatchFillData calldata fillData, + uint256 minBuyAmount + ) + external + payable + returns (uint256 outputTokenAmount); + + /// @dev Executes a sequence of fills "hopping" through the + /// path of tokens given by `fillData.tokens`. Refer to the + /// internal variant `_multiHopFill` for the allowed nested + /// operations. + /// @param fillData Encodes the path of tokens, the sell amount, + /// and the nested operations for this multi-hop fill. + /// @param minBuyAmount The minimum amount of the output token + /// to buy. Reverts if this amount is not met. + /// @return outputTokenAmount The amount of the output token bought. + function multiHopFill( + MultiHopFillData calldata fillData, + uint256 minBuyAmount + ) + external + payable + returns (uint256 outputTokenAmount); +} diff --git a/contracts/zero-ex/contracts/src/features/interfaces/INativeOrdersEvents.sol b/contracts/zero-ex/contracts/src/features/interfaces/INativeOrdersEvents.sol new file mode 100644 index 0000000000..d055f1705c --- /dev/null +++ b/contracts/zero-ex/contracts/src/features/interfaces/INativeOrdersEvents.sol @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: Apache-2.0 +/* + + Copyright 2021 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 Events emitted by NativeOrdersFeature. +interface INativeOrdersEvents { + + /// @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 pool The fee pool associated with this order. + event RfqOrderFilled( + bytes32 orderHash, + address maker, + address taker, + address makerToken, + address takerToken, + uint128 takerTokenFilledAmount, + uint128 makerTokenFilledAmount, + 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 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 PairCancelledLimitOrders( + address maker, + address makerToken, + address takerToken, + uint256 minValidSalt + ); + + /// @dev Emitted whenever 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 PairCancelledRfqOrders( + address maker, + address makerToken, + address takerToken, + uint256 minValidSalt + ); + + /// @dev Emitted when new addresses are allowed or disallowed to fill + /// orders with a given txOrigin. + /// @param origin The address doing the allowing. + /// @param addrs The address being allowed/disallowed. + /// @param allowed Indicates whether the address should be allowed. + event RfqOrderOriginsAllowed( + address origin, + address[] addrs, + bool allowed + ); +} diff --git a/contracts/zero-ex/contracts/src/features/INativeOrdersFeature.sol b/contracts/zero-ex/contracts/src/features/interfaces/INativeOrdersFeature.sol similarity index 80% rename from contracts/zero-ex/contracts/src/features/INativeOrdersFeature.sol rename to contracts/zero-ex/contracts/src/features/interfaces/INativeOrdersFeature.sol index a88a5ae0a5..9ddb5306bd 100644 --- a/contracts/zero-ex/contracts/src/features/INativeOrdersFeature.sol +++ b/contracts/zero-ex/contracts/src/features/interfaces/INativeOrdersFeature.sol @@ -21,98 +21,15 @@ 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"; +import "../libs/LibSignature.sol"; +import "../libs/LibNativeOrder.sol"; +import "./INativeOrdersEvents.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 pool The fee pool associated with this order. - event RfqOrderFilled( - bytes32 orderHash, - address maker, - address taker, - address makerToken, - address takerToken, - uint128 takerTokenFilledAmount, - uint128 makerTokenFilledAmount, - 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 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 PairCancelledLimitOrders( - address maker, - address makerToken, - address takerToken, - uint256 minValidSalt - ); - - /// @dev Emitted whenever 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 PairCancelledRfqOrders( - address maker, - address makerToken, - address takerToken, - uint256 minValidSalt - ); - - /// @dev Emitted when new addresses are allowed or disallowed to fill - /// orders with a given txOrigin. - /// @param origin The address doing the allowing. - /// @param addrs The address being allowed/disallowed. - /// @param allowed Indicates whether the address should be allowed. - event RfqOrderOriginsAllowed( - address origin, - address[] addrs, - bool allowed - ); +interface INativeOrdersFeature is + INativeOrdersEvents +{ /// @dev Transfers protocol fees from the `FeeCollector` pools into /// the staking contract. diff --git a/contracts/zero-ex/contracts/src/features/IOwnableFeature.sol b/contracts/zero-ex/contracts/src/features/interfaces/IOwnableFeature.sol similarity index 100% rename from contracts/zero-ex/contracts/src/features/IOwnableFeature.sol rename to contracts/zero-ex/contracts/src/features/interfaces/IOwnableFeature.sol diff --git a/contracts/zero-ex/contracts/src/features/ISimpleFunctionRegistryFeature.sol b/contracts/zero-ex/contracts/src/features/interfaces/ISimpleFunctionRegistryFeature.sol similarity index 100% rename from contracts/zero-ex/contracts/src/features/ISimpleFunctionRegistryFeature.sol rename to contracts/zero-ex/contracts/src/features/interfaces/ISimpleFunctionRegistryFeature.sol diff --git a/contracts/zero-ex/contracts/src/features/ITokenSpenderFeature.sol b/contracts/zero-ex/contracts/src/features/interfaces/ITokenSpenderFeature.sol similarity index 100% rename from contracts/zero-ex/contracts/src/features/ITokenSpenderFeature.sol rename to contracts/zero-ex/contracts/src/features/interfaces/ITokenSpenderFeature.sol diff --git a/contracts/zero-ex/contracts/src/features/ITransformERC20Feature.sol b/contracts/zero-ex/contracts/src/features/interfaces/ITransformERC20Feature.sol similarity index 98% rename from contracts/zero-ex/contracts/src/features/ITransformERC20Feature.sol rename to contracts/zero-ex/contracts/src/features/interfaces/ITransformERC20Feature.sol index 351859c8ad..836af87be1 100644 --- a/contracts/zero-ex/contracts/src/features/ITransformERC20Feature.sol +++ b/contracts/zero-ex/contracts/src/features/interfaces/ITransformERC20Feature.sol @@ -21,8 +21,8 @@ pragma solidity ^0.6.5; pragma experimental ABIEncoderV2; import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol"; -import "../transformers/IERC20Transformer.sol"; -import "../external/IFlashWallet.sol"; +import "../../transformers/IERC20Transformer.sol"; +import "../../external/IFlashWallet.sol"; /// @dev Feature to composably transform between ERC20 tokens. diff --git a/contracts/zero-ex/contracts/src/features/IUniswapFeature.sol b/contracts/zero-ex/contracts/src/features/interfaces/IUniswapFeature.sol similarity index 100% rename from contracts/zero-ex/contracts/src/features/IUniswapFeature.sol rename to contracts/zero-ex/contracts/src/features/interfaces/IUniswapFeature.sol diff --git a/contracts/zero-ex/contracts/src/features/libs/LibNativeOrder.sol b/contracts/zero-ex/contracts/src/features/libs/LibNativeOrder.sol index df7115801c..37b25bcd36 100644 --- a/contracts/zero-ex/contracts/src/features/libs/LibNativeOrder.sol +++ b/contracts/zero-ex/contracts/src/features/libs/LibNativeOrder.sol @@ -21,10 +21,15 @@ pragma solidity ^0.6.5; pragma experimental ABIEncoderV2; import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol"; +import "@0x/contracts-utils/contracts/src/v06/errors/LibRichErrorsV06.sol"; +import "@0x/contracts-utils/contracts/src/v06/LibSafeMathV06.sol"; +import "../../errors/LibNativeOrdersRichErrors.sol"; /// @dev A library for common native order operations. library LibNativeOrder { + using LibSafeMathV06 for uint256; + using LibRichErrorsV06 for bytes; enum OrderStatus { INVALID, @@ -216,4 +221,23 @@ library LibNativeOrder { structHash := keccak256(mem, 0x160) } } + + /// @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) + internal + { + 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/native_orders/NativeOrdersCancellation.sol b/contracts/zero-ex/contracts/src/features/native_orders/NativeOrdersCancellation.sol new file mode 100644 index 0000000000..69b8f5f929 --- /dev/null +++ b/contracts/zero-ex/contracts/src/features/native_orders/NativeOrdersCancellation.sol @@ -0,0 +1,266 @@ +// SPDX-License-Identifier: Apache-2.0 +/* + + Copyright 2021 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-utils/contracts/src/v06/errors/LibRichErrorsV06.sol"; +import "../../errors/LibNativeOrdersRichErrors.sol"; +import "../../storage/LibNativeOrdersStorage.sol"; +import "../interfaces/INativeOrdersEvents.sol"; +import "../libs/LibSignature.sol"; +import "../libs/LibNativeOrder.sol"; +import "./NativeOrdersInfo.sol"; + +/// @dev Feature for cancelling limit and RFQ orders. +abstract contract NativeOrdersCancellation is + INativeOrdersEvents, + NativeOrdersInfo +{ + using LibRichErrorsV06 for bytes; + + /// @dev Highest bit of a uint256, used to flag cancelled orders. + uint256 private constant HIGH_BIT = 1 << 255; + + constructor( + address zeroExAddress, + bytes32 greedyTokensBloomFilter + ) + internal + NativeOrdersInfo(zeroExAddress, greedyTokensBloomFilter) + { + // solhint-disable no-empty-blocks + } + + /// @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 + { + 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 + { + 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 + { + 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 + { + 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 + { + 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 PairCancelledLimitOrders( + 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 + { + require( + makerTokens.length == takerTokens.length && + makerTokens.length == minValidSalts.length, + "NativeOrdersFeature/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 + { + 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 PairCancelledRfqOrders( + 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 + { + require( + makerTokens.length == takerTokens.length && + makerTokens.length == minValidSalts.length, + "NativeOrdersFeature/MISMATCHED_PAIR_ORDERS_ARRAY_LENGTHS" + ); + + for (uint256 i = 0; i < makerTokens.length; ++i) { + cancelPairRfqOrders( + makerTokens[i], + takerTokens[i], + minValidSalts[i] + ); + } + } + + /// @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, maker); + } +} diff --git a/contracts/zero-ex/contracts/src/features/native_orders/NativeOrdersInfo.sol b/contracts/zero-ex/contracts/src/features/native_orders/NativeOrdersInfo.sol new file mode 100644 index 0000000000..6f80cf6162 --- /dev/null +++ b/contracts/zero-ex/contracts/src/features/native_orders/NativeOrdersInfo.sol @@ -0,0 +1,394 @@ +// SPDX-License-Identifier: Apache-2.0 +/* + + Copyright 2021 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-utils/contracts/src/v06/LibSafeMathV06.sol"; +import "@0x/contracts-utils/contracts/src/v06/LibMathV06.sol"; +import "../../fixins/FixinEIP712.sol"; +import "../../fixins/FixinTokenSpender.sol"; +import "../../storage/LibNativeOrdersStorage.sol"; +import "../libs/LibSignature.sol"; +import "../libs/LibNativeOrder.sol"; + + +/// @dev Feature for getting info about limit and RFQ orders. +abstract contract NativeOrdersInfo is + FixinEIP712, + FixinTokenSpender +{ + using LibSafeMathV06 for uint256; + using LibRichErrorsV06 for bytes; + + // @dev Params for `_getActualFillableTakerTokenAmount()`. + struct GetActualFillableTakerTokenAmountParams { + address maker; + IERC20TokenV06 makerToken; + uint128 orderMakerAmount; + uint128 orderTakerAmount; + LibNativeOrder.OrderInfo orderInfo; + } + + /// @dev Highest bit of a uint256, used to flag cancelled orders. + uint256 private constant HIGH_BIT = 1 << 255; + + constructor( + address zeroExAddress, + bytes32 greedyTokensBloomFilter + ) + internal + FixinEIP712(zeroExAddress) + FixinTokenSpender(greedyTokensBloomFilter) + { + // solhint-disable no-empty-blocks + } + + /// @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 + 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 + 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 + ); + + // Check for missing txOrigin. + if (order.txOrigin == address(0)) { + orderInfo.status = LibNativeOrder.OrderStatus.INVALID; + } + } + + /// @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 + 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 + view + returns (bytes32 orderHash) + { + return _getEIP712Hash( + LibNativeOrder.getRfqOrderStructHash(order) + ); + } + + /// @dev Get order info, fillable amount, and signature validity for a limit order. + /// Fillable amount is determined using balances and allowances of the maker. + /// @param order The limit order. + /// @param signature The order signature. + /// @return orderInfo Info about the order. + /// @return actualFillableTakerTokenAmount How much of the order is fillable + /// based on maker funds, in taker tokens. + /// @return isSignatureValid Whether the signature is valid. + function getLimitOrderRelevantState( + LibNativeOrder.LimitOrder memory order, + LibSignature.Signature calldata signature + ) + public + view + returns ( + LibNativeOrder.OrderInfo memory orderInfo, + uint128 actualFillableTakerTokenAmount, + bool isSignatureValid + ) + { + orderInfo = getLimitOrderInfo(order); + actualFillableTakerTokenAmount = _getActualFillableTakerTokenAmount( + GetActualFillableTakerTokenAmountParams({ + maker: order.maker, + makerToken: order.makerToken, + orderMakerAmount: order.makerAmount, + orderTakerAmount: order.takerAmount, + orderInfo: orderInfo + }) + ); + isSignatureValid = order.maker == + LibSignature.getSignerOfHash(orderInfo.orderHash, signature); + } + + /// @dev Get order info, fillable amount, and signature validity for an RFQ order. + /// Fillable amount is determined using balances and allowances of the maker. + /// @param order The RFQ order. + /// @param signature The order signature. + /// @return orderInfo Info about the order. + /// @return actualFillableTakerTokenAmount How much of the order is fillable + /// based on maker funds, in taker tokens. + /// @return isSignatureValid Whether the signature is valid. + function getRfqOrderRelevantState( + LibNativeOrder.RfqOrder memory order, + LibSignature.Signature memory signature + ) + public + view + returns ( + LibNativeOrder.OrderInfo memory orderInfo, + uint128 actualFillableTakerTokenAmount, + bool isSignatureValid + ) + { + orderInfo = getRfqOrderInfo(order); + actualFillableTakerTokenAmount = _getActualFillableTakerTokenAmount( + GetActualFillableTakerTokenAmountParams({ + maker: order.maker, + makerToken: order.makerToken, + orderMakerAmount: order.makerAmount, + orderTakerAmount: order.takerAmount, + orderInfo: orderInfo + }) + ); + isSignatureValid = order.maker == + LibSignature.getSignerOfHash(orderInfo.orderHash, signature); + } + + /// @dev Batch version of `getLimitOrderRelevantState()`, without reverting. + /// Orders that would normally cause `getLimitOrderRelevantState()` + /// to revert will have empty results. + /// @param orders The limit orders. + /// @param signatures The order signatures. + /// @return orderInfos Info about the orders. + /// @return actualFillableTakerTokenAmounts How much of each order is fillable + /// based on maker funds, in taker tokens. + /// @return isSignatureValids Whether each signature is valid for the order. + function batchGetLimitOrderRelevantStates( + LibNativeOrder.LimitOrder[] calldata orders, + LibSignature.Signature[] calldata signatures + ) + external + view + returns ( + LibNativeOrder.OrderInfo[] memory orderInfos, + uint128[] memory actualFillableTakerTokenAmounts, + bool[] memory isSignatureValids + ) + { + require( + orders.length == signatures.length, + "NativeOrdersFeature/MISMATCHED_ARRAY_LENGTHS" + ); + orderInfos = new LibNativeOrder.OrderInfo[](orders.length); + actualFillableTakerTokenAmounts = new uint128[](orders.length); + isSignatureValids = new bool[](orders.length); + for (uint256 i = 0; i < orders.length; ++i) { + try + this.getLimitOrderRelevantState(orders[i], signatures[i]) + returns ( + LibNativeOrder.OrderInfo memory orderInfo, + uint128 actualFillableTakerTokenAmount, + bool isSignatureValid + ) + { + orderInfos[i] = orderInfo; + actualFillableTakerTokenAmounts[i] = actualFillableTakerTokenAmount; + isSignatureValids[i] = isSignatureValid; + } + catch {} + } + } + + /// @dev Batch version of `getRfqOrderRelevantState()`, without reverting. + /// Orders that would normally cause `getRfqOrderRelevantState()` + /// to revert will have empty results. + /// @param orders The RFQ orders. + /// @param signatures The order signatures. + /// @return orderInfos Info about the orders. + /// @return actualFillableTakerTokenAmounts How much of each order is fillable + /// based on maker funds, in taker tokens. + /// @return isSignatureValids Whether each signature is valid for the order. + function batchGetRfqOrderRelevantStates( + LibNativeOrder.RfqOrder[] calldata orders, + LibSignature.Signature[] calldata signatures + ) + external + view + returns ( + LibNativeOrder.OrderInfo[] memory orderInfos, + uint128[] memory actualFillableTakerTokenAmounts, + bool[] memory isSignatureValids + ) + { + require( + orders.length == signatures.length, + "NativeOrdersFeature/MISMATCHED_ARRAY_LENGTHS" + ); + orderInfos = new LibNativeOrder.OrderInfo[](orders.length); + actualFillableTakerTokenAmounts = new uint128[](orders.length); + isSignatureValids = new bool[](orders.length); + for (uint256 i = 0; i < orders.length; ++i) { + try + this.getRfqOrderRelevantState(orders[i], signatures[i]) + returns ( + LibNativeOrder.OrderInfo memory orderInfo, + uint128 actualFillableTakerTokenAmount, + bool isSignatureValid + ) + { + orderInfos[i] = orderInfo; + actualFillableTakerTokenAmounts[i] = actualFillableTakerTokenAmount; + isSignatureValids[i] = isSignatureValid; + } + catch {} + } + } + + /// @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 Calculate the actual fillable taker token amount of an order + /// based on maker allowance and balances. + function _getActualFillableTakerTokenAmount( + GetActualFillableTakerTokenAmountParams memory params + ) + private + view + returns (uint128 actualFillableTakerTokenAmount) + { + if (params.orderMakerAmount == 0 || params.orderTakerAmount == 0) { + // Empty order. + return 0; + } + if (params.orderInfo.status != LibNativeOrder.OrderStatus.FILLABLE) { + // Not fillable. + return 0; + } + + // Get the fillable maker amount based on the order quantities and + // previously filled amount + uint256 fillableMakerTokenAmount = LibMathV06.getPartialAmountFloor( + uint256( + params.orderTakerAmount + - params.orderInfo.takerTokenFilledAmount + ), + uint256(params.orderTakerAmount), + uint256(params.orderMakerAmount) + ); + // Clamp it to the amount of maker tokens we can spend on behalf of the + // maker. + fillableMakerTokenAmount = LibSafeMathV06.min256( + fillableMakerTokenAmount, + _getSpendableERC20BalanceOf(params.makerToken, params.maker) + ); + // Convert to taker token amount. + return LibMathV06.getPartialAmountCeil( + fillableMakerTokenAmount, + uint256(params.orderMakerAmount), + uint256(params.orderTakerAmount) + ).safeDowncastToUint128(); + } +} diff --git a/contracts/zero-ex/contracts/src/features/native_orders/NativeOrdersProtocolFees.sol b/contracts/zero-ex/contracts/src/features/native_orders/NativeOrdersProtocolFees.sol new file mode 100644 index 0000000000..2d1810ea51 --- /dev/null +++ b/contracts/zero-ex/contracts/src/features/native_orders/NativeOrdersProtocolFees.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: Apache-2.0 +/* + + Copyright 2021 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/IEtherTokenV06.sol"; +import "@0x/contracts-utils/contracts/src/v06/errors/LibRichErrorsV06.sol"; +import "@0x/contracts-utils/contracts/src/v06/LibSafeMathV06.sol"; +import "../../fixins/FixinProtocolFees.sol"; +import "../../errors/LibNativeOrdersRichErrors.sol"; +import "../../vendor/v3/IStaking.sol"; + + +/// @dev Mixin for protocol fee utility functions. +abstract contract NativeOrdersProtocolFees is + FixinProtocolFees +{ + using LibSafeMathV06 for uint256; + using LibRichErrorsV06 for bytes; + + constructor( + IEtherTokenV06 weth, + IStaking staking, + FeeCollectorController feeCollectorController, + uint32 protocolFeeMultiplier + ) + internal + FixinProtocolFees(weth, staking, feeCollectorController, protocolFeeMultiplier) + { + // solhint-disable no-empty-blocks + } + + /// @dev Transfers protocol fees from the `FeeCollector` pools into + /// the staking contract. + /// @param poolIds Staking pool IDs + function transferProtocolFeesForPools(bytes32[] calldata poolIds) + external + { + for (uint256 i = 0; i < poolIds.length; ++i) { + _transferFeesForPool(poolIds[i]); + } + } + + /// @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) + { + return PROTOCOL_FEE_MULTIPLIER; + } +} diff --git a/contracts/zero-ex/contracts/src/features/native_orders/NativeOrdersSettlement.sol b/contracts/zero-ex/contracts/src/features/native_orders/NativeOrdersSettlement.sol new file mode 100644 index 0000000000..106f18956a --- /dev/null +++ b/contracts/zero-ex/contracts/src/features/native_orders/NativeOrdersSettlement.sol @@ -0,0 +1,569 @@ +// SPDX-License-Identifier: Apache-2.0 +/* + + Copyright 2021 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-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 "../../errors/LibNativeOrdersRichErrors.sol"; +import "../../fixins/FixinCommon.sol"; +import "../../storage/LibNativeOrdersStorage.sol"; +import "../../vendor/v3/IStaking.sol"; +import "../interfaces/INativeOrdersEvents.sol"; +import "../libs/LibSignature.sol"; +import "../libs/LibNativeOrder.sol"; +import "./NativeOrdersCancellation.sol"; +import "./NativeOrdersProtocolFees.sol"; + + +/// @dev Mixin for settling limit and RFQ orders. +abstract contract NativeOrdersSettlement is + INativeOrdersEvents, + NativeOrdersCancellation, + NativeOrdersProtocolFees, + FixinCommon +{ + 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; + } + + constructor( + address zeroExAddress, + IEtherTokenV06 weth, + IStaking staking, + FeeCollectorController feeCollectorController, + uint32 protocolFeeMultiplier, + bytes32 greedyTokensBloomFilter + ) + public + NativeOrdersCancellation(zeroExAddress, greedyTokensBloomFilter) + NativeOrdersProtocolFees(weth, staking, feeCollectorController, protocolFeeMultiplier) + { + // solhint-disable no-empty-blocks + } + + /// @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 + payable + returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount) + { + FillNativeOrderResults memory results = + _fillLimitOrderPrivate(FillLimitOrderPrivateParams({ + order: order, + signature: signature, + takerTokenFillAmount: takerTokenFillAmount, + taker: msg.sender, + sender: msg.sender + })); + LibNativeOrder.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 + returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount) + { + FillNativeOrderResults memory results = + _fillRfqOrderPrivate( + order, + signature, + takerTokenFillAmount, + msg.sender + ); + (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 + 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(); + } + LibNativeOrder.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 + 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(); + } + makerTokenFilledAmount = results.makerTokenFilledAmount; + } + + /// @dev Fill a limit order. Internal variant. ETH protocol fees can be + /// attached to this call. + /// @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 + virtual + payable + onlySelf + returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount) + { + FillNativeOrderResults memory results = + _fillLimitOrderPrivate(FillLimitOrderPrivateParams({ + order: order, + signature: signature, + takerTokenFillAmount: takerTokenFillAmount, + taker: taker, + sender: sender + })); + (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 + virtual + onlySelf + returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount) + { + FillNativeOrderResults memory results = + _fillRfqOrderPrivate( + order, + signature, + takerTokenFillAmount, + taker + ); + (takerTokenFilledAmount, makerTokenFilledAmount) = ( + results.takerTokenFilledAmount, + results.makerTokenFilledAmount + ); + } + + /// @dev Mark what tx.origin addresses are allowed to fill an order that + /// specifies the message sender as its txOrigin. + /// @param origins An array of origin addresses to update. + /// @param allowed True to register, false to unregister. + function registerAllowedRfqOrigins( + address[] memory origins, + bool allowed + ) + external + { + require(msg.sender == tx.origin, + "NativeOrdersFeature/NO_CONTRACT_ORIGINS"); + + LibNativeOrdersStorage.Storage storage stor = + LibNativeOrdersStorage.getStorage(); + + for (uint256 i = 0; i < origins.length; i++) { + stor.originRegistry[msg.sender][origins[i]] = allowed; + } + + emit RfqOrderOriginsAllowed(msg.sender, origins, allowed); + } + + /// @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 + )); + _transferERC20Tokens( + 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(); + } + + { + LibNativeOrdersStorage.Storage storage stor = + LibNativeOrdersStorage.getStorage(); + + // Must be fillable by the tx.origin. + if (order.txOrigin != tx.origin && !stor.originRegistry[order.txOrigin][tx.origin]) { + LibNativeOrdersRichErrors.OrderNotFillableByOriginError( + orderInfo.orderHash, + tx.origin, + order.txOrigin + ).rrevert(); + } + } + + // Must be fillable by the taker. + if (order.taker != address(0) && order.taker != taker) { + LibNativeOrdersRichErrors.OrderNotFillableByTakerError( + orderInfo.orderHash, + taker, + order.taker + ).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(); + } + } + + // 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, + 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. + _transferERC20Tokens( + settleInfo.takerToken, + settleInfo.taker, + settleInfo.maker, + takerTokenFilledAmount + ); + + // Transfer maker -> taker. + _transferERC20Tokens( + settleInfo.makerToken, + settleInfo.maker, + settleInfo.taker, + makerTokenFilledAmount + ); + } +} diff --git a/contracts/zero-ex/contracts/src/fixins/FixinCommon.sol b/contracts/zero-ex/contracts/src/fixins/FixinCommon.sol index 5eb918c072..7a129ba323 100644 --- a/contracts/zero-ex/contracts/src/fixins/FixinCommon.sol +++ b/contracts/zero-ex/contracts/src/fixins/FixinCommon.sol @@ -23,8 +23,8 @@ pragma experimental ABIEncoderV2; import "@0x/contracts-utils/contracts/src/v06/errors/LibRichErrorsV06.sol"; import "../errors/LibCommonRichErrors.sol"; import "../errors/LibOwnableRichErrors.sol"; -import "../features/IOwnableFeature.sol"; -import "../features/ISimpleFunctionRegistryFeature.sol"; +import "../features/interfaces/IOwnableFeature.sol"; +import "../features/interfaces/ISimpleFunctionRegistryFeature.sol"; /// @dev Common feature utilities. diff --git a/contracts/zero-ex/contracts/src/fixins/FixinTokenSpender.sol b/contracts/zero-ex/contracts/src/fixins/FixinTokenSpender.sol index b261fa2059..49f67f6760 100644 --- a/contracts/zero-ex/contracts/src/fixins/FixinTokenSpender.sol +++ b/contracts/zero-ex/contracts/src/fixins/FixinTokenSpender.sol @@ -22,7 +22,7 @@ pragma experimental ABIEncoderV2; import "@0x/contracts-erc20/contracts/src/v06/IEtherTokenV06.sol"; import "@0x/contracts-utils/contracts/src/v06/LibSafeMathV06.sol"; -import "../features/ITokenSpenderFeature.sol"; +import "../features/interfaces/ITokenSpenderFeature.sol"; import "../errors/LibSpenderRichErrors.sol"; import "../external/FeeCollector.sol"; import "../vendor/v3/IStaking.sol"; diff --git a/contracts/zero-ex/contracts/src/migrations/FullMigration.sol b/contracts/zero-ex/contracts/src/migrations/FullMigration.sol index 5d8db5c436..39ed12c9d2 100644 --- a/contracts/zero-ex/contracts/src/migrations/FullMigration.sol +++ b/contracts/zero-ex/contracts/src/migrations/FullMigration.sol @@ -21,7 +21,7 @@ pragma solidity ^0.6.5; pragma experimental ABIEncoderV2; import "../ZeroEx.sol"; -import "../features/IOwnableFeature.sol"; +import "../features/interfaces/IOwnableFeature.sol"; import "../features/TokenSpenderFeature.sol"; import "../features/TransformERC20Feature.sol"; import "../features/MetaTransactionsFeature.sol"; diff --git a/contracts/zero-ex/contracts/src/migrations/InitialMigration.sol b/contracts/zero-ex/contracts/src/migrations/InitialMigration.sol index 8db88413e8..0b5ee6b32a 100644 --- a/contracts/zero-ex/contracts/src/migrations/InitialMigration.sol +++ b/contracts/zero-ex/contracts/src/migrations/InitialMigration.sol @@ -21,7 +21,7 @@ pragma solidity ^0.6.5; pragma experimental ABIEncoderV2; import "../ZeroEx.sol"; -import "../features/IBootstrapFeature.sol"; +import "../features/interfaces/IBootstrapFeature.sol"; import "../features/SimpleFunctionRegistryFeature.sol"; import "../features/OwnableFeature.sol"; import "./LibBootstrap.sol"; diff --git a/contracts/zero-ex/contracts/src/transformers/FillQuoteTransformer.sol b/contracts/zero-ex/contracts/src/transformers/FillQuoteTransformer.sol index 903f978570..0398f42bde 100644 --- a/contracts/zero-ex/contracts/src/transformers/FillQuoteTransformer.sol +++ b/contracts/zero-ex/contracts/src/transformers/FillQuoteTransformer.sol @@ -26,7 +26,7 @@ import "@0x/contracts-erc20/contracts/src/v06/LibERC20TokenV06.sol"; import "@0x/contracts-utils/contracts/src/v06/LibSafeMathV06.sol"; import "@0x/contracts-utils/contracts/src/v06/LibMathV06.sol"; import "../errors/LibTransformERC20RichErrors.sol"; -import "../features/INativeOrdersFeature.sol"; +import "../features/interfaces/INativeOrdersFeature.sol"; import "../features/libs/LibNativeOrder.sol"; import "./bridges/IBridgeAdapter.sol"; import "./Transformer.sol"; @@ -132,8 +132,6 @@ contract FillQuoteTransformer is /// @param orderHash The hash of the order that was skipped. event ProtocolFeeUnfunded(bytes32 orderHash); - /// @dev Maximum uint256 value. - uint256 private constant MAX_UINT256 = uint256(-1); /// @dev The highest bit of a uint256 value. uint256 private constant HIGH_BIT = 2 ** 255; /// @dev Mask of the lower 255 bits of a uint256 value. diff --git a/contracts/zero-ex/contracts/src/vendor/IUniswapV2Pair.sol b/contracts/zero-ex/contracts/src/vendor/IUniswapV2Pair.sol new file mode 100644 index 0000000000..76deb4347d --- /dev/null +++ b/contracts/zero-ex/contracts/src/vendor/IUniswapV2Pair.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: Apache-2.0 +/* + + Copyright 2021 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.12; + + +interface IUniswapV2Pair { + event Swap( + address indexed sender, + uint256 amount0In, + uint256 amount1In, + uint256 amount0Out, + uint256 amount1Out, + address indexed to + ); + + function swap( + uint amount0Out, + uint amount1Out, + address to, + bytes calldata data + ) external; + + function getReserves() + external + view + returns ( + uint112 reserve0, + uint112 reserve1, + uint32 blockTimestampLast + ); +} diff --git a/contracts/zero-ex/contracts/test/TestInitialMigration.sol b/contracts/zero-ex/contracts/test/TestInitialMigration.sol index 5a1757ee3b..d71a1f2762 100644 --- a/contracts/zero-ex/contracts/test/TestInitialMigration.sol +++ b/contracts/zero-ex/contracts/test/TestInitialMigration.sol @@ -21,7 +21,7 @@ pragma solidity ^0.6.5; pragma experimental ABIEncoderV2; import "../src/ZeroEx.sol"; -import "../src/features/IBootstrapFeature.sol"; +import "../src/features/interfaces/IBootstrapFeature.sol"; import "../src/migrations/InitialMigration.sol"; diff --git a/contracts/zero-ex/contracts/test/TestMetaTransactionsNativeOrdersFeature.sol b/contracts/zero-ex/contracts/test/TestMetaTransactionsNativeOrdersFeature.sol index c15862dc9b..200b1352a0 100644 --- a/contracts/zero-ex/contracts/test/TestMetaTransactionsNativeOrdersFeature.sol +++ b/contracts/zero-ex/contracts/test/TestMetaTransactionsNativeOrdersFeature.sol @@ -20,8 +20,8 @@ pragma solidity ^0.6.5; pragma experimental ABIEncoderV2; +import "../src/features/interfaces/IMetaTransactionsFeature.sol"; import "../src/features/NativeOrdersFeature.sol"; -import "../src/features/IMetaTransactionsFeature.sol"; import "./TestFeeCollectorController.sol"; diff --git a/contracts/zero-ex/contracts/test/TestMetaTransactionsTransformERC20Feature.sol b/contracts/zero-ex/contracts/test/TestMetaTransactionsTransformERC20Feature.sol index 1ed7e51705..6d23c160a1 100644 --- a/contracts/zero-ex/contracts/test/TestMetaTransactionsTransformERC20Feature.sol +++ b/contracts/zero-ex/contracts/test/TestMetaTransactionsTransformERC20Feature.sol @@ -20,8 +20,8 @@ pragma solidity ^0.6.5; pragma experimental ABIEncoderV2; +import "../src/features/interfaces/IMetaTransactionsFeature.sol"; import "../src/features/TransformERC20Feature.sol"; -import "../src/features/IMetaTransactionsFeature.sol"; contract TestMetaTransactionsTransformERC20Feature is diff --git a/contracts/zero-ex/contracts/test/TestMigrator.sol b/contracts/zero-ex/contracts/test/TestMigrator.sol index 473586aa5d..26bb6d27a2 100644 --- a/contracts/zero-ex/contracts/test/TestMigrator.sol +++ b/contracts/zero-ex/contracts/test/TestMigrator.sol @@ -21,7 +21,7 @@ pragma solidity ^0.6.5; pragma experimental ABIEncoderV2; import "../src/migrations/LibMigrate.sol"; -import "../src/features/IOwnableFeature.sol"; +import "../src/features/interfaces/IOwnableFeature.sol"; contract TestMigrator { diff --git a/contracts/zero-ex/contracts/test/TestRfqOriginRegistration.sol b/contracts/zero-ex/contracts/test/TestRfqOriginRegistration.sol index 0fbba5ef3e..0f452112dd 100644 --- a/contracts/zero-ex/contracts/test/TestRfqOriginRegistration.sol +++ b/contracts/zero-ex/contracts/test/TestRfqOriginRegistration.sol @@ -20,7 +20,7 @@ pragma solidity ^0.6.5; pragma experimental ABIEncoderV2; -import "../src/features/INativeOrdersFeature.sol"; +import "../src/features/interfaces/INativeOrdersFeature.sol"; contract TestRfqOriginRegistration { function registerAllowedRfqOrigins( diff --git a/contracts/zero-ex/package.json b/contracts/zero-ex/package.json index 7735c3c085..0d8c1e02da 100644 --- a/contracts/zero-ex/package.json +++ b/contracts/zero-ex/package.json @@ -41,9 +41,9 @@ "rollback": "node ./lib/scripts/rollback.js" }, "config": { - "publicInterfaceContracts": "IZeroEx,ZeroEx,FullMigration,InitialMigration,IFlashWallet,IAllowanceTarget,IERC20Transformer,IOwnableFeature,ISimpleFunctionRegistryFeature,ITokenSpenderFeature,ITransformERC20Feature,FillQuoteTransformer,PayTakerTransformer,PositiveSlippageFeeTransformer,WethTransformer,OwnableFeature,SimpleFunctionRegistryFeature,TransformERC20Feature,TokenSpenderFeature,AffiliateFeeTransformer,MetaTransactionsFeature,LogMetadataTransformer,BridgeAdapter,LiquidityProviderFeature,ILiquidityProviderFeature,NativeOrdersFeature,INativeOrdersFeature,FeeCollectorController,FeeCollector,CurveLiquidityProvider", + "publicInterfaceContracts": "IZeroEx,ZeroEx,FullMigration,InitialMigration,IFlashWallet,IAllowanceTarget,IERC20Transformer,IOwnableFeature,ISimpleFunctionRegistryFeature,ITokenSpenderFeature,ITransformERC20Feature,FillQuoteTransformer,PayTakerTransformer,PositiveSlippageFeeTransformer,WethTransformer,OwnableFeature,SimpleFunctionRegistryFeature,TransformERC20Feature,TokenSpenderFeature,AffiliateFeeTransformer,MetaTransactionsFeature,LogMetadataTransformer,BridgeAdapter,LiquidityProviderFeature,ILiquidityProviderFeature,NativeOrdersFeature,INativeOrdersFeature,FeeCollectorController,FeeCollector,CurveLiquidityProvider,BatchFillNativeOrdersFeature,IBatchFillNativeOrdersFeature,MultiplexFeature,IMultiplexFeature", "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.", - "abis": "./test/generated-artifacts/@(AffiliateFeeTransformer|AllowanceTarget|BootstrapFeature|BridgeAdapter|BridgeSource|CurveLiquidityProvider|FeeCollector|FeeCollectorController|FillQuoteTransformer|FixinCommon|FixinEIP712|FixinProtocolFees|FixinReentrancyGuard|FixinTokenSpender|FlashWallet|FullMigration|IAllowanceTarget|IBootstrapFeature|IBridgeAdapter|IERC20Bridge|IERC20Transformer|IFeature|IFlashWallet|ILiquidityProvider|ILiquidityProviderFeature|ILiquidityProviderSandbox|IMetaTransactionsFeature|IMooniswapPool|INativeOrdersFeature|IOwnableFeature|ISimpleFunctionRegistryFeature|IStaking|ITestSimpleFunctionRegistryFeature|ITokenSpenderFeature|ITransformERC20Feature|IUniswapFeature|IZeroEx|InitialMigration|LibBootstrap|LibCommonRichErrors|LibERC20Transformer|LibFeeCollector|LibLiquidityProviderRichErrors|LibMetaTransactionsRichErrors|LibMetaTransactionsStorage|LibMigrate|LibNativeOrder|LibNativeOrdersRichErrors|LibNativeOrdersStorage|LibOwnableRichErrors|LibOwnableStorage|LibProxyRichErrors|LibProxyStorage|LibReentrancyGuardStorage|LibSignature|LibSignatureRichErrors|LibSimpleFunctionRegistryRichErrors|LibSimpleFunctionRegistryStorage|LibSpenderRichErrors|LibStorage|LibTokenSpenderStorage|LibTransformERC20RichErrors|LibTransformERC20Storage|LibWalletRichErrors|LiquidityProviderFeature|LiquidityProviderSandbox|LogMetadataTransformer|MetaTransactionsFeature|MixinBalancer|MixinBancor|MixinCoFiX|MixinCryptoCom|MixinCurve|MixinDodo|MixinDodoV2|MixinKyber|MixinMStable|MixinMooniswap|MixinOasis|MixinShell|MixinSushiswap|MixinUniswap|MixinUniswapV2|MixinZeroExBridge|MooniswapLiquidityProvider|NativeOrdersFeature|OwnableFeature|PayTakerTransformer|PermissionlessTransformerDeployer|PositiveSlippageFeeTransformer|SimpleFunctionRegistryFeature|TestBridge|TestCallTarget|TestCurve|TestDelegateCaller|TestFeeCollectorController|TestFillQuoteTransformerBridge|TestFillQuoteTransformerExchange|TestFillQuoteTransformerHost|TestFixinProtocolFees|TestFixinTokenSpender|TestFullMigration|TestInitialMigration|TestLibNativeOrder|TestLibSignature|TestLiquidityProvider|TestMetaTransactionsNativeOrdersFeature|TestMetaTransactionsTransformERC20Feature|TestMigrator|TestMintTokenERC20Transformer|TestMintableERC20Token|TestMooniswap|TestNativeOrdersFeature|TestPermissionlessTransformerDeployerSuicidal|TestPermissionlessTransformerDeployerTransformer|TestRfqOriginRegistration|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestStaking|TestTokenSpender|TestTokenSpenderERC20Token|TestTransformERC20|TestTransformerBase|TestTransformerDeployerTransformer|TestTransformerHost|TestWeth|TestWethTransformerHost|TestZeroExFeature|TokenSpenderFeature|TransformERC20Feature|Transformer|TransformerDeployer|UniswapFeature|WethTransformer|ZeroEx|ZeroExOptimized).json" + "abis": "./test/generated-artifacts/@(AffiliateFeeTransformer|AllowanceTarget|BatchFillNativeOrdersFeature|BootstrapFeature|BridgeAdapter|BridgeSource|CurveLiquidityProvider|FeeCollector|FeeCollectorController|FillQuoteTransformer|FixinCommon|FixinEIP712|FixinProtocolFees|FixinReentrancyGuard|FixinTokenSpender|FlashWallet|FullMigration|IAllowanceTarget|IBatchFillNativeOrdersFeature|IBootstrapFeature|IBridgeAdapter|IERC20Bridge|IERC20Transformer|IFeature|IFlashWallet|ILiquidityProvider|ILiquidityProviderFeature|ILiquidityProviderSandbox|IMetaTransactionsFeature|IMooniswapPool|IMultiplexFeature|INativeOrdersEvents|INativeOrdersFeature|IOwnableFeature|ISimpleFunctionRegistryFeature|IStaking|ITestSimpleFunctionRegistryFeature|ITokenSpenderFeature|ITransformERC20Feature|IUniswapFeature|IUniswapV2Pair|IZeroEx|InitialMigration|LibBootstrap|LibCommonRichErrors|LibERC20Transformer|LibFeeCollector|LibLiquidityProviderRichErrors|LibMetaTransactionsRichErrors|LibMetaTransactionsStorage|LibMigrate|LibNativeOrder|LibNativeOrdersRichErrors|LibNativeOrdersStorage|LibOwnableRichErrors|LibOwnableStorage|LibProxyRichErrors|LibProxyStorage|LibReentrancyGuardStorage|LibSignature|LibSignatureRichErrors|LibSimpleFunctionRegistryRichErrors|LibSimpleFunctionRegistryStorage|LibSpenderRichErrors|LibStorage|LibTokenSpenderStorage|LibTransformERC20RichErrors|LibTransformERC20Storage|LibWalletRichErrors|LiquidityProviderFeature|LiquidityProviderSandbox|LogMetadataTransformer|MetaTransactionsFeature|MixinBalancer|MixinBancor|MixinCoFiX|MixinCryptoCom|MixinCurve|MixinDodo|MixinDodoV2|MixinKyber|MixinMStable|MixinMooniswap|MixinOasis|MixinShell|MixinSushiswap|MixinUniswap|MixinUniswapV2|MixinZeroExBridge|MooniswapLiquidityProvider|MultiplexFeature|NativeOrdersCancellation|NativeOrdersFeature|NativeOrdersInfo|NativeOrdersProtocolFees|NativeOrdersSettlement|OwnableFeature|PayTakerTransformer|PermissionlessTransformerDeployer|PositiveSlippageFeeTransformer|SimpleFunctionRegistryFeature|TestBridge|TestCallTarget|TestCurve|TestDelegateCaller|TestFeeCollectorController|TestFillQuoteTransformerBridge|TestFillQuoteTransformerExchange|TestFillQuoteTransformerHost|TestFixinProtocolFees|TestFixinTokenSpender|TestFullMigration|TestInitialMigration|TestLibNativeOrder|TestLibSignature|TestLiquidityProvider|TestMetaTransactionsNativeOrdersFeature|TestMetaTransactionsTransformERC20Feature|TestMigrator|TestMintTokenERC20Transformer|TestMintableERC20Token|TestMooniswap|TestNativeOrdersFeature|TestPermissionlessTransformerDeployerSuicidal|TestPermissionlessTransformerDeployerTransformer|TestRfqOriginRegistration|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestStaking|TestTokenSpender|TestTokenSpenderERC20Token|TestTransformERC20|TestTransformerBase|TestTransformerDeployerTransformer|TestTransformerHost|TestWeth|TestWethTransformerHost|TestZeroExFeature|TokenSpenderFeature|TransformERC20Feature|Transformer|TransformerDeployer|UniswapFeature|WethTransformer|ZeroEx|ZeroExOptimized).json" }, "repository": { "type": "git", diff --git a/contracts/zero-ex/src/artifacts.ts b/contracts/zero-ex/src/artifacts.ts index 033dfdfe4a..2fbc57173d 100644 --- a/contracts/zero-ex/src/artifacts.ts +++ b/contracts/zero-ex/src/artifacts.ts @@ -6,6 +6,7 @@ import { ContractArtifact } from 'ethereum-types'; import * as AffiliateFeeTransformer from '../generated-artifacts/AffiliateFeeTransformer.json'; +import * as BatchFillNativeOrdersFeature from '../generated-artifacts/BatchFillNativeOrdersFeature.json'; import * as BridgeAdapter from '../generated-artifacts/BridgeAdapter.json'; import * as CurveLiquidityProvider from '../generated-artifacts/CurveLiquidityProvider.json'; import * as FeeCollector from '../generated-artifacts/FeeCollector.json'; @@ -13,9 +14,11 @@ import * as FeeCollectorController from '../generated-artifacts/FeeCollectorCont import * as FillQuoteTransformer from '../generated-artifacts/FillQuoteTransformer.json'; import * as FullMigration from '../generated-artifacts/FullMigration.json'; import * as IAllowanceTarget from '../generated-artifacts/IAllowanceTarget.json'; +import * as IBatchFillNativeOrdersFeature from '../generated-artifacts/IBatchFillNativeOrdersFeature.json'; import * as IERC20Transformer from '../generated-artifacts/IERC20Transformer.json'; import * as IFlashWallet from '../generated-artifacts/IFlashWallet.json'; import * as ILiquidityProviderFeature from '../generated-artifacts/ILiquidityProviderFeature.json'; +import * as IMultiplexFeature from '../generated-artifacts/IMultiplexFeature.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'; @@ -26,6 +29,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 MultiplexFeature from '../generated-artifacts/MultiplexFeature.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'; @@ -66,4 +70,8 @@ export const artifacts = { FeeCollectorController: FeeCollectorController as ContractArtifact, FeeCollector: FeeCollector as ContractArtifact, CurveLiquidityProvider: CurveLiquidityProvider as ContractArtifact, + BatchFillNativeOrdersFeature: BatchFillNativeOrdersFeature as ContractArtifact, + IBatchFillNativeOrdersFeature: IBatchFillNativeOrdersFeature as ContractArtifact, + MultiplexFeature: MultiplexFeature as ContractArtifact, + IMultiplexFeature: IMultiplexFeature as ContractArtifact, }; diff --git a/contracts/zero-ex/src/wrappers.ts b/contracts/zero-ex/src/wrappers.ts index 649ecfb7d0..5a2265a37e 100644 --- a/contracts/zero-ex/src/wrappers.ts +++ b/contracts/zero-ex/src/wrappers.ts @@ -4,6 +4,7 @@ * ----------------------------------------------------------------------------- */ export * from '../generated-wrappers/affiliate_fee_transformer'; +export * from '../generated-wrappers/batch_fill_native_orders_feature'; export * from '../generated-wrappers/bridge_adapter'; export * from '../generated-wrappers/curve_liquidity_provider'; export * from '../generated-wrappers/fee_collector'; @@ -11,9 +12,11 @@ export * from '../generated-wrappers/fee_collector_controller'; export * from '../generated-wrappers/fill_quote_transformer'; export * from '../generated-wrappers/full_migration'; export * from '../generated-wrappers/i_allowance_target'; +export * from '../generated-wrappers/i_batch_fill_native_orders_feature'; export * from '../generated-wrappers/i_erc20_transformer'; export * from '../generated-wrappers/i_flash_wallet'; export * from '../generated-wrappers/i_liquidity_provider_feature'; +export * from '../generated-wrappers/i_multiplex_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'; @@ -24,6 +27,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/multiplex_feature'; export * from '../generated-wrappers/native_orders_feature'; export * from '../generated-wrappers/ownable_feature'; export * from '../generated-wrappers/pay_taker_transformer'; diff --git a/contracts/zero-ex/test/artifacts.ts b/contracts/zero-ex/test/artifacts.ts index 0ee34e366e..c0b54e2e7b 100644 --- a/contracts/zero-ex/test/artifacts.ts +++ b/contracts/zero-ex/test/artifacts.ts @@ -7,6 +7,7 @@ import { ContractArtifact } from 'ethereum-types'; import * as AffiliateFeeTransformer from '../test/generated-artifacts/AffiliateFeeTransformer.json'; import * as AllowanceTarget from '../test/generated-artifacts/AllowanceTarget.json'; +import * as BatchFillNativeOrdersFeature from '../test/generated-artifacts/BatchFillNativeOrdersFeature.json'; import * as BootstrapFeature from '../test/generated-artifacts/BootstrapFeature.json'; import * as BridgeAdapter from '../test/generated-artifacts/BridgeAdapter.json'; import * as BridgeSource from '../test/generated-artifacts/BridgeSource.json'; @@ -22,6 +23,7 @@ import * as FixinTokenSpender from '../test/generated-artifacts/FixinTokenSpende import * as FlashWallet from '../test/generated-artifacts/FlashWallet.json'; import * as FullMigration from '../test/generated-artifacts/FullMigration.json'; import * as IAllowanceTarget from '../test/generated-artifacts/IAllowanceTarget.json'; +import * as IBatchFillNativeOrdersFeature from '../test/generated-artifacts/IBatchFillNativeOrdersFeature.json'; import * as IBootstrapFeature from '../test/generated-artifacts/IBootstrapFeature.json'; import * as IBridgeAdapter from '../test/generated-artifacts/IBridgeAdapter.json'; import * as IERC20Bridge from '../test/generated-artifacts/IERC20Bridge.json'; @@ -33,6 +35,8 @@ import * as ILiquidityProviderFeature from '../test/generated-artifacts/ILiquidi import * as ILiquidityProviderSandbox from '../test/generated-artifacts/ILiquidityProviderSandbox.json'; import * as IMetaTransactionsFeature from '../test/generated-artifacts/IMetaTransactionsFeature.json'; import * as IMooniswapPool from '../test/generated-artifacts/IMooniswapPool.json'; +import * as IMultiplexFeature from '../test/generated-artifacts/IMultiplexFeature.json'; +import * as INativeOrdersEvents from '../test/generated-artifacts/INativeOrdersEvents.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'; @@ -42,6 +46,7 @@ import * as ITestSimpleFunctionRegistryFeature from '../test/generated-artifacts import * as ITokenSpenderFeature from '../test/generated-artifacts/ITokenSpenderFeature.json'; import * as ITransformERC20Feature from '../test/generated-artifacts/ITransformERC20Feature.json'; import * as IUniswapFeature from '../test/generated-artifacts/IUniswapFeature.json'; +import * as IUniswapV2Pair from '../test/generated-artifacts/IUniswapV2Pair.json'; import * as IZeroEx from '../test/generated-artifacts/IZeroEx.json'; import * as LibBootstrap from '../test/generated-artifacts/LibBootstrap.json'; import * as LibCommonRichErrors from '../test/generated-artifacts/LibCommonRichErrors.json'; @@ -90,7 +95,12 @@ 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 MooniswapLiquidityProvider from '../test/generated-artifacts/MooniswapLiquidityProvider.json'; +import * as MultiplexFeature from '../test/generated-artifacts/MultiplexFeature.json'; +import * as NativeOrdersCancellation from '../test/generated-artifacts/NativeOrdersCancellation.json'; import * as NativeOrdersFeature from '../test/generated-artifacts/NativeOrdersFeature.json'; +import * as NativeOrdersInfo from '../test/generated-artifacts/NativeOrdersInfo.json'; +import * as NativeOrdersProtocolFees from '../test/generated-artifacts/NativeOrdersProtocolFees.json'; +import * as NativeOrdersSettlement from '../test/generated-artifacts/NativeOrdersSettlement.json'; import * as OwnableFeature from '../test/generated-artifacts/OwnableFeature.json'; import * as PayTakerTransformer from '../test/generated-artifacts/PayTakerTransformer.json'; import * as PermissionlessTransformerDeployer from '../test/generated-artifacts/PermissionlessTransformerDeployer.json'; @@ -167,27 +177,36 @@ export const artifacts = { LiquidityProviderSandbox: LiquidityProviderSandbox as ContractArtifact, PermissionlessTransformerDeployer: PermissionlessTransformerDeployer as ContractArtifact, TransformerDeployer: TransformerDeployer as ContractArtifact, + BatchFillNativeOrdersFeature: BatchFillNativeOrdersFeature as ContractArtifact, BootstrapFeature: BootstrapFeature as ContractArtifact, - IBootstrapFeature: IBootstrapFeature as ContractArtifact, - IFeature: IFeature as ContractArtifact, - ILiquidityProviderFeature: ILiquidityProviderFeature as ContractArtifact, - IMetaTransactionsFeature: IMetaTransactionsFeature as ContractArtifact, - INativeOrdersFeature: INativeOrdersFeature as ContractArtifact, - IOwnableFeature: IOwnableFeature as ContractArtifact, - ISimpleFunctionRegistryFeature: ISimpleFunctionRegistryFeature as ContractArtifact, - ITokenSpenderFeature: ITokenSpenderFeature as ContractArtifact, - ITransformERC20Feature: ITransformERC20Feature as ContractArtifact, - IUniswapFeature: IUniswapFeature as ContractArtifact, LiquidityProviderFeature: LiquidityProviderFeature as ContractArtifact, MetaTransactionsFeature: MetaTransactionsFeature as ContractArtifact, + MultiplexFeature: MultiplexFeature as ContractArtifact, NativeOrdersFeature: NativeOrdersFeature as ContractArtifact, OwnableFeature: OwnableFeature as ContractArtifact, SimpleFunctionRegistryFeature: SimpleFunctionRegistryFeature as ContractArtifact, TokenSpenderFeature: TokenSpenderFeature as ContractArtifact, TransformERC20Feature: TransformERC20Feature as ContractArtifact, UniswapFeature: UniswapFeature as ContractArtifact, + IBatchFillNativeOrdersFeature: IBatchFillNativeOrdersFeature as ContractArtifact, + IBootstrapFeature: IBootstrapFeature as ContractArtifact, + IFeature: IFeature as ContractArtifact, + ILiquidityProviderFeature: ILiquidityProviderFeature as ContractArtifact, + IMetaTransactionsFeature: IMetaTransactionsFeature as ContractArtifact, + IMultiplexFeature: IMultiplexFeature as ContractArtifact, + INativeOrdersEvents: INativeOrdersEvents as ContractArtifact, + INativeOrdersFeature: INativeOrdersFeature as ContractArtifact, + IOwnableFeature: IOwnableFeature as ContractArtifact, + ISimpleFunctionRegistryFeature: ISimpleFunctionRegistryFeature as ContractArtifact, + ITokenSpenderFeature: ITokenSpenderFeature as ContractArtifact, + ITransformERC20Feature: ITransformERC20Feature as ContractArtifact, + IUniswapFeature: IUniswapFeature as ContractArtifact, LibNativeOrder: LibNativeOrder as ContractArtifact, LibSignature: LibSignature as ContractArtifact, + NativeOrdersCancellation: NativeOrdersCancellation as ContractArtifact, + NativeOrdersInfo: NativeOrdersInfo as ContractArtifact, + NativeOrdersProtocolFees: NativeOrdersProtocolFees as ContractArtifact, + NativeOrdersSettlement: NativeOrdersSettlement as ContractArtifact, FixinCommon: FixinCommon as ContractArtifact, FixinEIP712: FixinEIP712 as ContractArtifact, FixinProtocolFees: FixinProtocolFees as ContractArtifact, @@ -238,6 +257,7 @@ export const artifacts = { MixinZeroExBridge: MixinZeroExBridge as ContractArtifact, ILiquidityProvider: ILiquidityProvider as ContractArtifact, IMooniswapPool: IMooniswapPool as ContractArtifact, + IUniswapV2Pair: IUniswapV2Pair as ContractArtifact, IERC20Bridge: IERC20Bridge as ContractArtifact, IStaking: IStaking as ContractArtifact, ITestSimpleFunctionRegistryFeature: ITestSimpleFunctionRegistryFeature as ContractArtifact, diff --git a/contracts/zero-ex/test/features/batch_fill_native_orders_test.ts b/contracts/zero-ex/test/features/batch_fill_native_orders_test.ts new file mode 100644 index 0000000000..6c3a4824de --- /dev/null +++ b/contracts/zero-ex/test/features/batch_fill_native_orders_test.ts @@ -0,0 +1,479 @@ +import { + blockchainTests, + constants, + describe, + expect, + getRandomPortion, + verifyEventsFromLogs, +} from '@0x/contracts-test-utils'; +import { LimitOrder, LimitOrderFields, OrderStatus, RevertErrors, RfqOrder, RfqOrderFields } from '@0x/protocol-utils'; +import { BigNumber } from '@0x/utils'; +import { TransactionReceiptWithDecodedLogs } from 'ethereum-types'; +import * as _ from 'lodash'; + +import { BatchFillNativeOrdersFeatureContract, IZeroExContract, IZeroExEvents } from '../../src/wrappers'; +import { artifacts } from '../artifacts'; +import { abis } from '../utils/abis'; +import { + assertOrderInfoEquals, + computeLimitOrderFilledAmounts, + computeRfqOrderFilledAmounts, + createExpiry, + getRandomLimitOrder, + getRandomRfqOrder, + NativeOrdersTestEnvironment, +} from '../utils/orders'; +import { TestMintableERC20TokenContract } from '../wrappers'; + +blockchainTests.resets('BatchFillNativeOrdersFeature', env => { + const { NULL_ADDRESS, ZERO_AMOUNT } = constants; + let maker: string; + let taker: string; + let zeroEx: IZeroExContract; + let feature: BatchFillNativeOrdersFeatureContract; + let verifyingContract: string; + let makerToken: TestMintableERC20TokenContract; + let takerToken: TestMintableERC20TokenContract; + let testUtils: NativeOrdersTestEnvironment; + + before(async () => { + testUtils = await NativeOrdersTestEnvironment.createAsync(env); + maker = testUtils.maker; + taker = testUtils.taker; + zeroEx = testUtils.zeroEx; + makerToken = testUtils.makerToken; + takerToken = testUtils.takerToken; + + verifyingContract = zeroEx.address; + const featureImpl = await BatchFillNativeOrdersFeatureContract.deployFrom0xArtifactAsync( + artifacts.BatchFillNativeOrdersFeature, + env.provider, + env.txDefaults, + artifacts, + zeroEx.address, + ); + const [owner] = await env.getAccountAddressesAsync(); + await zeroEx + .migrate(featureImpl.address, featureImpl.migrate().getABIEncodedTransactionData(), owner) + .awaitTransactionSuccessAsync(); + feature = new BatchFillNativeOrdersFeatureContract( + zeroEx.address, + env.provider, + { ...env.txDefaults, gasPrice: testUtils.gasPrice }, + abis, + ); + }); + + function getTestLimitOrder(fields: Partial = {}): LimitOrder { + return getRandomLimitOrder({ + maker, + verifyingContract, + chainId: 1337, + takerToken: takerToken.address, + makerToken: makerToken.address, + taker: NULL_ADDRESS, + sender: NULL_ADDRESS, + ...fields, + }); + } + function getTestRfqOrder(fields: Partial = {}): RfqOrder { + return getRandomRfqOrder({ + maker, + verifyingContract, + chainId: 1337, + takerToken: takerToken.address, + makerToken: makerToken.address, + txOrigin: taker, + ...fields, + }); + } + + describe('batchFillLimitOrders', () => { + async function assertExpectedFinalBalancesAsync( + orders: LimitOrder[], + takerTokenFillAmounts: BigNumber[] = orders.map(order => order.takerAmount), + takerTokenAlreadyFilledAmounts: BigNumber[] = orders.map(() => ZERO_AMOUNT), + receipt?: TransactionReceiptWithDecodedLogs, + ): Promise { + const expectedFeeRecipientBalances: { [feeRecipient: string]: BigNumber } = {}; + const { makerTokenFilledAmount, takerTokenFilledAmount } = orders + .map((order, i) => + computeLimitOrderFilledAmounts(order, takerTokenFillAmounts[i], takerTokenAlreadyFilledAmounts[i]), + ) + .reduce( + (previous, current, i) => { + _.update(expectedFeeRecipientBalances, orders[i].feeRecipient, balance => + (balance || ZERO_AMOUNT).plus(current.takerTokenFeeFilledAmount), + ); + return { + makerTokenFilledAmount: previous.makerTokenFilledAmount.plus( + current.makerTokenFilledAmount, + ), + takerTokenFilledAmount: previous.takerTokenFilledAmount.plus( + current.takerTokenFilledAmount, + ), + }; + }, + { makerTokenFilledAmount: ZERO_AMOUNT, takerTokenFilledAmount: ZERO_AMOUNT }, + ); + const makerBalance = await takerToken.balanceOf(maker).callAsync(); + const takerBalance = await makerToken.balanceOf(taker).callAsync(); + expect(makerBalance, 'maker token balance').to.bignumber.eq(takerTokenFilledAmount); + expect(takerBalance, 'taker token balance').to.bignumber.eq(makerTokenFilledAmount); + for (const [feeRecipient, expectedFeeRecipientBalance] of Object.entries(expectedFeeRecipientBalances)) { + const feeRecipientBalance = await takerToken.balanceOf(feeRecipient).callAsync(); + expect(feeRecipientBalance, `fee recipient balance`).to.bignumber.eq(expectedFeeRecipientBalance); + } + if (receipt) { + const balanceOfTakerNow = await env.web3Wrapper.getBalanceInWeiAsync(taker); + const balanceOfTakerBefore = await env.web3Wrapper.getBalanceInWeiAsync(taker, receipt.blockNumber - 1); + const protocolFees = testUtils.protocolFee.times(orders.length); + const totalCost = testUtils.gasPrice.times(receipt.gasUsed).plus(protocolFees); + expect(balanceOfTakerBefore.minus(totalCost), 'taker ETH balance').to.bignumber.eq(balanceOfTakerNow); + } + } + + it('Fully fills multiple orders', async () => { + const orders = [...new Array(3)].map(() => getTestLimitOrder({ takerTokenFeeAmount: ZERO_AMOUNT })); + const signatures = await Promise.all( + orders.map(order => order.getSignatureWithProviderAsync(env.provider)), + ); + await testUtils.prepareBalancesForOrdersAsync(orders); + const value = testUtils.protocolFee.times(orders.length); + const tx = await feature + .batchFillLimitOrders(orders, signatures, orders.map(order => order.takerAmount), false) + .awaitTransactionSuccessAsync({ from: taker, value }); + const [orderInfos] = await zeroEx.batchGetLimitOrderRelevantStates(orders, signatures).callAsync(); + orderInfos.map((orderInfo, i) => + assertOrderInfoEquals(orderInfo, { + status: OrderStatus.Filled, + orderHash: orders[i].getHash(), + takerTokenFilledAmount: orders[i].takerAmount, + }), + ); + verifyEventsFromLogs( + tx.logs, + orders.map(order => testUtils.createLimitOrderFilledEventArgs(order)), + IZeroExEvents.LimitOrderFilled, + ); + return assertExpectedFinalBalancesAsync(orders); + }); + it('Partially fills multiple orders', async () => { + const orders = [...new Array(3)].map(getTestLimitOrder); + const signatures = await Promise.all( + orders.map(order => order.getSignatureWithProviderAsync(env.provider)), + ); + await testUtils.prepareBalancesForOrdersAsync(orders); + const value = testUtils.protocolFee.times(orders.length); + const fillAmounts = orders.map(order => getRandomPortion(order.takerAmount)); + const tx = await feature + .batchFillLimitOrders(orders, signatures, fillAmounts, false) + .awaitTransactionSuccessAsync({ from: taker, value }); + const [orderInfos] = await zeroEx.batchGetLimitOrderRelevantStates(orders, signatures).callAsync(); + orderInfos.map((orderInfo, i) => + assertOrderInfoEquals(orderInfo, { + status: OrderStatus.Fillable, + orderHash: orders[i].getHash(), + takerTokenFilledAmount: fillAmounts[i], + }), + ); + verifyEventsFromLogs( + tx.logs, + orders.map((order, i) => testUtils.createLimitOrderFilledEventArgs(order, fillAmounts[i])), + IZeroExEvents.LimitOrderFilled, + ); + return assertExpectedFinalBalancesAsync(orders, fillAmounts); + }); + it('Fills multiple orders and refunds excess ETH', async () => { + const orders = [...new Array(3)].map(() => getTestLimitOrder({ takerTokenFeeAmount: ZERO_AMOUNT })); + const signatures = await Promise.all( + orders.map(order => order.getSignatureWithProviderAsync(env.provider)), + ); + await testUtils.prepareBalancesForOrdersAsync(orders); + const value = testUtils.protocolFee.times(orders.length).plus(420); + const tx = await feature + .batchFillLimitOrders(orders, signatures, orders.map(order => order.takerAmount), false) + .awaitTransactionSuccessAsync({ from: taker, value }); + const [orderInfos] = await zeroEx.batchGetLimitOrderRelevantStates(orders, signatures).callAsync(); + orderInfos.map((orderInfo, i) => + assertOrderInfoEquals(orderInfo, { + status: OrderStatus.Filled, + orderHash: orders[i].getHash(), + takerTokenFilledAmount: orders[i].takerAmount, + }), + ); + verifyEventsFromLogs( + tx.logs, + orders.map(order => testUtils.createLimitOrderFilledEventArgs(order)), + IZeroExEvents.LimitOrderFilled, + ); + return assertExpectedFinalBalancesAsync(orders); + }); + it('Skips over unfillable orders and refunds excess ETH', async () => { + const fillableOrders = [...new Array(3)].map(() => getTestLimitOrder({ takerTokenFeeAmount: ZERO_AMOUNT })); + const expiredOrder = getTestLimitOrder({ expiry: createExpiry(-1), takerTokenFeeAmount: ZERO_AMOUNT }); + const orders = [expiredOrder, ...fillableOrders]; + const signatures = await Promise.all( + orders.map(order => order.getSignatureWithProviderAsync(env.provider)), + ); + await testUtils.prepareBalancesForOrdersAsync(orders); + const value = testUtils.protocolFee.times(orders.length); + const tx = await feature + .batchFillLimitOrders(orders, signatures, orders.map(order => order.takerAmount), false) + .awaitTransactionSuccessAsync({ from: taker, value }); + const [orderInfos] = await zeroEx.batchGetLimitOrderRelevantStates(orders, signatures).callAsync(); + const [expiredOrderInfo, ...filledOrderInfos] = orderInfos; + assertOrderInfoEquals(expiredOrderInfo, { + status: OrderStatus.Expired, + orderHash: expiredOrder.getHash(), + takerTokenFilledAmount: ZERO_AMOUNT, + }); + filledOrderInfos.map((orderInfo, i) => + assertOrderInfoEquals(orderInfo, { + status: OrderStatus.Filled, + orderHash: fillableOrders[i].getHash(), + takerTokenFilledAmount: fillableOrders[i].takerAmount, + }), + ); + verifyEventsFromLogs( + tx.logs, + fillableOrders.map(order => testUtils.createLimitOrderFilledEventArgs(order)), + IZeroExEvents.LimitOrderFilled, + ); + return assertExpectedFinalBalancesAsync(fillableOrders); + }); + it('Fills multiple orders with revertIfIncomplete=true', async () => { + const orders = [...new Array(3)].map(() => getTestLimitOrder({ takerTokenFeeAmount: ZERO_AMOUNT })); + const signatures = await Promise.all( + orders.map(order => order.getSignatureWithProviderAsync(env.provider)), + ); + await testUtils.prepareBalancesForOrdersAsync(orders); + const value = testUtils.protocolFee.times(orders.length); + const tx = await feature + .batchFillLimitOrders(orders, signatures, orders.map(order => order.takerAmount), true) + .awaitTransactionSuccessAsync({ from: taker, value }); + const [orderInfos] = await zeroEx.batchGetLimitOrderRelevantStates(orders, signatures).callAsync(); + orderInfos.map((orderInfo, i) => + assertOrderInfoEquals(orderInfo, { + status: OrderStatus.Filled, + orderHash: orders[i].getHash(), + takerTokenFilledAmount: orders[i].takerAmount, + }), + ); + verifyEventsFromLogs( + tx.logs, + orders.map(order => testUtils.createLimitOrderFilledEventArgs(order)), + IZeroExEvents.LimitOrderFilled, + ); + return assertExpectedFinalBalancesAsync(orders); + }); + it('If revertIfIncomplete==true, reverts on an unfillable order', async () => { + const fillableOrders = [...new Array(3)].map(() => getTestLimitOrder({ takerTokenFeeAmount: ZERO_AMOUNT })); + const expiredOrder = getTestLimitOrder({ expiry: createExpiry(-1), takerTokenFeeAmount: ZERO_AMOUNT }); + const orders = [expiredOrder, ...fillableOrders]; + const signatures = await Promise.all( + orders.map(order => order.getSignatureWithProviderAsync(env.provider)), + ); + await testUtils.prepareBalancesForOrdersAsync(orders); + const value = testUtils.protocolFee.times(orders.length); + const tx = feature + .batchFillLimitOrders(orders, signatures, orders.map(order => order.takerAmount), true) + .awaitTransactionSuccessAsync({ from: taker, value }); + return expect(tx).to.revertWith( + new RevertErrors.NativeOrders.BatchFillIncompleteError( + expiredOrder.getHash(), + ZERO_AMOUNT, + expiredOrder.takerAmount, + ), + ); + }); + it('If revertIfIncomplete==true, reverts on an incomplete fill ', async () => { + const fillableOrders = [...new Array(3)].map(() => getTestLimitOrder({ takerTokenFeeAmount: ZERO_AMOUNT })); + const partiallyFilledOrder = getTestLimitOrder({ takerTokenFeeAmount: ZERO_AMOUNT }); + const partialFillAmount = getRandomPortion(partiallyFilledOrder.takerAmount); + await testUtils.fillLimitOrderAsync(partiallyFilledOrder, { fillAmount: partialFillAmount }); + const orders = [partiallyFilledOrder, ...fillableOrders]; + const signatures = await Promise.all( + orders.map(order => order.getSignatureWithProviderAsync(env.provider)), + ); + await testUtils.prepareBalancesForOrdersAsync(orders); + const value = testUtils.protocolFee.times(orders.length); + const tx = feature + .batchFillLimitOrders(orders, signatures, orders.map(order => order.takerAmount), true) + .awaitTransactionSuccessAsync({ from: taker, value }); + return expect(tx).to.revertWith( + new RevertErrors.NativeOrders.BatchFillIncompleteError( + partiallyFilledOrder.getHash(), + partiallyFilledOrder.takerAmount.minus(partialFillAmount), + partiallyFilledOrder.takerAmount, + ), + ); + }); + }); + describe('batchFillRfqOrders', () => { + async function assertExpectedFinalBalancesAsync( + orders: RfqOrder[], + takerTokenFillAmounts: BigNumber[] = orders.map(order => order.takerAmount), + takerTokenAlreadyFilledAmounts: BigNumber[] = orders.map(() => ZERO_AMOUNT), + ): Promise { + const { makerTokenFilledAmount, takerTokenFilledAmount } = orders + .map((order, i) => + computeRfqOrderFilledAmounts(order, takerTokenFillAmounts[i], takerTokenAlreadyFilledAmounts[i]), + ) + .reduce((previous, current) => ({ + makerTokenFilledAmount: previous.makerTokenFilledAmount.plus(current.makerTokenFilledAmount), + takerTokenFilledAmount: previous.takerTokenFilledAmount.plus(current.takerTokenFilledAmount), + })); + 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); + } + + it('Fully fills multiple orders', async () => { + const orders = [...new Array(3)].map(() => getTestRfqOrder()); + const signatures = await Promise.all( + orders.map(order => order.getSignatureWithProviderAsync(env.provider)), + ); + await testUtils.prepareBalancesForOrdersAsync(orders); + const tx = await feature + .batchFillRfqOrders(orders, signatures, orders.map(order => order.takerAmount), false) + .awaitTransactionSuccessAsync({ from: taker }); + const [orderInfos] = await zeroEx.batchGetRfqOrderRelevantStates(orders, signatures).callAsync(); + orderInfos.map((orderInfo, i) => + assertOrderInfoEquals(orderInfo, { + status: OrderStatus.Filled, + orderHash: orders[i].getHash(), + takerTokenFilledAmount: orders[i].takerAmount, + }), + ); + verifyEventsFromLogs( + tx.logs, + orders.map(order => testUtils.createRfqOrderFilledEventArgs(order)), + IZeroExEvents.RfqOrderFilled, + ); + return assertExpectedFinalBalancesAsync(orders); + }); + it('Partially fills multiple orders', async () => { + const orders = [...new Array(3)].map(() => getTestRfqOrder()); + const signatures = await Promise.all( + orders.map(order => order.getSignatureWithProviderAsync(env.provider)), + ); + const fillAmounts = orders.map(order => getRandomPortion(order.takerAmount)); + await testUtils.prepareBalancesForOrdersAsync(orders); + const tx = await feature + .batchFillRfqOrders(orders, signatures, fillAmounts, false) + .awaitTransactionSuccessAsync({ from: taker }); + const [orderInfos] = await zeroEx.batchGetRfqOrderRelevantStates(orders, signatures).callAsync(); + orderInfos.map((orderInfo, i) => + assertOrderInfoEquals(orderInfo, { + status: OrderStatus.Fillable, + orderHash: orders[i].getHash(), + takerTokenFilledAmount: fillAmounts[i], + }), + ); + verifyEventsFromLogs( + tx.logs, + orders.map((order, i) => testUtils.createRfqOrderFilledEventArgs(order, fillAmounts[i])), + IZeroExEvents.RfqOrderFilled, + ); + return assertExpectedFinalBalancesAsync(orders, fillAmounts); + }); + it('Skips over unfillable orders', async () => { + const fillableOrders = [...new Array(3)].map(() => getTestRfqOrder()); + const expiredOrder = getTestRfqOrder({ expiry: createExpiry(-1) }); + const orders = [expiredOrder, ...fillableOrders]; + const signatures = await Promise.all( + orders.map(order => order.getSignatureWithProviderAsync(env.provider)), + ); + await testUtils.prepareBalancesForOrdersAsync(orders); + const tx = await feature + .batchFillRfqOrders(orders, signatures, orders.map(order => order.takerAmount), false) + .awaitTransactionSuccessAsync({ from: taker }); + const [orderInfos] = await zeroEx.batchGetRfqOrderRelevantStates(orders, signatures).callAsync(); + const [expiredOrderInfo, ...filledOrderInfos] = orderInfos; + assertOrderInfoEquals(expiredOrderInfo, { + status: OrderStatus.Expired, + orderHash: expiredOrder.getHash(), + takerTokenFilledAmount: ZERO_AMOUNT, + }); + filledOrderInfos.map((orderInfo, i) => + assertOrderInfoEquals(orderInfo, { + status: OrderStatus.Filled, + orderHash: fillableOrders[i].getHash(), + takerTokenFilledAmount: fillableOrders[i].takerAmount, + }), + ); + verifyEventsFromLogs( + tx.logs, + fillableOrders.map(order => testUtils.createRfqOrderFilledEventArgs(order)), + IZeroExEvents.RfqOrderFilled, + ); + return assertExpectedFinalBalancesAsync(fillableOrders); + }); + it('Fills multiple orders with revertIfIncomplete=true', async () => { + const orders = [...new Array(3)].map(() => getTestRfqOrder()); + const signatures = await Promise.all( + orders.map(order => order.getSignatureWithProviderAsync(env.provider)), + ); + await testUtils.prepareBalancesForOrdersAsync(orders); + const tx = await feature + .batchFillRfqOrders(orders, signatures, orders.map(order => order.takerAmount), true) + .awaitTransactionSuccessAsync({ from: taker }); + const [orderInfos] = await zeroEx.batchGetRfqOrderRelevantStates(orders, signatures).callAsync(); + orderInfos.map((orderInfo, i) => + assertOrderInfoEquals(orderInfo, { + status: OrderStatus.Filled, + orderHash: orders[i].getHash(), + takerTokenFilledAmount: orders[i].takerAmount, + }), + ); + verifyEventsFromLogs( + tx.logs, + orders.map(order => testUtils.createRfqOrderFilledEventArgs(order)), + IZeroExEvents.RfqOrderFilled, + ); + return assertExpectedFinalBalancesAsync(orders); + }); + it('If revertIfIncomplete==true, reverts on an unfillable order', async () => { + const fillableOrders = [...new Array(3)].map(() => getTestRfqOrder()); + const expiredOrder = getTestRfqOrder({ expiry: createExpiry(-1) }); + const orders = [expiredOrder, ...fillableOrders]; + const signatures = await Promise.all( + orders.map(order => order.getSignatureWithProviderAsync(env.provider)), + ); + await testUtils.prepareBalancesForOrdersAsync(orders); + const tx = feature + .batchFillRfqOrders(orders, signatures, orders.map(order => order.takerAmount), true) + .awaitTransactionSuccessAsync({ from: taker }); + return expect(tx).to.revertWith( + new RevertErrors.NativeOrders.BatchFillIncompleteError( + expiredOrder.getHash(), + ZERO_AMOUNT, + expiredOrder.takerAmount, + ), + ); + }); + it('If revertIfIncomplete==true, reverts on an incomplete fill ', async () => { + const fillableOrders = [...new Array(3)].map(() => getTestRfqOrder()); + const partiallyFilledOrder = getTestRfqOrder(); + const partialFillAmount = getRandomPortion(partiallyFilledOrder.takerAmount); + await testUtils.fillRfqOrderAsync(partiallyFilledOrder, partialFillAmount); + const orders = [partiallyFilledOrder, ...fillableOrders]; + const signatures = await Promise.all( + orders.map(order => order.getSignatureWithProviderAsync(env.provider)), + ); + await testUtils.prepareBalancesForOrdersAsync(orders); + const tx = feature + .batchFillRfqOrders(orders, signatures, orders.map(order => order.takerAmount), true) + .awaitTransactionSuccessAsync({ from: taker }); + return expect(tx).to.revertWith( + new RevertErrors.NativeOrders.BatchFillIncompleteError( + partiallyFilledOrder.getHash(), + partiallyFilledOrder.takerAmount.minus(partialFillAmount), + partiallyFilledOrder.takerAmount, + ), + ); + }); + }); +}); diff --git a/contracts/zero-ex/test/features/multiplex_test.ts b/contracts/zero-ex/test/features/multiplex_test.ts new file mode 100644 index 0000000000..3df817a8d4 --- /dev/null +++ b/contracts/zero-ex/test/features/multiplex_test.ts @@ -0,0 +1,764 @@ +import { + artifacts as erc20Artifacts, + ERC20TokenContract, + WETH9Contract, + WETH9DepositEventArgs, + WETH9Events, + WETH9WithdrawalEventArgs, +} from '@0x/contracts-erc20'; +import { blockchainTests, constants, expect, filterLogsToArguments, toBaseUnitAmount } from '@0x/contracts-test-utils'; +import { + BridgeSource, + encodeFillQuoteTransformerData, + encodePayTakerTransformerData, + FillQuoteTransformerOrderType, + FillQuoteTransformerSide, + findTransformerNonce, + RfqOrder, + SIGNATURE_ABI, +} from '@0x/protocol-utils'; +import { AbiEncoder, BigNumber, logUtils } from '@0x/utils'; +import * as _ from 'lodash'; + +import { artifacts } from '../artifacts'; +import { abis } from '../utils/abis'; +import { getRandomRfqOrder } from '../utils/orders'; +import { + BridgeAdapterBridgeFillEventArgs, + BridgeAdapterEvents, + IUniswapV2PairEvents, + IUniswapV2PairSwapEventArgs, + IZeroExContract, + IZeroExEvents, + IZeroExRfqOrderFilledEventArgs, + MultiplexFeatureContract, + MultiplexFeatureEvents, + MultiplexFeatureLiquidityProviderSwapEventArgs, + SimpleFunctionRegistryFeatureContract, +} from '../wrappers'; + +const HIGH_BIT = new BigNumber(2).pow(255); +function encodeFractionalFillAmount(frac: number): BigNumber { + return HIGH_BIT.plus(new BigNumber(frac).times('1e18').integerValue()); +} + +const EP_GOVERNOR = '0x618f9c67ce7bf1a50afa1e7e0238422601b0ff6e'; +const DAI_WALLET = '0xbe0eb53f46cd790cd13851d5eff43d12404d33e8'; +const WETH_WALLET = '0x1e0447b19bb6ecfdae1e4ae1694b0c3659614e4e'; +const USDC_WALLET = '0xbe0eb53f46cd790cd13851d5eff43d12404d33e8'; +blockchainTests.configure({ + fork: { + unlockedAccounts: [EP_GOVERNOR, DAI_WALLET, WETH_WALLET, USDC_WALLET], + }, +}); + +interface WrappedBatchCall { + selector: string; + sellAmount: BigNumber; + data: string; +} + +blockchainTests.fork.skip('Multiplex feature', env => { + const DAI_ADDRESS = '0x6b175474e89094c44da98b954eedeac495271d0f'; + const dai = new ERC20TokenContract(DAI_ADDRESS, env.provider, env.txDefaults); + const ETH_TOKEN_ADDRESS = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'; + const WETH_ADDRESS = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'; + const weth = new WETH9Contract(WETH_ADDRESS, env.provider, env.txDefaults); + const USDC_ADDRESS = '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48'; + const USDT_ADDRESS = '0xdac17f958d2ee523a2206206994597c13d831ec7'; + const usdt = new ERC20TokenContract(USDT_ADDRESS, env.provider, env.txDefaults); + const LON_ADDRESS = '0x0000000000095413afc295d19edeb1ad7b71c952'; + const PLP_SANDBOX_ADDRESS = '0x407b4128e9ecad8769b2332312a9f655cb9f5f3a'; + const WETH_DAI_PLP_ADDRESS = '0x1db681925786441ba82adefac7bf492089665ca0'; + const WETH_USDC_PLP_ADDRESS = '0x8463c03c0c57ff19fa8b431e0d3a34e2df89888e'; + const USDC_USDT_PLP_ADDRESS = '0xc340ef96449514cea4dfa11d847a06d7f03d437c'; + const GREEDY_TOKENS_BLOOM_FILTER = '0x0000100800000480002c00401000000820000000000000020000001010800001'; + const BALANCER_WETH_DAI = '0x8b6e6e7b5b3801fed2cafd4b22b8a16c2f2db21a'; + const fqtNonce = findTransformerNonce( + '0xfa6282736af206cb4cfc5cb786d82aecdf1186f9', + '0x39dce47a67ad34344eab877eae3ef1fa2a1d50bb', + ); + const payTakerNonce = findTransformerNonce( + '0x4638a7ebe75b911b995d0ec73a81e4f85f41f24e', + '0x39dce47a67ad34344eab877eae3ef1fa2a1d50bb', + ); + + let zeroEx: IZeroExContract; + let multiplex: MultiplexFeatureContract; + let rfqMaker: string; + let flashWalletAddress: string; + + before(async () => { + const erc20Abis = _.mapValues(erc20Artifacts, v => v.compilerOutput.abi); + [rfqMaker] = await env.getAccountAddressesAsync(); + zeroEx = new IZeroExContract('0xdef1c0ded9bec7f1a1670819833240f027b25eff', env.provider, env.txDefaults, { + ...abis, + ...erc20Abis, + }); + flashWalletAddress = await zeroEx.getTransformWallet().callAsync(); + const registry = new SimpleFunctionRegistryFeatureContract(zeroEx.address, env.provider, env.txDefaults, { + ...abis, + ...erc20Abis, + }); + multiplex = new MultiplexFeatureContract(zeroEx.address, env.provider, env.txDefaults, { + ...abis, + ...erc20Abis, + }); + const multiplexImpl = await MultiplexFeatureContract.deployFrom0xArtifactAsync( + artifacts.MultiplexFeature, + env.provider, + env.txDefaults, + artifacts, + zeroEx.address, + WETH_ADDRESS, + PLP_SANDBOX_ADDRESS, + GREEDY_TOKENS_BLOOM_FILTER, + ); + await registry + .extend(multiplex.getSelector('batchFill'), multiplexImpl.address) + .awaitTransactionSuccessAsync({ from: EP_GOVERNOR, gasPrice: 0 }, { shouldValidate: false }); + await registry + .extend(multiplex.getSelector('multiHopFill'), multiplexImpl.address) + .awaitTransactionSuccessAsync({ from: EP_GOVERNOR, gasPrice: 0 }, { shouldValidate: false }); + await dai + .approve(zeroEx.address, constants.MAX_UINT256) + .awaitTransactionSuccessAsync({ from: DAI_WALLET, gasPrice: 0 }, { shouldValidate: false }); + await weth + .transfer(rfqMaker, toBaseUnitAmount(100)) + .awaitTransactionSuccessAsync({ from: WETH_WALLET, gasPrice: 0 }, { shouldValidate: false }); + await weth + .approve(zeroEx.address, constants.MAX_UINT256) + .awaitTransactionSuccessAsync({ from: rfqMaker, gasPrice: 0 }, { shouldValidate: false }); + }); + describe('batchFill', () => { + let rfqDataEncoder: AbiEncoder.DataType; + let uniswapCall: WrappedBatchCall; + let sushiswapCall: WrappedBatchCall; + let plpCall: WrappedBatchCall; + let rfqCall: WrappedBatchCall; + let rfqOrder: RfqOrder; + before(async () => { + rfqDataEncoder = AbiEncoder.create([ + { name: 'order', type: 'tuple', components: RfqOrder.STRUCT_ABI }, + { name: 'signature', type: 'tuple', components: SIGNATURE_ABI }, + ]); + rfqOrder = getRandomRfqOrder({ + maker: rfqMaker, + verifyingContract: zeroEx.address, + chainId: 1, + takerToken: DAI_ADDRESS, + makerToken: WETH_ADDRESS, + makerAmount: toBaseUnitAmount(100), + takerAmount: toBaseUnitAmount(100), + txOrigin: DAI_WALLET, + }); + rfqCall = { + selector: zeroEx.getSelector('_fillRfqOrder'), + sellAmount: toBaseUnitAmount(1), + data: rfqDataEncoder.encode({ + order: rfqOrder, + signature: await rfqOrder.getSignatureWithProviderAsync(env.provider), + }), + }; + const uniswapDataEncoder = AbiEncoder.create([ + { name: 'tokens', type: 'address[]' }, + { name: 'isSushi', type: 'bool' }, + ]); + const plpDataEncoder = AbiEncoder.create([ + { name: 'provider', type: 'address' }, + { name: 'auxiliaryData', type: 'bytes' }, + ]); + uniswapCall = { + selector: multiplex.getSelector('_sellToUniswap'), + sellAmount: toBaseUnitAmount(1.01), + data: uniswapDataEncoder.encode({ tokens: [DAI_ADDRESS, WETH_ADDRESS], isSushi: false }), + }; + sushiswapCall = { + selector: multiplex.getSelector('_sellToUniswap'), + sellAmount: toBaseUnitAmount(1.02), + data: uniswapDataEncoder.encode({ tokens: [DAI_ADDRESS, WETH_ADDRESS], isSushi: true }), + }; + plpCall = { + selector: multiplex.getSelector('_sellToLiquidityProvider'), + sellAmount: toBaseUnitAmount(1.03), + data: plpDataEncoder.encode({ + provider: WETH_DAI_PLP_ADDRESS, + auxiliaryData: constants.NULL_BYTES, + }), + }; + }); + it('MultiplexFeature.batchFill(RFQ, unused Uniswap fallback)', async () => { + const batchFillData = { + inputToken: DAI_ADDRESS, + outputToken: WETH_ADDRESS, + sellAmount: rfqCall.sellAmount, + calls: [rfqCall, uniswapCall], + }; + const tx = await multiplex + .batchFill(batchFillData, constants.ZERO_AMOUNT) + .awaitTransactionSuccessAsync({ from: DAI_WALLET, gasPrice: 0 }, { shouldValidate: false }); + logUtils.log(`${tx.gasUsed} gas used`); + + const [rfqEvent] = filterLogsToArguments( + tx.logs, + IZeroExEvents.RfqOrderFilled, + ); + expect(rfqEvent.maker).to.equal(rfqMaker); + expect(rfqEvent.taker).to.equal(DAI_WALLET); + expect(rfqEvent.makerToken).to.equal(WETH_ADDRESS); + expect(rfqEvent.takerToken).to.equal(DAI_ADDRESS); + expect(rfqEvent.takerTokenFilledAmount).to.bignumber.equal(rfqCall.sellAmount); + expect(rfqEvent.makerTokenFilledAmount).to.bignumber.equal(rfqCall.sellAmount); + }); + it('MultiplexFeature.batchFill(expired RFQ, Uniswap fallback)', async () => { + const expiredRfqOrder = getRandomRfqOrder({ + maker: rfqMaker, + verifyingContract: zeroEx.address, + chainId: 1, + takerToken: DAI_ADDRESS, + makerToken: WETH_ADDRESS, + makerAmount: toBaseUnitAmount(100), + takerAmount: toBaseUnitAmount(100), + txOrigin: DAI_WALLET, + expiry: new BigNumber(0), + }); + const expiredRfqCall = { + selector: zeroEx.getSelector('_fillRfqOrder'), + sellAmount: toBaseUnitAmount(1.23), + data: rfqDataEncoder.encode({ + order: expiredRfqOrder, + signature: await expiredRfqOrder.getSignatureWithProviderAsync(env.provider), + }), + }; + + const batchFillData = { + inputToken: DAI_ADDRESS, + outputToken: WETH_ADDRESS, + sellAmount: expiredRfqCall.sellAmount, + calls: [expiredRfqCall, uniswapCall], + }; + const tx = await multiplex + .batchFill(batchFillData, constants.ZERO_AMOUNT) + .awaitTransactionSuccessAsync({ from: DAI_WALLET, gasPrice: 0 }, { shouldValidate: false }); + logUtils.log(`${tx.gasUsed} gas used`); + const [uniswapEvent] = filterLogsToArguments( + tx.logs, + IUniswapV2PairEvents.Swap, + ); + expect(uniswapEvent.sender, 'Uniswap Swap event sender').to.equal(zeroEx.address); + expect(uniswapEvent.to, 'Uniswap Swap event to').to.equal(DAI_WALLET); + expect( + BigNumber.max(uniswapEvent.amount0In, uniswapEvent.amount1In), + 'Uniswap Swap event input amount', + ).to.bignumber.equal(uniswapCall.sellAmount); + expect( + BigNumber.max(uniswapEvent.amount0Out, uniswapEvent.amount1Out), + 'Uniswap Swap event output amount', + ).to.bignumber.gt(0); + }); + it('MultiplexFeature.batchFill(expired RFQ, Balancer FQT fallback)', async () => { + const expiredRfqOrder = getRandomRfqOrder({ + maker: rfqMaker, + verifyingContract: zeroEx.address, + chainId: 1, + takerToken: DAI_ADDRESS, + makerToken: WETH_ADDRESS, + makerAmount: toBaseUnitAmount(100), + takerAmount: toBaseUnitAmount(100), + txOrigin: DAI_WALLET, + expiry: new BigNumber(0), + }); + const expiredRfqCall = { + selector: zeroEx.getSelector('_fillRfqOrder'), + sellAmount: toBaseUnitAmount(1.23), + data: rfqDataEncoder.encode({ + order: expiredRfqOrder, + signature: await expiredRfqOrder.getSignatureWithProviderAsync(env.provider), + }), + }; + const poolEncoder = AbiEncoder.create([{ name: 'poolAddress', type: 'address' }]); + const fqtData = encodeFillQuoteTransformerData({ + side: FillQuoteTransformerSide.Sell, + sellToken: DAI_ADDRESS, + buyToken: WETH_ADDRESS, + bridgeOrders: [ + { + source: BridgeSource.Balancer, + takerTokenAmount: expiredRfqCall.sellAmount, + makerTokenAmount: expiredRfqCall.sellAmount, + bridgeData: poolEncoder.encode([BALANCER_WETH_DAI]), + }, + ], + limitOrders: [], + rfqOrders: [], + fillSequence: [FillQuoteTransformerOrderType.Bridge], + fillAmount: expiredRfqCall.sellAmount, + refundReceiver: constants.NULL_ADDRESS, + }); + const payTakerData = encodePayTakerTransformerData({ + tokens: [WETH_ADDRESS], + amounts: [constants.MAX_UINT256], + }); + const transformERC20Encoder = AbiEncoder.create([ + { + name: 'transformations', + type: 'tuple[]', + components: [{ name: 'deploymentNonce', type: 'uint32' }, { name: 'data', type: 'bytes' }], + }, + { name: 'ethValue', type: 'uint256' }, + ]); + const balancerFqtCall = { + selector: zeroEx.getSelector('_transformERC20'), + sellAmount: expiredRfqCall.sellAmount, + data: transformERC20Encoder.encode({ + transformations: [ + { + deploymentNonce: fqtNonce, + data: fqtData, + }, + { + deploymentNonce: payTakerNonce, + data: payTakerData, + }, + ], + ethValue: constants.ZERO_AMOUNT, + }), + }; + + const batchFillData = { + inputToken: DAI_ADDRESS, + outputToken: WETH_ADDRESS, + sellAmount: expiredRfqCall.sellAmount, + calls: [expiredRfqCall, balancerFqtCall], + }; + const tx = await multiplex + .batchFill(batchFillData, constants.ZERO_AMOUNT) + .awaitTransactionSuccessAsync({ from: DAI_WALLET, gasPrice: 0 }, { shouldValidate: false }); + logUtils.log(`${tx.gasUsed} gas used`); + const [bridgeFillEvent] = filterLogsToArguments( + tx.logs, + BridgeAdapterEvents.BridgeFill, + ); + expect(bridgeFillEvent.source).to.bignumber.equal(BridgeSource.Balancer); + expect(bridgeFillEvent.inputToken).to.equal(DAI_ADDRESS); + expect(bridgeFillEvent.outputToken).to.equal(WETH_ADDRESS); + expect(bridgeFillEvent.inputTokenAmount).to.bignumber.equal(expiredRfqCall.sellAmount); + expect(bridgeFillEvent.outputTokenAmount).to.bignumber.gt(0); + }); + it('MultiplexFeature.batchFill(Sushiswap, PLP, Uniswap, RFQ)', async () => { + const batchFillData = { + inputToken: DAI_ADDRESS, + outputToken: WETH_ADDRESS, + sellAmount: BigNumber.sum( + sushiswapCall.sellAmount, + plpCall.sellAmount, + uniswapCall.sellAmount, + rfqCall.sellAmount, + ), + calls: [sushiswapCall, plpCall, uniswapCall, rfqCall], + }; + const tx = await multiplex + .batchFill(batchFillData, constants.ZERO_AMOUNT) + .awaitTransactionSuccessAsync({ from: DAI_WALLET, gasPrice: 0 }, { shouldValidate: false }); + logUtils.log(`${tx.gasUsed} gas used`); + const [sushiswapEvent, uniswapEvent] = filterLogsToArguments( + tx.logs, + IUniswapV2PairEvents.Swap, + ); + expect(sushiswapEvent.sender, 'Sushiswap Swap event sender').to.equal(zeroEx.address); + expect(sushiswapEvent.to, 'Sushiswap Swap event to').to.equal(DAI_WALLET); + expect( + BigNumber.max(sushiswapEvent.amount0In, sushiswapEvent.amount1In), + 'Sushiswap Swap event input amount', + ).to.bignumber.equal(sushiswapCall.sellAmount); + expect( + BigNumber.max(sushiswapEvent.amount0Out, sushiswapEvent.amount1Out), + 'Sushiswap Swap event output amount', + ).to.bignumber.gt(0); + expect(uniswapEvent.sender, 'Uniswap Swap event sender').to.equal(zeroEx.address); + expect(uniswapEvent.to, 'Uniswap Swap event to').to.equal(DAI_WALLET); + expect( + BigNumber.max(uniswapEvent.amount0In, uniswapEvent.amount1In), + 'Uniswap Swap event input amount', + ).to.bignumber.equal(uniswapCall.sellAmount); + expect( + BigNumber.max(uniswapEvent.amount0Out, uniswapEvent.amount1Out), + 'Uniswap Swap event output amount', + ).to.bignumber.gt(0); + + const [plpEvent] = filterLogsToArguments( + tx.logs, + MultiplexFeatureEvents.LiquidityProviderSwap, + ); + expect(plpEvent.inputToken, 'LiquidityProviderSwap event inputToken').to.equal(batchFillData.inputToken); + expect(plpEvent.outputToken, 'LiquidityProviderSwap event outputToken').to.equal(batchFillData.outputToken); + expect(plpEvent.inputTokenAmount, 'LiquidityProviderSwap event inputToken').to.bignumber.equal( + plpCall.sellAmount, + ); + expect(plpEvent.outputTokenAmount, 'LiquidityProviderSwap event outputTokenAmount').to.bignumber.gt(0); + expect(plpEvent.provider, 'LiquidityProviderSwap event provider address').to.equal(WETH_DAI_PLP_ADDRESS); + expect(plpEvent.recipient, 'LiquidityProviderSwap event recipient address').to.equal(DAI_WALLET); + + const [rfqEvent] = filterLogsToArguments( + tx.logs, + IZeroExEvents.RfqOrderFilled, + ); + expect(rfqEvent.maker).to.equal(rfqMaker); + expect(rfqEvent.taker).to.equal(DAI_WALLET); + expect(rfqEvent.makerToken).to.equal(WETH_ADDRESS); + expect(rfqEvent.takerToken).to.equal(DAI_ADDRESS); + expect(rfqEvent.takerTokenFilledAmount).to.bignumber.equal(rfqCall.sellAmount); + expect(rfqEvent.makerTokenFilledAmount).to.bignumber.equal(rfqCall.sellAmount); + }); + }); + describe('multiHopFill', () => { + let uniswapDataEncoder: AbiEncoder.DataType; + let plpDataEncoder: AbiEncoder.DataType; + let curveEncoder: AbiEncoder.DataType; + let transformERC20Encoder: AbiEncoder.DataType; + let batchFillEncoder: AbiEncoder.DataType; + let multiHopFillEncoder: AbiEncoder.DataType; + + before(async () => { + uniswapDataEncoder = AbiEncoder.create([ + { name: 'tokens', type: 'address[]' }, + { name: 'isSushi', type: 'bool' }, + ]); + plpDataEncoder = AbiEncoder.create([ + { name: 'provider', type: 'address' }, + { name: 'auxiliaryData', type: 'bytes' }, + ]); + curveEncoder = AbiEncoder.create([ + { name: 'curveAddress', type: 'address' }, + { name: 'exchangeFunctionSelector', type: 'bytes4' }, + { name: 'fromTokenIdx', type: 'int128' }, + { name: 'toTokenIdx', type: 'int128' }, + ]); + transformERC20Encoder = AbiEncoder.create([ + { + name: 'transformations', + type: 'tuple[]', + components: [{ name: 'deploymentNonce', type: 'uint32' }, { name: 'data', type: 'bytes' }], + }, + { name: 'ethValue', type: 'uint256' }, + ]); + batchFillEncoder = AbiEncoder.create([ + { + name: 'calls', + type: 'tuple[]', + components: [ + { name: 'selector', type: 'bytes4' }, + { name: 'sellAmount', type: 'uint256' }, + { name: 'data', type: 'bytes' }, + ], + }, + { name: 'ethValue', type: 'uint256' }, + ]); + multiHopFillEncoder = AbiEncoder.create([ + { name: 'tokens', type: 'address[]' }, + { + name: 'calls', + type: 'tuple[]', + components: [{ name: 'selector', type: 'bytes4' }, { name: 'data', type: 'bytes' }], + }, + { name: 'ethValue', type: 'uint256' }, + ]); + }); + it('MultiplexFeature.multiHopFill(DAI ––Curve––> USDC ––Uni––> WETH ––unwrap––> ETH)', async () => { + const sellAmount = toBaseUnitAmount(1000000); // 1M DAI + const fqtData = encodeFillQuoteTransformerData({ + side: FillQuoteTransformerSide.Sell, + sellToken: DAI_ADDRESS, + buyToken: USDC_ADDRESS, + bridgeOrders: [ + { + source: BridgeSource.Curve, + takerTokenAmount: sellAmount, + makerTokenAmount: sellAmount, + bridgeData: curveEncoder.encode([ + '0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7', // 3-pool + '0x3df02124', // `exchange` selector + 0, // DAI + 1, // USDC + ]), + }, + ], + limitOrders: [], + rfqOrders: [], + fillSequence: [FillQuoteTransformerOrderType.Bridge], + fillAmount: sellAmount, + refundReceiver: constants.NULL_ADDRESS, + }); + const payTakerData = encodePayTakerTransformerData({ + tokens: [USDC_ADDRESS], + amounts: [constants.MAX_UINT256], + }); + const curveFqtCall = { + selector: zeroEx.getSelector('_transformERC20'), + sellAmount, + data: transformERC20Encoder.encode({ + transformations: [ + { + deploymentNonce: fqtNonce, + data: fqtData, + }, + { + deploymentNonce: payTakerNonce, + data: payTakerData, + }, + ], + ethValue: constants.ZERO_AMOUNT, + }), + }; + const uniswapCall = { + selector: multiplex.getSelector('_sellToUniswap'), + data: uniswapDataEncoder.encode({ tokens: [USDC_ADDRESS, WETH_ADDRESS], isSushi: false }), + }; + const unwrapEthCall = { + selector: weth.getSelector('withdraw'), + data: constants.NULL_BYTES, + }; + const multiHopFillData = { + tokens: [DAI_ADDRESS, USDC_ADDRESS, WETH_ADDRESS, ETH_TOKEN_ADDRESS], + sellAmount, + calls: [curveFqtCall, uniswapCall, unwrapEthCall], + }; + const tx = await multiplex + .multiHopFill(multiHopFillData, constants.ZERO_AMOUNT) + .awaitTransactionSuccessAsync({ from: DAI_WALLET, gasPrice: 0 }, { shouldValidate: false }); + logUtils.log(`${tx.gasUsed} gas used`); + const [bridgeFillEvent] = filterLogsToArguments( + tx.logs, + BridgeAdapterEvents.BridgeFill, + ); + expect(bridgeFillEvent.source).to.bignumber.equal(BridgeSource.Curve); + expect(bridgeFillEvent.inputToken).to.equal(DAI_ADDRESS); + expect(bridgeFillEvent.outputToken).to.equal(USDC_ADDRESS); + expect(bridgeFillEvent.inputTokenAmount).to.bignumber.equal(sellAmount); + expect(bridgeFillEvent.outputTokenAmount).to.bignumber.gt(0); + const [uniswapEvent] = filterLogsToArguments( + tx.logs, + IUniswapV2PairEvents.Swap, + ); + expect(uniswapEvent.sender, 'Uniswap Swap event sender').to.equal(zeroEx.address); + expect(uniswapEvent.to, 'Uniswap Swap event to').to.equal(zeroEx.address); + const uniswapInputAmount = BigNumber.max(uniswapEvent.amount0In, uniswapEvent.amount1In); + expect(uniswapInputAmount, 'Uniswap Swap event input amount').to.bignumber.equal( + bridgeFillEvent.outputTokenAmount, + ); + const uniswapOutputAmount = BigNumber.max(uniswapEvent.amount0Out, uniswapEvent.amount1Out); + expect(uniswapOutputAmount, 'Uniswap Swap event output amount').to.bignumber.gt(0); + const [wethWithdrawalEvent] = filterLogsToArguments( + tx.logs, + WETH9Events.Withdrawal, + ); + expect(wethWithdrawalEvent._owner, 'WETH Withdrawal event _owner').to.equal(zeroEx.address); + expect(wethWithdrawalEvent._value, 'WETH Withdrawal event _value').to.bignumber.equal(uniswapOutputAmount); + }); + it('MultiplexFeature.multiHopFill(ETH ––wrap–-> WETH ––Uni––> USDC ––Curve––> DAI)', async () => { + const sellAmount = toBaseUnitAmount(1); // 1 ETH + const fqtData = encodeFillQuoteTransformerData({ + side: FillQuoteTransformerSide.Sell, + sellToken: USDC_ADDRESS, + buyToken: DAI_ADDRESS, + bridgeOrders: [ + { + source: BridgeSource.Curve, + takerTokenAmount: constants.MAX_UINT256, + makerTokenAmount: constants.MAX_UINT256, + bridgeData: curveEncoder.encode([ + '0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7', // 3-pool + '0x3df02124', // `exchange` selector + 1, // USDC + 0, // DAI + ]), + }, + ], + limitOrders: [], + rfqOrders: [], + fillSequence: [FillQuoteTransformerOrderType.Bridge], + fillAmount: constants.MAX_UINT256, + refundReceiver: constants.NULL_ADDRESS, + }); + const payTakerData = encodePayTakerTransformerData({ + tokens: [DAI_ADDRESS], + amounts: [constants.MAX_UINT256], + }); + const curveFqtCall = { + selector: zeroEx.getSelector('_transformERC20'), + data: transformERC20Encoder.encode({ + transformations: [ + { + deploymentNonce: fqtNonce, + data: fqtData, + }, + { + deploymentNonce: payTakerNonce, + data: payTakerData, + }, + ], + ethValue: constants.ZERO_AMOUNT, + }), + }; + const uniswapCall = { + selector: multiplex.getSelector('_sellToUniswap'), + data: uniswapDataEncoder.encode({ tokens: [WETH_ADDRESS, USDC_ADDRESS], isSushi: false }), + }; + const wrapEthCall = { + selector: weth.getSelector('deposit'), + data: constants.NULL_BYTES, + }; + const multiHopFillData = { + tokens: [ETH_TOKEN_ADDRESS, WETH_ADDRESS, USDC_ADDRESS, DAI_ADDRESS], + sellAmount, + calls: [wrapEthCall, uniswapCall, curveFqtCall], + }; + const tx = await multiplex + .multiHopFill(multiHopFillData, constants.ZERO_AMOUNT) + .awaitTransactionSuccessAsync( + { from: rfqMaker, gasPrice: 0, value: sellAmount }, + { shouldValidate: false }, + ); + logUtils.log(`${tx.gasUsed} gas used`); + + const [wethDepositEvent] = filterLogsToArguments(tx.logs, WETH9Events.Deposit); + expect(wethDepositEvent._owner, 'WETH Deposit event _owner').to.equal(zeroEx.address); + expect(wethDepositEvent._value, 'WETH Deposit event _value').to.bignumber.equal(sellAmount); + + const [uniswapEvent] = filterLogsToArguments( + tx.logs, + IUniswapV2PairEvents.Swap, + ); + expect(uniswapEvent.sender, 'Uniswap Swap event sender').to.equal(zeroEx.address); + expect(uniswapEvent.to, 'Uniswap Swap event to').to.equal(flashWalletAddress); + const uniswapInputAmount = BigNumber.max(uniswapEvent.amount0In, uniswapEvent.amount1In); + expect(uniswapInputAmount, 'Uniswap Swap event input amount').to.bignumber.equal(sellAmount); + const uniswapOutputAmount = BigNumber.max(uniswapEvent.amount0Out, uniswapEvent.amount1Out); + expect(uniswapOutputAmount, 'Uniswap Swap event output amount').to.bignumber.gt(0); + + const [bridgeFillEvent] = filterLogsToArguments( + tx.logs, + BridgeAdapterEvents.BridgeFill, + ); + expect(bridgeFillEvent.source).to.bignumber.equal(BridgeSource.Curve); + expect(bridgeFillEvent.inputToken).to.equal(USDC_ADDRESS); + expect(bridgeFillEvent.outputToken).to.equal(DAI_ADDRESS); + expect(bridgeFillEvent.inputTokenAmount).to.bignumber.equal(uniswapOutputAmount); + expect(bridgeFillEvent.outputTokenAmount).to.bignumber.gt(0); + }); + it.skip('MultiplexFeature.multiHopFill() complex scenario', async () => { + /* + + /––––PLP–––> USDC + / \ + / PLP + /––Uni (via USDC)–––\ + / V + ETH ––wrap––> WETH ––––Uni/Sushi–––> USDT ––Sushi––> LON + \ ^ + ––––––––––––––– Uni –––––––––––––––––/ + */ + // Taker has to have approved the EP for the intermediate tokens :/ + await weth + .approve(zeroEx.address, constants.MAX_UINT256) + .awaitTransactionSuccessAsync({ from: rfqMaker, gasPrice: 0 }, { shouldValidate: false }); + await usdt + .approve(zeroEx.address, constants.MAX_UINT256) + .awaitTransactionSuccessAsync({ from: rfqMaker, gasPrice: 0 }, { shouldValidate: false }); + + const sellAmount = toBaseUnitAmount(1); // 1 ETH + const wethUsdcPlpCall = { + selector: multiplex.getSelector('_sellToLiquidityProvider'), + data: plpDataEncoder.encode({ + provider: WETH_USDC_PLP_ADDRESS, + auxiliaryData: constants.NULL_BYTES, + }), + }; + const usdcUsdtPlpCall = { + selector: multiplex.getSelector('_sellToLiquidityProvider'), + data: plpDataEncoder.encode({ + provider: USDC_USDT_PLP_ADDRESS, + auxiliaryData: constants.NULL_BYTES, + }), + }; + const wethUsdcUsdtMultiHopCall = { + selector: multiplex.getSelector('_multiHopFill'), + sellAmount: encodeFractionalFillAmount(0.25), + data: multiHopFillEncoder.encode({ + tokens: [WETH_ADDRESS, USDC_ADDRESS, USDT_ADDRESS], + calls: [wethUsdcPlpCall, usdcUsdtPlpCall], + ethValue: constants.ZERO_AMOUNT, + }), + }; + const wethUsdcUsdtUniswapCall = { + selector: multiplex.getSelector('_sellToUniswap'), + sellAmount: encodeFractionalFillAmount(0.25), + data: uniswapDataEncoder.encode({ tokens: [WETH_ADDRESS, USDC_ADDRESS, USDT_ADDRESS], isSushi: false }), + }; + const wethUsdtUniswapCall = { + selector: multiplex.getSelector('_sellToUniswap'), + sellAmount: encodeFractionalFillAmount(0.25), + data: uniswapDataEncoder.encode({ tokens: [WETH_ADDRESS, USDT_ADDRESS], isSushi: false }), + }; + const wethUsdtSushiswapCall = { + selector: multiplex.getSelector('_sellToUniswap'), + sellAmount: encodeFractionalFillAmount(0.25), + data: uniswapDataEncoder.encode({ tokens: [WETH_ADDRESS, USDT_ADDRESS], isSushi: true }), + }; + const wethUsdtBatchCall = { + selector: multiplex.getSelector('_batchFill'), + data: batchFillEncoder.encode({ + calls: [ + wethUsdcUsdtMultiHopCall, + wethUsdcUsdtUniswapCall, + wethUsdtUniswapCall, + wethUsdtSushiswapCall, + ], + ethValue: constants.ZERO_AMOUNT, + }), + }; + const usdtLonSushiCall = { + selector: multiplex.getSelector('_sellToUniswap'), + data: uniswapDataEncoder.encode({ tokens: [USDT_ADDRESS, LON_ADDRESS], isSushi: true }), + }; + const wethUsdtLonMultiHopCall = { + selector: multiplex.getSelector('_multiHopFill'), + sellAmount: encodeFractionalFillAmount(0.8), + data: multiHopFillEncoder.encode({ + tokens: [WETH_ADDRESS, USDT_ADDRESS], + calls: [wethUsdtBatchCall, usdtLonSushiCall], + ethValue: constants.ZERO_AMOUNT, + }), + }; + const wethLonUniswapCall = { + selector: multiplex.getSelector('_sellToUniswap'), + sellAmount: encodeFractionalFillAmount(0.2), + data: uniswapDataEncoder.encode({ tokens: [WETH_ADDRESS, LON_ADDRESS], isSushi: false }), + }; + + const wethLonBatchFillCall = { + selector: multiplex.getSelector('_batchFill'), + data: batchFillEncoder.encode({ + calls: [wethUsdtLonMultiHopCall, wethLonUniswapCall], + ethValue: constants.ZERO_AMOUNT, + }), + }; + const wrapEthCall = { + selector: weth.getSelector('deposit'), + data: constants.NULL_BYTES, + }; + const multiHopFillData = { + tokens: [ETH_TOKEN_ADDRESS, WETH_ADDRESS, LON_ADDRESS], + sellAmount, + calls: [wrapEthCall, wethLonBatchFillCall], + }; + const tx = await multiplex + .multiHopFill(multiHopFillData, constants.ZERO_AMOUNT) + .awaitTransactionSuccessAsync( + { from: rfqMaker, gasPrice: 0, value: sellAmount }, + { shouldValidate: false }, + ); + logUtils.log(`${tx.gasUsed} gas used`); + }); + }); +}); diff --git a/contracts/zero-ex/test/features/native_orders_feature_test.ts b/contracts/zero-ex/test/features/native_orders_feature_test.ts index 4e65d520d4..393019950b 100644 --- a/contracts/zero-ex/test/features/native_orders_feature_test.ts +++ b/contracts/zero-ex/test/features/native_orders_feature_test.ts @@ -3,25 +3,28 @@ import { constants, describe, expect, + getRandomPortion, randomAddress, verifyEventsFromLogs, } from '@0x/contracts-test-utils'; -import { - LimitOrder, - LimitOrderFields, - OrderInfo, - OrderStatus, - RevertErrors, - RfqOrder, - RfqOrderFields, -} from '@0x/protocol-utils'; +import { LimitOrder, LimitOrderFields, OrderStatus, RevertErrors, RfqOrder, RfqOrderFields } from '@0x/protocol-utils'; import { AnyRevertError, BigNumber } from '@0x/utils'; import { TransactionReceiptWithDecodedLogs } from 'ethereum-types'; import { IZeroExContract, IZeroExEvents } from '../../src/wrappers'; import { artifacts } from '../artifacts'; import { fullMigrateAsync } from '../utils/migration'; -import { getRandomLimitOrder, getRandomRfqOrder } from '../utils/orders'; +import { + assertOrderInfoEquals, + computeLimitOrderFilledAmounts, + computeRfqOrderFilledAmounts, + createExpiry, + getActualFillableTakerTokenAmount, + getFillableMakerTokenAmount, + getRandomLimitOrder, + getRandomRfqOrder, + NativeOrdersTestEnvironment, +} from '../utils/orders'; import { TestMintableERC20TokenContract, TestRfqOriginRegistrationContract } from '../wrappers'; blockchainTests.resets('NativeOrdersFeature', env => { @@ -39,6 +42,7 @@ blockchainTests.resets('NativeOrdersFeature', env => { let takerToken: TestMintableERC20TokenContract; let wethToken: TestMintableERC20TokenContract; let testRfqOriginRegistration: TestRfqOriginRegistrationContract; + let testUtils: NativeOrdersTestEnvironment; before(async () => { let owner; @@ -78,6 +82,16 @@ blockchainTests.resets('NativeOrdersFeature', env => { env.txDefaults, artifacts, ); + testUtils = new NativeOrdersTestEnvironment( + maker, + taker, + makerToken, + takerToken, + zeroEx, + GAS_PRICE, + SINGLE_PROTOCOL_FEE, + env, + ); }); function getTestLimitOrder(fields: Partial = {}): LimitOrder { @@ -105,27 +119,6 @@ blockchainTests.resets('NativeOrdersFeature', env => { }); } - 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(); @@ -149,26 +142,6 @@ blockchainTests.resets('NativeOrdersFeature', env => { }); }); - async function fillLimitOrderAsync( - order: LimitOrder, - opts: Partial<{ - fillAmount: BigNumber | number; - taker: string; - protocolFee?: BigNumber | number; - }> = {}, - ): Promise { - const { fillAmount, taker: _taker, protocolFee } = { - taker, - fillAmount: order.takerAmount, - ...opts, - }; - await prepareBalancesForOrderAsync(order, _taker); - const _protocolFee = protocolFee === undefined ? SINGLE_PROTOCOL_FEE : protocolFee; - return zeroEx - .fillLimitOrder(order, await order.getSignatureWithProviderAsync(env.provider), new BigNumber(fillAmount)) - .awaitTransactionSuccessAsync({ from: _taker, value: _protocolFee }); - } - describe('getLimitOrderInfo()', () => { it('unfilled order', async () => { const order = getTestLimitOrder(); @@ -205,7 +178,7 @@ blockchainTests.resets('NativeOrdersFeature', env => { const expiry = createExpiry(60); const order = getTestLimitOrder({ expiry }); // Fill the order first. - await fillLimitOrderAsync(order); + await testUtils.fillLimitOrderAsync(order); // Advance time to expire the order. await env.web3Wrapper.increaseTimeAsync(61); const info = await zeroEx.getLimitOrderInfo(order).callAsync(); @@ -219,7 +192,7 @@ blockchainTests.resets('NativeOrdersFeature', env => { it('filled order', async () => { const order = getTestLimitOrder(); // Fill the order first. - await fillLimitOrderAsync(order); + await testUtils.fillLimitOrderAsync(order); const info = await zeroEx.getLimitOrderInfo(order).callAsync(); assertOrderInfoEquals(info, { status: OrderStatus.Filled, @@ -232,7 +205,7 @@ blockchainTests.resets('NativeOrdersFeature', env => { const order = getTestLimitOrder(); const fillAmount = order.takerAmount.minus(1); // Fill the order first. - await fillLimitOrderAsync(order, { fillAmount }); + await testUtils.fillLimitOrderAsync(order, { fillAmount }); const info = await zeroEx.getLimitOrderInfo(order).callAsync(); assertOrderInfoEquals(info, { status: OrderStatus.Fillable, @@ -244,7 +217,7 @@ blockchainTests.resets('NativeOrdersFeature', env => { it('filled then cancelled order', async () => { const order = getTestLimitOrder(); // Fill the order first. - await fillLimitOrderAsync(order); + await testUtils.fillLimitOrderAsync(order); await zeroEx.cancelLimitOrder(order).awaitTransactionSuccessAsync({ from: maker }); const info = await zeroEx.getLimitOrderInfo(order).callAsync(); assertOrderInfoEquals(info, { @@ -258,7 +231,7 @@ blockchainTests.resets('NativeOrdersFeature', env => { const order = getTestLimitOrder(); const fillAmount = order.takerAmount.minus(1); // Fill the order first. - await fillLimitOrderAsync(order, { fillAmount }); + await testUtils.fillLimitOrderAsync(order, { fillAmount }); await zeroEx.cancelLimitOrder(order).awaitTransactionSuccessAsync({ from: maker }); const info = await zeroEx.getLimitOrderInfo(order).callAsync(); assertOrderInfoEquals(info, { @@ -269,17 +242,6 @@ blockchainTests.resets('NativeOrdersFeature', env => { }); }); - 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 }); - } - describe('getRfqOrderInfo()', () => { it('unfilled order', async () => { const order = getTestRfqOrder(); @@ -316,7 +278,7 @@ blockchainTests.resets('NativeOrdersFeature', env => { it('filled then expired order', async () => { const expiry = createExpiry(60); const order = getTestRfqOrder({ expiry }); - await prepareBalancesForOrderAsync(order); + await testUtils.prepareBalancesForOrdersAsync([order]); const sig = await order.getSignatureWithProviderAsync(env.provider); // Fill the order first. await zeroEx.fillRfqOrder(order, sig, order.takerAmount).awaitTransactionSuccessAsync({ from: taker }); @@ -333,7 +295,7 @@ blockchainTests.resets('NativeOrdersFeature', env => { it('filled order', async () => { const order = getTestRfqOrder(); // Fill the order first. - await fillRfqOrderAsync(order, order.takerAmount, taker); + await testUtils.fillRfqOrderAsync(order, order.takerAmount, taker); const info = await zeroEx.getRfqOrderInfo(order).callAsync(); assertOrderInfoEquals(info, { status: OrderStatus.Filled, @@ -346,7 +308,7 @@ blockchainTests.resets('NativeOrdersFeature', env => { const order = getTestRfqOrder(); const fillAmount = order.takerAmount.minus(1); // Fill the order first. - await fillRfqOrderAsync(order, fillAmount); + await testUtils.fillRfqOrderAsync(order, fillAmount); const info = await zeroEx.getRfqOrderInfo(order).callAsync(); assertOrderInfoEquals(info, { status: OrderStatus.Fillable, @@ -358,7 +320,7 @@ blockchainTests.resets('NativeOrdersFeature', env => { it('filled then cancelled order', async () => { const order = getTestRfqOrder(); // Fill the order first. - await fillRfqOrderAsync(order); + await testUtils.fillRfqOrderAsync(order); await zeroEx.cancelRfqOrder(order).awaitTransactionSuccessAsync({ from: maker }); const info = await zeroEx.getRfqOrderInfo(order).callAsync(); assertOrderInfoEquals(info, { @@ -372,7 +334,7 @@ blockchainTests.resets('NativeOrdersFeature', env => { const order = getTestRfqOrder(); const fillAmount = order.takerAmount.minus(1); // Fill the order first. - await fillRfqOrderAsync(order, fillAmount); + await testUtils.fillRfqOrderAsync(order, fillAmount); await zeroEx.cancelRfqOrder(order).awaitTransactionSuccessAsync({ from: maker }); const info = await zeroEx.getRfqOrderInfo(order).callAsync(); assertOrderInfoEquals(info, { @@ -408,7 +370,7 @@ blockchainTests.resets('NativeOrdersFeature', env => { it('can cancel a fully filled order', async () => { const order = getTestLimitOrder(); - await fillLimitOrderAsync(order); + await testUtils.fillLimitOrderAsync(order); const receipt = await zeroEx.cancelLimitOrder(order).awaitTransactionSuccessAsync({ from: maker }); verifyEventsFromLogs( receipt.logs, @@ -421,7 +383,7 @@ blockchainTests.resets('NativeOrdersFeature', env => { it('can cancel a partially filled order', async () => { const order = getTestLimitOrder(); - await fillLimitOrderAsync(order, { fillAmount: order.takerAmount.minus(1) }); + await testUtils.fillLimitOrderAsync(order, { fillAmount: order.takerAmount.minus(1) }); const receipt = await zeroEx.cancelLimitOrder(order).awaitTransactionSuccessAsync({ from: maker }); verifyEventsFromLogs( receipt.logs, @@ -482,7 +444,7 @@ blockchainTests.resets('NativeOrdersFeature', env => { it('can cancel a fully filled order', async () => { const order = getTestRfqOrder(); - await fillRfqOrderAsync(order); + await testUtils.fillRfqOrderAsync(order); const receipt = await zeroEx.cancelRfqOrder(order).awaitTransactionSuccessAsync({ from: maker }); verifyEventsFromLogs( receipt.logs, @@ -495,7 +457,7 @@ blockchainTests.resets('NativeOrdersFeature', env => { it('can cancel a partially filled order', async () => { const order = getTestRfqOrder(); - await fillRfqOrderAsync(order, order.takerAmount.minus(1)); + await testUtils.fillRfqOrderAsync(order, order.takerAmount.minus(1)); const receipt = await zeroEx.cancelRfqOrder(order).awaitTransactionSuccessAsync({ from: maker }); verifyEventsFromLogs( receipt.logs, @@ -747,63 +709,6 @@ blockchainTests.resets('NativeOrdersFeature', env => { }); }); - 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); - const protocolFee = order.taker !== NULL_ADDRESS ? ZERO_AMOUNT : SINGLE_PROTOCOL_FEE; - return { - taker, - takerTokenFilledAmount, - makerTokenFilledAmount, - takerTokenFeeFilledAmount, - orderHash: order.getHash(), - maker: order.maker, - feeRecipient: order.feeRecipient, - makerToken: order.makerToken, - takerToken: order.takerToken, - protocolFeePaid: protocolFee, - pool: order.pool, - }; - } - async function assertExpectedFinalBalancesFromLimitOrderFillAsync( order: LimitOrder, opts: Partial<{ @@ -841,10 +746,10 @@ blockchainTests.resets('NativeOrdersFeature', env => { describe('fillLimitOrder()', () => { it('can fully fill an order', async () => { const order = getTestLimitOrder(); - const receipt = await fillLimitOrderAsync(order); + const receipt = await testUtils.fillLimitOrderAsync(order); verifyEventsFromLogs( receipt.logs, - [createLimitOrderFilledEventArgs(order)], + [testUtils.createLimitOrderFilledEventArgs(order)], IZeroExEvents.LimitOrderFilled, ); assertOrderInfoEquals(await zeroEx.getLimitOrderInfo(order).callAsync(), { @@ -858,10 +763,10 @@ blockchainTests.resets('NativeOrdersFeature', env => { it('can partially fill an order', async () => { const order = getTestLimitOrder(); const fillAmount = order.takerAmount.minus(1); - const receipt = await fillLimitOrderAsync(order, { fillAmount }); + const receipt = await testUtils.fillLimitOrderAsync(order, { fillAmount }); verifyEventsFromLogs( receipt.logs, - [createLimitOrderFilledEventArgs(order, fillAmount)], + [testUtils.createLimitOrderFilledEventArgs(order, fillAmount)], IZeroExEvents.LimitOrderFilled, ); assertOrderInfoEquals(await zeroEx.getLimitOrderInfo(order).callAsync(), { @@ -869,24 +774,26 @@ blockchainTests.resets('NativeOrdersFeature', env => { status: OrderStatus.Fillable, takerTokenFilledAmount: fillAmount, }); - await assertExpectedFinalBalancesFromLimitOrderFillAsync(order, { takerTokenFillAmount: fillAmount }); + await assertExpectedFinalBalancesFromLimitOrderFillAsync(order, { + takerTokenFillAmount: 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 }); + let receipt = await testUtils.fillLimitOrderAsync(order, { fillAmount }); verifyEventsFromLogs( receipt.logs, - [createLimitOrderFilledEventArgs(order, fillAmount)], + [testUtils.createLimitOrderFilledEventArgs(order, fillAmount)], IZeroExEvents.LimitOrderFilled, ); const alreadyFilledAmount = fillAmount; fillAmount = order.takerAmount.minus(fillAmount); - receipt = await fillLimitOrderAsync(order, { fillAmount }); + receipt = await testUtils.fillLimitOrderAsync(order, { fillAmount }); verifyEventsFromLogs( receipt.logs, - [createLimitOrderFilledEventArgs(order, fillAmount, alreadyFilledAmount)], + [testUtils.createLimitOrderFilledEventArgs(order, fillAmount, alreadyFilledAmount)], IZeroExEvents.LimitOrderFilled, ); assertOrderInfoEquals(await zeroEx.getLimitOrderInfo(order).callAsync(), { @@ -899,10 +806,10 @@ blockchainTests.resets('NativeOrdersFeature', env => { it('clamps fill amount to remaining available', async () => { const order = getTestLimitOrder(); const fillAmount = order.takerAmount.plus(1); - const receipt = await fillLimitOrderAsync(order, { fillAmount }); + const receipt = await testUtils.fillLimitOrderAsync(order, { fillAmount }); verifyEventsFromLogs( receipt.logs, - [createLimitOrderFilledEventArgs(order, fillAmount)], + [testUtils.createLimitOrderFilledEventArgs(order, fillAmount)], IZeroExEvents.LimitOrderFilled, ); assertOrderInfoEquals(await zeroEx.getLimitOrderInfo(order).callAsync(), { @@ -910,24 +817,26 @@ blockchainTests.resets('NativeOrdersFeature', env => { status: OrderStatus.Filled, takerTokenFilledAmount: order.takerAmount, }); - await assertExpectedFinalBalancesFromLimitOrderFillAsync(order, { takerTokenFillAmount: fillAmount }); + await assertExpectedFinalBalancesFromLimitOrderFillAsync(order, { + takerTokenFillAmount: 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 }); + let receipt = await testUtils.fillLimitOrderAsync(order, { fillAmount }); verifyEventsFromLogs( receipt.logs, - [createLimitOrderFilledEventArgs(order, fillAmount)], + [testUtils.createLimitOrderFilledEventArgs(order, fillAmount)], IZeroExEvents.LimitOrderFilled, ); const alreadyFilledAmount = fillAmount; fillAmount = order.takerAmount.minus(fillAmount).plus(1); - receipt = await fillLimitOrderAsync(order, { fillAmount }); + receipt = await testUtils.fillLimitOrderAsync(order, { fillAmount }); verifyEventsFromLogs( receipt.logs, - [createLimitOrderFilledEventArgs(order, fillAmount, alreadyFilledAmount)], + [testUtils.createLimitOrderFilledEventArgs(order, fillAmount, alreadyFilledAmount)], IZeroExEvents.LimitOrderFilled, ); assertOrderInfoEquals(await zeroEx.getLimitOrderInfo(order).callAsync(), { @@ -939,7 +848,7 @@ blockchainTests.resets('NativeOrdersFeature', env => { it('cannot fill an expired order', async () => { const order = getTestLimitOrder({ expiry: createExpiry(-60) }); - const tx = fillLimitOrderAsync(order); + const tx = testUtils.fillLimitOrderAsync(order); return expect(tx).to.revertWith( new RevertErrors.NativeOrders.OrderNotFillableError(order.getHash(), OrderStatus.Expired), ); @@ -948,7 +857,7 @@ blockchainTests.resets('NativeOrdersFeature', env => { it('cannot fill a cancelled order', async () => { const order = getTestLimitOrder(); await zeroEx.cancelLimitOrder(order).awaitTransactionSuccessAsync({ from: maker }); - const tx = fillLimitOrderAsync(order); + const tx = testUtils.fillLimitOrderAsync(order); return expect(tx).to.revertWith( new RevertErrors.NativeOrders.OrderNotFillableError(order.getHash(), OrderStatus.Cancelled), ); @@ -959,7 +868,7 @@ blockchainTests.resets('NativeOrdersFeature', env => { await zeroEx .cancelPairLimitOrders(makerToken.address, takerToken.address, order.salt.plus(1)) .awaitTransactionSuccessAsync({ from: maker }); - const tx = fillLimitOrderAsync(order); + const tx = testUtils.fillLimitOrderAsync(order); return expect(tx).to.revertWith( new RevertErrors.NativeOrders.OrderNotFillableError(order.getHash(), OrderStatus.Cancelled), ); @@ -967,7 +876,7 @@ blockchainTests.resets('NativeOrdersFeature', env => { it('non-taker cannot fill order', async () => { const order = getTestLimitOrder({ taker }); - const tx = fillLimitOrderAsync(order, { fillAmount: order.takerAmount, taker: notTaker }); + const tx = testUtils.fillLimitOrderAsync(order, { fillAmount: order.takerAmount, taker: notTaker }); return expect(tx).to.revertWith( new RevertErrors.NativeOrders.OrderNotFillableByTakerError(order.getHash(), notTaker, order.taker), ); @@ -975,7 +884,7 @@ blockchainTests.resets('NativeOrdersFeature', env => { it('non-sender cannot fill order', async () => { const order = getTestLimitOrder({ sender: taker }); - const tx = fillLimitOrderAsync(order, { fillAmount: order.takerAmount, taker: notTaker }); + const tx = testUtils.fillLimitOrderAsync(order, { fillAmount: order.takerAmount, taker: notTaker }); return expect(tx).to.revertWith( new RevertErrors.NativeOrders.OrderNotFillableBySenderError(order.getHash(), notTaker, order.sender), ); @@ -985,7 +894,7 @@ blockchainTests.resets('NativeOrdersFeature', env => { const order = getTestLimitOrder(); // Overwrite chainId to result in a different hash and therefore different // signature. - const tx = fillLimitOrderAsync(order.clone({ chainId: 1234 })); + const tx = testUtils.fillLimitOrderAsync(order.clone({ chainId: 1234 })); return expect(tx).to.revertWith( new RevertErrors.NativeOrders.OrderNotSignedByMakerError(order.getHash(), undefined, order.maker), ); @@ -993,7 +902,7 @@ blockchainTests.resets('NativeOrdersFeature', env => { it('fails if no protocol fee attached', async () => { const order = getTestLimitOrder(); - await prepareBalancesForOrderAsync(order); + await testUtils.prepareBalancesForOrdersAsync([order]); const tx = zeroEx .fillLimitOrder( order, @@ -1008,62 +917,24 @@ blockchainTests.resets('NativeOrdersFeature', env => { it('refunds excess protocol fee', async () => { const order = getTestLimitOrder(); - const receipt = await fillLimitOrderAsync(order, { protocolFee: SINGLE_PROTOCOL_FEE.plus(1) }); + const receipt = await testUtils.fillLimitOrderAsync(order, { protocolFee: SINGLE_PROTOCOL_FEE.plus(1) }); verifyEventsFromLogs( receipt.logs, - [createLimitOrderFilledEventArgs(order)], + [testUtils.createLimitOrderFilledEventArgs(order)], IZeroExEvents.LimitOrderFilled, ); await assertExpectedFinalBalancesFromLimitOrderFillAsync(order, { receipt }); }); }); - 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, - pool: order.pool, - }; - } + describe('registerAllowedRfqOrigins()', () => { + it('cannot register through a contract', async () => { + const tx = testRfqOriginRegistration + .registerAllowedRfqOrigins(zeroEx.address, [], true) + .awaitTransactionSuccessAsync(); + expect(tx).to.revertWith('NativeOrdersFeature/NO_CONTRACT_ORIGINS'); + }); + }); async function assertExpectedFinalBalancesFromRfqOrderFillAsync( order: RfqOrder, @@ -1081,20 +952,15 @@ blockchainTests.resets('NativeOrdersFeature', env => { expect(takerBalance).to.bignumber.eq(makerTokenFilledAmount); } - describe('registerAllowedRfqOrigins()', () => { - it('cannot register through a contract', async () => { - const tx = testRfqOriginRegistration - .registerAllowedRfqOrigins(zeroEx.address, [], true) - .awaitTransactionSuccessAsync(); - expect(tx).to.revertWith('NativeOrdersFeature/NO_CONTRACT_ORIGINS'); - }); - }); - describe('fillRfqOrder()', () => { it('can fully fill an order', async () => { const order = getTestRfqOrder(); - const receipt = await fillRfqOrderAsync(order); - verifyEventsFromLogs(receipt.logs, [createRfqOrderFilledEventArgs(order)], IZeroExEvents.RfqOrderFilled); + const receipt = await testUtils.fillRfqOrderAsync(order); + verifyEventsFromLogs( + receipt.logs, + [testUtils.createRfqOrderFilledEventArgs(order)], + IZeroExEvents.RfqOrderFilled, + ); assertOrderInfoEquals(await zeroEx.getRfqOrderInfo(order).callAsync(), { orderHash: order.getHash(), status: OrderStatus.Filled, @@ -1106,10 +972,10 @@ blockchainTests.resets('NativeOrdersFeature', env => { it('can partially fill an order', async () => { const order = getTestRfqOrder(); const fillAmount = order.takerAmount.minus(1); - const receipt = await fillRfqOrderAsync(order, fillAmount); + const receipt = await testUtils.fillRfqOrderAsync(order, fillAmount); verifyEventsFromLogs( receipt.logs, - [createRfqOrderFilledEventArgs(order, fillAmount)], + [testUtils.createRfqOrderFilledEventArgs(order, fillAmount)], IZeroExEvents.RfqOrderFilled, ); assertOrderInfoEquals(await zeroEx.getRfqOrderInfo(order).callAsync(), { @@ -1123,18 +989,18 @@ blockchainTests.resets('NativeOrdersFeature', env => { 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); + let receipt = await testUtils.fillRfqOrderAsync(order, fillAmount); verifyEventsFromLogs( receipt.logs, - [createRfqOrderFilledEventArgs(order, fillAmount)], + [testUtils.createRfqOrderFilledEventArgs(order, fillAmount)], IZeroExEvents.RfqOrderFilled, ); const alreadyFilledAmount = fillAmount; fillAmount = order.takerAmount.minus(fillAmount); - receipt = await fillRfqOrderAsync(order, fillAmount); + receipt = await testUtils.fillRfqOrderAsync(order, fillAmount); verifyEventsFromLogs( receipt.logs, - [createRfqOrderFilledEventArgs(order, fillAmount, alreadyFilledAmount)], + [testUtils.createRfqOrderFilledEventArgs(order, fillAmount, alreadyFilledAmount)], IZeroExEvents.RfqOrderFilled, ); assertOrderInfoEquals(await zeroEx.getRfqOrderInfo(order).callAsync(), { @@ -1147,10 +1013,10 @@ blockchainTests.resets('NativeOrdersFeature', env => { it('clamps fill amount to remaining available', async () => { const order = getTestRfqOrder(); const fillAmount = order.takerAmount.plus(1); - const receipt = await fillRfqOrderAsync(order, fillAmount); + const receipt = await testUtils.fillRfqOrderAsync(order, fillAmount); verifyEventsFromLogs( receipt.logs, - [createRfqOrderFilledEventArgs(order, fillAmount)], + [testUtils.createRfqOrderFilledEventArgs(order, fillAmount)], IZeroExEvents.RfqOrderFilled, ); assertOrderInfoEquals(await zeroEx.getRfqOrderInfo(order).callAsync(), { @@ -1164,18 +1030,18 @@ blockchainTests.resets('NativeOrdersFeature', env => { 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); + let receipt = await testUtils.fillRfqOrderAsync(order, fillAmount); verifyEventsFromLogs( receipt.logs, - [createRfqOrderFilledEventArgs(order, fillAmount)], + [testUtils.createRfqOrderFilledEventArgs(order, fillAmount)], IZeroExEvents.RfqOrderFilled, ); const alreadyFilledAmount = fillAmount; fillAmount = order.takerAmount.minus(fillAmount).plus(1); - receipt = await fillRfqOrderAsync(order, fillAmount); + receipt = await testUtils.fillRfqOrderAsync(order, fillAmount); verifyEventsFromLogs( receipt.logs, - [createRfqOrderFilledEventArgs(order, fillAmount, alreadyFilledAmount)], + [testUtils.createRfqOrderFilledEventArgs(order, fillAmount, alreadyFilledAmount)], IZeroExEvents.RfqOrderFilled, ); assertOrderInfoEquals(await zeroEx.getRfqOrderInfo(order).callAsync(), { @@ -1187,7 +1053,7 @@ blockchainTests.resets('NativeOrdersFeature', env => { it('cannot fill an order with wrong tx.origin', async () => { const order = getTestRfqOrder(); - const tx = fillRfqOrderAsync(order, order.takerAmount, notTaker); + const tx = testUtils.fillRfqOrderAsync(order, order.takerAmount, notTaker); return expect(tx).to.revertWith( new RevertErrors.NativeOrders.OrderNotFillableByOriginError(order.getHash(), notTaker, taker), ); @@ -1210,7 +1076,7 @@ blockchainTests.resets('NativeOrdersFeature', env => { ], IZeroExEvents.RfqOrderOriginsAllowed, ); - return fillRfqOrderAsync(order, order.takerAmount, notTaker); + return testUtils.fillRfqOrderAsync(order, order.takerAmount, notTaker); }); it('cannot fill an order with registered then unregistered tx.origin', async () => { @@ -1232,7 +1098,7 @@ blockchainTests.resets('NativeOrdersFeature', env => { IZeroExEvents.RfqOrderOriginsAllowed, ); - const tx = fillRfqOrderAsync(order, order.takerAmount, notTaker); + const tx = testUtils.fillRfqOrderAsync(order, order.takerAmount, notTaker); return expect(tx).to.revertWith( new RevertErrors.NativeOrders.OrderNotFillableByOriginError(order.getHash(), notTaker, taker), ); @@ -1240,7 +1106,7 @@ blockchainTests.resets('NativeOrdersFeature', env => { it('cannot fill an order with a zero tx.origin', async () => { const order = getTestRfqOrder({ txOrigin: NULL_ADDRESS }); - const tx = fillRfqOrderAsync(order, order.takerAmount, notTaker); + const tx = testUtils.fillRfqOrderAsync(order, order.takerAmount, notTaker); return expect(tx).to.revertWith( new RevertErrors.NativeOrders.OrderNotFillableError(order.getHash(), OrderStatus.Invalid), ); @@ -1248,7 +1114,7 @@ blockchainTests.resets('NativeOrdersFeature', env => { it('non-taker cannot fill order', async () => { const order = getTestRfqOrder({ taker, txOrigin: notTaker }); - const tx = fillRfqOrderAsync(order, order.takerAmount, notTaker); + const tx = testUtils.fillRfqOrderAsync(order, order.takerAmount, notTaker); return expect(tx).to.revertWith( new RevertErrors.NativeOrders.OrderNotFillableByTakerError(order.getHash(), notTaker, order.taker), ); @@ -1256,7 +1122,7 @@ blockchainTests.resets('NativeOrdersFeature', env => { it('cannot fill an expired order', async () => { const order = getTestRfqOrder({ expiry: createExpiry(-60) }); - const tx = fillRfqOrderAsync(order); + const tx = testUtils.fillRfqOrderAsync(order); return expect(tx).to.revertWith( new RevertErrors.NativeOrders.OrderNotFillableError(order.getHash(), OrderStatus.Expired), ); @@ -1265,7 +1131,7 @@ blockchainTests.resets('NativeOrdersFeature', env => { it('cannot fill a cancelled order', async () => { const order = getTestRfqOrder(); await zeroEx.cancelRfqOrder(order).awaitTransactionSuccessAsync({ from: maker }); - const tx = fillRfqOrderAsync(order); + const tx = testUtils.fillRfqOrderAsync(order); return expect(tx).to.revertWith( new RevertErrors.NativeOrders.OrderNotFillableError(order.getHash(), OrderStatus.Cancelled), ); @@ -1276,7 +1142,7 @@ blockchainTests.resets('NativeOrdersFeature', env => { await zeroEx .cancelPairRfqOrders(makerToken.address, takerToken.address, order.salt.plus(1)) .awaitTransactionSuccessAsync({ from: maker }); - const tx = fillRfqOrderAsync(order); + const tx = testUtils.fillRfqOrderAsync(order); return expect(tx).to.revertWith( new RevertErrors.NativeOrders.OrderNotFillableError(order.getHash(), OrderStatus.Cancelled), ); @@ -1286,7 +1152,7 @@ blockchainTests.resets('NativeOrdersFeature', env => { const order = getTestRfqOrder(); // Overwrite chainId to result in a different hash and therefore different // signature. - const tx = fillRfqOrderAsync(order.clone({ chainId: 1234 })); + const tx = testUtils.fillRfqOrderAsync(order.clone({ chainId: 1234 })); return expect(tx).to.revertWith( new RevertErrors.NativeOrders.OrderNotSignedByMakerError(order.getHash(), undefined, order.maker), ); @@ -1294,7 +1160,7 @@ blockchainTests.resets('NativeOrdersFeature', env => { it('fails if ETH is attached', async () => { const order = getTestRfqOrder(); - await prepareBalancesForOrderAsync(order, taker); + await testUtils.prepareBalancesForOrdersAsync([order], taker); const tx = zeroEx .fillRfqOrder(order, await order.getSignatureWithProviderAsync(env.provider), order.takerAmount) .awaitTransactionSuccessAsync({ from: taker, value: 1 }); @@ -1306,20 +1172,20 @@ blockchainTests.resets('NativeOrdersFeature', env => { describe('fillOrKillLimitOrder()', () => { it('can fully fill an order', async () => { const order = getTestLimitOrder(); - await prepareBalancesForOrderAsync(order); + await testUtils.prepareBalancesForOrdersAsync([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)], + [testUtils.createLimitOrderFilledEventArgs(order)], IZeroExEvents.LimitOrderFilled, ); }); it('reverts if cannot fill the exact amount', async () => { const order = getTestLimitOrder(); - await prepareBalancesForOrderAsync(order); + await testUtils.prepareBalancesForOrdersAsync([order]); const fillAmount = order.takerAmount.plus(1); const tx = zeroEx .fillOrKillLimitOrder(order, await order.getSignatureWithProviderAsync(env.provider), fillAmount) @@ -1331,7 +1197,7 @@ blockchainTests.resets('NativeOrdersFeature', env => { it('refunds excess protocol fee', async () => { const order = getTestLimitOrder(); - await prepareBalancesForOrderAsync(order); + await testUtils.prepareBalancesForOrdersAsync([order]); const takerBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(taker); const receipt = await zeroEx .fillOrKillLimitOrder(order, await order.getSignatureWithProviderAsync(env.provider), order.takerAmount) @@ -1345,16 +1211,20 @@ blockchainTests.resets('NativeOrdersFeature', env => { describe('fillOrKillRfqOrder()', () => { it('can fully fill an order', async () => { const order = getTestRfqOrder(); - await prepareBalancesForOrderAsync(order); + await testUtils.prepareBalancesForOrdersAsync([order]); const receipt = await zeroEx .fillOrKillRfqOrder(order, await order.getSignatureWithProviderAsync(env.provider), order.takerAmount) .awaitTransactionSuccessAsync({ from: taker }); - verifyEventsFromLogs(receipt.logs, [createRfqOrderFilledEventArgs(order)], IZeroExEvents.RfqOrderFilled); + verifyEventsFromLogs( + receipt.logs, + [testUtils.createRfqOrderFilledEventArgs(order)], + IZeroExEvents.RfqOrderFilled, + ); }); it('reverts if cannot fill the exact amount', async () => { const order = getTestRfqOrder(); - await prepareBalancesForOrderAsync(order); + await testUtils.prepareBalancesForOrdersAsync([order]); const fillAmount = order.takerAmount.plus(1); const tx = zeroEx .fillOrKillRfqOrder(order, await order.getSignatureWithProviderAsync(env.provider), fillAmount) @@ -1366,7 +1236,7 @@ blockchainTests.resets('NativeOrdersFeature', env => { it('fails if ETH is attached', async () => { const order = getTestRfqOrder(); - await prepareBalancesForOrderAsync(order); + await testUtils.prepareBalancesForOrdersAsync([order]); const tx = zeroEx .fillOrKillRfqOrder(order, await order.getSignatureWithProviderAsync(env.provider), order.takerAmount) .awaitTransactionSuccessAsync({ from: taker, value: 1 }); @@ -1385,34 +1255,6 @@ blockchainTests.resets('NativeOrdersFeature', env => { await makerToken.approve(zeroEx.address, allowance).awaitTransactionSuccessAsync({ from: maker }); } - function getFillableMakerTokenAmount( - order: LimitOrder | RfqOrder, - takerTokenFilledAmount: BigNumber = ZERO_AMOUNT, - ): BigNumber { - return order.takerAmount - .minus(takerTokenFilledAmount) - .times(order.makerAmount) - .div(order.takerAmount) - .integerValue(BigNumber.ROUND_DOWN); - } - - function getActualFillableTakerTokenAmount( - order: LimitOrder | RfqOrder, - makerBalance: BigNumber = order.makerAmount, - makerAllowance: BigNumber = order.makerAmount, - takerTokenFilledAmount: BigNumber = ZERO_AMOUNT, - ): BigNumber { - const fillableMakerTokenAmount = getFillableMakerTokenAmount(order, takerTokenFilledAmount); - return BigNumber.min(fillableMakerTokenAmount, makerBalance, makerAllowance) - .times(order.takerAmount) - .div(order.makerAmount) - .integerValue(BigNumber.ROUND_UP); - } - - function getRandomFraction(precision: number = 2): string { - return Math.random().toPrecision(precision); - } - describe('getLimitOrderRelevantState()', () => { it('works with an empty order', async () => { const order = getTestLimitOrder({ @@ -1487,7 +1329,7 @@ blockchainTests.resets('NativeOrdersFeature', env => { await takerToken .mint(taker, order.takerAmount.plus(order.takerTokenFeeAmount)) .awaitTransactionSuccessAsync(); - await fillLimitOrderAsync(order); + await testUtils.fillLimitOrderAsync(order); // Partially fill the order. const [orderInfo, fillableTakerAmount, isSignatureValid] = await zeroEx .getLimitOrderRelevantState(order, await order.getSignatureWithProviderAsync(env.provider)) @@ -1509,12 +1351,12 @@ blockchainTests.resets('NativeOrdersFeature', env => { .mint(taker, order.takerAmount.plus(order.takerTokenFeeAmount)) .awaitTransactionSuccessAsync(); // Partially fill the order. - const fillAmount = order.takerAmount.times(getRandomFraction()).integerValue(); - await fillLimitOrderAsync(order, { fillAmount }); + const fillAmount = getRandomPortion(order.takerAmount); + await testUtils.fillLimitOrderAsync(order, { fillAmount }); // Reduce maker funds to be < remaining. const remainingMakerAmount = getFillableMakerTokenAmount(order, fillAmount); - const balance = remainingMakerAmount.times(getRandomFraction()).integerValue(); - const allowance = remainingMakerAmount.times(getRandomFraction()).integerValue(); + const balance = getRandomPortion(remainingMakerAmount); + const allowance = getRandomPortion(remainingMakerAmount); await fundOrderMakerAsync(order, balance, allowance); // Get order state. const [orderInfo, fillableTakerAmount, isSignatureValid] = await zeroEx @@ -1604,7 +1446,7 @@ blockchainTests.resets('NativeOrdersFeature', env => { // Fully Fund maker and taker. await fundOrderMakerAsync(order); await takerToken.mint(taker, order.takerAmount); - await fillRfqOrderAsync(order); + await testUtils.fillRfqOrderAsync(order); // Partially fill the order. const [orderInfo, fillableTakerAmount, isSignatureValid] = await zeroEx .getRfqOrderRelevantState(order, await order.getSignatureWithProviderAsync(env.provider)) @@ -1624,12 +1466,12 @@ blockchainTests.resets('NativeOrdersFeature', env => { await fundOrderMakerAsync(order); await takerToken.mint(taker, order.takerAmount).awaitTransactionSuccessAsync(); // Partially fill the order. - const fillAmount = order.takerAmount.times(getRandomFraction()).integerValue(); - await fillRfqOrderAsync(order, fillAmount); + const fillAmount = getRandomPortion(order.takerAmount); + await testUtils.fillRfqOrderAsync(order, fillAmount); // Reduce maker funds to be < remaining. const remainingMakerAmount = getFillableMakerTokenAmount(order, fillAmount); - const balance = remainingMakerAmount.times(getRandomFraction()).integerValue(); - const allowance = remainingMakerAmount.times(getRandomFraction()).integerValue(); + const balance = getRandomPortion(remainingMakerAmount); + const allowance = getRandomPortion(remainingMakerAmount); await fundOrderMakerAsync(order, balance, allowance); // Get order state. const [orderInfo, fillableTakerAmount, isSignatureValid] = await zeroEx diff --git a/contracts/zero-ex/test/utils/orders.ts b/contracts/zero-ex/test/utils/orders.ts index 5a80df408b..77204d81e5 100644 --- a/contracts/zero-ex/test/utils/orders.ts +++ b/contracts/zero-ex/test/utils/orders.ts @@ -1,6 +1,181 @@ -import { getRandomInteger, randomAddress } from '@0x/contracts-test-utils'; -import { LimitOrder, LimitOrderFields, RfqOrder, RfqOrderFields } from '@0x/protocol-utils'; +import { + BlockchainTestsEnvironment, + constants, + expect, + getRandomInteger, + randomAddress, +} from '@0x/contracts-test-utils'; +import { LimitOrder, LimitOrderFields, OrderBase, OrderInfo, RfqOrder, RfqOrderFields } from '@0x/protocol-utils'; import { BigNumber, hexUtils } from '@0x/utils'; +import { TransactionReceiptWithDecodedLogs } from 'ethereum-types'; + +import { IZeroExContract, IZeroExLimitOrderFilledEventArgs, IZeroExRfqOrderFilledEventArgs } from '../../src/wrappers'; +import { artifacts } from '../artifacts'; +import { fullMigrateAsync } from '../utils/migration'; +import { TestMintableERC20TokenContract } from '../wrappers'; + +const { ZERO_AMOUNT: ZERO, NULL_ADDRESS } = constants; + +interface RfqOrderFilledAmounts { + makerTokenFilledAmount: BigNumber; + takerTokenFilledAmount: BigNumber; +} + +interface LimitOrderFilledAmounts { + makerTokenFilledAmount: BigNumber; + takerTokenFilledAmount: BigNumber; + takerTokenFeeFilledAmount: BigNumber; +} + +export class NativeOrdersTestEnvironment { + public static async createAsync( + env: BlockchainTestsEnvironment, + gasPrice: BigNumber = new BigNumber('123e9'), + protocolFeeMultiplier: number = 70e3, + ): Promise { + const [owner, maker, taker] = await env.getAccountAddressesAsync(); + const [makerToken, takerToken] = await Promise.all( + [...new Array(2)].map(async () => + TestMintableERC20TokenContract.deployFrom0xArtifactAsync( + artifacts.TestMintableERC20Token, + env.provider, + { ...env.txDefaults, gasPrice }, + artifacts, + ), + ), + ); + const zeroEx = await fullMigrateAsync(owner, env.provider, env.txDefaults, {}, { protocolFeeMultiplier }); + await makerToken.approve(zeroEx.address, constants.MAX_UINT256).awaitTransactionSuccessAsync({ from: maker }); + await takerToken.approve(zeroEx.address, constants.MAX_UINT256).awaitTransactionSuccessAsync({ from: taker }); + return new NativeOrdersTestEnvironment( + maker, + taker, + makerToken, + takerToken, + zeroEx, + gasPrice, + gasPrice.times(protocolFeeMultiplier), + env, + ); + } + + constructor( + public readonly maker: string, + public readonly taker: string, + public readonly makerToken: TestMintableERC20TokenContract, + public readonly takerToken: TestMintableERC20TokenContract, + public readonly zeroEx: IZeroExContract, + public readonly gasPrice: BigNumber, + public readonly protocolFee: BigNumber, + private readonly _env: BlockchainTestsEnvironment, + ) {} + + public async prepareBalancesForOrdersAsync( + orders: LimitOrder[] | RfqOrder[], + taker: string = this.taker, + ): Promise { + await this.makerToken + .mint(this.maker, BigNumber.sum(...(orders as OrderBase[]).map(order => order.makerAmount))) + .awaitTransactionSuccessAsync(); + await this.takerToken + .mint( + taker, + BigNumber.sum( + ...(orders as OrderBase[]).map(order => + order.takerAmount.plus(order instanceof LimitOrder ? order.takerTokenFeeAmount : 0), + ), + ), + ) + .awaitTransactionSuccessAsync(); + } + + public async fillLimitOrderAsync( + order: LimitOrder, + opts: Partial<{ + fillAmount: BigNumber | number; + taker: string; + protocolFee: BigNumber | number; + }> = {}, + ): Promise { + const { fillAmount, taker, protocolFee } = { + taker: this.taker, + fillAmount: order.takerAmount, + ...opts, + }; + await this.prepareBalancesForOrdersAsync([order], taker); + const value = protocolFee === undefined ? this.protocolFee : protocolFee; + return this.zeroEx + .fillLimitOrder( + order, + await order.getSignatureWithProviderAsync(this._env.provider), + new BigNumber(fillAmount), + ) + .awaitTransactionSuccessAsync({ from: taker, value }); + } + + public async fillRfqOrderAsync( + order: RfqOrder, + fillAmount: BigNumber | number = order.takerAmount, + taker: string = this.taker, + ): Promise { + await this.prepareBalancesForOrdersAsync([order], taker); + return this.zeroEx + .fillRfqOrder( + order, + await order.getSignatureWithProviderAsync(this._env.provider), + new BigNumber(fillAmount), + ) + .awaitTransactionSuccessAsync({ from: taker }); + } + + public createLimitOrderFilledEventArgs( + order: LimitOrder, + takerTokenFillAmount: BigNumber = order.takerAmount, + takerTokenAlreadyFilledAmount: BigNumber = ZERO, + ): IZeroExLimitOrderFilledEventArgs { + const { + makerTokenFilledAmount, + takerTokenFilledAmount, + takerTokenFeeFilledAmount, + } = computeLimitOrderFilledAmounts(order, takerTokenFillAmount, takerTokenAlreadyFilledAmount); + const protocolFee = order.taker !== NULL_ADDRESS ? ZERO : this.protocolFee; + return { + takerTokenFilledAmount, + makerTokenFilledAmount, + takerTokenFeeFilledAmount, + orderHash: order.getHash(), + maker: order.maker, + taker: this.taker, + feeRecipient: order.feeRecipient, + makerToken: order.makerToken, + takerToken: order.takerToken, + protocolFeePaid: protocolFee, + pool: order.pool, + }; + } + + public createRfqOrderFilledEventArgs( + order: RfqOrder, + takerTokenFillAmount: BigNumber = order.takerAmount, + takerTokenAlreadyFilledAmount: BigNumber = ZERO, + ): IZeroExRfqOrderFilledEventArgs { + const { makerTokenFilledAmount, takerTokenFilledAmount } = computeRfqOrderFilledAmounts( + order, + takerTokenFillAmount, + takerTokenAlreadyFilledAmount, + ); + return { + takerTokenFilledAmount, + makerTokenFilledAmount, + orderHash: order.getHash(), + maker: order.maker, + taker: this.taker, + makerToken: order.makerToken, + takerToken: order.takerToken, + pool: order.pool, + }; + } +} /** * Generate a random limit order. @@ -40,3 +215,105 @@ export function getRandomRfqOrder(fields: Partial = {}): RfqOrde ...fields, }); } + +/** + * Asserts the fields of an OrderInfo object. + */ +export function assertOrderInfoEquals(actual: OrderInfo, expected: OrderInfo): void { + expect(actual.status, 'Order status').to.eq(expected.status); + expect(actual.orderHash, 'Order hash').to.eq(expected.orderHash); + expect(actual.takerTokenFilledAmount, 'Order takerTokenFilledAmount').to.bignumber.eq( + expected.takerTokenFilledAmount, + ); +} + +/** + * Creates an order expiry field. + */ +export function createExpiry(deltaSeconds: number = 60): BigNumber { + return new BigNumber(Math.floor(Date.now() / 1000) + deltaSeconds); +} + +/** + * Computes the maker, taker, and taker token fee amounts filled for + * the given limit order. + */ +export function computeLimitOrderFilledAmounts( + order: LimitOrder, + takerTokenFillAmount: BigNumber = order.takerAmount, + takerTokenAlreadyFilledAmount: BigNumber = ZERO, +): 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, + }; +} + +/** + * Computes the maker and taker amounts filled for the given RFQ order. + */ +export function computeRfqOrderFilledAmounts( + order: RfqOrder, + takerTokenFillAmount: BigNumber = order.takerAmount, + takerTokenAlreadyFilledAmount: BigNumber = ZERO, +): 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, + }; +} + +/** + * Computes the remaining fillable amount in maker token for + * the given order. + */ +export function getFillableMakerTokenAmount( + order: LimitOrder | RfqOrder, + takerTokenFilledAmount: BigNumber = ZERO, +): BigNumber { + return order.takerAmount + .minus(takerTokenFilledAmount) + .times(order.makerAmount) + .div(order.takerAmount) + .integerValue(BigNumber.ROUND_DOWN); +} + +/** + * Computes the remaining fillable amnount in taker token, based on + * the amount already filled and the maker's balance/allowance. + */ +export function getActualFillableTakerTokenAmount( + order: LimitOrder | RfqOrder, + makerBalance: BigNumber = order.makerAmount, + makerAllowance: BigNumber = order.makerAmount, + takerTokenFilledAmount: BigNumber = ZERO, +): BigNumber { + const fillableMakerTokenAmount = getFillableMakerTokenAmount(order, takerTokenFilledAmount); + return BigNumber.min(fillableMakerTokenAmount, makerBalance, makerAllowance) + .times(order.takerAmount) + .div(order.makerAmount) + .integerValue(BigNumber.ROUND_UP); +} diff --git a/contracts/zero-ex/test/wrappers.ts b/contracts/zero-ex/test/wrappers.ts index 3feacb7589..3c84d1d708 100644 --- a/contracts/zero-ex/test/wrappers.ts +++ b/contracts/zero-ex/test/wrappers.ts @@ -5,6 +5,7 @@ */ export * from '../test/generated-wrappers/affiliate_fee_transformer'; export * from '../test/generated-wrappers/allowance_target'; +export * from '../test/generated-wrappers/batch_fill_native_orders_feature'; export * from '../test/generated-wrappers/bootstrap_feature'; export * from '../test/generated-wrappers/bridge_adapter'; export * from '../test/generated-wrappers/bridge_source'; @@ -20,6 +21,7 @@ export * from '../test/generated-wrappers/fixin_token_spender'; export * from '../test/generated-wrappers/flash_wallet'; export * from '../test/generated-wrappers/full_migration'; export * from '../test/generated-wrappers/i_allowance_target'; +export * from '../test/generated-wrappers/i_batch_fill_native_orders_feature'; export * from '../test/generated-wrappers/i_bootstrap_feature'; export * from '../test/generated-wrappers/i_bridge_adapter'; export * from '../test/generated-wrappers/i_erc20_bridge'; @@ -31,6 +33,8 @@ 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_mooniswap_pool'; +export * from '../test/generated-wrappers/i_multiplex_feature'; +export * from '../test/generated-wrappers/i_native_orders_events'; export * from '../test/generated-wrappers/i_native_orders_feature'; export * from '../test/generated-wrappers/i_ownable_feature'; export * from '../test/generated-wrappers/i_simple_function_registry_feature'; @@ -39,6 +43,7 @@ export * from '../test/generated-wrappers/i_test_simple_function_registry_featur export * from '../test/generated-wrappers/i_token_spender_feature'; export * from '../test/generated-wrappers/i_transform_erc20_feature'; export * from '../test/generated-wrappers/i_uniswap_feature'; +export * from '../test/generated-wrappers/i_uniswap_v2_pair'; export * from '../test/generated-wrappers/i_zero_ex'; export * from '../test/generated-wrappers/initial_migration'; export * from '../test/generated-wrappers/lib_bootstrap'; @@ -88,7 +93,12 @@ 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/mooniswap_liquidity_provider'; +export * from '../test/generated-wrappers/multiplex_feature'; +export * from '../test/generated-wrappers/native_orders_cancellation'; export * from '../test/generated-wrappers/native_orders_feature'; +export * from '../test/generated-wrappers/native_orders_info'; +export * from '../test/generated-wrappers/native_orders_protocol_fees'; +export * from '../test/generated-wrappers/native_orders_settlement'; export * from '../test/generated-wrappers/ownable_feature'; export * from '../test/generated-wrappers/pay_taker_transformer'; export * from '../test/generated-wrappers/permissionless_transformer_deployer'; diff --git a/contracts/zero-ex/tsconfig.json b/contracts/zero-ex/tsconfig.json index 25b07b586a..b0bafb8853 100644 --- a/contracts/zero-ex/tsconfig.json +++ b/contracts/zero-ex/tsconfig.json @@ -4,6 +4,7 @@ "include": ["./src/**/*", "./test/**/*", "./generated-wrappers/**/*", "./scripts/**/*"], "files": [ "generated-artifacts/AffiliateFeeTransformer.json", + "generated-artifacts/BatchFillNativeOrdersFeature.json", "generated-artifacts/BridgeAdapter.json", "generated-artifacts/CurveLiquidityProvider.json", "generated-artifacts/FeeCollector.json", @@ -11,9 +12,11 @@ "generated-artifacts/FillQuoteTransformer.json", "generated-artifacts/FullMigration.json", "generated-artifacts/IAllowanceTarget.json", + "generated-artifacts/IBatchFillNativeOrdersFeature.json", "generated-artifacts/IERC20Transformer.json", "generated-artifacts/IFlashWallet.json", "generated-artifacts/ILiquidityProviderFeature.json", + "generated-artifacts/IMultiplexFeature.json", "generated-artifacts/INativeOrdersFeature.json", "generated-artifacts/IOwnableFeature.json", "generated-artifacts/ISimpleFunctionRegistryFeature.json", @@ -24,6 +27,7 @@ "generated-artifacts/LiquidityProviderFeature.json", "generated-artifacts/LogMetadataTransformer.json", "generated-artifacts/MetaTransactionsFeature.json", + "generated-artifacts/MultiplexFeature.json", "generated-artifacts/NativeOrdersFeature.json", "generated-artifacts/OwnableFeature.json", "generated-artifacts/PayTakerTransformer.json", @@ -35,6 +39,7 @@ "generated-artifacts/ZeroEx.json", "test/generated-artifacts/AffiliateFeeTransformer.json", "test/generated-artifacts/AllowanceTarget.json", + "test/generated-artifacts/BatchFillNativeOrdersFeature.json", "test/generated-artifacts/BootstrapFeature.json", "test/generated-artifacts/BridgeAdapter.json", "test/generated-artifacts/BridgeSource.json", @@ -50,6 +55,7 @@ "test/generated-artifacts/FlashWallet.json", "test/generated-artifacts/FullMigration.json", "test/generated-artifacts/IAllowanceTarget.json", + "test/generated-artifacts/IBatchFillNativeOrdersFeature.json", "test/generated-artifacts/IBootstrapFeature.json", "test/generated-artifacts/IBridgeAdapter.json", "test/generated-artifacts/IERC20Bridge.json", @@ -61,6 +67,8 @@ "test/generated-artifacts/ILiquidityProviderSandbox.json", "test/generated-artifacts/IMetaTransactionsFeature.json", "test/generated-artifacts/IMooniswapPool.json", + "test/generated-artifacts/IMultiplexFeature.json", + "test/generated-artifacts/INativeOrdersEvents.json", "test/generated-artifacts/INativeOrdersFeature.json", "test/generated-artifacts/IOwnableFeature.json", "test/generated-artifacts/ISimpleFunctionRegistryFeature.json", @@ -69,6 +77,7 @@ "test/generated-artifacts/ITokenSpenderFeature.json", "test/generated-artifacts/ITransformERC20Feature.json", "test/generated-artifacts/IUniswapFeature.json", + "test/generated-artifacts/IUniswapV2Pair.json", "test/generated-artifacts/IZeroEx.json", "test/generated-artifacts/InitialMigration.json", "test/generated-artifacts/LibBootstrap.json", @@ -118,7 +127,12 @@ "test/generated-artifacts/MixinUniswapV2.json", "test/generated-artifacts/MixinZeroExBridge.json", "test/generated-artifacts/MooniswapLiquidityProvider.json", + "test/generated-artifacts/MultiplexFeature.json", + "test/generated-artifacts/NativeOrdersCancellation.json", "test/generated-artifacts/NativeOrdersFeature.json", + "test/generated-artifacts/NativeOrdersInfo.json", + "test/generated-artifacts/NativeOrdersProtocolFees.json", + "test/generated-artifacts/NativeOrdersSettlement.json", "test/generated-artifacts/OwnableFeature.json", "test/generated-artifacts/PayTakerTransformer.json", "test/generated-artifacts/PermissionlessTransformerDeployer.json", diff --git a/packages/contract-artifacts/CHANGELOG.json b/packages/contract-artifacts/CHANGELOG.json index ca0d263f28..038a137d50 100644 --- a/packages/contract-artifacts/CHANGELOG.json +++ b/packages/contract-artifacts/CHANGELOG.json @@ -1,4 +1,13 @@ [ + { + "version": "3.13.0", + "changes": [ + { + "note": "Update IZeroEx artifact", + "pr": 140 + } + ] + }, { "version": "3.12.0", "changes": [ diff --git a/packages/contract-artifacts/artifacts/IZeroEx.json b/packages/contract-artifacts/artifacts/IZeroEx.json index bbc11dd6d8..049da0e6bb 100644 --- a/packages/contract-artifacts/artifacts/IZeroEx.json +++ b/packages/contract-artifacts/artifacts/IZeroEx.json @@ -3,6 +3,16 @@ "contractName": "IZeroEx", "compilerOutput": { "abi": [ + { + "anonymous": false, + "inputs": [ + { "indexed": false, "internalType": "bytes32", "name": "orderHash", "type": "bytes32" }, + { "indexed": false, "internalType": "address", "name": "maker", "type": "address" }, + { "indexed": false, "internalType": "uint64", "name": "expiry", "type": "uint64" } + ], + "name": "ExpiredRfqOrder", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -39,26 +49,11 @@ { "anonymous": false, "inputs": [ - { - "indexed": false, - "internalType": "contract IERC20TokenV06", - "name": "inputToken", - "type": "address" - }, - { - "indexed": false, - "internalType": "contract IERC20TokenV06", - "name": "outputToken", - "type": "address" - }, + { "indexed": false, "internalType": "address", "name": "inputToken", "type": "address" }, + { "indexed": false, "internalType": "address", "name": "outputToken", "type": "address" }, { "indexed": false, "internalType": "uint256", "name": "inputTokenAmount", "type": "uint256" }, { "indexed": false, "internalType": "uint256", "name": "outputTokenAmount", "type": "uint256" }, - { - "indexed": false, - "internalType": "contract ILiquidityProvider", - "name": "provider", - "type": "address" - }, + { "indexed": false, "internalType": "address", "name": "provider", "type": "address" }, { "indexed": false, "internalType": "address", "name": "recipient", "type": "address" } ], "name": "LiquidityProviderSwap", @@ -444,6 +439,127 @@ "stateMutability": "payable", "type": "function" }, + { + "inputs": [ + { + "components": [ + { "internalType": "contract IERC20TokenV06", "name": "inputToken", "type": "address" }, + { "internalType": "contract IERC20TokenV06", "name": "outputToken", "type": "address" }, + { "internalType": "uint256", "name": "sellAmount", "type": "uint256" }, + { + "components": [ + { "internalType": "bytes4", "name": "selector", "type": "bytes4" }, + { "internalType": "uint256", "name": "sellAmount", "type": "uint256" }, + { "internalType": "bytes", "name": "data", "type": "bytes" } + ], + "internalType": "struct IMultiplexFeature.WrappedBatchCall[]", + "name": "calls", + "type": "tuple[]" + } + ], + "internalType": "struct IMultiplexFeature.BatchFillData", + "name": "fillData", + "type": "tuple" + }, + { "internalType": "uint256", "name": "minBuyAmount", "type": "uint256" } + ], + "name": "batchFill", + "outputs": [{ "internalType": "uint256", "name": "outputTokenAmount", "type": "uint256" }], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { "internalType": "contract IERC20TokenV06", "name": "makerToken", "type": "address" }, + { "internalType": "contract IERC20TokenV06", "name": "takerToken", "type": "address" }, + { "internalType": "uint128", "name": "makerAmount", "type": "uint128" }, + { "internalType": "uint128", "name": "takerAmount", "type": "uint128" }, + { "internalType": "uint128", "name": "takerTokenFeeAmount", "type": "uint128" }, + { "internalType": "address", "name": "maker", "type": "address" }, + { "internalType": "address", "name": "taker", "type": "address" }, + { "internalType": "address", "name": "sender", "type": "address" }, + { "internalType": "address", "name": "feeRecipient", "type": "address" }, + { "internalType": "bytes32", "name": "pool", "type": "bytes32" }, + { "internalType": "uint64", "name": "expiry", "type": "uint64" }, + { "internalType": "uint256", "name": "salt", "type": "uint256" } + ], + "internalType": "struct LibNativeOrder.LimitOrder[]", + "name": "orders", + "type": "tuple[]" + }, + { + "components": [ + { + "internalType": "enum LibSignature.SignatureType", + "name": "signatureType", + "type": "uint8" + }, + { "internalType": "uint8", "name": "v", "type": "uint8" }, + { "internalType": "bytes32", "name": "r", "type": "bytes32" }, + { "internalType": "bytes32", "name": "s", "type": "bytes32" } + ], + "internalType": "struct LibSignature.Signature[]", + "name": "signatures", + "type": "tuple[]" + }, + { "internalType": "uint128[]", "name": "takerTokenFillAmounts", "type": "uint128[]" }, + { "internalType": "bool", "name": "revertIfIncomplete", "type": "bool" } + ], + "name": "batchFillLimitOrders", + "outputs": [ + { "internalType": "uint128[]", "name": "takerTokenFilledAmounts", "type": "uint128[]" }, + { "internalType": "uint128[]", "name": "makerTokenFilledAmounts", "type": "uint128[]" } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { "internalType": "contract IERC20TokenV06", "name": "makerToken", "type": "address" }, + { "internalType": "contract IERC20TokenV06", "name": "takerToken", "type": "address" }, + { "internalType": "uint128", "name": "makerAmount", "type": "uint128" }, + { "internalType": "uint128", "name": "takerAmount", "type": "uint128" }, + { "internalType": "address", "name": "maker", "type": "address" }, + { "internalType": "address", "name": "taker", "type": "address" }, + { "internalType": "address", "name": "txOrigin", "type": "address" }, + { "internalType": "bytes32", "name": "pool", "type": "bytes32" }, + { "internalType": "uint64", "name": "expiry", "type": "uint64" }, + { "internalType": "uint256", "name": "salt", "type": "uint256" } + ], + "internalType": "struct LibNativeOrder.RfqOrder[]", + "name": "orders", + "type": "tuple[]" + }, + { + "components": [ + { + "internalType": "enum LibSignature.SignatureType", + "name": "signatureType", + "type": "uint8" + }, + { "internalType": "uint8", "name": "v", "type": "uint8" }, + { "internalType": "bytes32", "name": "r", "type": "bytes32" }, + { "internalType": "bytes32", "name": "s", "type": "bytes32" } + ], + "internalType": "struct LibSignature.Signature[]", + "name": "signatures", + "type": "tuple[]" + }, + { "internalType": "uint128[]", "name": "takerTokenFillAmounts", "type": "uint128[]" }, + { "internalType": "bool", "name": "revertIfIncomplete", "type": "bool" } + ], + "name": "batchFillRfqOrders", + "outputs": [ + { "internalType": "uint128[]", "name": "takerTokenFilledAmounts", "type": "uint128[]" }, + { "internalType": "uint128[]", "name": "makerTokenFilledAmounts", "type": "uint128[]" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -1221,6 +1337,33 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "components": [ + { "internalType": "address[]", "name": "tokens", "type": "address[]" }, + { "internalType": "uint256", "name": "sellAmount", "type": "uint256" }, + { + "components": [ + { "internalType": "bytes4", "name": "selector", "type": "bytes4" }, + { "internalType": "bytes", "name": "data", "type": "bytes" } + ], + "internalType": "struct IMultiplexFeature.WrappedMultiHopCall[]", + "name": "calls", + "type": "tuple[]" + } + ], + "internalType": "struct IMultiplexFeature.MultiHopFillData", + "name": "fillData", + "type": "tuple" + }, + { "internalType": "uint256", "name": "minBuyAmount", "type": "uint256" } + ], + "name": "multiHopFill", + "outputs": [{ "internalType": "uint256", "name": "outputTokenAmount", "type": "uint256" }], + "stateMutability": "payable", + "type": "function" + }, { "inputs": [], "name": "owner", @@ -1402,6 +1545,40 @@ }, "returns": { "returnResults": "The ABI-encoded results of the underlying calls." } }, + "batchFill((address,address,uint256,(bytes4,uint256,bytes)[]),uint256)": { + "details": "Executes a batch of fills selling `fillData.inputToken` for `fillData.outputToken` in sequence. Refer to the internal variant `_batchFill` for the allowed nested operations.", + "params": { + "fillData": "Encodes the input/output tokens, the sell amount, and the nested operations for this batch fill.", + "minBuyAmount": "The minimum amount of `fillData.outputToken` to buy. Reverts if this amount is not met." + }, + "returns": { "outputTokenAmount": "The amount of the output token bought." } + }, + "batchFillLimitOrders((address,address,uint128,uint128,uint128,address,address,address,address,bytes32,uint64,uint256)[],(uint8,uint8,bytes32,bytes32)[],uint128[],bool)": { + "details": "Fills multiple limit orders.", + "params": { + "orders": "Array of limit orders.", + "revertIfIncomplete": "If true, reverts if this function fails to fill the full fill amount for any individual order.", + "signatures": "Array of signatures corresponding to each order.", + "takerTokenFillAmounts": "Array of desired amounts to fill each order." + }, + "returns": { + "makerTokenFilledAmounts": "Array of amounts filled, in maker token.", + "takerTokenFilledAmounts": "Array of amounts filled, in taker token." + } + }, + "batchFillRfqOrders((address,address,uint128,uint128,address,address,address,bytes32,uint64,uint256)[],(uint8,uint8,bytes32,bytes32)[],uint128[],bool)": { + "details": "Fills multiple RFQ orders.", + "params": { + "orders": "Array of RFQ orders.", + "revertIfIncomplete": "If true, reverts if this function fails to fill the full fill amount for any individual order.", + "signatures": "Array of signatures corresponding to each order.", + "takerTokenFillAmounts": "Array of desired amounts to fill each order." + }, + "returns": { + "makerTokenFilledAmounts": "Array of amounts filled, in maker token.", + "takerTokenFilledAmounts": "Array of amounts filled, in taker token." + } + }, "batchGetLimitOrderRelevantStates((address,address,uint128,uint128,uint128,address,address,address,address,bytes32,uint64,uint256)[],(uint8,uint8,bytes32,bytes32)[])": { "details": "Batch version of `getLimitOrderRelevantState()`, without reverting. Orders that would normally cause `getLimitOrderRelevantState()` to revert will have empty results.", "params": { "orders": "The limit orders.", "signatures": "The order signatures." }, @@ -1600,6 +1777,14 @@ "target": "The migrator contract address." } }, + "multiHopFill((address[],uint256,(bytes4,bytes)[]),uint256)": { + "details": "Executes a sequence of fills \"hopping\" through the path of tokens given by `fillData.tokens`. Refer to the internal variant `_multiHopFill` for the allowed nested operations.", + "params": { + "fillData": "Encodes the path of tokens, the sell amount, and the nested operations for this multi-hop fill.", + "minBuyAmount": "The minimum amount of the output token to buy. Reverts if this amount is not met." + }, + "returns": { "outputTokenAmount": "The amount of the output token bought." } + }, "owner()": { "details": "The owner of this contract.", "returns": { "ownerAddress": "The owner address." } diff --git a/packages/contract-wrappers/CHANGELOG.json b/packages/contract-wrappers/CHANGELOG.json index 73b8eabe17..221201225e 100644 --- a/packages/contract-wrappers/CHANGELOG.json +++ b/packages/contract-wrappers/CHANGELOG.json @@ -1,4 +1,13 @@ [ + { + "version": "13.14.0", + "changes": [ + { + "note": "Update IZeroExContract wrapper", + "pr": 140 + } + ] + }, { "version": "13.13.0", "changes": [ diff --git a/packages/contract-wrappers/src/generated-wrappers/i_zero_ex.ts b/packages/contract-wrappers/src/generated-wrappers/i_zero_ex.ts index f6e71f9cc7..3154fb760a 100644 --- a/packages/contract-wrappers/src/generated-wrappers/i_zero_ex.ts +++ b/packages/contract-wrappers/src/generated-wrappers/i_zero_ex.ts @@ -36,6 +36,7 @@ import * as ethers from 'ethers'; // tslint:enable:no-unused-variable export type IZeroExEventArgs = + | IZeroExExpiredRfqOrderEventArgs | IZeroExLimitOrderFilledEventArgs | IZeroExLiquidityProviderSwapEventArgs | IZeroExMetaTransactionExecutedEventArgs @@ -52,6 +53,7 @@ export type IZeroExEventArgs = | IZeroExTransformerDeployerUpdatedEventArgs; export enum IZeroExEvents { + ExpiredRfqOrder = 'ExpiredRfqOrder', LimitOrderFilled = 'LimitOrderFilled', LiquidityProviderSwap = 'LiquidityProviderSwap', MetaTransactionExecuted = 'MetaTransactionExecuted', @@ -68,6 +70,12 @@ export enum IZeroExEvents { TransformerDeployerUpdated = 'TransformerDeployerUpdated', } +export interface IZeroExExpiredRfqOrderEventArgs extends DecodedLogArgs { + orderHash: string; + maker: string; + expiry: BigNumber; +} + export interface IZeroExLimitOrderFilledEventArgs extends DecodedLogArgs { orderHash: string; maker: string; @@ -284,6 +292,29 @@ export class IZeroExContract extends BaseContract { */ public static ABI(): ContractAbi { const abi = [ + { + anonymous: false, + inputs: [ + { + name: 'orderHash', + type: 'bytes32', + indexed: false, + }, + { + name: 'maker', + type: 'address', + indexed: false, + }, + { + name: 'expiry', + type: 'uint64', + indexed: false, + }, + ], + name: 'ExpiredRfqOrder', + outputs: [], + type: 'event', + }, { anonymous: false, inputs: [ @@ -1193,6 +1224,253 @@ export class IZeroExContract extends BaseContract { stateMutability: 'payable', type: 'function', }, + { + inputs: [ + { + name: 'fillData', + type: 'tuple', + components: [ + { + name: 'inputToken', + type: 'address', + }, + { + name: 'outputToken', + type: 'address', + }, + { + name: 'sellAmount', + type: 'uint256', + }, + { + name: 'calls', + type: 'tuple[]', + components: [ + { + name: 'selector', + type: 'bytes4', + }, + { + name: 'sellAmount', + type: 'uint256', + }, + { + name: 'data', + type: 'bytes', + }, + ], + }, + ], + }, + { + name: 'minBuyAmount', + type: 'uint256', + }, + ], + name: 'batchFill', + outputs: [ + { + name: 'outputTokenAmount', + type: 'uint256', + }, + ], + stateMutability: 'payable', + type: 'function', + }, + { + inputs: [ + { + name: 'orders', + type: 'tuple[]', + components: [ + { + name: 'makerToken', + type: 'address', + }, + { + name: 'takerToken', + type: 'address', + }, + { + name: 'makerAmount', + type: 'uint128', + }, + { + name: 'takerAmount', + type: 'uint128', + }, + { + name: 'takerTokenFeeAmount', + type: 'uint128', + }, + { + name: 'maker', + type: 'address', + }, + { + name: 'taker', + type: 'address', + }, + { + name: 'sender', + type: 'address', + }, + { + name: 'feeRecipient', + type: 'address', + }, + { + name: 'pool', + type: 'bytes32', + }, + { + name: 'expiry', + type: 'uint64', + }, + { + name: 'salt', + type: 'uint256', + }, + ], + }, + { + name: 'signatures', + type: 'tuple[]', + components: [ + { + name: 'signatureType', + type: 'uint8', + }, + { + name: 'v', + type: 'uint8', + }, + { + name: 'r', + type: 'bytes32', + }, + { + name: 's', + type: 'bytes32', + }, + ], + }, + { + name: 'takerTokenFillAmounts', + type: 'uint128[]', + }, + { + name: 'revertIfIncomplete', + type: 'bool', + }, + ], + name: 'batchFillLimitOrders', + outputs: [ + { + name: 'takerTokenFilledAmounts', + type: 'uint128[]', + }, + { + name: 'makerTokenFilledAmounts', + type: 'uint128[]', + }, + ], + stateMutability: 'payable', + type: 'function', + }, + { + inputs: [ + { + name: 'orders', + type: 'tuple[]', + components: [ + { + name: 'makerToken', + type: 'address', + }, + { + name: 'takerToken', + type: 'address', + }, + { + name: 'makerAmount', + type: 'uint128', + }, + { + name: 'takerAmount', + type: 'uint128', + }, + { + name: 'maker', + type: 'address', + }, + { + name: 'taker', + type: 'address', + }, + { + name: 'txOrigin', + type: 'address', + }, + { + name: 'pool', + type: 'bytes32', + }, + { + name: 'expiry', + type: 'uint64', + }, + { + name: 'salt', + type: 'uint256', + }, + ], + }, + { + name: 'signatures', + type: 'tuple[]', + components: [ + { + name: 'signatureType', + type: 'uint8', + }, + { + name: 'v', + type: 'uint8', + }, + { + name: 'r', + type: 'bytes32', + }, + { + name: 's', + type: 'bytes32', + }, + ], + }, + { + name: 'takerTokenFillAmounts', + type: 'uint128[]', + }, + { + name: 'revertIfIncomplete', + type: 'bool', + }, + ], + name: 'batchFillRfqOrders', + outputs: [ + { + name: 'takerTokenFilledAmounts', + type: 'uint128[]', + }, + { + name: 'makerTokenFilledAmounts', + type: 'uint128[]', + }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, { inputs: [ { @@ -2804,6 +3082,51 @@ export class IZeroExContract extends BaseContract { stateMutability: 'nonpayable', type: 'function', }, + { + inputs: [ + { + name: 'fillData', + type: 'tuple', + components: [ + { + name: 'tokens', + type: 'address[]', + }, + { + name: 'sellAmount', + type: 'uint256', + }, + { + name: 'calls', + type: 'tuple[]', + components: [ + { + name: 'selector', + type: 'bytes4', + }, + { + name: 'data', + type: 'bytes', + }, + ], + }, + ], + }, + { + name: 'minBuyAmount', + type: 'uint256', + }, + ], + name: 'multiHopFill', + outputs: [ + { + name: 'outputTokenAmount', + type: 'uint256', + }, + ], + stateMutability: 'payable', + type: 'function', + }, { inputs: [], name: 'owner', @@ -3705,6 +4028,240 @@ export class IZeroExContract extends BaseContract { }, }; } + /** + * Executes a batch of fills selling `fillData.inputToken` + * for `fillData.outputToken` in sequence. Refer to the + * internal variant `_batchFill` for the allowed nested + * operations. + * @param fillData Encodes the input/output tokens, the sell amount, and + * the nested operations for this batch fill. + * @param minBuyAmount The minimum amount of `fillData.outputToken` to + * buy. Reverts if this amount is not met. + */ + public batchFill( + fillData: { + inputToken: string; + outputToken: string; + sellAmount: BigNumber; + calls: Array<{ selector: string; sellAmount: BigNumber; data: string }>; + }, + minBuyAmount: BigNumber, + ): ContractTxFunctionObj { + const self = (this as any) as IZeroExContract; + + assert.isBigNumber('minBuyAmount', minBuyAmount); + const functionSignature = 'batchFill((address,address,uint256,(bytes4,uint256,bytes)[]),uint256)'; + + return { + async sendTransactionAsync( + txData?: Partial | undefined, + opts: SendTransactionOpts = { shouldValidate: true }, + ): Promise { + const txDataWithDefaults = await self._applyDefaultsToTxDataAsync( + { data: this.getABIEncodedTransactionData(), ...txData }, + this.estimateGasAsync.bind(this), + ); + if (opts.shouldValidate !== false) { + await this.callAsync(txDataWithDefaults); + } + return self._web3Wrapper.sendTransactionAsync(txDataWithDefaults); + }, + awaitTransactionSuccessAsync( + txData?: Partial, + opts: AwaitTransactionSuccessOpts = { shouldValidate: true }, + ): PromiseWithTransactionHash { + return self._promiseWithTransactionHash(this.sendTransactionAsync(txData, opts), opts); + }, + async estimateGasAsync(txData?: Partial | undefined): Promise { + const txDataWithDefaults = await self._applyDefaultsToTxDataAsync({ + data: this.getABIEncodedTransactionData(), + ...txData, + }); + return self._web3Wrapper.estimateGasAsync(txDataWithDefaults); + }, + async callAsync(callData: Partial = {}, defaultBlock?: BlockParam): Promise { + BaseContract._assertCallParams(callData, defaultBlock); + const rawCallResult = await self._performCallAsync( + { data: this.getABIEncodedTransactionData(), ...callData }, + defaultBlock, + ); + const abiEncoder = self._lookupAbiEncoder(functionSignature); + BaseContract._throwIfUnexpectedEmptyCallResult(rawCallResult, abiEncoder); + return abiEncoder.strictDecodeReturnValue(rawCallResult); + }, + getABIEncodedTransactionData(): string { + return self._strictEncodeArguments(functionSignature, [fillData, minBuyAmount]); + }, + }; + } + /** + * Fills multiple limit orders. + * @param orders Array of limit orders. + * @param signatures Array of signatures corresponding to each order. + * @param takerTokenFillAmounts Array of desired amounts to fill each order. + * @param revertIfIncomplete If true, reverts if this function fails to + * fill the full fill amount for any individual order. + */ + public batchFillLimitOrders( + orders: Array<{ + makerToken: string; + takerToken: string; + makerAmount: BigNumber; + takerAmount: BigNumber; + takerTokenFeeAmount: BigNumber; + maker: string; + taker: string; + sender: string; + feeRecipient: string; + pool: string; + expiry: BigNumber; + salt: BigNumber; + }>, + signatures: Array<{ signatureType: number | BigNumber; v: number | BigNumber; r: string; s: string }>, + takerTokenFillAmounts: BigNumber[], + revertIfIncomplete: boolean, + ): ContractTxFunctionObj<[BigNumber[], BigNumber[]]> { + const self = (this as any) as IZeroExContract; + assert.isArray('orders', orders); + assert.isArray('signatures', signatures); + assert.isArray('takerTokenFillAmounts', takerTokenFillAmounts); + assert.isBoolean('revertIfIncomplete', revertIfIncomplete); + const functionSignature = + 'batchFillLimitOrders((address,address,uint128,uint128,uint128,address,address,address,address,bytes32,uint64,uint256)[],(uint8,uint8,bytes32,bytes32)[],uint128[],bool)'; + + return { + async sendTransactionAsync( + txData?: Partial | undefined, + opts: SendTransactionOpts = { shouldValidate: true }, + ): Promise { + const txDataWithDefaults = await self._applyDefaultsToTxDataAsync( + { data: this.getABIEncodedTransactionData(), ...txData }, + this.estimateGasAsync.bind(this), + ); + if (opts.shouldValidate !== false) { + await this.callAsync(txDataWithDefaults); + } + return self._web3Wrapper.sendTransactionAsync(txDataWithDefaults); + }, + awaitTransactionSuccessAsync( + txData?: Partial, + opts: AwaitTransactionSuccessOpts = { shouldValidate: true }, + ): PromiseWithTransactionHash { + return self._promiseWithTransactionHash(this.sendTransactionAsync(txData, opts), opts); + }, + async estimateGasAsync(txData?: Partial | undefined): Promise { + const txDataWithDefaults = await self._applyDefaultsToTxDataAsync({ + data: this.getABIEncodedTransactionData(), + ...txData, + }); + return self._web3Wrapper.estimateGasAsync(txDataWithDefaults); + }, + async callAsync( + callData: Partial = {}, + defaultBlock?: BlockParam, + ): Promise<[BigNumber[], BigNumber[]]> { + BaseContract._assertCallParams(callData, defaultBlock); + const rawCallResult = await self._performCallAsync( + { data: this.getABIEncodedTransactionData(), ...callData }, + defaultBlock, + ); + const abiEncoder = self._lookupAbiEncoder(functionSignature); + BaseContract._throwIfUnexpectedEmptyCallResult(rawCallResult, abiEncoder); + return abiEncoder.strictDecodeReturnValue<[BigNumber[], BigNumber[]]>(rawCallResult); + }, + getABIEncodedTransactionData(): string { + return self._strictEncodeArguments(functionSignature, [ + orders, + signatures, + takerTokenFillAmounts, + revertIfIncomplete, + ]); + }, + }; + } + /** + * Fills multiple RFQ orders. + * @param orders Array of RFQ orders. + * @param signatures Array of signatures corresponding to each order. + * @param takerTokenFillAmounts Array of desired amounts to fill each order. + * @param revertIfIncomplete If true, reverts if this function fails to + * fill the full fill amount for any individual order. + */ + public batchFillRfqOrders( + orders: Array<{ + makerToken: string; + takerToken: string; + makerAmount: BigNumber; + takerAmount: BigNumber; + maker: string; + taker: string; + txOrigin: string; + pool: string; + expiry: BigNumber; + salt: BigNumber; + }>, + signatures: Array<{ signatureType: number | BigNumber; v: number | BigNumber; r: string; s: string }>, + takerTokenFillAmounts: BigNumber[], + revertIfIncomplete: boolean, + ): ContractTxFunctionObj<[BigNumber[], BigNumber[]]> { + const self = (this as any) as IZeroExContract; + assert.isArray('orders', orders); + assert.isArray('signatures', signatures); + assert.isArray('takerTokenFillAmounts', takerTokenFillAmounts); + assert.isBoolean('revertIfIncomplete', revertIfIncomplete); + const functionSignature = + 'batchFillRfqOrders((address,address,uint128,uint128,address,address,address,bytes32,uint64,uint256)[],(uint8,uint8,bytes32,bytes32)[],uint128[],bool)'; + + return { + async sendTransactionAsync( + txData?: Partial | undefined, + opts: SendTransactionOpts = { shouldValidate: true }, + ): Promise { + const txDataWithDefaults = await self._applyDefaultsToTxDataAsync( + { data: this.getABIEncodedTransactionData(), ...txData }, + this.estimateGasAsync.bind(this), + ); + if (opts.shouldValidate !== false) { + await this.callAsync(txDataWithDefaults); + } + return self._web3Wrapper.sendTransactionAsync(txDataWithDefaults); + }, + awaitTransactionSuccessAsync( + txData?: Partial, + opts: AwaitTransactionSuccessOpts = { shouldValidate: true }, + ): PromiseWithTransactionHash { + return self._promiseWithTransactionHash(this.sendTransactionAsync(txData, opts), opts); + }, + async estimateGasAsync(txData?: Partial | undefined): Promise { + const txDataWithDefaults = await self._applyDefaultsToTxDataAsync({ + data: this.getABIEncodedTransactionData(), + ...txData, + }); + return self._web3Wrapper.estimateGasAsync(txDataWithDefaults); + }, + async callAsync( + callData: Partial = {}, + defaultBlock?: BlockParam, + ): Promise<[BigNumber[], BigNumber[]]> { + BaseContract._assertCallParams(callData, defaultBlock); + const rawCallResult = await self._performCallAsync( + { data: this.getABIEncodedTransactionData(), ...callData }, + defaultBlock, + ); + const abiEncoder = self._lookupAbiEncoder(functionSignature); + BaseContract._throwIfUnexpectedEmptyCallResult(rawCallResult, abiEncoder); + return abiEncoder.strictDecodeReturnValue<[BigNumber[], BigNumber[]]>(rawCallResult); + }, + getABIEncodedTransactionData(): string { + return self._strictEncodeArguments(functionSignature, [ + orders, + signatures, + takerTokenFillAmounts, + revertIfIncomplete, + ]); + }, + }; + } /** * Batch version of `getLimitOrderRelevantState()`, without reverting. * Orders that would normally cause `getLimitOrderRelevantState()` @@ -5679,6 +6236,67 @@ export class IZeroExContract extends BaseContract { }, }; } + /** + * Executes a sequence of fills "hopping" through the + * path of tokens given by `fillData.tokens`. Refer to the + * internal variant `_multiHopFill` for the allowed nested + * operations. + * @param fillData Encodes the path of tokens, the sell amount, and the + * nested operations for this multi-hop fill. + * @param minBuyAmount The minimum amount of the output token to buy. + * Reverts if this amount is not met. + */ + public multiHopFill( + fillData: { tokens: string[]; sellAmount: BigNumber; calls: Array<{ selector: string; data: string }> }, + minBuyAmount: BigNumber, + ): ContractTxFunctionObj { + const self = (this as any) as IZeroExContract; + + assert.isBigNumber('minBuyAmount', minBuyAmount); + const functionSignature = 'multiHopFill((address[],uint256,(bytes4,bytes)[]),uint256)'; + + return { + async sendTransactionAsync( + txData?: Partial | undefined, + opts: SendTransactionOpts = { shouldValidate: true }, + ): Promise { + const txDataWithDefaults = await self._applyDefaultsToTxDataAsync( + { data: this.getABIEncodedTransactionData(), ...txData }, + this.estimateGasAsync.bind(this), + ); + if (opts.shouldValidate !== false) { + await this.callAsync(txDataWithDefaults); + } + return self._web3Wrapper.sendTransactionAsync(txDataWithDefaults); + }, + awaitTransactionSuccessAsync( + txData?: Partial, + opts: AwaitTransactionSuccessOpts = { shouldValidate: true }, + ): PromiseWithTransactionHash { + return self._promiseWithTransactionHash(this.sendTransactionAsync(txData, opts), opts); + }, + async estimateGasAsync(txData?: Partial | undefined): Promise { + const txDataWithDefaults = await self._applyDefaultsToTxDataAsync({ + data: this.getABIEncodedTransactionData(), + ...txData, + }); + return self._web3Wrapper.estimateGasAsync(txDataWithDefaults); + }, + async callAsync(callData: Partial = {}, defaultBlock?: BlockParam): Promise { + BaseContract._assertCallParams(callData, defaultBlock); + const rawCallResult = await self._performCallAsync( + { data: this.getABIEncodedTransactionData(), ...callData }, + defaultBlock, + ); + const abiEncoder = self._lookupAbiEncoder(functionSignature); + BaseContract._throwIfUnexpectedEmptyCallResult(rawCallResult, abiEncoder); + return abiEncoder.strictDecodeReturnValue(rawCallResult); + }, + getABIEncodedTransactionData(): string { + return self._strictEncodeArguments(functionSignature, [fillData, minBuyAmount]); + }, + }; + } /** * The owner of this contract. */ diff --git a/packages/contract-wrappers/src/index.ts b/packages/contract-wrappers/src/index.ts index 7096ea284c..b48c5ab510 100644 --- a/packages/contract-wrappers/src/index.ts +++ b/packages/contract-wrappers/src/index.ts @@ -125,6 +125,7 @@ export { IZeroExContract, IZeroExEventArgs, IZeroExEvents, + IZeroExExpiredRfqOrderEventArgs, IZeroExLiquidityProviderSwapEventArgs, IZeroExMetaTransactionExecutedEventArgs, IZeroExMigratedEventArgs, diff --git a/packages/protocol-utils/src/revert-errors/native_orders.ts b/packages/protocol-utils/src/revert-errors/native_orders.ts index 33e66a5d1e..56d8d41ff4 100644 --- a/packages/protocol-utils/src/revert-errors/native_orders.ts +++ b/packages/protocol-utils/src/revert-errors/native_orders.ts @@ -110,6 +110,20 @@ export class OnlyOrderMakerAllowed extends RevertError { } } +export class BatchFillIncompleteError extends RevertError { + constructor(orderHash?: string, takerTokenFilledAmount?: Numberish, takerTokenFillAmount?: Numberish) { + super( + 'BatchFillIncompleteError', + 'BatchFillIncompleteError(bytes32 orderHash, uint256 takerTokenFilledAmount, uint256 takerTokenFillAmount)', + { + orderHash, + takerTokenFilledAmount, + takerTokenFillAmount, + }, + ); + } +} + const types = [ ProtocolFeeRefundFailed, OrderNotFillableByOriginError, @@ -120,6 +134,7 @@ const types = [ CancelSaltTooLowError, FillOrKillFailedError, OnlyOrderMakerAllowed, + BatchFillIncompleteError, ]; // Register the types we've defined.