From b46eeadc64485288add5940a210e1a7d0bcb5481 Mon Sep 17 00:00:00 2001 From: mzhu25 Date: Thu, 12 Aug 2021 17:09:46 -0700 Subject: [PATCH] Feat/multiplex/v2 (#263) * Refactor Multiplex into multiple files * Pull UniswapV3 into separate file * Add support for multihop nested within batch sell * Add useSelfBalance and recipient to _fillRfqOrder * Expose onlySelf variant in UniswapV3Feature for Multiplex * Add useSelfBalance and recipient to _transformERC20 * Add support for proportional fill amounts in batchSell * Comments and renaming * Unit tests * Use caps for immutables * Rename taker -> recipient in TransformContext and SettleOrderInfo * lint * Address nits * Swallow reverts for LiquidityProvider and UniswapV2 batch sells * Address spot-check findings (#279) * Check didSucceed in _callWithOptionalBooleanResult * Add takerToken=ETH support to OtcOrdersFeature (#287) * Add takerToken=ETH support to OtcOrdersFeature * Add batchFillTakerSignedOtcOrders * Add support for OTC to Multiplex * Address PR feedback * Update TransformERC20Feature (#303) * remove multiplex_utils * Update changelog * unbreak tests --- .../contracts/src/v06/LibERC20TokenV06.sol | 53 +- contracts/test-utils/src/log_utils.ts | 2 +- .../contracts/src/DefaultPoolOperator.sol | 7 +- contracts/zero-ex/CHANGELOG.json | 21 + .../src/errors/LibNativeOrdersRichErrors.sol | 17 - .../features/BatchFillNativeOrdersFeature.sol | 4 +- .../src/features/MetaTransactionsFeature.sol | 10 +- .../src/features/MultiplexFeature.sol | 820 ----- .../src/features/NativeOrdersFeature.sol | 2 +- .../src/features/OtcOrdersFeature.sol | 547 ++-- .../src/features/TransformERC20Feature.sol | 200 +- .../src/features/UniswapV3Feature.sol | 30 +- .../features/interfaces/IMultiplexFeature.sol | 214 +- .../interfaces/INativeOrdersFeature.sol | 7 +- .../features/interfaces/IOtcOrdersFeature.sol | 90 +- .../interfaces/ITransformERC20Feature.sol | 4 + .../features/interfaces/IUniswapV3Feature.sol | 16 + .../features/multiplex/MultiplexFeature.sol | 742 +++++ .../multiplex/MultiplexLiquidityProvider.sol | 202 ++ .../src/features/multiplex/MultiplexOtc.sol | 94 + .../src/features/multiplex/MultiplexRfq.sol | 93 + .../multiplex/MultiplexTransformERC20.sol | 64 + .../features/multiplex/MultiplexUniswapV2.sol | 290 ++ .../features/multiplex/MultiplexUniswapV3.sol | 123 + .../native_orders/NativeOrdersSettlement.sol | 179 +- .../src/transformers/FillQuoteTransformer.sol | 8 +- .../src/transformers/IERC20Transformer.sol | 4 +- .../transformers/LogMetadataTransformer.sol | 2 +- .../src/transformers/PayTakerTransformer.sol | 2 +- .../test/TestFillQuoteTransformerHost.sol | 4 +- .../contracts/test/TestLiquidityProvider.sol | 15 +- ...estMetaTransactionsNativeOrdersFeature.sol | 4 +- .../test/TestMintTokenERC20Transformer.sol | 27 +- .../contracts/test/TestMintableERC20Token.sol | 7 + .../contracts/test/TestUniswapV2Factory.sol | 44 + .../contracts/test/TestUniswapV2Pool.sol | 67 + contracts/zero-ex/contracts/test/TestWeth.sol | 6 + .../test/TestWethTransformerHost.sol | 2 +- contracts/zero-ex/package.json | 2 +- contracts/zero-ex/test/artifacts.ts | 18 +- .../test/features/liquidity_provider_test.ts | 2 - .../test/features/meta_transactions_test.ts | 2 + .../zero-ex/test/features/multiplex_test.ts | 2796 ++++++++++++----- .../zero-ex/test/features/otc_orders_test.ts | 224 +- .../test/features/transform_erc20_test.ts | 20 +- contracts/zero-ex/test/full_migration_test.ts | 5 +- .../affiliate_fee_transformer_test.ts | 6 +- .../pay_taker_transformer_test.ts | 8 +- .../positive_slippage_fee_transformer_test.ts | 6 +- contracts/zero-ex/test/utils/orders.ts | 50 +- contracts/zero-ex/test/wrappers.ts | 8 + contracts/zero-ex/tsconfig.json | 8 + .../src/revert-errors/native_orders.ts | 15 - 53 files changed, 5092 insertions(+), 2101 deletions(-) delete mode 100644 contracts/zero-ex/contracts/src/features/MultiplexFeature.sol create mode 100644 contracts/zero-ex/contracts/src/features/multiplex/MultiplexFeature.sol create mode 100644 contracts/zero-ex/contracts/src/features/multiplex/MultiplexLiquidityProvider.sol create mode 100644 contracts/zero-ex/contracts/src/features/multiplex/MultiplexOtc.sol create mode 100644 contracts/zero-ex/contracts/src/features/multiplex/MultiplexRfq.sol create mode 100644 contracts/zero-ex/contracts/src/features/multiplex/MultiplexTransformERC20.sol create mode 100644 contracts/zero-ex/contracts/src/features/multiplex/MultiplexUniswapV2.sol create mode 100644 contracts/zero-ex/contracts/src/features/multiplex/MultiplexUniswapV3.sol create mode 100644 contracts/zero-ex/contracts/test/TestUniswapV2Factory.sol create mode 100644 contracts/zero-ex/contracts/test/TestUniswapV2Pool.sol diff --git a/contracts/erc20/contracts/src/v06/LibERC20TokenV06.sol b/contracts/erc20/contracts/src/v06/LibERC20TokenV06.sol index 9bfb47c7b8..13f0a1c7c1 100644 --- a/contracts/erc20/contracts/src/v06/LibERC20TokenV06.sol +++ b/contracts/erc20/contracts/src/v06/LibERC20TokenV06.sol @@ -28,7 +28,7 @@ library LibERC20TokenV06 { bytes constant private DECIMALS_CALL_DATA = hex"313ce567"; /// @dev Calls `IERC20TokenV06(token).approve()`. - /// Reverts if the result fails `isSuccessfulResult()` or the call reverts. + /// Reverts if the return data is invalid or the call reverts. /// @param token The address of the token contract. /// @param spender The address that receives an allowance. /// @param allowance The allowance to set. @@ -49,7 +49,7 @@ library LibERC20TokenV06 { /// @dev Calls `IERC20TokenV06(token).approve()` and sets the allowance to the /// maximum if the current approval is not already >= an amount. - /// Reverts if the result fails `isSuccessfulResult()` or the call reverts. + /// Reverts if the return data is invalid or the call reverts. /// @param token The address of the token contract. /// @param spender The address that receives an allowance. /// @param amount The minimum allowance needed. @@ -66,7 +66,7 @@ library LibERC20TokenV06 { } /// @dev Calls `IERC20TokenV06(token).transfer()`. - /// Reverts if the result fails `isSuccessfulResult()` or the call reverts. + /// Reverts if the return data is invalid or the call reverts. /// @param token The address of the token contract. /// @param to The address that receives the tokens /// @param amount Number of tokens to transfer. @@ -86,7 +86,7 @@ library LibERC20TokenV06 { } /// @dev Calls `IERC20TokenV06(token).transferFrom()`. - /// Reverts if the result fails `isSuccessfulResult()` or the call reverts. + /// Reverts if the return data is invalid or the call reverts. /// @param token The address of the token contract. /// @param from The owner of the tokens. /// @param to The address that receives the tokens @@ -168,27 +168,6 @@ library LibERC20TokenV06 { } } - /// @dev Check if the data returned by a non-static call to an ERC20 token - /// is a successful result. Supported functions are `transfer()`, - /// `transferFrom()`, and `approve()`. - /// @param resultData The raw data returned by a non-static call to the ERC20 token. - /// @return isSuccessful Whether the result data indicates success. - function isSuccessfulResult(bytes memory resultData) - internal - pure - returns (bool isSuccessful) - { - if (resultData.length == 0) { - return true; - } - if (resultData.length >= 32) { - uint256 result = LibBytesV06.readUint256(resultData, 0); - if (result == 1) { - return true; - } - } - } - /// @dev Executes a call on address `target` with calldata `callData` /// and asserts that either nothing was returned or a single boolean /// was returned equal to `true`. @@ -201,9 +180,31 @@ library LibERC20TokenV06 { private { (bool didSucceed, bytes memory resultData) = target.call(callData); - if (didSucceed && isSuccessfulResult(resultData)) { + // Revert if the call reverted. + if (!didSucceed) { + LibRichErrorsV06.rrevert(resultData); + } + // If we get back 0 returndata, this may be a non-standard ERC-20 that + // does not return a boolean. Check that it at least contains code. + if (resultData.length == 0) { + uint256 size; + assembly { size := extcodesize(target) } + require(size > 0, "invalid token address, contains no code"); return; } + // If we get back at least 32 bytes, we know the target address + // contains code, and we assume it is a token that returned a boolean + // success value, which must be true. + if (resultData.length >= 32) { + uint256 result = LibBytesV06.readUint256(resultData, 0); + if (result == 1) { + return; + } else { + LibRichErrorsV06.rrevert(resultData); + } + } + // If 0 < returndatasize < 32, the target is a contract, but not a + // valid token. LibRichErrorsV06.rrevert(resultData); } } diff --git a/contracts/test-utils/src/log_utils.ts b/contracts/test-utils/src/log_utils.ts index 313b7e07d6..345d85b1c3 100644 --- a/contracts/test-utils/src/log_utils.ts +++ b/contracts/test-utils/src/log_utils.ts @@ -40,6 +40,6 @@ export function verifyEventsFromLogs( const _logs = filterLogsToArguments(logs, eventName); expect(_logs.length, `Number of ${eventName} events emitted`).to.eq(expectedEvents.length); _logs.forEach((log, index) => { - expect(log, `${eventName} event ${index}`).to.deep.equal(expectedEvents[index]); + expect(log, `${eventName} event ${index}`).to.deep.equal({ ...log, ...expectedEvents[index] }); }); } diff --git a/contracts/treasury/contracts/src/DefaultPoolOperator.sol b/contracts/treasury/contracts/src/DefaultPoolOperator.sol index d9115aa710..c97f856800 100644 --- a/contracts/treasury/contracts/src/DefaultPoolOperator.sol +++ b/contracts/treasury/contracts/src/DefaultPoolOperator.sol @@ -21,13 +21,10 @@ pragma solidity ^0.6.12; pragma experimental ABIEncoderV2; import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol"; -import "@0x/contracts-erc20/contracts/src/v06/LibERC20TokenV06.sol"; import "./IStaking.sol"; contract DefaultPoolOperator { - using LibERC20TokenV06 for IERC20TokenV06; - // Immutables IStaking public immutable stakingProxy; IERC20TokenV06 public immutable weth; @@ -57,7 +54,7 @@ contract DefaultPoolOperator { function returnStakingRewards() external { - uint256 wethBalance = weth.compatBalanceOf(address(this)); - weth.compatTransfer(address(stakingProxy), wethBalance); + uint256 wethBalance = weth.balanceOf(address(this)); + weth.transfer(address(stakingProxy), wethBalance); } } diff --git a/contracts/zero-ex/CHANGELOG.json b/contracts/zero-ex/CHANGELOG.json index 30519da308..06b007d9a2 100644 --- a/contracts/zero-ex/CHANGELOG.json +++ b/contracts/zero-ex/CHANGELOG.json @@ -1,4 +1,25 @@ [ + { + "version": "0.28.0", + "changes": [ + { + "note": "Transfer output tokens in TransformERC20Feature", + "pr": 279 + }, + { + "note": "Add support for takerToken=0xeee... in OtcOrdersFeature", + "pr": 287 + }, + { + "note": "Add support for OTC orders in MultiplexFeature", + "pr": 287 + }, + { + "note": "Multiplex v2: Refactor into multiple files, add ETH support, and other miscellanea", + "pr": 263 + } + ] + }, { "timestamp": 1628665757, "version": "0.27.1", diff --git a/contracts/zero-ex/contracts/src/errors/LibNativeOrdersRichErrors.sol b/contracts/zero-ex/contracts/src/errors/LibNativeOrdersRichErrors.sol index bc28f86301..a12ab14281 100644 --- a/contracts/zero-ex/contracts/src/errors/LibNativeOrdersRichErrors.sol +++ b/contracts/zero-ex/contracts/src/errors/LibNativeOrdersRichErrors.sol @@ -88,23 +88,6 @@ library LibNativeOrdersRichErrors { ); } - function OrderNotSignedByTakerError( - bytes32 orderHash, - address signer, - address taker - ) - internal - pure - returns (bytes memory) - { - return abi.encodeWithSelector( - bytes4(keccak256("OrderNotSignedByTakerError(bytes32,address,address)")), - orderHash, - signer, - taker - ); - } - function InvalidSignerError( address maker, address signer diff --git a/contracts/zero-ex/contracts/src/features/BatchFillNativeOrdersFeature.sol b/contracts/zero-ex/contracts/src/features/BatchFillNativeOrdersFeature.sol index 1b67b33af4..61f0bd818f 100644 --- a/contracts/zero-ex/contracts/src/features/BatchFillNativeOrdersFeature.sol +++ b/contracts/zero-ex/contracts/src/features/BatchFillNativeOrdersFeature.sol @@ -48,7 +48,7 @@ contract BatchFillNativeOrdersFeature is /// @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); + uint256 public immutable override FEATURE_VERSION = _encodeVersion(1, 1, 0); constructor(address zeroExAddress) public @@ -170,6 +170,8 @@ contract BatchFillNativeOrdersFeature is orders[i], signatures[i], takerTokenFillAmounts[i], + msg.sender, + false, msg.sender ) returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount) diff --git a/contracts/zero-ex/contracts/src/features/MetaTransactionsFeature.sol b/contracts/zero-ex/contracts/src/features/MetaTransactionsFeature.sol index 189feb1c2a..d85d6a1199 100644 --- a/contracts/zero-ex/contracts/src/features/MetaTransactionsFeature.sol +++ b/contracts/zero-ex/contracts/src/features/MetaTransactionsFeature.sol @@ -78,7 +78,7 @@ contract MetaTransactionsFeature is /// @dev Name of this feature. string public constant override FEATURE_NAME = "MetaTransactions"; /// @dev Version of this feature. - uint256 public immutable override FEATURE_VERSION = _encodeVersion(1, 1, 1); + uint256 public immutable override FEATURE_VERSION = _encodeVersion(1, 2, 0); /// @dev EIP712 typehash of the `MetaTransactionData` struct. bytes32 public immutable MTX_EIP712_TYPEHASH = keccak256( "MetaTransactionData(" @@ -415,7 +415,9 @@ contract MetaTransactionsFeature is outputToken: args.outputToken, inputTokenAmount: args.inputTokenAmount, minOutputTokenAmount: args.minOutputTokenAmount, - transformations: args.transformations + transformations: args.transformations, + useSelfBalance: false, + recipient: state.mtx.signer }) ), state.mtx.value @@ -498,7 +500,9 @@ contract MetaTransactionsFeature is order, signature, takerTokenFillAmount, - state.mtx.signer // taker is mtx signer + state.mtx.signer, // taker is mtx signer + false, + state.mtx.signer ), state.mtx.value ); diff --git a/contracts/zero-ex/contracts/src/features/MultiplexFeature.sol b/contracts/zero-ex/contracts/src/features/MultiplexFeature.sol deleted file mode 100644 index 7b1201fc7f..0000000000 --- a/contracts/zero-ex/contracts/src/features/MultiplexFeature.sol +++ /dev/null @@ -1,820 +0,0 @@ -// 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 "./interfaces/IUniswapV3Feature.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, 1, 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_ - ) - public - FixinEIP712(zeroExAddress) - { - 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 == IUniswapV3Feature.sellTokenForTokenToUniswapV3.selector) { - (bool success, bytes memory resultData) = address(this).delegatecall( - abi.encodeWithSelector( - IUniswapV3Feature.sellTokenForTokenToUniswapV3.selector, - wrappedCall.data, - inputTokenAmount, - 0, - msg.sender - ) - ); - if (success) { - uint256 outputTokenAmount_ = abi.decode(resultData, (uint256)); - // 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. - _transferERC20TokensFrom( - 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 { - _transferERC20TokensFrom( - 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. - _transferERC20TokensFrom( - 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); - _transferERC20TokensFrom( - 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 bb51defe36..444ebd4151 100644 --- a/contracts/zero-ex/contracts/src/features/NativeOrdersFeature.sol +++ b/contracts/zero-ex/contracts/src/features/NativeOrdersFeature.sol @@ -34,7 +34,7 @@ contract NativeOrdersFeature is /// @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, 2, 0); + uint256 public immutable override FEATURE_VERSION = _encodeVersion(1, 3, 0); constructor( address zeroExAddress, diff --git a/contracts/zero-ex/contracts/src/features/OtcOrdersFeature.sol b/contracts/zero-ex/contracts/src/features/OtcOrdersFeature.sol index 6721f3a2bc..b1de06f0cf 100644 --- a/contracts/zero-ex/contracts/src/features/OtcOrdersFeature.sol +++ b/contracts/zero-ex/contracts/src/features/OtcOrdersFeature.sol @@ -47,21 +47,12 @@ contract OtcOrdersFeature is using LibSafeMathV06 for uint256; using LibSafeMathV06 for uint128; - /// @dev Options for handling ETH/WETH conversion - /// @param LeaveAsWeth Neither unwrap nor wrap. - /// @param WrapEth Wrap attached ETH. - /// @param UnwrapWeth Unwrap WETH before transferring - /// to taker. - enum WethOptions { - LeaveAsWeth, - WrapEth, - UnwrapWeth - } - /// @dev Name of this feature. string public constant override FEATURE_NAME = "OtcOrders"; /// @dev Version of this feature. uint256 public immutable override FEATURE_VERSION = _encodeVersion(1, 0, 0); + /// @dev ETH pseudo-token address. + address constant private ETH_TOKEN_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; /// @dev The WETH token contract. IEtherTokenV06 private immutable WETH; @@ -80,8 +71,12 @@ contract OtcOrdersFeature is returns (bytes4 success) { _registerFeatureFunction(this.fillOtcOrder.selector); + _registerFeatureFunction(this.fillOtcOrderForEth.selector); _registerFeatureFunction(this.fillOtcOrderWithEth.selector); + _registerFeatureFunction(this.fillTakerSignedOtcOrderForEth.selector); _registerFeatureFunction(this.fillTakerSignedOtcOrder.selector); + _registerFeatureFunction(this.batchFillTakerSignedOtcOrders.selector); + _registerFeatureFunction(this._fillOtcOrder.selector); _registerFeatureFunction(this.getOtcOrderInfo.selector); _registerFeatureFunction(this.getOtcOrderHash.selector); _registerFeatureFunction(this.lastOtcTxOriginNonce.selector); @@ -93,36 +88,90 @@ contract OtcOrdersFeature is /// @param makerSignature The order signature from the maker. /// @param takerTokenFillAmount Maximum taker token amount to fill this /// order with. - /// @param unwrapWeth Whether or not to unwrap bought WETH into ETH - /// before transferring it to the taker. Should be set to false - /// if the maker token is not WETH. /// @return takerTokenFilledAmount How much taker token was filled. /// @return makerTokenFilledAmount How much maker token was filled. function fillOtcOrder( LibNativeOrder.OtcOrder memory order, LibSignature.Signature memory makerSignature, - uint128 takerTokenFillAmount, - bool unwrapWeth + uint128 takerTokenFillAmount ) public override returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount) { - if (!_isSenderValidTaker(order.taker)) { - bytes32 orderHash = getOtcOrderHash(order); - LibNativeOrdersRichErrors.OrderNotFillableByTakerError( - orderHash, - msg.sender, - order.taker - ).rrevert(); - } - LibSignature.Signature memory nullSignature; - return _fillOtcOrderPrivate( - order, + LibNativeOrder.OtcOrderInfo memory orderInfo = getOtcOrderInfo(order); + _validateOtcOrder( + order, + orderInfo, makerSignature, - nullSignature, + msg.sender + ); + (takerTokenFilledAmount, makerTokenFilledAmount) = _settleOtcOrder( + order, takerTokenFillAmount, - unwrapWeth ? WethOptions.UnwrapWeth : WethOptions.LeaveAsWeth + msg.sender, + msg.sender + ); + + emit OtcOrderFilled( + orderInfo.orderHash, + order.maker, + msg.sender, + address(order.makerToken), + address(order.takerToken), + makerTokenFilledAmount, + takerTokenFilledAmount + ); + } + + /// @dev Fill an OTC order for up to `takerTokenFillAmount` taker tokens. + /// Unwraps bought WETH into ETH. before sending it to + /// the taker. + /// @param order The OTC order. + /// @param makerSignature The order signature from the maker. + /// @param takerTokenFillAmount Maximum taker token amount to fill this + /// order with. + /// @return takerTokenFilledAmount How much taker token was filled. + /// @return makerTokenFilledAmount How much maker token was filled. + function fillOtcOrderForEth( + LibNativeOrder.OtcOrder memory order, + LibSignature.Signature memory makerSignature, + uint128 takerTokenFillAmount + ) + public + override + returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount) + { + require( + order.makerToken == WETH, + "OtcOrdersFeature::fillOtcOrderForEth/MAKER_TOKEN_NOT_WETH" + ); + LibNativeOrder.OtcOrderInfo memory orderInfo = getOtcOrderInfo(order); + _validateOtcOrder( + order, + orderInfo, + makerSignature, + msg.sender + ); + (takerTokenFilledAmount, makerTokenFilledAmount) = _settleOtcOrder( + order, + takerTokenFillAmount, + msg.sender, + address(this) + ); + // Unwrap WETH + WETH.withdraw(makerTokenFilledAmount); + // Transfer ETH to taker + _transferEth(msg.sender, makerTokenFilledAmount); + + emit OtcOrderFilled( + orderInfo.orderHash, + order.maker, + msg.sender, + address(order.makerToken), + address(order.takerToken), + makerTokenFilledAmount, + takerTokenFilledAmount ); } @@ -141,21 +190,47 @@ contract OtcOrdersFeature is payable returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount) { - if (!_isSenderValidTaker(order.taker)) { - bytes32 orderHash = getOtcOrderHash(order); - LibNativeOrdersRichErrors.OrderNotFillableByTakerError( - orderHash, - msg.sender, - order.taker - ).rrevert(); + if (order.takerToken == WETH) { + // Wrap ETH + WETH.deposit{value: msg.value}(); + } else { + require( + address(order.takerToken) == ETH_TOKEN_ADDRESS, + "OtcOrdersFeature::fillOtcOrderWithEth/INVALID_TAKER_TOKEN" + ); } - LibSignature.Signature memory nullSignature; - return _fillOtcOrderPrivate( - order, + + LibNativeOrder.OtcOrderInfo memory orderInfo = getOtcOrderInfo(order); + _validateOtcOrder( + order, + orderInfo, makerSignature, - nullSignature, + msg.sender + ); + + (takerTokenFilledAmount, makerTokenFilledAmount) = _settleOtcOrder( + order, msg.value.safeDowncastToUint128(), - WethOptions.WrapEth + address(this), + msg.sender + ); + if (takerTokenFilledAmount < msg.value) { + uint256 refundAmount = msg.value - uint256(takerTokenFilledAmount); + if (order.takerToken == WETH) { + WETH.withdraw(refundAmount); + } + // Refund unused ETH + _transferEth(msg.sender, refundAmount); + } + + emit OtcOrderFilled( + orderInfo.orderHash, + order.maker, + msg.sender, + address(order.makerToken), + address(order.takerToken), + makerTokenFilledAmount, + takerTokenFilledAmount ); } @@ -164,112 +239,28 @@ contract OtcOrdersFeature is /// @param order The OTC order. /// @param makerSignature The order signature from the maker. /// @param takerSignature The order signature from the taker. - /// @param unwrapWeth Whether or not to unwrap bought WETH into ETH - /// before transferring it to the taker. Should be set to false - /// if the maker token is not WETH. - /// @return takerTokenFilledAmount How much taker token was filled. - /// @return makerTokenFilledAmount How much maker token was filled. function fillTakerSignedOtcOrder( LibNativeOrder.OtcOrder memory order, LibSignature.Signature memory makerSignature, - LibSignature.Signature memory takerSignature, - bool unwrapWeth + LibSignature.Signature memory takerSignature ) public override - returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount) - { - return _fillOtcOrderPrivate( - order, - makerSignature, - takerSignature, - order.takerAmount, - unwrapWeth ? WethOptions.UnwrapWeth : WethOptions.LeaveAsWeth - ); - } - - /// @dev Fill an OTC order. Private variant. - /// @param order The OTC order. - /// @param makerSignature The order signature from the maker. - /// @param takerSignature The order signature from the taker. - /// Ignored if msg.sender == order.taker. - /// @param takerTokenFillAmount Maximum taker token amount to - /// fill this order with. - /// @return takerTokenFilledAmount How much taker token was filled. - /// @return makerTokenFilledAmount How much maker token was filled. - function _fillOtcOrderPrivate( - LibNativeOrder.OtcOrder memory order, - LibSignature.Signature memory makerSignature, - LibSignature.Signature memory takerSignature, - uint128 takerTokenFillAmount, - WethOptions wethOptions - ) - private - returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount) { LibNativeOrder.OtcOrderInfo memory orderInfo = getOtcOrderInfo(order); - - // Must be fillable. - if (orderInfo.status != LibNativeOrder.OrderStatus.FILLABLE) { - LibNativeOrdersRichErrors.OrderNotFillableError( - orderInfo.orderHash, - uint8(orderInfo.status) - ).rrevert(); - } - - address taker = msg.sender; - { - 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(); - } - - // Maker signature must be valid for the order. - address makerSigner = LibSignature.getSignerOfHash(orderInfo.orderHash, makerSignature); - if ( - makerSigner != order.maker && - !stor.orderSignerRegistry[order.maker][makerSigner] - ) { - LibNativeOrdersRichErrors.OrderNotSignedByMakerError( - orderInfo.orderHash, - makerSigner, - order.maker - ).rrevert(); - } - - // If msg.sender is not the taker, validate the taker signature. - if (!_isSenderValidTaker(order.taker)) { - address takerSigner = LibSignature.getSignerOfHash(orderInfo.orderHash, takerSignature); - if ( - takerSigner != order.taker && - !stor.orderSignerRegistry[order.taker][takerSigner] - ) { - LibNativeOrdersRichErrors.OrderNotSignedByTakerError( - orderInfo.orderHash, - takerSigner, - order.taker - ).rrevert(); - } - taker = order.taker; - } - } - - // Settle between the maker and taker. - (takerTokenFilledAmount, makerTokenFilledAmount) = _settleOtcOrder( + address taker = LibSignature.getSignerOfHash(orderInfo.orderHash, takerSignature); + + _validateOtcOrder( + order, + orderInfo, + makerSignature, + taker + ); + _settleOtcOrder( order, + order.takerAmount, taker, - takerTokenFillAmount, - wethOptions + taker ); emit OtcOrderFilled( @@ -278,22 +269,227 @@ contract OtcOrdersFeature is taker, address(order.makerToken), address(order.takerToken), - takerTokenFilledAmount, - makerTokenFilledAmount + order.makerAmount, + order.takerAmount ); } + /// @dev Fully fill an OTC order. "Meta-transaction" variant, + /// requires order to be signed by both maker and taker. + /// Unwraps bought WETH into ETH. before sending it to + /// the taker. + /// @param order The OTC order. + /// @param makerSignature The order signature from the maker. + /// @param takerSignature The order signature from the taker. + function fillTakerSignedOtcOrderForEth( + LibNativeOrder.OtcOrder memory order, + LibSignature.Signature memory makerSignature, + LibSignature.Signature memory takerSignature + ) + public + override + { + require( + order.makerToken == WETH, + "OtcOrdersFeature::fillTakerSignedOtcOrder/MAKER_TOKEN_NOT_WETH" + ); + LibNativeOrder.OtcOrderInfo memory orderInfo = getOtcOrderInfo(order); + address taker = LibSignature.getSignerOfHash(orderInfo.orderHash, takerSignature); + + _validateOtcOrder( + order, + orderInfo, + makerSignature, + taker + ); + _settleOtcOrder( + order, + order.takerAmount, + taker, + address(this) + ); + // Unwrap WETH + WETH.withdraw(order.makerAmount); + // Transfer ETH to taker + _transferEth(taker, order.makerAmount); + + emit OtcOrderFilled( + orderInfo.orderHash, + order.maker, + taker, + address(order.makerToken), + address(order.takerToken), + order.makerAmount, + order.takerAmount + ); + } + + /// @dev Fills multiple taker-signed OTC orders. + /// @param orders Array of OTC orders. + /// @param makerSignatures Array of maker signatures for each order. + /// @param takerSignatures Array of taker signatures for each order. + /// @param unwrapWeth Array of booleans representing whether or not + /// to unwrap bought WETH into ETH for each order. Should be set + /// to false if the maker token is not WETH. + /// @return successes Array of booleans representing whether or not + /// each order in `orders` was filled successfully. + function batchFillTakerSignedOtcOrders( + LibNativeOrder.OtcOrder[] memory orders, + LibSignature.Signature[] memory makerSignatures, + LibSignature.Signature[] memory takerSignatures, + bool[] memory unwrapWeth + ) + public + override + returns (bool[] memory successes) + { + require( + orders.length == makerSignatures.length && + orders.length == takerSignatures.length && + orders.length == unwrapWeth.length, + "OtcOrdersFeature::batchFillTakerSignedOtcOrders/MISMATCHED_ARRAY_LENGTHS" + ); + successes = new bool[](orders.length); + for (uint256 i = 0; i != orders.length; i++) { + bytes4 fnSelector = unwrapWeth[i] + ? this.fillTakerSignedOtcOrderForEth.selector + : this.fillTakerSignedOtcOrder.selector; + // Swallow reverts + (successes[i], ) = _implementation.delegatecall( + abi.encodeWithSelector( + fnSelector, + orders[i], + makerSignatures[i], + takerSignatures[i] + ) + ); + } + } + + /// @dev Fill an OTC order for up to `takerTokenFillAmount` taker tokens. + /// Internal variant. + /// @param order The OTC order. + /// @param makerSignature The order signature from the maker. + /// @param takerTokenFillAmount Maximum taker token amount to fill this + /// order with. + /// @param taker The address to fill the order in the context of. + /// @param useSelfBalance Whether to use the Exchange Proxy's balance + /// of input tokens. + /// @param recipient The recipient of the bought maker tokens. + /// @return takerTokenFilledAmount How much taker token was filled. + /// @return makerTokenFilledAmount How much maker token was filled. + function _fillOtcOrder( + LibNativeOrder.OtcOrder memory order, + LibSignature.Signature memory makerSignature, + uint128 takerTokenFillAmount, + address taker, + bool useSelfBalance, + address recipient + ) + public + override + onlySelf + returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount) + { + LibNativeOrder.OtcOrderInfo memory orderInfo = getOtcOrderInfo(order); + _validateOtcOrder( + order, + orderInfo, + makerSignature, + taker + ); + (takerTokenFilledAmount, makerTokenFilledAmount) = _settleOtcOrder( + order, + takerTokenFillAmount, + useSelfBalance ? address(this) : taker, + recipient + ); + + emit OtcOrderFilled( + orderInfo.orderHash, + order.maker, + taker, + address(order.makerToken), + address(order.takerToken), + makerTokenFilledAmount, + takerTokenFilledAmount + ); + } + + /// @dev Validates an OTC order, reverting if the order cannot be + /// filled by the given taker. + /// @param order The OTC order. + /// @param orderInfo Info on the order. + /// @param makerSignature The order signature from the maker. + /// @param taker The order taker. + function _validateOtcOrder( + LibNativeOrder.OtcOrder memory order, + LibNativeOrder.OtcOrderInfo memory orderInfo, + LibSignature.Signature memory makerSignature, + address taker + ) + private + view + { + // Must be fillable. + if (orderInfo.status != LibNativeOrder.OrderStatus.FILLABLE) { + LibNativeOrdersRichErrors.OrderNotFillableError( + orderInfo.orderHash, + uint8(orderInfo.status) + ).rrevert(); + } + + // Must be a valid taker for the order. + if (order.taker != address(0) && order.taker != taker) { + LibNativeOrdersRichErrors.OrderNotFillableByTakerError( + orderInfo.orderHash, + taker, + order.taker + ).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(); + } + + // Maker signature must be valid for the order. + address makerSigner = LibSignature.getSignerOfHash(orderInfo.orderHash, makerSignature); + if ( + makerSigner != order.maker && + !stor.orderSignerRegistry[order.maker][makerSigner] + ) { + LibNativeOrdersRichErrors.OrderNotSignedByMakerError( + orderInfo.orderHash, + makerSigner, + order.maker + ).rrevert(); + } + } + /// @dev Settle the trade between an OTC order's maker and taker. /// @param order The OTC order. /// @param takerTokenFillAmount Maximum taker token amount to fill this /// order with. + /// @param payer The address holding the taker tokens. + /// @param recipient The recipient of the maker tokens. /// @return takerTokenFilledAmount How much taker token was filled. /// @return makerTokenFilledAmount How much maker token was filled. function _settleOtcOrder( LibNativeOrder.OtcOrder memory order, - address taker, uint128 takerTokenFillAmount, - WethOptions wethOptions + address payer, + address recipient ) private returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount) @@ -326,57 +522,34 @@ contract OtcOrdersFeature is )); } - if (wethOptions == WethOptions.WrapEth) { - require( - order.takerToken == WETH, - "OtcOrdersFeature/INVALID_WRAP_ETH" - ); - // Wrap ETH - WETH.deposit{value: takerTokenFilledAmount}(); - // Transfer WETH to maker - WETH.transfer(order.maker, takerTokenFilledAmount); - if (takerTokenFilledAmount < msg.value) { - // Refund unused ETH - _transferEth( - msg.sender, - msg.value - uint256(takerTokenFilledAmount) - ); + if (payer == address(this)) { + if (address(order.takerToken) == ETH_TOKEN_ADDRESS) { + // Transfer ETH to the maker. + payable(order.maker).transfer(takerTokenFilledAmount); + } else { + // Transfer this -> maker. + _transferERC20Tokens( + order.takerToken, + order.maker, + takerTokenFilledAmount + ); } } else { // Transfer taker -> maker _transferERC20TokensFrom( order.takerToken, - taker, + payer, order.maker, takerTokenFilledAmount ); } - - if (wethOptions == WethOptions.UnwrapWeth) { - require( - order.makerToken == WETH, - "OtcOrdersFeature/INVALID_UNWRAP_WETH" - ); - // Transfer maker tokens in - _transferERC20TokensFrom( - order.makerToken, - order.maker, - address(this), - makerTokenFilledAmount - ); - // Unwrap WETH - WETH.withdraw(makerTokenFilledAmount); - // Transfer ETH to taker - _transferEth(taker, makerTokenFilledAmount); - } else { - // Transfer maker -> taker. - _transferERC20TokensFrom( - order.makerToken, - order.maker, - taker, - makerTokenFilledAmount - ); - } + // Transfer maker -> recipient. + _transferERC20TokensFrom( + order.makerToken, + order.maker, + recipient, + makerTokenFilledAmount + ); } /// @dev Get the order info for an OTC order. @@ -461,12 +634,4 @@ contract OtcOrdersFeature is revertData.rrevert(); } } - - function _isSenderValidTaker(address orderTaker) - private - view - returns (bool) - { - return orderTaker == address(0) || orderTaker == msg.sender; - } } diff --git a/contracts/zero-ex/contracts/src/features/TransformERC20Feature.sol b/contracts/zero-ex/contracts/src/features/TransformERC20Feature.sol index bc969a0ed5..959eb0ff0b 100644 --- a/contracts/zero-ex/contracts/src/features/TransformERC20Feature.sol +++ b/contracts/zero-ex/contracts/src/features/TransformERC20Feature.sol @@ -22,6 +22,7 @@ pragma experimental ABIEncoderV2; import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol"; import "@0x/contracts-erc20/contracts/src/v06/LibERC20TokenV06.sol"; +import "@0x/contracts-utils/contracts/src/v06/LibBytesV06.sol"; import "@0x/contracts-utils/contracts/src/v06/errors/LibRichErrorsV06.sol"; import "@0x/contracts-utils/contracts/src/v06/LibSafeMathV06.sol"; import "../errors/LibTransformERC20RichErrors.sol"; @@ -51,16 +52,14 @@ contract TransformERC20Feature is struct TransformERC20PrivateState { IFlashWallet wallet; address transformerDeployer; - uint256 takerOutputTokenBalanceBefore; - uint256 takerOutputTokenBalanceAfter; + uint256 recipientOutputTokenBalanceBefore; + uint256 recipientOutputTokenBalanceAfter; } /// @dev Name of this feature. string public constant override FEATURE_NAME = "TransformERC20"; /// @dev Version of this feature. - uint256 public immutable override FEATURE_VERSION = _encodeVersion(1, 3, 1); - - constructor() public {} + uint256 public immutable override FEATURE_VERSION = _encodeVersion(1, 4, 0); /// @dev Initialize and register this feature. /// Should be delegatecalled by `Migrate.migrate()`. @@ -76,7 +75,7 @@ contract TransformERC20Feature is _registerFeatureFunction(this.setTransformerDeployer.selector); _registerFeatureFunction(this.setQuoteSigner.selector); _registerFeatureFunction(this.getQuoteSigner.selector); - _registerFeatureFunction(this.transformERC20.selector); + _registerFeatureFunction(this.transformERC20Staging.selector); _registerFeatureFunction(this._transformERC20.selector); if (this.getTransformWallet() == IFlashWallet(address(0))) { // Create the transform wallet if it doesn't exist. @@ -146,6 +145,44 @@ contract TransformERC20Feature is LibTransformERC20Storage.getStorage().wallet = wallet; } + /// @dev Wrapper for `transformERC20`. This selector will be temporarily + /// registered to the Exchange Proxy so that we can migrate 0x API + /// with no downtime. Once 0x API has been updated to point to this + /// function, we can safely re-register `transformERC20`, point + /// 0x API back to `transformERC20`, and deregister this function. + /// @param inputToken The token being provided by the sender. + /// If `0xeee...`, ETH is implied and should be provided with the call.` + /// @param outputToken The token to be acquired by the sender. + /// `0xeee...` implies ETH. + /// @param inputTokenAmount The amount of `inputToken` to take from the sender. + /// If set to `uint256(-1)`, the entire spendable balance of the taker + /// will be solt. + /// @param minOutputTokenAmount The minimum amount of `outputToken` the sender + /// must receive for the entire transformation to succeed. If set to zero, + /// the minimum output token transfer will not be asserted. + /// @param transformations The transformations to execute on the token balance(s) + /// in sequence. + /// @return outputTokenAmount The amount of `outputToken` received by the sender. + function transformERC20Staging( + IERC20TokenV06 inputToken, + IERC20TokenV06 outputToken, + uint256 inputTokenAmount, + uint256 minOutputTokenAmount, + Transformation[] memory transformations + ) + public + payable + returns (uint256 outputTokenAmount) + { + return transformERC20( + inputToken, + outputToken, + inputTokenAmount, + minOutputTokenAmount, + transformations + ); + } + /// @dev Executes a series of transformations to convert an ERC20 `inputToken` /// to an ERC20 `outputToken`. /// @param inputToken The token being provided by the sender. @@ -180,7 +217,9 @@ contract TransformERC20Feature is outputToken: outputToken, inputTokenAmount: inputTokenAmount, minOutputTokenAmount: minOutputTokenAmount, - transformations: transformations + transformations: transformations, + useSelfBalance: false, + recipient: msg.sender }) ); } @@ -208,7 +247,7 @@ contract TransformERC20Feature is { // If the input token amount is -1 and we are not selling ETH, // transform the taker's entire spendable balance. - if (args.inputTokenAmount == uint256(-1)) { + if (!args.useSelfBalance && args.inputTokenAmount == uint256(-1)) { if (LibERC20Transformer.isTokenETH(args.inputToken)) { // We can't pull more ETH from the taker, so we just set the // input token amount to the value attached to the call. @@ -225,17 +264,12 @@ contract TransformERC20Feature is state.wallet = getTransformWallet(); state.transformerDeployer = getTransformerDeployer(); - // Remember the initial output token balance of the taker. - state.takerOutputTokenBalanceBefore = - LibERC20Transformer.getTokenBalanceOf(args.outputToken, args.taker); + // Remember the initial output token balance of the recipient. + state.recipientOutputTokenBalanceBefore = + LibERC20Transformer.getTokenBalanceOf(args.outputToken, args.recipient); // Pull input tokens from the taker to the wallet and transfer attached ETH. - _transferInputTokensAndAttachedEth( - args.inputToken, - args.taker, - address(state.wallet), - args.inputTokenAmount - ); + _transferInputTokensAndAttachedEth(args, address(state.wallet)); { // Perform transformations. @@ -244,22 +278,29 @@ contract TransformERC20Feature is state.wallet, args.transformations[i], state.transformerDeployer, - args.taker + args.recipient ); } + // Transfer output tokens from wallet to recipient + outputTokenAmount = _executeOutputTokenTransfer( + args.outputToken, + state.wallet, + args.recipient + ); } - // Compute how much output token has been transferred to the taker. - state.takerOutputTokenBalanceAfter = - LibERC20Transformer.getTokenBalanceOf(args.outputToken, args.taker); - if (state.takerOutputTokenBalanceAfter < state.takerOutputTokenBalanceBefore) { + // Compute how much output token has been transferred to the recipient. + state.recipientOutputTokenBalanceAfter = + LibERC20Transformer.getTokenBalanceOf(args.outputToken, args.recipient); + if (state.recipientOutputTokenBalanceAfter < state.recipientOutputTokenBalanceBefore) { LibTransformERC20RichErrors.NegativeTransformERC20OutputError( address(args.outputToken), - state.takerOutputTokenBalanceBefore - state.takerOutputTokenBalanceAfter + state.recipientOutputTokenBalanceBefore - state.recipientOutputTokenBalanceAfter ).rrevert(); } - outputTokenAmount = state.takerOutputTokenBalanceAfter.safeSub( - state.takerOutputTokenBalanceBefore + outputTokenAmount = LibSafeMathV06.min256( + outputTokenAmount, + state.recipientOutputTokenBalanceAfter.safeSub(state.recipientOutputTokenBalanceBefore) ); // Ensure enough output token has been sent to the taker. if (outputTokenAmount < args.minOutputTokenAmount) { @@ -292,38 +333,49 @@ contract TransformERC20Feature is return LibTransformERC20Storage.getStorage().wallet; } - /// @dev Transfer input tokens from the taker and any attached ETH to `to` - /// @param inputToken The token to pull from the taker. - /// @param from The from (taker) address. + /// @dev Transfer input tokens and any attached ETH to `to` + /// @param args A `TransformERC20Args` struct. /// @param to The recipient of tokens and ETH. - /// @param amount Amount of `inputToken` tokens to transfer. function _transferInputTokensAndAttachedEth( - IERC20TokenV06 inputToken, - address from, - address payable to, - uint256 amount + TransformERC20Args memory args, + address payable to ) private { + if ( + LibERC20Transformer.isTokenETH(args.inputToken) && + msg.value < args.inputTokenAmount + ) { + // Token is ETH, so the caller must attach enough ETH to the call. + LibTransformERC20RichErrors.InsufficientEthAttachedError( + msg.value, + args.inputTokenAmount + ).rrevert(); + } + // Transfer any attached ETH. if (msg.value != 0) { to.transfer(msg.value); } + // Transfer input tokens. - if (!LibERC20Transformer.isTokenETH(inputToken) && amount != 0) { - // Token is not ETH, so pull ERC20 tokens. - _transferERC20TokensFrom( - inputToken, - from, - to, - amount - ); - } else if (msg.value < amount) { - // Token is ETH, so the caller must attach enough ETH to the call. - LibTransformERC20RichErrors.InsufficientEthAttachedError( - msg.value, - amount - ).rrevert(); + if (!LibERC20Transformer.isTokenETH(args.inputToken)) { + if (args.useSelfBalance) { + // Use EP balance input token. + _transferERC20Tokens( + args.inputToken, + to, + args.inputTokenAmount + ); + } else { + // Pull ERC20 tokens from taker. + _transferERC20TokensFrom( + args.inputToken, + args.taker, + to, + args.inputTokenAmount + ); + } } } @@ -331,12 +383,12 @@ contract TransformERC20Feature is /// @param wallet The wallet instance. /// @param transformation The transformation. /// @param transformerDeployer The address of the transformer deployer. - /// @param taker The taker address. + /// @param recipient The recipient address. function _executeTransformation( IFlashWallet wallet, Transformation memory transformation, address transformerDeployer, - address payable taker + address payable recipient ) private { @@ -354,7 +406,7 @@ contract TransformERC20Feature is IERC20Transformer.transform.selector, IERC20Transformer.TransformContext({ sender: msg.sender, - taker: taker, + recipient: recipient, data: transformation.data }) ) @@ -370,4 +422,52 @@ contract TransformERC20Feature is ).rrevert(); } } + + function _executeOutputTokenTransfer( + IERC20TokenV06 outputToken, + IFlashWallet wallet, + address payable recipient + ) + private + returns (uint256 transferAmount) + { + transferAmount = + LibERC20Transformer.getTokenBalanceOf(outputToken, address(wallet)); + if (LibERC20Transformer.isTokenETH(outputToken)) { + wallet.executeCall( + recipient, + "", + transferAmount + ); + } else { + bytes memory resultData = wallet.executeCall( + payable(address(outputToken)), + abi.encodeWithSelector( + IERC20TokenV06.transfer.selector, + recipient, + transferAmount + ), + 0 + ); + if (resultData.length == 0) { + // If we get back 0 returndata, this may be a non-standard ERC-20 that + // does not return a boolean. Check that it at least contains code. + uint256 size; + assembly { size := extcodesize(outputToken) } + require(size > 0, "invalid token address, contains no code"); + } else if (resultData.length >= 32) { + // If we get back at least 32 bytes, we know the target address + // contains code, and we assume it is a token that returned a boolean + // success value, which must be true. + uint256 result = LibBytesV06.readUint256(resultData, 0); + if (result != 1) { + LibRichErrorsV06.rrevert(resultData); + } + } else { + // If 0 < returndatasize < 32, the target is a contract, but not a + // valid token. + LibRichErrorsV06.rrevert(resultData); + } + } + } } diff --git a/contracts/zero-ex/contracts/src/features/UniswapV3Feature.sol b/contracts/zero-ex/contracts/src/features/UniswapV3Feature.sol index 6889ce19d1..5b383c3fc0 100644 --- a/contracts/zero-ex/contracts/src/features/UniswapV3Feature.sol +++ b/contracts/zero-ex/contracts/src/features/UniswapV3Feature.sol @@ -40,7 +40,7 @@ contract UniswapV3Feature is /// @dev Name of this feature. string public constant override FEATURE_NAME = "UniswapV3Feature"; /// @dev Version of this feature. - uint256 public immutable override FEATURE_VERSION = _encodeVersion(1, 0, 0); + uint256 public immutable override FEATURE_VERSION = _encodeVersion(1, 1, 0); /// @dev WETH contract. IEtherTokenV06 private immutable WETH; /// @dev UniswapV3 Factory contract address prepended with '0xff' and left-aligned. @@ -88,6 +88,7 @@ contract UniswapV3Feature is _registerFeatureFunction(this.sellEthForTokenToUniswapV3.selector); _registerFeatureFunction(this.sellTokenForEthToUniswapV3.selector); _registerFeatureFunction(this.sellTokenForTokenToUniswapV3.selector); + _registerFeatureFunction(this._sellHeldTokenForTokenToUniswapV3.selector); _registerFeatureFunction(this.uniswapV3SwapCallback.selector); return LibMigrate.MIGRATE_SUCCESS; } @@ -175,6 +176,33 @@ contract UniswapV3Feature is ); } + /// @dev Sell a token for another token directly against uniswap v3. + /// Private variant, uses tokens held by `address(this)`. + /// @param encodedPath Uniswap-encoded path. + /// @param sellAmount amount of the first token in the path to sell. + /// @param minBuyAmount Minimum amount of the last token in the path to buy. + /// @param recipient The recipient of the bought tokens. Can be zero for sender. + /// @return buyAmount Amount of the last token in the path bought. + function _sellHeldTokenForTokenToUniswapV3( + bytes memory encodedPath, + uint256 sellAmount, + uint256 minBuyAmount, + address recipient + ) + public + override + onlySelf + returns (uint256 buyAmount) + { + buyAmount = _swap( + encodedPath, + sellAmount, + minBuyAmount, + address(this), + _normalizeRecipient(recipient) + ); + } + /// @dev The UniswapV3 pool swap callback which pays the funds requested /// by the caller/pool to the pool. Can only be called by a valid /// UniswapV3 pool. diff --git a/contracts/zero-ex/contracts/src/features/interfaces/IMultiplexFeature.sol b/contracts/zero-ex/contracts/src/features/interfaces/IMultiplexFeature.sol index e5ca748e03..ac6f529c29 100644 --- a/contracts/zero-ex/contracts/src/features/interfaces/IMultiplexFeature.sol +++ b/contracts/zero-ex/contracts/src/features/interfaces/IMultiplexFeature.sol @@ -24,9 +24,21 @@ import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol"; interface IMultiplexFeature { + // Identifies the type of subcall. + enum MultiplexSubcall { + Invalid, + RFQ, + OTC, + UniswapV2, + UniswapV3, + LiquidityProvider, + TransformERC20, + BatchSell, + MultiHopSell + } - // Parameters for `batchFill`. - struct BatchFillData { + // Parameters for a batch sell. + struct BatchSellParams { // The token being sold. IERC20TokenV06 inputToken; // The token being bought. @@ -34,84 +46,182 @@ interface IMultiplexFeature { // The amount of `inputToken` to sell. uint256 sellAmount; // The nested calls to perform. - WrappedBatchCall[] calls; + BatchSellSubcall[] calls; + // Whether to use the Exchange Proxy's balance + // of input tokens. + bool useSelfBalance; + // The recipient of the bought output tokens. + address recipient; } - // Represents a call nested within a `batchFill`. - struct WrappedBatchCall { - // The selector of the function to call. - bytes4 selector; - // Amount of `inputToken` to sell. + // Represents a constituent call of a batch sell. + struct BatchSellSubcall { + // The function to call. + MultiplexSubcall id; + // Amount of input token to sell. If the highest bit is 1, + // this value represents a proportion of the total + // `sellAmount` of the batch sell. See `_normalizeSellAmount` + // for details. uint256 sellAmount; // ABI-encoded parameters needed to perform the call. bytes data; } - // Parameters for `multiHopFill`. - struct MultiHopFillData { + // Parameters for a multi-hop sell. + struct MultiHopSellParams { // 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; + MultiHopSellSubcall[] calls; + // Whether to use the Exchange Proxy's balance + // of input tokens. + bool useSelfBalance; + // The recipient of the bought output tokens. + address recipient; } - // Represents a call nested within a `multiHopFill`. - struct WrappedMultiHopCall { - // The selector of the function to call. - bytes4 selector; + // Represents a constituent call of a multi-hop sell. + struct MultiHopSellSubcall { + // The function to call. + MultiplexSubcall id; // ABI-encoded parameters needed to perform the call. bytes data; } - event LiquidityProviderSwap( - address inputToken, - address outputToken, - uint256 inputTokenAmount, - uint256 outputTokenAmount, - address provider, - address recipient - ); + struct BatchSellState { + // Tracks the amount of input token sold. + uint256 soldAmount; + // Tracks the amount of output token bought. + uint256 boughtAmount; + } - event ExpiredRfqOrder( - bytes32 orderHash, - address maker, - uint64 expiry - ); + struct MultiHopSellState { + // This variable is used for the input and output amounts of + // each hop. After the final hop, this will contain the output + // amount of the multi-hop sell. + uint256 outputTokenAmount; + // For each hop in a multi-hop sell, `from` is the + // address that holds the input tokens of the hop, + // `to` is the address that receives the output tokens + // of the hop. + // See `_computeHopTarget` for details. + address from; + address to; + // The index of the current hop in the multi-hop chain. + uint256 hopIndex; + } - /// @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, + /// @dev Sells attached ETH for `outputToken` using the provided + /// calls. + /// @param outputToken The token to buy. + /// @param calls The calls to use to sell the attached ETH. + /// @param minBuyAmount The minimum amount of `outputToken` that + /// must be bought for this function to not revert. + /// @return boughtAmount The amount of `outputToken` bought. + function multiplexBatchSellEthForToken( + IERC20TokenV06 outputToken, + BatchSellSubcall[] calldata calls, uint256 minBuyAmount ) external payable - returns (uint256 outputTokenAmount); + returns (uint256 boughtAmount); - /// @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, + /// @dev Sells `sellAmount` of the given `inputToken` for ETH + /// using the provided calls. + /// @param inputToken The token to sell. + /// @param calls The calls to use to sell the input tokens. + /// @param sellAmount The amount of `inputToken` to sell. + /// @param minBuyAmount The minimum amount of ETH that + /// must be bought for this function to not revert. + /// @return boughtAmount The amount of ETH bought. + function multiplexBatchSellTokenForEth( + IERC20TokenV06 inputToken, + BatchSellSubcall[] calldata calls, + uint256 sellAmount, + uint256 minBuyAmount + ) + external + returns (uint256 boughtAmount); + + /// @dev Sells `sellAmount` of the given `inputToken` for + /// `outputToken` using the provided calls. + /// @param inputToken The token to sell. + /// @param outputToken The token to buy. + /// @param calls The calls to use to sell the input tokens. + /// @param sellAmount The amount of `inputToken` to sell. + /// @param minBuyAmount The minimum amount of `outputToken` + /// that must be bought for this function to not revert. + /// @return boughtAmount The amount of `outputToken` bought. + function multiplexBatchSellTokenForToken( + IERC20TokenV06 inputToken, + IERC20TokenV06 outputToken, + BatchSellSubcall[] calldata calls, + uint256 sellAmount, + uint256 minBuyAmount + ) + external + returns (uint256 boughtAmount); + + /// @dev Sells attached ETH via the given sequence of tokens + /// and calls. `tokens[0]` must be WETH. + /// The last token in `tokens` is the output token that + /// will ultimately be sent to `msg.sender` + /// @param tokens The sequence of tokens to use for the sell, + /// i.e. `tokens[i]` will be sold for `tokens[i+1]` via + /// `calls[i]`. + /// @param calls The sequence of calls to use for the sell. + /// @param minBuyAmount The minimum amount of output tokens that + /// must be bought for this function to not revert. + /// @return boughtAmount The amount of output tokens bought. + function multiplexMultiHopSellEthForToken( + address[] calldata tokens, + MultiHopSellSubcall[] calldata calls, uint256 minBuyAmount ) external payable - returns (uint256 outputTokenAmount); + returns (uint256 boughtAmount); + + /// @dev Sells `sellAmount` of the input token (`tokens[0]`) + /// for ETH via the given sequence of tokens and calls. + /// The last token in `tokens` must be WETH. + /// @param tokens The sequence of tokens to use for the sell, + /// i.e. `tokens[i]` will be sold for `tokens[i+1]` via + /// `calls[i]`. + /// @param calls The sequence of calls to use for the sell. + /// @param minBuyAmount The minimum amount of ETH that + /// must be bought for this function to not revert. + /// @return boughtAmount The amount of ETH bought. + function multiplexMultiHopSellTokenForEth( + address[] calldata tokens, + MultiHopSellSubcall[] calldata calls, + uint256 sellAmount, + uint256 minBuyAmount + ) + external + returns (uint256 boughtAmount); + + /// @dev Sells `sellAmount` of the input token (`tokens[0]`) + /// via the given sequence of tokens and calls. + /// The last token in `tokens` is the output token that + /// will ultimately be sent to `msg.sender` + /// @param tokens The sequence of tokens to use for the sell, + /// i.e. `tokens[i]` will be sold for `tokens[i+1]` via + /// `calls[i]`. + /// @param calls The sequence of calls to use for the sell. + /// @param minBuyAmount The minimum amount of output tokens that + /// must be bought for this function to not revert. + /// @return boughtAmount The amount of output tokens bought. + function multiplexMultiHopSellTokenForToken( + address[] calldata tokens, + MultiHopSellSubcall[] calldata calls, + uint256 sellAmount, + uint256 minBuyAmount + ) + external + returns (uint256 boughtAmount); } diff --git a/contracts/zero-ex/contracts/src/features/interfaces/INativeOrdersFeature.sol b/contracts/zero-ex/contracts/src/features/interfaces/INativeOrdersFeature.sol index b85130b0a8..26a57c645d 100644 --- a/contracts/zero-ex/contracts/src/features/interfaces/INativeOrdersFeature.sol +++ b/contracts/zero-ex/contracts/src/features/interfaces/INativeOrdersFeature.sol @@ -126,13 +126,18 @@ interface INativeOrdersFeature is /// @param signature The order signature. /// @param takerTokenFillAmount Maximum taker token to fill this order with. /// @param taker The order taker. + /// @param useSelfBalance Whether to use the ExchangeProxy's transient + /// balance of taker tokens to fill the order. + /// @param recipient The recipient of the maker tokens. /// @return takerTokenFilledAmount How much maker token was filled. /// @return makerTokenFilledAmount How much maker token was filled. function _fillRfqOrder( LibNativeOrder.RfqOrder calldata order, LibSignature.Signature calldata signature, uint128 takerTokenFillAmount, - address taker + address taker, + bool useSelfBalance, + address recipient ) external returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount); diff --git a/contracts/zero-ex/contracts/src/features/interfaces/IOtcOrdersFeature.sol b/contracts/zero-ex/contracts/src/features/interfaces/IOtcOrdersFeature.sol index 60bd5e3924..a8da27eaee 100644 --- a/contracts/zero-ex/contracts/src/features/interfaces/IOtcOrdersFeature.sol +++ b/contracts/zero-ex/contracts/src/features/interfaces/IOtcOrdersFeature.sol @@ -31,16 +31,16 @@ interface IOtcOrdersFeature { /// @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 takerTokenFilledAmount How much taker token was filled. event OtcOrderFilled( bytes32 orderHash, address maker, address taker, address makerToken, address takerToken, - uint128 takerTokenFilledAmount, - uint128 makerTokenFilledAmount + uint128 makerTokenFilledAmount, + uint128 takerTokenFilledAmount ); /// @dev Fill an OTC order for up to `takerTokenFillAmount` taker tokens. @@ -48,15 +48,29 @@ interface IOtcOrdersFeature { /// @param makerSignature The order signature from the maker. /// @param takerTokenFillAmount Maximum taker token amount to fill this /// order with. - /// @param unwrapWeth Whether or not to unwrap bought WETH into ETH - /// before transferring it to the taker. Should be set to false /// @return takerTokenFilledAmount How much taker token was filled. /// @return makerTokenFilledAmount How much maker token was filled. function fillOtcOrder( LibNativeOrder.OtcOrder calldata order, LibSignature.Signature calldata makerSignature, - uint128 takerTokenFillAmount, - bool unwrapWeth + uint128 takerTokenFillAmount + ) + external + returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount); + + /// @dev Fill an OTC order for up to `takerTokenFillAmount` taker tokens. + /// Unwraps bought WETH into ETH before sending it to + /// the taker. + /// @param order The OTC order. + /// @param makerSignature The order signature from the maker. + /// @param takerTokenFillAmount Maximum taker token amount to fill this + /// order with. + /// @return takerTokenFilledAmount How much taker token was filled. + /// @return makerTokenFilledAmount How much maker token was filled. + function fillOtcOrderForEth( + LibNativeOrder.OtcOrder calldata order, + LibSignature.Signature calldata makerSignature, + uint128 takerTokenFillAmount ) external returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount); @@ -80,16 +94,64 @@ interface IOtcOrdersFeature { /// @param order The OTC order. /// @param makerSignature The order signature from the maker. /// @param takerSignature The order signature from the taker. - /// @param unwrapWeth Whether or not to unwrap bought WETH into ETH - /// before transferring it to the taker. Should be set to false - /// if the maker token is not WETH. - /// @return takerTokenFilledAmount How much taker token was filled. - /// @return makerTokenFilledAmount How much maker token was filled. function fillTakerSignedOtcOrder( LibNativeOrder.OtcOrder calldata order, LibSignature.Signature calldata makerSignature, - LibSignature.Signature calldata takerSignature, - bool unwrapWeth + LibSignature.Signature calldata takerSignature + ) + external; + + /// @dev Fully fill an OTC order. "Meta-transaction" variant, + /// requires order to be signed by both maker and taker. + /// Unwraps bought WETH into ETH before sending it to + /// the taker. + /// @param order The OTC order. + /// @param makerSignature The order signature from the maker. + /// @param takerSignature The order signature from the taker. + function fillTakerSignedOtcOrderForEth( + LibNativeOrder.OtcOrder calldata order, + LibSignature.Signature calldata makerSignature, + LibSignature.Signature calldata takerSignature + ) + external; + + /// @dev Fills multiple taker-signed OTC orders. + /// @param orders Array of OTC orders. + /// @param makerSignatures Array of maker signatures for each order. + /// @param takerSignatures Array of taker signatures for each order. + /// @param unwrapWeth Array of booleans representing whether or not + /// to unwrap bought WETH into ETH for each order. Should be set + /// to false if the maker token is not WETH. + /// @return successes Array of booleans representing whether or not + /// each order in `orders` was filled successfully. + function batchFillTakerSignedOtcOrders( + LibNativeOrder.OtcOrder[] calldata orders, + LibSignature.Signature[] calldata makerSignatures, + LibSignature.Signature[] calldata takerSignatures, + bool[] calldata unwrapWeth + ) + external + returns (bool[] memory successes); + + /// @dev Fill an OTC order for up to `takerTokenFillAmount` taker tokens. + /// Internal variant. + /// @param order The OTC order. + /// @param makerSignature The order signature from the maker. + /// @param takerTokenFillAmount Maximum taker token amount to fill this + /// order with. + /// @param taker The address to fill the order in the context of. + /// @param useSelfBalance Whether to use the Exchange Proxy's balance + /// of input tokens. + /// @param recipient The recipient of the bought maker tokens. + /// @return takerTokenFilledAmount How much taker token was filled. + /// @return makerTokenFilledAmount How much maker token was filled. + function _fillOtcOrder( + LibNativeOrder.OtcOrder calldata order, + LibSignature.Signature calldata makerSignature, + uint128 takerTokenFillAmount, + address taker, + bool useSelfBalance, + address recipient ) external returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount); diff --git a/contracts/zero-ex/contracts/src/features/interfaces/ITransformERC20Feature.sol b/contracts/zero-ex/contracts/src/features/interfaces/ITransformERC20Feature.sol index 836af87be1..93b00a2566 100644 --- a/contracts/zero-ex/contracts/src/features/interfaces/ITransformERC20Feature.sol +++ b/contracts/zero-ex/contracts/src/features/interfaces/ITransformERC20Feature.sol @@ -59,6 +59,10 @@ interface ITransformERC20Feature { // The transformations to execute on the token balance(s) // in sequence. Transformation[] transformations; + // Whether to use the Exchange Proxy's balance of `inputToken`. + bool useSelfBalance; + // The recipient of the bought `outputToken`. + address payable recipient; } /// @dev Raised upon a successful `transformERC20`. diff --git a/contracts/zero-ex/contracts/src/features/interfaces/IUniswapV3Feature.sol b/contracts/zero-ex/contracts/src/features/interfaces/IUniswapV3Feature.sol index 9ffa0ef311..19d25ca5c0 100644 --- a/contracts/zero-ex/contracts/src/features/interfaces/IUniswapV3Feature.sol +++ b/contracts/zero-ex/contracts/src/features/interfaces/IUniswapV3Feature.sol @@ -70,6 +70,22 @@ interface IUniswapV3Feature { external returns (uint256 buyAmount); + /// @dev Sell a token for another token directly against uniswap v3. + /// Private variant, uses tokens held by `address(this)`. + /// @param encodedPath Uniswap-encoded path. + /// @param sellAmount amount of the first token in the path to sell. + /// @param minBuyAmount Minimum amount of the last token in the path to buy. + /// @param recipient The recipient of the bought tokens. Can be zero for sender. + /// @return buyAmount Amount of the last token in the path bought. + function _sellHeldTokenForTokenToUniswapV3( + bytes memory encodedPath, + uint256 sellAmount, + uint256 minBuyAmount, + address recipient + ) + external + returns (uint256 buyAmount); + /// @dev The UniswapV3 pool swap callback which pays the funds requested /// by the caller/pool to the pool. Can only be called by a valid /// UniswapV3 pool. diff --git a/contracts/zero-ex/contracts/src/features/multiplex/MultiplexFeature.sol b/contracts/zero-ex/contracts/src/features/multiplex/MultiplexFeature.sol new file mode 100644 index 0000000000..c8e898b1fa --- /dev/null +++ b/contracts/zero-ex/contracts/src/features/multiplex/MultiplexFeature.sol @@ -0,0 +1,742 @@ +// 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/LibSafeMathV06.sol"; +import "../../external/ILiquidityProviderSandbox.sol"; +import "../../fixins/FixinCommon.sol"; +import "../../fixins/FixinEIP712.sol"; +import "../../migrations/LibMigrate.sol"; +import "../interfaces/IFeature.sol"; +import "../interfaces/IMultiplexFeature.sol"; +import "./MultiplexLiquidityProvider.sol"; +import "./MultiplexOtc.sol"; +import "./MultiplexRfq.sol"; +import "./MultiplexTransformERC20.sol"; +import "./MultiplexUniswapV2.sol"; +import "./MultiplexUniswapV3.sol"; + + +/// @dev This feature enables efficient batch and multi-hop trades +/// using different liquidity sources. +contract MultiplexFeature is + IFeature, + IMultiplexFeature, + FixinCommon, + MultiplexLiquidityProvider, + MultiplexOtc, + MultiplexRfq, + MultiplexTransformERC20, + MultiplexUniswapV2, + MultiplexUniswapV3 +{ + /// @dev Name of this feature. + string public constant override FEATURE_NAME = "MultiplexFeature"; + /// @dev Version of this feature. + uint256 public immutable override FEATURE_VERSION = _encodeVersion(2, 0, 0); + /// @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. + uint256 private constant LOWER_255_BITS = HIGH_BIT - 1; + + /// @dev The WETH token contract. + IEtherTokenV06 private immutable WETH; + + constructor( + address zeroExAddress, + IEtherTokenV06 weth, + ILiquidityProviderSandbox sandbox, + address uniswapFactory, + address sushiswapFactory, + bytes32 uniswapPairInitCodeHash, + bytes32 sushiswapPairInitCodeHash + ) + public + FixinEIP712(zeroExAddress) + MultiplexLiquidityProvider(sandbox) + MultiplexUniswapV2( + uniswapFactory, + sushiswapFactory, + uniswapPairInitCodeHash, + sushiswapPairInitCodeHash + ) + { + WETH = weth; + } + + /// @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.multiplexBatchSellEthForToken.selector); + _registerFeatureFunction(this.multiplexBatchSellTokenForEth.selector); + _registerFeatureFunction(this.multiplexBatchSellTokenForToken.selector); + _registerFeatureFunction(this.multiplexMultiHopSellEthForToken.selector); + _registerFeatureFunction(this.multiplexMultiHopSellTokenForEth.selector); + _registerFeatureFunction(this.multiplexMultiHopSellTokenForToken.selector); + return LibMigrate.MIGRATE_SUCCESS; + } + + /// @dev Sells attached ETH for `outputToken` using the provided + /// calls. + /// @param outputToken The token to buy. + /// @param calls The calls to use to sell the attached ETH. + /// @param minBuyAmount The minimum amount of `outputToken` that + /// must be bought for this function to not revert. + /// @return boughtAmount The amount of `outputToken` bought. + function multiplexBatchSellEthForToken( + IERC20TokenV06 outputToken, + BatchSellSubcall[] memory calls, + uint256 minBuyAmount + ) + public + override + payable + returns (uint256 boughtAmount) + { + // Wrap ETH. + WETH.deposit{value: msg.value}(); + // WETH is now held by this contract, + // so `useSelfBalance` is true. + return _multiplexBatchSell( + BatchSellParams({ + inputToken: WETH, + outputToken: outputToken, + sellAmount: msg.value, + calls: calls, + useSelfBalance: true, + recipient: msg.sender + }), + minBuyAmount + ); + } + + /// @dev Sells `sellAmount` of the given `inputToken` for ETH + /// using the provided calls. + /// @param inputToken The token to sell. + /// @param calls The calls to use to sell the input tokens. + /// @param sellAmount The amount of `inputToken` to sell. + /// @param minBuyAmount The minimum amount of ETH that + /// must be bought for this function to not revert. + /// @return boughtAmount The amount of ETH bought. + function multiplexBatchSellTokenForEth( + IERC20TokenV06 inputToken, + BatchSellSubcall[] memory calls, + uint256 sellAmount, + uint256 minBuyAmount + ) + public + override + returns (uint256 boughtAmount) + { + // The outputToken is implicitly WETH. The `recipient` + // of the WETH is set to this contract, since we + // must unwrap the WETH and transfer the resulting ETH. + boughtAmount = _multiplexBatchSell( + BatchSellParams({ + inputToken: inputToken, + outputToken: WETH, + sellAmount: sellAmount, + calls: calls, + useSelfBalance: false, + recipient: address(this) + }), + minBuyAmount + ); + // Unwrap WETH. + WETH.withdraw(boughtAmount); + // Transfer ETH to `msg.sender`. + _transferEth(msg.sender, boughtAmount); + } + + /// @dev Sells `sellAmount` of the given `inputToken` for + /// `outputToken` using the provided calls. + /// @param inputToken The token to sell. + /// @param outputToken The token to buy. + /// @param calls The calls to use to sell the input tokens. + /// @param sellAmount The amount of `inputToken` to sell. + /// @param minBuyAmount The minimum amount of `outputToken` + /// that must be bought for this function to not revert. + /// @return boughtAmount The amount of `outputToken` bought. + function multiplexBatchSellTokenForToken( + IERC20TokenV06 inputToken, + IERC20TokenV06 outputToken, + BatchSellSubcall[] memory calls, + uint256 sellAmount, + uint256 minBuyAmount + ) + public + override + returns (uint256 boughtAmount) + { + return _multiplexBatchSell( + BatchSellParams({ + inputToken: inputToken, + outputToken: outputToken, + sellAmount: sellAmount, + calls: calls, + useSelfBalance: false, + recipient: msg.sender + }), + minBuyAmount + ); + } + + /// @dev Executes a batch sell and checks that at least + /// `minBuyAmount` of `outputToken` was bought. + /// @param params Batch sell parameters. + /// @param minBuyAmount The minimum amount of `outputToken` that + /// must be bought for this function to not revert. + /// @return boughtAmount The amount of `outputToken` bought. + function _multiplexBatchSell( + BatchSellParams memory params, + uint256 minBuyAmount + ) + private + returns (uint256 boughtAmount) + { + // Cache the recipient's initial balance of the output token. + uint256 balanceBefore = params.outputToken.balanceOf(params.recipient); + // Execute the batch sell. + BatchSellState memory state = _executeBatchSell(params); + // Compute the change in balance of the output token. + uint256 balanceDelta = params.outputToken.balanceOf(params.recipient) + .safeSub(balanceBefore); + // Use the minimum of the balanceDelta and the returned bought + // amount in case of weird tokens and whatnot. + boughtAmount = LibSafeMathV06.min256(balanceDelta, state.boughtAmount); + // Enforce `minBuyAmount`. + require( + boughtAmount >= minBuyAmount, + "MultiplexFeature::_multiplexBatchSell/UNDERBOUGHT" + ); + } + + /// @dev Sells attached ETH via the given sequence of tokens + /// and calls. `tokens[0]` must be WETH. + /// The last token in `tokens` is the output token that + /// will ultimately be sent to `msg.sender` + /// @param tokens The sequence of tokens to use for the sell, + /// i.e. `tokens[i]` will be sold for `tokens[i+1]` via + /// `calls[i]`. + /// @param calls The sequence of calls to use for the sell. + /// @param minBuyAmount The minimum amount of output tokens that + /// must be bought for this function to not revert. + /// @return boughtAmount The amount of output tokens bought. + function multiplexMultiHopSellEthForToken( + address[] memory tokens, + MultiHopSellSubcall[] memory calls, + uint256 minBuyAmount + ) + public + override + payable + returns (uint256 boughtAmount) + { + // First token must be WETH. + require( + tokens[0] == address(WETH), + "MultiplexFeature::multiplexMultiHopSellEthForToken/NOT_WETH" + ); + // Wrap ETH. + WETH.deposit{value: msg.value}(); + // WETH is now held by this contract, + // so `useSelfBalance` is true. + return _multiplexMultiHopSell( + MultiHopSellParams({ + tokens: tokens, + sellAmount: msg.value, + calls: calls, + useSelfBalance: true, + recipient: msg.sender + }), + minBuyAmount + ); + } + + /// @dev Sells `sellAmount` of the input token (`tokens[0]`) + /// for ETH via the given sequence of tokens and calls. + /// The last token in `tokens` must be WETH. + /// @param tokens The sequence of tokens to use for the sell, + /// i.e. `tokens[i]` will be sold for `tokens[i+1]` via + /// `calls[i]`. + /// @param calls The sequence of calls to use for the sell. + /// @param sellAmount The amount of `inputToken` to sell. + /// @param minBuyAmount The minimum amount of ETH that + /// must be bought for this function to not revert. + /// @return boughtAmount The amount of ETH bought. + function multiplexMultiHopSellTokenForEth( + address[] memory tokens, + MultiHopSellSubcall[] memory calls, + uint256 sellAmount, + uint256 minBuyAmount + ) + public + override + returns (uint256 boughtAmount) + { + // Last token must be WETH. + require( + tokens[tokens.length - 1] == address(WETH), + "MultiplexFeature::multiplexMultiHopSellTokenForEth/NOT_WETH" + ); + // The `recipient of the WETH is set to this contract, since + // we must unwrap the WETH and transfer the resulting ETH. + boughtAmount = _multiplexMultiHopSell( + MultiHopSellParams({ + tokens: tokens, + sellAmount: sellAmount, + calls: calls, + useSelfBalance: false, + recipient: address(this) + }), + minBuyAmount + ); + // Unwrap WETH. + WETH.withdraw(boughtAmount); + // Transfer ETH to `msg.sender`. + _transferEth(msg.sender, boughtAmount); + } + + /// @dev Sells `sellAmount` of the input token (`tokens[0]`) + /// via the given sequence of tokens and calls. + /// The last token in `tokens` is the output token that + /// will ultimately be sent to `msg.sender` + /// @param tokens The sequence of tokens to use for the sell, + /// i.e. `tokens[i]` will be sold for `tokens[i+1]` via + /// `calls[i]`. + /// @param calls The sequence of calls to use for the sell. + /// @param sellAmount The amount of `inputToken` to sell. + /// @param minBuyAmount The minimum amount of output tokens that + /// must be bought for this function to not revert. + /// @return boughtAmount The amount of output tokens bought. + function multiplexMultiHopSellTokenForToken( + address[] memory tokens, + MultiHopSellSubcall[] memory calls, + uint256 sellAmount, + uint256 minBuyAmount + ) + public + override + returns (uint256 boughtAmount) + { + return _multiplexMultiHopSell( + MultiHopSellParams({ + tokens: tokens, + sellAmount: sellAmount, + calls: calls, + useSelfBalance: false, + recipient: msg.sender + }), + minBuyAmount + ); + } + + /// @dev Executes a multi-hop sell and checks that at least + /// `minBuyAmount` of output tokens were bought. + /// @param params Multi-hop sell parameters. + /// @param minBuyAmount The minimum amount of output tokens that + /// must be bought for this function to not revert. + /// @return boughtAmount The amount of output tokens bought. + function _multiplexMultiHopSell( + MultiHopSellParams memory params, + uint256 minBuyAmount + ) + private + returns (uint256 boughtAmount) + { + // 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( + params.tokens.length == params.calls.length + 1, + "MultiplexFeature::_multiplexMultiHopSell/MISMATCHED_ARRAY_LENGTHS" + ); + // The output token is the last token in the path. + IERC20TokenV06 outputToken = IERC20TokenV06( + params.tokens[params.tokens.length - 1] + ); + // Cache the recipient's balance of the output token. + uint256 balanceBefore = outputToken.balanceOf(params.recipient); + // Execute the multi-hop sell. + MultiHopSellState memory state = _executeMultiHopSell(params); + // Compute the change in balance of the output token. + uint256 balanceDelta = outputToken.balanceOf(params.recipient) + .safeSub(balanceBefore); + // Use the minimum of the balanceDelta and the returned bought + // amount in case of weird tokens and whatnot. + boughtAmount = LibSafeMathV06.min256(balanceDelta, state.outputTokenAmount); + // Enforce `minBuyAmount`. + require( + boughtAmount >= minBuyAmount, + "MultiplexFeature::_multiplexMultiHopSell/UNDERBOUGHT" + ); + } + + /// @dev Iterates through the constituent calls of a batch + /// sell and executes each one, until the full amount + // has been sold. + /// @param params Batch sell parameters. + /// @return state A struct containing the amounts of `inputToken` + /// sold and `outputToken` bought. + function _executeBatchSell(BatchSellParams memory params) + private + returns (BatchSellState memory state) + { + // Iterate through the calls and execute each one + // until the full amount has been sold. + for (uint256 i = 0; i != params.calls.length; i++) { + // Check if we've hit our target. + if (state.soldAmount >= params.sellAmount) { break; } + BatchSellSubcall memory subcall = params.calls[i]; + // Compute the input token amount. + uint256 inputTokenAmount = _normalizeSellAmount( + subcall.sellAmount, + params.sellAmount, + state.soldAmount + ); + if (subcall.id == MultiplexSubcall.RFQ) { + _batchSellRfqOrder( + state, + params, + subcall.data, + inputTokenAmount + ); + } else if (subcall.id == MultiplexSubcall.OTC) { + _batchSellOtcOrder( + state, + params, + subcall.data, + inputTokenAmount + ); + } else if (subcall.id == MultiplexSubcall.UniswapV2) { + _batchSellUniswapV2( + state, + params, + subcall.data, + inputTokenAmount + ); + } else if (subcall.id == MultiplexSubcall.UniswapV3) { + _batchSellUniswapV3( + state, + params, + subcall.data, + inputTokenAmount + ); + } else if (subcall.id == MultiplexSubcall.LiquidityProvider) { + _batchSellLiquidityProvider( + state, + params, + subcall.data, + inputTokenAmount + ); + } else if (subcall.id == MultiplexSubcall.TransformERC20) { + _batchSellTransformERC20( + state, + params, + subcall.data, + inputTokenAmount + ); + } else if (subcall.id == MultiplexSubcall.MultiHopSell) { + _nestedMultiHopSell( + state, + params, + subcall.data, + inputTokenAmount + ); + } else { + revert("MultiplexFeature::_executeBatchSell/INVALID_SUBCALL"); + } + } + require( + state.soldAmount == params.sellAmount, + "MultiplexFeature::_executeBatchSell/INCORRECT_AMOUNT_SOLD" + ); + } + + // This function executes a sequence of fills "hopping" through the + // path of tokens given by `params.tokens`. + function _executeMultiHopSell(MultiHopSellParams memory params) + private + returns (MultiHopSellState memory state) + { + // This variable is used for the input and output amounts of + // each hop. After the final hop, this will contain the output + // amount of the multi-hop fill. + state.outputTokenAmount = params.sellAmount; + // The first call may expect the input tokens to be held by + // `msg.sender`, `address(this)`, or some other address. + // Compute the expected address and transfer the input tokens + // there if necessary. + state.from = _computeHopTarget(params, 0); + // If the input tokens are currently held by `msg.sender` but + // the first hop expects them elsewhere, perform a `transferFrom`. + if (!params.useSelfBalance && state.from != msg.sender) { + _transferERC20TokensFrom( + IERC20TokenV06(params.tokens[0]), + msg.sender, + state.from, + params.sellAmount + ); + } + // If the input tokens are currently held by `address(this)` but + // the first hop expects them elsewhere, perform a `transfer`. + if (params.useSelfBalance && state.from != address(this)) { + _transferERC20Tokens( + IERC20TokenV06(params.tokens[0]), + state.from, + params.sellAmount + ); + } + // Iterate through the calls and execute each one. + for (state.hopIndex = 0; state.hopIndex != params.calls.length; state.hopIndex++) { + MultiHopSellSubcall memory subcall = params.calls[state.hopIndex]; + // Compute the recipient of the tokens that will be + // bought by the current hop. + state.to = _computeHopTarget(params, state.hopIndex + 1); + + if (subcall.id == MultiplexSubcall.UniswapV2) { + _multiHopSellUniswapV2( + state, + params, + subcall.data + ); + } else if (subcall.id == MultiplexSubcall.UniswapV3) { + _multiHopSellUniswapV3(state, subcall.data); + } else if (subcall.id == MultiplexSubcall.LiquidityProvider) { + _multiHopSellLiquidityProvider( + state, + params, + subcall.data + ); + } else if (subcall.id == MultiplexSubcall.BatchSell) { + _nestedBatchSell( + state, + params, + subcall.data + ); + } else { + revert("MultiplexFeature::_executeMultiHopSell/INVALID_SUBCALL"); + } + // The recipient of the current hop will be the source + // of tokens for the next hop. + state.from = state.to; + } + } + + function _nestedMultiHopSell( + IMultiplexFeature.BatchSellState memory state, + IMultiplexFeature.BatchSellParams memory params, + bytes memory data, + uint256 sellAmount + ) + private + { + MultiHopSellParams memory multiHopParams; + // Decode the tokens and calls for the nested + // multi-hop sell. + ( + multiHopParams.tokens, + multiHopParams.calls + ) = abi.decode( + data, + (address[], MultiHopSellSubcall[]) + ); + multiHopParams.sellAmount = sellAmount; + // If the batch sell is using input tokens held by + // `address(this)`, then so should the nested + // multi-hop sell. + multiHopParams.useSelfBalance = params.useSelfBalance; + // Likewise, the recipient of the multi-hop sell is + // equal to the recipient of its containing batch sell. + multiHopParams.recipient = params.recipient; + // Execute the nested multi-hop sell. + uint256 outputTokenAmount = + _executeMultiHopSell(multiHopParams).outputTokenAmount; + // Increment the sold and bought amounts. + state.soldAmount = state.soldAmount.safeAdd(sellAmount); + state.boughtAmount = state.boughtAmount.safeAdd(outputTokenAmount); + } + + function _nestedBatchSell( + IMultiplexFeature.MultiHopSellState memory state, + IMultiplexFeature.MultiHopSellParams memory params, + bytes memory data + ) + private + { + BatchSellParams memory batchSellParams; + // Decode the calls for the nested batch sell. + batchSellParams.calls = abi.decode( + data, + (BatchSellSubcall[]) + ); + // The input and output tokens of the batch + // sell are the current and next tokens in + // `params.tokens`, respectively. + batchSellParams.inputToken = IERC20TokenV06( + params.tokens[state.hopIndex] + ); + batchSellParams.outputToken = IERC20TokenV06( + params.tokens[state.hopIndex + 1] + ); + // The `sellAmount` for the batch sell is the + // `outputTokenAmount` from the previous hop. + batchSellParams.sellAmount = state.outputTokenAmount; + // If the nested batch sell is the first hop + // and `useSelfBalance` for the containing multi- + // hop sell is false, the nested batch sell should + // pull tokens from `msg.sender` (so `batchSellParams.useSelfBalance` + // should be false). Otherwise `batchSellParams.useSelfBalance` + // should be true. + batchSellParams.useSelfBalance = state.hopIndex > 0 || params.useSelfBalance; + // `state.to` has been populated with the address + // that should receive the output tokens of the + // batch sell. + batchSellParams.recipient = state.to; + // Execute the nested batch sell. + state.outputTokenAmount = + _executeBatchSell(batchSellParams).boughtAmount; + } + + // Transfers some amount of ETH to the given recipient and + // reverts if the transfer fails. + function _transferEth(address payable recipient, uint256 amount) + private + { + (bool success,) = recipient.call{value: amount}(""); + require(success, "MultiplexFeature::_transferEth/TRANSFER_FAILED"); + } + + // This function computes the "target" address of hop index `i` within + // a multi-hop sell. + // If `i == 0`, the target is the address which should hold the input + // tokens prior to executing `calls[0]`. Otherwise, it is the address + // that should receive `tokens[i]` upon executing `calls[i-1]`. + function _computeHopTarget( + MultiHopSellParams memory params, + uint256 i + ) + private + view + returns (address target) + { + if (i == params.calls.length) { + // The last call should send the output tokens to the + // multi-hop sell recipient. + target = params.recipient; + } else { + MultiHopSellSubcall memory subcall = params.calls[i]; + if (subcall.id == MultiplexSubcall.UniswapV2) { + // UniswapV2 (and Sushiswap) allow tokens to be + // transferred into the pair contract before `swap` + // is called, so we compute the pair contract's address. + (address[] memory tokens, bool isSushi) = abi.decode( + subcall.data, + (address[], bool) + ); + target = _computeUniswapPairAddress( + tokens[0], + tokens[1], + isSushi + ); + } else if (subcall.id == MultiplexSubcall.LiquidityProvider) { + // Similar to UniswapV2, LiquidityProvider contracts + // allow tokens to be transferred in before the swap + // is executed, so we the target is the address encoded + // in the subcall data. + (target,) = abi.decode( + subcall.data, + (address, bytes) + ); + } else if ( + subcall.id == MultiplexSubcall.UniswapV3 || + subcall.id == MultiplexSubcall.BatchSell + ) { + // UniswapV3 uses a callback to pull in the tokens being + // sold to it. The callback implemented in `UniswapV3Feature` + // can either: + // - call `transferFrom` to move tokens from `msg.sender` to the + // UniswapV3 pool, or + // - call `transfer` to move tokens from `address(this)` to the + // UniswapV3 pool. + // A nested batch sell is similar, in that it can either: + // - use tokens from `msg.sender`, or + // - use tokens held by `address(this)`. + + // Suppose UniswapV3/BatchSell is the first call in the multi-hop + // path. The input tokens are either held by `msg.sender`, + // or in the case of `multiplexMultiHopSellEthForToken` WETH is + // held by `address(this)`. The target is set accordingly. + + // If this is _not_ the first call in the multi-hop path, we + // are dealing with an "intermediate" token in the multi-hop path, + // which `msg.sender` may not have an allowance set for. Thus + // target must be set to `address(this)` for `i > 0`. + if (i == 0 && !params.useSelfBalance) { + target = msg.sender; + } else { + target = address(this); + } + } else { + revert("MultiplexFeature::_computeHopTarget/INVALID_SUBCALL"); + } + } + require( + target != address(0), + "MultiplexFeature::_computeHopTarget/TARGET_IS_NULL" + ); + } + + // If `rawAmount` encodes a proportion of `totalSellAmount`, this function + // converts it to an absolute quantity. Caps the normalized amount to + // the remaining sell amount (`totalSellAmount - soldAmount`). + function _normalizeSellAmount( + uint256 rawAmount, + uint256 totalSellAmount, + uint256 soldAmount + ) + private + pure + returns (uint256 normalized) + { + if ((rawAmount & HIGH_BIT) == HIGH_BIT) { + // If the high bit of `rawAmount` is set then the lower 255 bits + // specify a fraction of `totalSellAmount`. + return LibSafeMathV06.min256( + totalSellAmount + * LibSafeMathV06.min256(rawAmount & LOWER_255_BITS, 1e18) + / 1e18, + totalSellAmount.safeSub(soldAmount) + ); + } else { + return LibSafeMathV06.min256( + rawAmount, + totalSellAmount.safeSub(soldAmount) + ); + } + } +} diff --git a/contracts/zero-ex/contracts/src/features/multiplex/MultiplexLiquidityProvider.sol b/contracts/zero-ex/contracts/src/features/multiplex/MultiplexLiquidityProvider.sol new file mode 100644 index 0000000000..15f7892cfc --- /dev/null +++ b/contracts/zero-ex/contracts/src/features/multiplex/MultiplexLiquidityProvider.sol @@ -0,0 +1,202 @@ +// 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/LibERC20TokenV06.sol"; +import "@0x/contracts-utils/contracts/src/v06/LibSafeMathV06.sol"; +import "../../external/ILiquidityProviderSandbox.sol"; +import "../../fixins/FixinCommon.sol"; +import "../../fixins/FixinTokenSpender.sol"; +import "../../vendor/ILiquidityProvider.sol"; +import "../interfaces/IMultiplexFeature.sol"; + + +abstract contract MultiplexLiquidityProvider is + FixinCommon, + FixinTokenSpender +{ + using LibERC20TokenV06 for IERC20TokenV06; + using LibSafeMathV06 for uint256; + + // Same event fired by LiquidityProviderFeature + event LiquidityProviderSwap( + address inputToken, + address outputToken, + uint256 inputTokenAmount, + uint256 outputTokenAmount, + address provider, + address recipient + ); + + /// @dev The sandbox contract address. + ILiquidityProviderSandbox private immutable SANDBOX; + + constructor(ILiquidityProviderSandbox sandbox) + internal + { + SANDBOX = sandbox; + } + + // A payable external function that we can delegatecall to + // swallow reverts and roll back the input token transfer. + function _batchSellLiquidityProviderExternal( + IMultiplexFeature.BatchSellParams calldata params, + bytes calldata wrappedCallData, + uint256 sellAmount + ) + external + payable + returns (uint256 boughtAmount) + { + // Revert if not a delegatecall. + require( + address(this) != _implementation, + "MultiplexLiquidityProvider::_batchSellLiquidityProviderExternal/ONLY_DELEGATECALL" + ); + + // Decode the provider address and auxiliary data. + (address provider, bytes memory auxiliaryData) = abi.decode( + wrappedCallData, + (address, bytes) + ); + + if (params.useSelfBalance) { + // If `useSelfBalance` is true, use the input tokens + // held by `address(this)`. + _transferERC20Tokens( + params.inputToken, + provider, + sellAmount + ); + } else { + // Otherwise, transfer the input tokens from `msg.sender`. + _transferERC20TokensFrom( + params.inputToken, + msg.sender, + provider, + sellAmount + ); + } + // Cache the recipient's balance of the output token. + uint256 balanceBefore = params.outputToken + .balanceOf(params.recipient); + // Execute the swap. + SANDBOX.executeSellTokenForToken( + ILiquidityProvider(provider), + params.inputToken, + params.outputToken, + params.recipient, + 0, + auxiliaryData + ); + // Compute amount of output token received by the + // recipient. + boughtAmount = params.outputToken + .balanceOf(params.recipient) + .safeSub(balanceBefore); + + emit LiquidityProviderSwap( + address(params.inputToken), + address(params.outputToken), + sellAmount, + boughtAmount, + provider, + params.recipient + ); + } + + function _batchSellLiquidityProvider( + IMultiplexFeature.BatchSellState memory state, + IMultiplexFeature.BatchSellParams memory params, + bytes memory wrappedCallData, + uint256 sellAmount + ) + internal + { + // Swallow reverts + (bool success, bytes memory resultData) = _implementation.delegatecall( + abi.encodeWithSelector( + this._batchSellLiquidityProviderExternal.selector, + params, + wrappedCallData, + sellAmount + ) + ); + if (success) { + // Decode the output token amount on success. + uint256 boughtAmount = abi.decode(resultData, (uint256)); + // Increment the sold and bought amounts. + state.soldAmount = state.soldAmount.safeAdd(sellAmount); + state.boughtAmount = state.boughtAmount.safeAdd(boughtAmount); + } + } + + // This function is called after tokens have already been transferred + // into the liquidity provider contract (in the previous hop). + function _multiHopSellLiquidityProvider( + IMultiplexFeature.MultiHopSellState memory state, + IMultiplexFeature.MultiHopSellParams memory params, + bytes memory wrappedCallData + ) + internal + { + IERC20TokenV06 inputToken = IERC20TokenV06(params.tokens[state.hopIndex]); + IERC20TokenV06 outputToken = IERC20TokenV06(params.tokens[state.hopIndex + 1]); + // Decode the provider address and auxiliary data. + (address provider, bytes memory auxiliaryData) = abi.decode( + wrappedCallData, + (address, bytes) + ); + // Cache the recipient's balance of the output token. + uint256 balanceBefore = outputToken + .balanceOf(state.to); + // Execute the swap. + SANDBOX.executeSellTokenForToken( + ILiquidityProvider(provider), + inputToken, + outputToken, + state.to, + 0, + auxiliaryData + ); + // The previous `ouputTokenAmount` was effectively the + // input amount for this call. Cache the value before + // overwriting it with the new output token amount so + // that both the input and ouput amounts can be in the + // `LiquidityProviderSwap` event. + uint256 sellAmount = state.outputTokenAmount; + // Compute amount of output token received by the + // recipient. + state.outputTokenAmount = outputToken + .balanceOf(state.to) + .safeSub(balanceBefore); + + emit LiquidityProviderSwap( + address(inputToken), + address(outputToken), + sellAmount, + state.outputTokenAmount, + provider, + state.to + ); + } +} diff --git a/contracts/zero-ex/contracts/src/features/multiplex/MultiplexOtc.sol b/contracts/zero-ex/contracts/src/features/multiplex/MultiplexOtc.sol new file mode 100644 index 0000000000..dc212047ab --- /dev/null +++ b/contracts/zero-ex/contracts/src/features/multiplex/MultiplexOtc.sol @@ -0,0 +1,94 @@ +// 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/LibSafeMathV06.sol"; +import "../../fixins/FixinEIP712.sol"; +import "../interfaces/IMultiplexFeature.sol"; +import "../interfaces/IOtcOrdersFeature.sol"; +import "../libs/LibNativeOrder.sol"; + + +abstract contract MultiplexOtc is + FixinEIP712 +{ + using LibSafeMathV06 for uint256; + + event ExpiredOtcOrder( + bytes32 orderHash, + address maker, + uint64 expiry + ); + + function _batchSellOtcOrder( + IMultiplexFeature.BatchSellState memory state, + IMultiplexFeature.BatchSellParams memory params, + bytes memory wrappedCallData, + uint256 sellAmount + ) + internal + { + // Decode the Otc order and signature. + ( + LibNativeOrder.OtcOrder memory order, + LibSignature.Signature memory signature + ) = abi.decode( + wrappedCallData, + (LibNativeOrder.OtcOrder, LibSignature.Signature) + ); + // Validate tokens. + require( + order.takerToken == params.inputToken && + order.makerToken == params.outputToken, + "MultiplexOtc::_batchSellOtcOrder/OTC_ORDER_INVALID_TOKENS" + ); + // Pre-emptively check if the order is expired. + uint64 expiry = uint64(order.expiryAndNonce >> 192); + if (expiry <= uint64(block.timestamp)) { + bytes32 orderHash = _getEIP712Hash( + LibNativeOrder.getOtcOrderStructHash(order) + ); + emit ExpiredOtcOrder( + orderHash, + order.maker, + expiry + ); + return; + } + // Try filling the Otc order. Swallows reverts. + try + IOtcOrdersFeature(address(this))._fillOtcOrder + ( + order, + signature, + sellAmount.safeDowncastToUint128(), + msg.sender, + params.useSelfBalance, + params.recipient + ) + returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount) + { + // Increment the sold and bought amounts. + state.soldAmount = state.soldAmount.safeAdd(takerTokenFilledAmount); + state.boughtAmount = state.boughtAmount.safeAdd(makerTokenFilledAmount); + } catch {} + } +} diff --git a/contracts/zero-ex/contracts/src/features/multiplex/MultiplexRfq.sol b/contracts/zero-ex/contracts/src/features/multiplex/MultiplexRfq.sol new file mode 100644 index 0000000000..910d82f5c4 --- /dev/null +++ b/contracts/zero-ex/contracts/src/features/multiplex/MultiplexRfq.sol @@ -0,0 +1,93 @@ +// 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/LibSafeMathV06.sol"; +import "../../fixins/FixinEIP712.sol"; +import "../interfaces/IMultiplexFeature.sol"; +import "../interfaces/INativeOrdersFeature.sol"; +import "../libs/LibNativeOrder.sol"; + + +abstract contract MultiplexRfq is + FixinEIP712 +{ + using LibSafeMathV06 for uint256; + + event ExpiredRfqOrder( + bytes32 orderHash, + address maker, + uint64 expiry + ); + + function _batchSellRfqOrder( + IMultiplexFeature.BatchSellState memory state, + IMultiplexFeature.BatchSellParams memory params, + bytes memory wrappedCallData, + uint256 sellAmount + ) + internal + { + // Decode the RFQ order and signature. + ( + LibNativeOrder.RfqOrder memory order, + LibSignature.Signature memory signature + ) = abi.decode( + wrappedCallData, + (LibNativeOrder.RfqOrder, LibSignature.Signature) + ); + // Pre-emptively check if the order is expired. + if (order.expiry <= uint64(block.timestamp)) { + bytes32 orderHash = _getEIP712Hash( + LibNativeOrder.getRfqOrderStructHash(order) + ); + emit ExpiredRfqOrder( + orderHash, + order.maker, + order.expiry + ); + return; + } + // Validate tokens. + require( + order.takerToken == params.inputToken && + order.makerToken == params.outputToken, + "MultiplexRfq::_batchSellRfqOrder/RFQ_ORDER_INVALID_TOKENS" + ); + // Try filling the RFQ order. Swallows reverts. + try + INativeOrdersFeature(address(this))._fillRfqOrder + ( + order, + signature, + sellAmount.safeDowncastToUint128(), + msg.sender, + params.useSelfBalance, + params.recipient + ) + returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount) + { + // Increment the sold and bought amounts. + state.soldAmount = state.soldAmount.safeAdd(takerTokenFilledAmount); + state.boughtAmount = state.boughtAmount.safeAdd(makerTokenFilledAmount); + } catch {} + } +} diff --git a/contracts/zero-ex/contracts/src/features/multiplex/MultiplexTransformERC20.sol b/contracts/zero-ex/contracts/src/features/multiplex/MultiplexTransformERC20.sol new file mode 100644 index 0000000000..5a6dc35f6d --- /dev/null +++ b/contracts/zero-ex/contracts/src/features/multiplex/MultiplexTransformERC20.sol @@ -0,0 +1,64 @@ +// 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/LibSafeMathV06.sol"; +import "../interfaces/IMultiplexFeature.sol"; +import "../interfaces/ITransformERC20Feature.sol"; + + +abstract contract MultiplexTransformERC20 { + + using LibSafeMathV06 for uint256; + + function _batchSellTransformERC20( + IMultiplexFeature.BatchSellState memory state, + IMultiplexFeature.BatchSellParams memory params, + bytes memory wrappedCallData, + uint256 sellAmount + ) + internal + { + ITransformERC20Feature.TransformERC20Args memory args; + // We want the TransformedERC20 event to have + // `msg.sender` as the taker. + args.taker = msg.sender; + args.inputToken = params.inputToken; + args.outputToken = params.outputToken; + args.inputTokenAmount = sellAmount; + args.minOutputTokenAmount = 0; + args.useSelfBalance = params.useSelfBalance; + args.recipient = payable(params.recipient); + (args.transformations) = abi.decode( + wrappedCallData, + (ITransformERC20Feature.Transformation[]) + ); + // Execute the transformations and swallow reverts. + try ITransformERC20Feature(address(this))._transformERC20 + (args) + returns (uint256 outputTokenAmount) + { + // Increment the sold and bought amounts. + state.soldAmount = state.soldAmount.safeAdd(sellAmount); + state.boughtAmount = state.boughtAmount.safeAdd(outputTokenAmount); + } catch {} + } +} diff --git a/contracts/zero-ex/contracts/src/features/multiplex/MultiplexUniswapV2.sol b/contracts/zero-ex/contracts/src/features/multiplex/MultiplexUniswapV2.sol new file mode 100644 index 0000000000..815dac87f0 --- /dev/null +++ b/contracts/zero-ex/contracts/src/features/multiplex/MultiplexUniswapV2.sol @@ -0,0 +1,290 @@ +// 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 "../../fixins/FixinCommon.sol"; +import "../../fixins/FixinTokenSpender.sol"; +import "../../vendor/IUniswapV2Pair.sol"; +import "../interfaces/IMultiplexFeature.sol"; + + +abstract contract MultiplexUniswapV2 is + FixinCommon, + FixinTokenSpender +{ + using LibSafeMathV06 for uint256; + + // address of the UniswapV2Factory contract. + address private immutable UNISWAP_FACTORY; + // address of the (Sushiswap) UniswapV2Factory contract. + address private immutable SUSHISWAP_FACTORY; + // Init code hash of the UniswapV2Pair contract. + bytes32 private immutable UNISWAP_PAIR_INIT_CODE_HASH; + // Init code hash of the (Sushiswap) UniswapV2Pair contract. + bytes32 private immutable SUSHISWAP_PAIR_INIT_CODE_HASH; + + constructor( + address uniswapFactory, + address sushiswapFactory, + bytes32 uniswapPairInitCodeHash, + bytes32 sushiswapPairInitCodeHash + ) + internal + { + UNISWAP_FACTORY = uniswapFactory; + SUSHISWAP_FACTORY = sushiswapFactory; + UNISWAP_PAIR_INIT_CODE_HASH = uniswapPairInitCodeHash; + SUSHISWAP_PAIR_INIT_CODE_HASH = sushiswapPairInitCodeHash; + } + + // A payable external function that we can delegatecall to + // swallow reverts and roll back the input token transfer. + function _batchSellUniswapV2External( + IMultiplexFeature.BatchSellParams calldata params, + bytes calldata wrappedCallData, + uint256 sellAmount + ) + external + payable + returns (uint256 boughtAmount) + { + // Revert is not a delegatecall. + require( + address(this) != _implementation, + "MultiplexLiquidityProvider::_batchSellUniswapV2External/ONLY_DELEGATECALL" + ); + + (address[] memory tokens, bool isSushi) = abi.decode( + wrappedCallData, + (address[], bool) + ); + // Validate tokens + require( + tokens.length >= 2 && + tokens[0] == address(params.inputToken) && + tokens[tokens.length - 1] == address(params.outputToken), + "MultiplexUniswapV2::_batchSellUniswapV2/INVALID_TOKENS" + ); + // Compute the address of the first Uniswap pair + // contract that will execute a swap. + address firstPairAddress = _computeUniswapPairAddress( + tokens[0], + tokens[1], + isSushi + ); + // `_sellToUniswapV2` assumes the input tokens have been + // transferred into the pair contract before it is called, + // so we transfer the tokens in now (either from `msg.sender` + // or using the Exchange Proxy's balance). + if (params.useSelfBalance) { + _transferERC20Tokens( + IERC20TokenV06(tokens[0]), + firstPairAddress, + sellAmount + ); + } else { + _transferERC20TokensFrom( + IERC20TokenV06(tokens[0]), + msg.sender, + firstPairAddress, + sellAmount + ); + } + // Execute the Uniswap/Sushiswap trade. + return _sellToUniswapV2( + tokens, + sellAmount, + isSushi, + firstPairAddress, + params.recipient + ); + } + + function _batchSellUniswapV2( + IMultiplexFeature.BatchSellState memory state, + IMultiplexFeature.BatchSellParams memory params, + bytes memory wrappedCallData, + uint256 sellAmount + ) + internal + { + // Swallow reverts + (bool success, bytes memory resultData) = _implementation.delegatecall( + abi.encodeWithSelector( + this._batchSellUniswapV2External.selector, + params, + wrappedCallData, + sellAmount + ) + ); + if (success) { + // Decode the output token amount on success. + uint256 boughtAmount = abi.decode(resultData, (uint256)); + // Increment the sold and bought amounts. + state.soldAmount = state.soldAmount.safeAdd(sellAmount); + state.boughtAmount = state.boughtAmount.safeAdd(boughtAmount); + } + } + + function _multiHopSellUniswapV2( + IMultiplexFeature.MultiHopSellState memory state, + IMultiplexFeature.MultiHopSellParams memory params, + bytes memory wrappedCallData + ) + internal + { + (address[] memory tokens, bool isSushi) = abi.decode( + wrappedCallData, + (address[], bool) + ); + // Validate the tokens + require( + tokens.length >= 2 && + tokens[0] == params.tokens[state.hopIndex] && + tokens[tokens.length - 1] == params.tokens[state.hopIndex + 1], + "MultiplexUniswapV2::_multiHopSellUniswapV2/INVALID_TOKENS" + ); + // Execute the Uniswap/Sushiswap trade. + state.outputTokenAmount = _sellToUniswapV2( + tokens, + state.outputTokenAmount, + isSushi, + state.from, + state.to + ); + } + + function _sellToUniswapV2( + address[] memory tokens, + uint256 sellAmount, + bool isSushi, + address pairAddress, + address recipient + ) + private + returns (uint256 outputTokenAmount) + { + // Iterate through `tokens` perform a swap against the Uniswap + // pair contract for each `(tokens[i], tokens[i+1])`. + for (uint256 i = 0; i < tokens.length - 1; i++) { + (address inputToken, address outputToken) = (tokens[i], tokens[i + 1]); + // Compute the output token amount + outputTokenAmount = _computeUniswapOutputAmount( + pairAddress, + inputToken, + outputToken, + sellAmount + ); + (uint256 amount0Out, uint256 amount1Out) = inputToken < outputToken + ? (uint256(0), outputTokenAmount) + : (outputTokenAmount, uint256(0)); + // The Uniswap pair contract will transfer the output tokens to + // the next pair contract if there is one, otherwise transfer to + // `recipient`. + address to = i < tokens.length - 2 + ? _computeUniswapPairAddress(outputToken, tokens[i + 2], isSushi) + : recipient; + // Execute the swap. + IUniswapV2Pair(pairAddress).swap( + amount0Out, + amount1Out, + to, + new bytes(0) + ); + // To avoid recomputing the pair address of the next pair, store + // `to` in `pairAddress`. + pairAddress = to; + // The outputTokenAmount + sellAmount = outputTokenAmount; + } + } + + // Computes the Uniswap/Sushiswap pair contract address for the + // given tokens. + function _computeUniswapPairAddress( + address tokenA, + address tokenB, + bool isSushi + ) + internal + view + returns (address pairAddress) + { + // Tokens are lexicographically sorted in the Uniswap contract. + (address token0, address token1) = tokenA < tokenB + ? (tokenA, tokenB) + : (tokenB, tokenA); + if (isSushi) { + // Use the Sushiswap factory address and codehash + return address(uint256(keccak256(abi.encodePacked( + hex'ff', + SUSHISWAP_FACTORY, + keccak256(abi.encodePacked(token0, token1)), + SUSHISWAP_PAIR_INIT_CODE_HASH + )))); + } else { + // Use the Uniswap factory address and codehash + return address(uint256(keccak256(abi.encodePacked( + hex'ff', + UNISWAP_FACTORY, + keccak256(abi.encodePacked(token0, token1)), + UNISWAP_PAIR_INIT_CODE_HASH + )))); + } + } + + // 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) + { + // Input amount should be non-zero. + require( + inputAmount > 0, + "MultiplexUniswapV2::_computeUniswapOutputAmount/INSUFFICIENT_INPUT_AMOUNT" + ); + // Query the reserves of the pair contract. + (uint256 reserve0, uint256 reserve1,) = IUniswapV2Pair(pairAddress).getReserves(); + // Reserves must be non-zero. + require( + reserve0 > 0 && reserve1 > 0, + 'MultiplexUniswapV2::_computeUniswapOutputAmount/INSUFFICIENT_LIQUIDITY' + ); + // Tokens are lexicographically sorted in the Uniswap contract. + (uint256 inputReserve, uint256 outputReserve) = inputToken < outputToken + ? (reserve0, reserve1) + : (reserve1, reserve0); + // Compute the output amount. + uint256 inputAmountWithFee = inputAmount.safeMul(997); + uint256 numerator = inputAmountWithFee.safeMul(outputReserve); + uint256 denominator = inputReserve.safeMul(1000).safeAdd(inputAmountWithFee); + return numerator / denominator; + } +} diff --git a/contracts/zero-ex/contracts/src/features/multiplex/MultiplexUniswapV3.sol b/contracts/zero-ex/contracts/src/features/multiplex/MultiplexUniswapV3.sol new file mode 100644 index 0000000000..325bd58166 --- /dev/null +++ b/contracts/zero-ex/contracts/src/features/multiplex/MultiplexUniswapV3.sol @@ -0,0 +1,123 @@ +// 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 "../../fixins/FixinTokenSpender.sol"; +import "../interfaces/IMultiplexFeature.sol"; +import "../interfaces/IUniswapV3Feature.sol"; + + +abstract contract MultiplexUniswapV3 is + FixinTokenSpender +{ + using LibSafeMathV06 for uint256; + + function _batchSellUniswapV3( + IMultiplexFeature.BatchSellState memory state, + IMultiplexFeature.BatchSellParams memory params, + bytes memory wrappedCallData, + uint256 sellAmount + ) + internal + { + bool success; + bytes memory resultData; + if (params.useSelfBalance) { + // If the tokens are held by `address(this)`, we call + // the `onlySelf` variant `_sellHeldTokenForTokenToUniswapV3`, + // which uses the Exchange Proxy's balance of input token. + (success, resultData) = address(this).call( + abi.encodeWithSelector( + IUniswapV3Feature._sellHeldTokenForTokenToUniswapV3.selector, + wrappedCallData, + sellAmount, + 0, + params.recipient + ) + ); + } else { + // Otherwise, we self-delegatecall the normal variant + // `sellTokenForTokenToUniswapV3`, which pulls the input token + // from `msg.sender`. + (success, resultData) = address(this).delegatecall( + abi.encodeWithSelector( + IUniswapV3Feature.sellTokenForTokenToUniswapV3.selector, + wrappedCallData, + sellAmount, + 0, + params.recipient + ) + ); + } + if (success) { + // Decode the output token amount on success. + uint256 outputTokenAmount = abi.decode(resultData, (uint256)); + // Increment the sold and bought amounts. + state.soldAmount = state.soldAmount.safeAdd(sellAmount); + state.boughtAmount = state.boughtAmount.safeAdd(outputTokenAmount); + } + } + + function _multiHopSellUniswapV3( + IMultiplexFeature.MultiHopSellState memory state, + bytes memory wrappedCallData + ) + internal + { + bool success; + bytes memory resultData; + if (state.from == address(this)) { + // If the tokens are held by `address(this)`, we call + // the `onlySelf` variant `_sellHeldTokenForTokenToUniswapV3`, + // which uses the Exchange Proxy's balance of input token. + (success, resultData) = address(this).call( + abi.encodeWithSelector( + IUniswapV3Feature._sellHeldTokenForTokenToUniswapV3.selector, + wrappedCallData, + state.outputTokenAmount, + 0, + state.to + ) + ); + } else { + // Otherwise, we self-delegatecall the normal variant + // `sellTokenForTokenToUniswapV3`, which pulls the input token + // from `msg.sender`. + (success, resultData) = address(this).delegatecall( + abi.encodeWithSelector( + IUniswapV3Feature.sellTokenForTokenToUniswapV3.selector, + wrappedCallData, + state.outputTokenAmount, + 0, + state.to + ) + ); + } + if (success) { + // Decode the output token amount on success. + state.outputTokenAmount = abi.decode(resultData, (uint256)); + } else { + revert("MultiplexUniswapV3::_multiHopSellUniswapV3/SWAP_FAILED"); + } + } +} diff --git a/contracts/zero-ex/contracts/src/features/native_orders/NativeOrdersSettlement.sol b/contracts/zero-ex/contracts/src/features/native_orders/NativeOrdersSettlement.sol index 0005387234..75b39000e1 100644 --- a/contracts/zero-ex/contracts/src/features/native_orders/NativeOrdersSettlement.sol +++ b/contracts/zero-ex/contracts/src/features/native_orders/NativeOrdersSettlement.sol @@ -52,8 +52,10 @@ abstract contract NativeOrdersSettlement is bytes32 orderHash; // Maker of the order. address maker; - // Taker of the order. - address taker; + // The address holding the taker tokens. + address payer; + // Recipient of the maker tokens. + address recipient; // Maker token. IERC20TokenV06 makerToken; // Taker token. @@ -82,6 +84,22 @@ abstract contract NativeOrdersSettlement is address sender; } + /// @dev Params for `_fillRfqOrderPrivate()` + struct FillRfqOrderPrivateParams { + LibNativeOrder.RfqOrder order; + // The order signature. + LibSignature.Signature signature; + // Maximum taker token to fill this order with. + uint128 takerTokenFillAmount; + // The order taker. + address taker; + // Whether to use the Exchange Proxy's balance + // of taker tokens. + bool useSelfBalance; + // The recipient of the maker tokens. + address recipient; + } + // @dev Fill results returned by `_fillLimitOrderPrivate()` and /// `_fillRfqOrderPrivate()`. struct FillNativeOrderResults { @@ -154,12 +172,14 @@ abstract contract NativeOrdersSettlement is returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount) { FillNativeOrderResults memory results = - _fillRfqOrderPrivate( - order, - signature, - takerTokenFillAmount, - msg.sender - ); + _fillRfqOrderPrivate(FillRfqOrderPrivateParams({ + order: order, + signature: signature, + takerTokenFillAmount: takerTokenFillAmount, + taker: msg.sender, + useSelfBalance: false, + recipient: msg.sender + })); (takerTokenFilledAmount, makerTokenFilledAmount) = ( results.takerTokenFilledAmount, results.makerTokenFilledAmount @@ -220,12 +240,14 @@ abstract contract NativeOrdersSettlement is returns (uint128 makerTokenFilledAmount) { FillNativeOrderResults memory results = - _fillRfqOrderPrivate( - order, - signature, - takerTokenFillAmount, - msg.sender - ); + _fillRfqOrderPrivate(FillRfqOrderPrivateParams({ + order: order, + signature: signature, + takerTokenFillAmount: takerTokenFillAmount, + taker: msg.sender, + useSelfBalance: false, + recipient: msg.sender + })); // Must have filled exactly the amount requested. if (results.takerTokenFilledAmount < takerTokenFillAmount) { LibNativeOrdersRichErrors.FillOrKillFailedError( @@ -260,33 +282,36 @@ abstract contract NativeOrdersSettlement is returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount) { FillNativeOrderResults memory results = - _fillLimitOrderPrivate(FillLimitOrderPrivateParams({ - order: order, - signature: signature, - takerTokenFillAmount: takerTokenFillAmount, - taker: taker, - sender: sender - })); + _fillLimitOrderPrivate(FillLimitOrderPrivateParams( + order, + signature, + takerTokenFillAmount, + taker, + 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`). + /// @dev Fill an RFQ order. Internal variant. /// @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. + /// @param useSelfBalance Whether to use the ExchangeProxy's transient + /// balance of taker tokens to fill the order. + /// @param recipient The recipient of the maker tokens. /// @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 + address taker, + bool useSelfBalance, + address recipient ) public virtual @@ -294,12 +319,14 @@ abstract contract NativeOrdersSettlement is returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount) { FillNativeOrderResults memory results = - _fillRfqOrderPrivate( + _fillRfqOrderPrivate(FillRfqOrderPrivateParams( order, signature, takerTokenFillAmount, - taker - ); + taker, + useSelfBalance, + recipient + )); (takerTokenFilledAmount, makerTokenFilledAmount) = ( results.takerTokenFilledAmount, results.makerTokenFilledAmount @@ -387,7 +414,8 @@ abstract contract NativeOrdersSettlement is SettleOrderInfo({ orderHash: orderInfo.orderHash, maker: params.order.maker, - taker: params.taker, + payer: params.taker, + recipient: params.taker, makerToken: IERC20TokenV06(params.order.makerToken), takerToken: IERC20TokenV06(params.order.takerToken), makerAmount: params.order.makerAmount, @@ -427,22 +455,14 @@ abstract contract NativeOrdersSettlement is ); } - /// @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. + /// @dev Fill an RFQ order. Private variant. + /// @param params Function params. /// @return results Results of the fill. - function _fillRfqOrderPrivate( - LibNativeOrder.RfqOrder memory order, - LibSignature.Signature memory signature, - uint128 takerTokenFillAmount, - address taker - ) + function _fillRfqOrderPrivate(FillRfqOrderPrivateParams memory params) private returns (FillNativeOrderResults memory results) { - LibNativeOrder.OrderInfo memory orderInfo = getRfqOrderInfo(order); + LibNativeOrder.OrderInfo memory orderInfo = getRfqOrderInfo(params.order); // Must be fillable. if (orderInfo.status != LibNativeOrder.OrderStatus.FILLABLE) { @@ -457,32 +477,41 @@ abstract contract NativeOrdersSettlement is LibNativeOrdersStorage.getStorage(); // Must be fillable by the tx.origin. - if (order.txOrigin != tx.origin && !stor.originRegistry[order.txOrigin][tx.origin]) { + if ( + params.order.txOrigin != tx.origin && + !stor.originRegistry[params.order.txOrigin][tx.origin] + ) { LibNativeOrdersRichErrors.OrderNotFillableByOriginError( orderInfo.orderHash, tx.origin, - order.txOrigin + params.order.txOrigin ).rrevert(); } } // Must be fillable by the taker. - if (order.taker != address(0) && order.taker != taker) { + if (params.order.taker != address(0) && params.order.taker != params.taker) { LibNativeOrdersRichErrors.OrderNotFillableByTakerError( orderInfo.orderHash, - taker, - order.taker + params.taker, + params.order.taker ).rrevert(); } // Signature must be valid for the order. { - address signer = LibSignature.getSignerOfHash(orderInfo.orderHash, signature); - if (signer != order.maker && !isValidOrderSigner(order.maker, signer)) { + address signer = LibSignature.getSignerOfHash( + orderInfo.orderHash, + params.signature + ); + if ( + signer != params.order.maker && + !isValidOrderSigner(params.order.maker, signer) + ) { LibNativeOrdersRichErrors.OrderNotSignedByMakerError( orderInfo.orderHash, signer, - order.maker + params.order.maker ).rrevert(); } } @@ -491,26 +520,27 @@ abstract contract NativeOrdersSettlement is (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, + maker: params.order.maker, + payer: params.useSelfBalance ? address(this) : params.taker, + recipient: params.recipient, + makerToken: IERC20TokenV06(params.order.makerToken), + takerToken: IERC20TokenV06(params.order.takerToken), + makerAmount: params.order.makerAmount, + takerAmount: params.order.takerAmount, + takerTokenFillAmount: params.takerTokenFillAmount, takerTokenFilledAmount: orderInfo.takerTokenFilledAmount }) ); emit RfqOrderFilled( orderInfo.orderHash, - order.maker, - taker, - address(order.makerToken), - address(order.takerToken), + params.order.maker, + params.taker, + address(params.order.makerToken), + address(params.order.takerToken), results.takerTokenFilledAmount, results.makerTokenFilledAmount, - order.pool + params.order.pool ); } @@ -549,19 +579,28 @@ abstract contract NativeOrdersSettlement is // function if the order is cancelled. settleInfo.takerTokenFilledAmount.safeAdd128(takerTokenFilledAmount); - // Transfer taker -> maker. - _transferERC20TokensFrom( - settleInfo.takerToken, - settleInfo.taker, - settleInfo.maker, - takerTokenFilledAmount - ); + if (settleInfo.payer == address(this)) { + // Transfer this -> maker. + _transferERC20Tokens( + settleInfo.takerToken, + settleInfo.maker, + takerTokenFilledAmount + ); + } else { + // Transfer taker -> maker. + _transferERC20TokensFrom( + settleInfo.takerToken, + settleInfo.payer, + settleInfo.maker, + takerTokenFilledAmount + ); + } - // Transfer maker -> taker. + // Transfer maker -> recipient. _transferERC20TokensFrom( settleInfo.makerToken, settleInfo.maker, - settleInfo.taker, + settleInfo.recipient, makerTokenFilledAmount ); } diff --git a/contracts/zero-ex/contracts/src/transformers/FillQuoteTransformer.sol b/contracts/zero-ex/contracts/src/transformers/FillQuoteTransformer.sol index 0398f42bde..93b53c8eb4 100644 --- a/contracts/zero-ex/contracts/src/transformers/FillQuoteTransformer.sol +++ b/contracts/zero-ex/contracts/src/transformers/FillQuoteTransformer.sol @@ -137,8 +137,8 @@ contract FillQuoteTransformer is /// @dev Mask of the lower 255 bits of a uint256 value. uint256 private constant LOWER_255_BITS = HIGH_BIT - 1; /// @dev If `refundReceiver` is set to this address, unpsent - /// protocol fees will be sent to the taker. - address private constant REFUND_RECEIVER_TAKER = address(1); + /// protocol fees will be sent to the transform recipient. + address private constant REFUND_RECEIVER_RECIPIENT = address(1); /// @dev If `refundReceiver` is set to this address, unpsent /// protocol fees will be sent to the sender. address private constant REFUND_RECEIVER_SENDER = address(2); @@ -272,8 +272,8 @@ contract FillQuoteTransformer is // Refund unspent protocol fees. if (state.ethRemaining > 0 && data.refundReceiver != address(0)) { bool transferSuccess; - if (data.refundReceiver == REFUND_RECEIVER_TAKER) { - (transferSuccess,) = context.taker.call{value: state.ethRemaining}(""); + if (data.refundReceiver == REFUND_RECEIVER_RECIPIENT) { + (transferSuccess,) = context.recipient.call{value: state.ethRemaining}(""); } else if (data.refundReceiver == REFUND_RECEIVER_SENDER) { (transferSuccess,) = context.sender.call{value: state.ethRemaining}(""); } else { diff --git a/contracts/zero-ex/contracts/src/transformers/IERC20Transformer.sol b/contracts/zero-ex/contracts/src/transformers/IERC20Transformer.sol index 27b4d5da0a..0700aecd87 100644 --- a/contracts/zero-ex/contracts/src/transformers/IERC20Transformer.sol +++ b/contracts/zero-ex/contracts/src/transformers/IERC20Transformer.sol @@ -30,9 +30,9 @@ interface IERC20Transformer { struct TransformContext { // The caller of `TransformERC20.transformERC20()`. address payable sender; - // taker The taker address, which may be distinct from `sender` in the case + // The recipient address, which may be distinct from `sender` e.g. in // meta-transactions. - address payable taker; + address payable recipient; // Arbitrary data to pass to the transformer. bytes data; } diff --git a/contracts/zero-ex/contracts/src/transformers/LogMetadataTransformer.sol b/contracts/zero-ex/contracts/src/transformers/LogMetadataTransformer.sol index 8b56b9705c..571d48efa4 100644 --- a/contracts/zero-ex/contracts/src/transformers/LogMetadataTransformer.sol +++ b/contracts/zero-ex/contracts/src/transformers/LogMetadataTransformer.sol @@ -41,7 +41,7 @@ contract LogMetadataTransformer is override returns (bytes4 success) { - emit TransformerMetadata(context.sender, context.taker, context.data); + emit TransformerMetadata(context.sender, context.recipient, context.data); return LibERC20Transformer.TRANSFORMER_SUCCESS; } } diff --git a/contracts/zero-ex/contracts/src/transformers/PayTakerTransformer.sol b/contracts/zero-ex/contracts/src/transformers/PayTakerTransformer.sol index 3134e179fa..7fdb2a949d 100644 --- a/contracts/zero-ex/contracts/src/transformers/PayTakerTransformer.sol +++ b/contracts/zero-ex/contracts/src/transformers/PayTakerTransformer.sol @@ -75,7 +75,7 @@ contract PayTakerTransformer is amount = data.tokens[i].getTokenBalanceOf(address(this)); } if (amount != 0) { - data.tokens[i].transformerTransfer(context.taker, amount); + data.tokens[i].transformerTransfer(context.recipient, amount); } } return LibERC20Transformer.TRANSFORMER_SUCCESS; diff --git a/contracts/zero-ex/contracts/test/TestFillQuoteTransformerHost.sol b/contracts/zero-ex/contracts/test/TestFillQuoteTransformerHost.sol index feb3f7127a..8aebfda530 100644 --- a/contracts/zero-ex/contracts/test/TestFillQuoteTransformerHost.sol +++ b/contracts/zero-ex/contracts/test/TestFillQuoteTransformerHost.sol @@ -33,7 +33,7 @@ contract TestFillQuoteTransformerHost is TestMintableERC20Token inputToken, uint256 inputTokenAmount, address payable sender, - address payable taker, + address payable recipient, bytes calldata data ) external @@ -47,7 +47,7 @@ contract TestFillQuoteTransformerHost is transformer, IERC20Transformer.TransformContext({ sender: sender, - taker: taker, + recipient: recipient, data: data }) ); diff --git a/contracts/zero-ex/contracts/test/TestLiquidityProvider.sol b/contracts/zero-ex/contracts/test/TestLiquidityProvider.sol index 31dfb3cb37..7110bc2d69 100644 --- a/contracts/zero-ex/contracts/test/TestLiquidityProvider.sol +++ b/contracts/zero-ex/contracts/test/TestLiquidityProvider.sol @@ -46,16 +46,6 @@ contract TestLiquidityProvider { uint256 inputTokenBalance ); - IERC20TokenV06 public immutable xAsset; - IERC20TokenV06 public immutable yAsset; - - constructor(IERC20TokenV06 xAsset_, IERC20TokenV06 yAsset_) - public - { - xAsset = xAsset_; - yAsset = yAsset_; - } - receive() external payable {} /// @dev Trades `inputToken` for `outputToken`. The amount of `inputToken` @@ -83,6 +73,8 @@ contract TestLiquidityProvider { minBuyAmount, IERC20TokenV06(inputToken).balanceOf(address(this)) ); + uint256 outputTokenBalance = IERC20TokenV06(outputToken).balanceOf(address(this)); + IERC20TokenV06(outputToken).transfer(recipient, outputTokenBalance); } /// @dev Trades ETH for token. ETH must be sent to the contract prior to @@ -106,6 +98,8 @@ contract TestLiquidityProvider { minBuyAmount, address(this).balance ); + uint256 outputTokenBalance = IERC20TokenV06(outputToken).balanceOf(address(this)); + IERC20TokenV06(outputToken).transfer(recipient, outputTokenBalance); } /// @dev Trades token for ETH. The token must be sent to the contract prior @@ -129,5 +123,6 @@ contract TestLiquidityProvider { minBuyAmount, IERC20TokenV06(inputToken).balanceOf(address(this)) ); + recipient.transfer(address(this).balance); } } diff --git a/contracts/zero-ex/contracts/test/TestMetaTransactionsNativeOrdersFeature.sol b/contracts/zero-ex/contracts/test/TestMetaTransactionsNativeOrdersFeature.sol index eb1d3ee3ac..165fffd66e 100644 --- a/contracts/zero-ex/contracts/test/TestMetaTransactionsNativeOrdersFeature.sol +++ b/contracts/zero-ex/contracts/test/TestMetaTransactionsNativeOrdersFeature.sol @@ -89,7 +89,9 @@ contract TestMetaTransactionsNativeOrdersFeature is LibNativeOrder.RfqOrder memory order, LibSignature.Signature memory signature, uint128 takerTokenFillAmount, - address taker + address taker, + bool /* useSelfBalance */, + address /* recipient */ ) public override diff --git a/contracts/zero-ex/contracts/test/TestMintTokenERC20Transformer.sol b/contracts/zero-ex/contracts/test/TestMintTokenERC20Transformer.sol index b7df9d4de5..7ec9950bf9 100644 --- a/contracts/zero-ex/contracts/test/TestMintTokenERC20Transformer.sol +++ b/contracts/zero-ex/contracts/test/TestMintTokenERC20Transformer.sol @@ -57,7 +57,7 @@ contract TestMintTokenERC20Transformer is address(this), msg.sender, context.sender, - context.taker, + context.recipient, context.data, LibERC20Transformer.isTokenETH(data.inputToken) ? address(this).balance @@ -71,15 +71,22 @@ contract TestMintTokenERC20Transformer is data.inputToken.transfer(address(0), data.burnAmount); } // Mint output tokens. - if (LibERC20Transformer.isTokenETH(IERC20TokenV06(address(data.outputToken)))) { - context.taker.transfer(data.mintAmount); - } else { - data.outputToken.mint( - context.taker, - data.mintAmount - ); - // Burn fees from output. - data.outputToken.burn(context.taker, data.feeAmount); + if (!LibERC20Transformer.isTokenETH(IERC20TokenV06(address(data.outputToken)))) { + if (data.feeAmount > data.mintAmount) { + data.outputToken.burn( + context.recipient, + data.feeAmount - data.mintAmount + ); + } else { + data.outputToken.mint( + address(this), + data.mintAmount + ); + data.outputToken.burn( + context.recipient, + data.feeAmount + ); + } } return LibERC20Transformer.TRANSFORMER_SUCCESS; } diff --git a/contracts/zero-ex/contracts/test/TestMintableERC20Token.sol b/contracts/zero-ex/contracts/test/TestMintableERC20Token.sol index 25dde8c4bc..c76aa5aa8c 100644 --- a/contracts/zero-ex/contracts/test/TestMintableERC20Token.sol +++ b/contracts/zero-ex/contracts/test/TestMintableERC20Token.sol @@ -22,6 +22,12 @@ pragma experimental ABIEncoderV2; contract TestMintableERC20Token { + event Transfer( + address token, + address from, + address to, + uint256 value + ); mapping(address => uint256) public balanceOf; mapping(address => mapping(address => uint256)) public allowance; @@ -81,6 +87,7 @@ contract TestMintableERC20Token { require(balanceOf[from] >= amount, "TestMintableERC20Token/INSUFFICIENT_FUNDS"); balanceOf[from] -= amount; balanceOf[to] += amount; + emit Transfer(address(this), from, to, amount); return true; } diff --git a/contracts/zero-ex/contracts/test/TestUniswapV2Factory.sol b/contracts/zero-ex/contracts/test/TestUniswapV2Factory.sol new file mode 100644 index 0000000000..4a311919ff --- /dev/null +++ b/contracts/zero-ex/contracts/test/TestUniswapV2Factory.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.6; +pragma experimental ABIEncoderV2; +import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol"; +import "./TestUniswapV2Pool.sol"; + +contract TestUniswapV2Factory { + struct CreationParameters { + IERC20TokenV06 token0; + IERC20TokenV06 token1; + } + + event PoolCreated(TestUniswapV2Pool pool); + + bytes32 public immutable POOL_INIT_CODE_HASH; + mapping (IERC20TokenV06 => mapping (IERC20TokenV06 => TestUniswapV2Pool)) public getPool; + CreationParameters public creationParameters; + + constructor() public { + POOL_INIT_CODE_HASH = keccak256(type(TestUniswapV2Pool).creationCode); + } + + function createPool(IERC20TokenV06 tokenA, IERC20TokenV06 tokenB) + external + returns (TestUniswapV2Pool pool) + { + (IERC20TokenV06 token0, IERC20TokenV06 token1) = tokenA < tokenB + ? (tokenA, tokenB) + : (tokenB, tokenA); + require( + getPool[token0][token1] == TestUniswapV2Pool(0), + "TestUniswapV2Factory/POOL_ALREADY_EXISTS" + ); + creationParameters = CreationParameters({ + token0: token0, + token1: token1 + }); + pool = new TestUniswapV2Pool + { salt: keccak256(abi.encodePacked(token0, token1)) }(); + getPool[token0][token1] = pool; + getPool[token1][token0] = pool; + emit PoolCreated(pool); + } +} diff --git a/contracts/zero-ex/contracts/test/TestUniswapV2Pool.sol b/contracts/zero-ex/contracts/test/TestUniswapV2Pool.sol new file mode 100644 index 0000000000..1451748155 --- /dev/null +++ b/contracts/zero-ex/contracts/test/TestUniswapV2Pool.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.6; +pragma experimental ABIEncoderV2; +import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol"; +import "../src/vendor/IUniswapV2Pair.sol"; + +interface IUniswapV2PoolDeployer { + struct CreationParameters { + IERC20TokenV06 token0; + IERC20TokenV06 token1; + } + + function creationParameters() external view returns (CreationParameters memory); +} + +contract TestUniswapV2Pool is IUniswapV2Pair { + IERC20TokenV06 public immutable token0; + IERC20TokenV06 public immutable token1; + + uint112 reserve0; + uint112 reserve1; + uint32 blockTimestampLast; + + constructor() public { + IUniswapV2PoolDeployer.CreationParameters memory params = + IUniswapV2PoolDeployer(msg.sender).creationParameters(); + (token0, token1) = (params.token0, params.token1); + } + + function swap( + uint256 amount0Out, + uint256 amount1Out, + address to, + bytes calldata /* data */ + ) + external + override + { + if (amount0Out > 0) { + token0.transfer(to, amount0Out); + } + if (amount1Out > 0) { + token1.transfer(to, amount1Out); + } + } + + function setReserves( + uint112 reserve0_, + uint112 reserve1_, + uint32 blockTimestampLast_ + ) + external + { + reserve0 = reserve0_; + reserve1 = reserve1_; + blockTimestampLast = blockTimestampLast_; + } + + function getReserves() + external + override + view + returns (uint112, uint112, uint32) + { + return (reserve0, reserve1, blockTimestampLast); + } +} diff --git a/contracts/zero-ex/contracts/test/TestWeth.sol b/contracts/zero-ex/contracts/test/TestWeth.sol index 9c3e9efe74..37007d284a 100644 --- a/contracts/zero-ex/contracts/test/TestWeth.sol +++ b/contracts/zero-ex/contracts/test/TestWeth.sol @@ -26,11 +26,15 @@ import "./TestMintableERC20Token.sol"; contract TestWeth is TestMintableERC20Token { + event Deposit(address owner, uint256 value); + event Withdrawal(address owner, uint256 value); + function deposit() external payable { this.mint(msg.sender, msg.value); + emit Deposit(msg.sender, msg.value); } function depositTo(address owner) @@ -38,6 +42,7 @@ contract TestWeth is payable { this.mint(owner, msg.value); + emit Deposit(owner, msg.value); } function withdraw(uint256 amount) @@ -46,5 +51,6 @@ contract TestWeth is require(balanceOf[msg.sender] >= amount, "TestWeth/INSUFFICIENT_FUNDS"); balanceOf[msg.sender] -= amount; msg.sender.transfer(amount); + emit Withdrawal(msg.sender, amount); } } diff --git a/contracts/zero-ex/contracts/test/TestWethTransformerHost.sol b/contracts/zero-ex/contracts/test/TestWethTransformerHost.sol index e5138b37b1..cae441fb27 100644 --- a/contracts/zero-ex/contracts/test/TestWethTransformerHost.sol +++ b/contracts/zero-ex/contracts/test/TestWethTransformerHost.sol @@ -53,7 +53,7 @@ contract TestWethTransformerHost is transformer, IERC20Transformer.TransformContext({ sender: msg.sender, - taker: msg.sender, + recipient: msg.sender, data: data }) ); diff --git a/contracts/zero-ex/package.json b/contracts/zero-ex/package.json index 6898dcdd6b..03610406e9 100644 --- a/contracts/zero-ex/package.json +++ b/contracts/zero-ex/package.json @@ -43,7 +43,7 @@ "config": { "publicInterfaceContracts": "IZeroEx,ZeroEx,FullMigration,InitialMigration,IFlashWallet,IERC20Transformer,IOwnableFeature,ISimpleFunctionRegistryFeature,ITransformERC20Feature,FillQuoteTransformer,PayTakerTransformer,PositiveSlippageFeeTransformer,WethTransformer,OwnableFeature,SimpleFunctionRegistryFeature,TransformERC20Feature,AffiliateFeeTransformer,MetaTransactionsFeature,LogMetadataTransformer,BridgeAdapter,LiquidityProviderFeature,ILiquidityProviderFeature,NativeOrdersFeature,INativeOrdersFeature,FeeCollectorController,FeeCollector,CurveLiquidityProvider,BatchFillNativeOrdersFeature,IBatchFillNativeOrdersFeature,MultiplexFeature,IMultiplexFeature,OtcOrdersFeature,IOtcOrdersFeature", "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.", - "abis": "./test/generated-artifacts/@(AffiliateFeeTransformer|BatchFillNativeOrdersFeature|BootstrapFeature|BridgeAdapter|BridgeProtocols|CurveLiquidityProvider|FeeCollector|FeeCollectorController|FillQuoteTransformer|FixinCommon|FixinEIP712|FixinProtocolFees|FixinReentrancyGuard|FixinTokenSpender|FlashWallet|FullMigration|IBatchFillNativeOrdersFeature|IBootstrapFeature|IBridgeAdapter|IERC20Bridge|IERC20Transformer|IFeature|IFlashWallet|ILiquidityProvider|ILiquidityProviderFeature|ILiquidityProviderSandbox|IMetaTransactionsFeature|IMooniswapPool|IMultiplexFeature|INativeOrdersEvents|INativeOrdersFeature|IOtcOrdersFeature|IOwnableFeature|IPancakeSwapFeature|ISimpleFunctionRegistryFeature|IStaking|ITestSimpleFunctionRegistryFeature|ITokenSpenderFeature|ITransformERC20Feature|IUniswapFeature|IUniswapV2Pair|IUniswapV3Feature|IUniswapV3Pool|IZeroEx|InitialMigration|LibBootstrap|LibCommonRichErrors|LibERC20Transformer|LibFeeCollector|LibLiquidityProviderRichErrors|LibMetaTransactionsRichErrors|LibMetaTransactionsStorage|LibMigrate|LibNativeOrder|LibNativeOrdersRichErrors|LibNativeOrdersStorage|LibOtcOrdersStorage|LibOwnableRichErrors|LibOwnableStorage|LibProxyRichErrors|LibProxyStorage|LibReentrancyGuardStorage|LibSignature|LibSignatureRichErrors|LibSimpleFunctionRegistryRichErrors|LibSimpleFunctionRegistryStorage|LibStorage|LibTransformERC20RichErrors|LibTransformERC20Storage|LibWalletRichErrors|LiquidityProviderFeature|LiquidityProviderSandbox|LogMetadataTransformer|MetaTransactionsFeature|MixinBalancer|MixinBalancerV2|MixinBancor|MixinClipper|MixinCoFiX|MixinCryptoCom|MixinCurve|MixinCurveV2|MixinDodo|MixinDodoV2|MixinKyber|MixinKyberDmm|MixinLido|MixinMStable|MixinMakerPSM|MixinMooniswap|MixinNerve|MixinOasis|MixinShell|MixinUniswap|MixinUniswapV2|MixinUniswapV3|MixinZeroExBridge|MooniswapLiquidityProvider|MultiplexFeature|NativeOrdersCancellation|NativeOrdersFeature|NativeOrdersInfo|NativeOrdersProtocolFees|NativeOrdersSettlement|OtcOrdersFeature|OwnableFeature|PancakeSwapFeature|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|TestNoEthRecipient|TestOrderSignerRegistryWithContractWallet|TestPermissionlessTransformerDeployerSuicidal|TestPermissionlessTransformerDeployerTransformer|TestRfqOriginRegistration|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestStaking|TestTokenSpenderERC20Token|TestTransformERC20|TestTransformerBase|TestTransformerDeployerTransformer|TestTransformerHost|TestUniswapV3Factory|TestUniswapV3Feature|TestUniswapV3Pool|TestWeth|TestWethTransformerHost|TestZeroExFeature|TransformERC20Feature|Transformer|TransformerDeployer|UniswapFeature|UniswapV3Feature|WethTransformer|ZeroEx|ZeroExOptimized).json" + "abis": "./test/generated-artifacts/@(AffiliateFeeTransformer|BatchFillNativeOrdersFeature|BootstrapFeature|BridgeAdapter|BridgeProtocols|CurveLiquidityProvider|FeeCollector|FeeCollectorController|FillQuoteTransformer|FixinCommon|FixinEIP712|FixinProtocolFees|FixinReentrancyGuard|FixinTokenSpender|FlashWallet|FullMigration|IBatchFillNativeOrdersFeature|IBootstrapFeature|IBridgeAdapter|IERC20Bridge|IERC20Transformer|IFeature|IFlashWallet|ILiquidityProvider|ILiquidityProviderFeature|ILiquidityProviderSandbox|IMetaTransactionsFeature|IMooniswapPool|IMultiplexFeature|INativeOrdersEvents|INativeOrdersFeature|IOtcOrdersFeature|IOwnableFeature|IPancakeSwapFeature|ISimpleFunctionRegistryFeature|IStaking|ITestSimpleFunctionRegistryFeature|ITokenSpenderFeature|ITransformERC20Feature|IUniswapFeature|IUniswapV2Pair|IUniswapV3Feature|IUniswapV3Pool|IZeroEx|InitialMigration|LibBootstrap|LibCommonRichErrors|LibERC20Transformer|LibFeeCollector|LibLiquidityProviderRichErrors|LibMetaTransactionsRichErrors|LibMetaTransactionsStorage|LibMigrate|LibNativeOrder|LibNativeOrdersRichErrors|LibNativeOrdersStorage|LibOtcOrdersStorage|LibOwnableRichErrors|LibOwnableStorage|LibProxyRichErrors|LibProxyStorage|LibReentrancyGuardStorage|LibSignature|LibSignatureRichErrors|LibSimpleFunctionRegistryRichErrors|LibSimpleFunctionRegistryStorage|LibStorage|LibTransformERC20RichErrors|LibTransformERC20Storage|LibWalletRichErrors|LiquidityProviderFeature|LiquidityProviderSandbox|LogMetadataTransformer|MetaTransactionsFeature|MixinBalancer|MixinBalancerV2|MixinBancor|MixinClipper|MixinCoFiX|MixinCryptoCom|MixinCurve|MixinCurveV2|MixinDodo|MixinDodoV2|MixinKyber|MixinKyberDmm|MixinLido|MixinMStable|MixinMakerPSM|MixinMooniswap|MixinNerve|MixinOasis|MixinShell|MixinUniswap|MixinUniswapV2|MixinUniswapV3|MixinZeroExBridge|MooniswapLiquidityProvider|MultiplexFeature|MultiplexLiquidityProvider|MultiplexOtc|MultiplexRfq|MultiplexTransformERC20|MultiplexUniswapV2|MultiplexUniswapV3|NativeOrdersCancellation|NativeOrdersFeature|NativeOrdersInfo|NativeOrdersProtocolFees|NativeOrdersSettlement|OtcOrdersFeature|OwnableFeature|PancakeSwapFeature|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|TestNoEthRecipient|TestOrderSignerRegistryWithContractWallet|TestPermissionlessTransformerDeployerSuicidal|TestPermissionlessTransformerDeployerTransformer|TestRfqOriginRegistration|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestStaking|TestTokenSpenderERC20Token|TestTransformERC20|TestTransformerBase|TestTransformerDeployerTransformer|TestTransformerHost|TestUniswapV2Factory|TestUniswapV2Pool|TestUniswapV3Factory|TestUniswapV3Feature|TestUniswapV3Pool|TestWeth|TestWethTransformerHost|TestZeroExFeature|TransformERC20Feature|Transformer|TransformerDeployer|UniswapFeature|UniswapV3Feature|WethTransformer|ZeroEx|ZeroExOptimized).json" }, "repository": { "type": "git", diff --git a/contracts/zero-ex/test/artifacts.ts b/contracts/zero-ex/test/artifacts.ts index 860cbeb10a..cbbb76b505 100644 --- a/contracts/zero-ex/test/artifacts.ts +++ b/contracts/zero-ex/test/artifacts.ts @@ -104,6 +104,12 @@ import * as MixinUniswapV3 from '../test/generated-artifacts/MixinUniswapV3.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 MultiplexLiquidityProvider from '../test/generated-artifacts/MultiplexLiquidityProvider.json'; +import * as MultiplexOtc from '../test/generated-artifacts/MultiplexOtc.json'; +import * as MultiplexRfq from '../test/generated-artifacts/MultiplexRfq.json'; +import * as MultiplexTransformERC20 from '../test/generated-artifacts/MultiplexTransformERC20.json'; +import * as MultiplexUniswapV2 from '../test/generated-artifacts/MultiplexUniswapV2.json'; +import * as MultiplexUniswapV3 from '../test/generated-artifacts/MultiplexUniswapV3.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'; @@ -151,6 +157,8 @@ import * as TestTransformerBase from '../test/generated-artifacts/TestTransforme import * as TestTransformERC20 from '../test/generated-artifacts/TestTransformERC20.json'; import * as TestTransformerDeployerTransformer from '../test/generated-artifacts/TestTransformerDeployerTransformer.json'; import * as TestTransformerHost from '../test/generated-artifacts/TestTransformerHost.json'; +import * as TestUniswapV2Factory from '../test/generated-artifacts/TestUniswapV2Factory.json'; +import * as TestUniswapV2Pool from '../test/generated-artifacts/TestUniswapV2Pool.json'; import * as TestUniswapV3Factory from '../test/generated-artifacts/TestUniswapV3Factory.json'; import * as TestUniswapV3Feature from '../test/generated-artifacts/TestUniswapV3Feature.json'; import * as TestUniswapV3Pool from '../test/generated-artifacts/TestUniswapV3Pool.json'; @@ -192,7 +200,6 @@ export const artifacts = { BootstrapFeature: BootstrapFeature as ContractArtifact, LiquidityProviderFeature: LiquidityProviderFeature as ContractArtifact, MetaTransactionsFeature: MetaTransactionsFeature as ContractArtifact, - MultiplexFeature: MultiplexFeature as ContractArtifact, NativeOrdersFeature: NativeOrdersFeature as ContractArtifact, OtcOrdersFeature: OtcOrdersFeature as ContractArtifact, OwnableFeature: OwnableFeature as ContractArtifact, @@ -219,6 +226,13 @@ export const artifacts = { IUniswapV3Feature: IUniswapV3Feature as ContractArtifact, LibNativeOrder: LibNativeOrder as ContractArtifact, LibSignature: LibSignature as ContractArtifact, + MultiplexFeature: MultiplexFeature as ContractArtifact, + MultiplexLiquidityProvider: MultiplexLiquidityProvider as ContractArtifact, + MultiplexOtc: MultiplexOtc as ContractArtifact, + MultiplexRfq: MultiplexRfq as ContractArtifact, + MultiplexTransformERC20: MultiplexTransformERC20 as ContractArtifact, + MultiplexUniswapV2: MultiplexUniswapV2 as ContractArtifact, + MultiplexUniswapV3: MultiplexUniswapV3 as ContractArtifact, NativeOrdersCancellation: NativeOrdersCancellation as ContractArtifact, NativeOrdersInfo: NativeOrdersInfo as ContractArtifact, NativeOrdersProtocolFees: NativeOrdersProtocolFees as ContractArtifact, @@ -320,6 +334,8 @@ export const artifacts = { TestTransformerBase: TestTransformerBase as ContractArtifact, TestTransformerDeployerTransformer: TestTransformerDeployerTransformer as ContractArtifact, TestTransformerHost: TestTransformerHost as ContractArtifact, + TestUniswapV2Factory: TestUniswapV2Factory as ContractArtifact, + TestUniswapV2Pool: TestUniswapV2Pool as ContractArtifact, TestUniswapV3Factory: TestUniswapV3Factory as ContractArtifact, TestUniswapV3Feature: TestUniswapV3Feature as ContractArtifact, TestUniswapV3Pool: TestUniswapV3Pool as ContractArtifact, diff --git a/contracts/zero-ex/test/features/liquidity_provider_test.ts b/contracts/zero-ex/test/features/liquidity_provider_test.ts index e312567103..eaafc57ff4 100644 --- a/contracts/zero-ex/test/features/liquidity_provider_test.ts +++ b/contracts/zero-ex/test/features/liquidity_provider_test.ts @@ -72,8 +72,6 @@ blockchainTests('LiquidityProvider feature', env => { env.provider, env.txDefaults, artifacts, - token.address, - weth.address, ); }); blockchainTests.resets('Sandbox', () => { diff --git a/contracts/zero-ex/test/features/meta_transactions_test.ts b/contracts/zero-ex/test/features/meta_transactions_test.ts index 5f1d8b8772..fdc660946d 100644 --- a/contracts/zero-ex/test/features/meta_transactions_test.ts +++ b/contracts/zero-ex/test/features/meta_transactions_test.ts @@ -354,6 +354,8 @@ blockchainTests.resets('MetaTransactions feature', env => { inputTokenAmount: args.inputTokenAmount, minOutputTokenAmount: args.minOutputTokenAmount, transformations: args.transformations, + useSelfBalance: false, + recipient: mtx.signer, }) .getABIEncodedTransactionData(); return expect(tx).to.revertWith( diff --git a/contracts/zero-ex/test/features/multiplex_test.ts b/contracts/zero-ex/test/features/multiplex_test.ts index 29f1995f6a..70a9c56f82 100644 --- a/contracts/zero-ex/test/features/multiplex_test.ts +++ b/contracts/zero-ex/test/features/multiplex_test.ts @@ -1,774 +1,2140 @@ 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 { - BridgeProtocol, - encodeBridgeSourceId, - encodeFillQuoteTransformerData, - encodePayTakerTransformerData, - FillQuoteTransformerOrderType, - FillQuoteTransformerSide, - findTransformerNonce, - RfqOrder, - SIGNATURE_ABI, -} from '@0x/protocol-utils'; -import { AbiEncoder, BigNumber, logUtils } from '@0x/utils'; -import * as _ from 'lodash'; + blockchainTests, + constants, + expect, + getRandomInteger, + toBaseUnitAmount, + verifyEventsFromLogs, +} from '@0x/contracts-test-utils'; +import { OtcOrder, RfqOrder, SIGNATURE_ABI } from '@0x/protocol-utils'; +import { AbiEncoder, BigNumber, hexUtils } from '@0x/utils'; +import { LogWithDecodedArgs } from 'ethereum-types'; +import { IZeroExContract, IZeroExEvents } from '../../src/wrappers'; import { artifacts } from '../artifacts'; import { abis } from '../utils/abis'; -import { getRandomRfqOrder } from '../utils/orders'; +import { fullMigrateAsync } from '../utils/migration'; +import { getRandomOtcOrder, getRandomRfqOrder } from '../utils/orders'; import { - BridgeAdapterBridgeFillEventArgs, - BridgeAdapterEvents, - IUniswapV2PairEvents, - IUniswapV2PairSwapEventArgs, - IZeroExContract, - IZeroExEvents, - IZeroExRfqOrderFilledEventArgs, + IOwnableFeatureContract, + LiquidityProviderSandboxContract, MultiplexFeatureContract, MultiplexFeatureEvents, - MultiplexFeatureLiquidityProviderSwapEventArgs, - SimpleFunctionRegistryFeatureContract, + OtcOrdersFeatureContract, + TestLiquidityProviderContract, + TestMintableERC20TokenContract, + TestMintableERC20TokenEvents, + TestMintTokenERC20TransformerContract, + TestMintTokenERC20TransformerEvents, + TestUniswapV2FactoryContract, + TestUniswapV2FactoryPoolCreatedEventArgs, + TestUniswapV2PoolContract, + TestUniswapV3FactoryContract, + TestUniswapV3FactoryPoolCreatedEventArgs, + TestUniswapV3PoolContract, + TestWethContract, + TestWethEvents, + UniswapV3FeatureContract, } from '../wrappers'; +interface TransferEvent { + token: string; + from: string; + to: string; + value?: BigNumber; +} + +enum MultiplexSubcall { + Invalid, + Rfq, + Otc, + UniswapV2, + UniswapV3, + LiquidityProvider, + TransformERC20, + BatchSell, + MultiHopSell, +} + +interface MultiHopSellSubcall { + id: MultiplexSubcall; + data: string; +} + +interface BatchSellSubcall extends MultiHopSellSubcall { + sellAmount: BigNumber; +} + 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 BALANCER_WETH_DAI = '0x8b6e6e7b5b3801fed2cafd4b22b8a16c2f2db21a'; - const CURVE_BRIDGE_SOURCE_ID = encodeBridgeSourceId(BridgeProtocol.Curve, 'Curve'); - const BALANCER_BRIDGE_SOURCE_ID = encodeBridgeSourceId(BridgeProtocol.Bancor, 'Balancer'); - const fqtNonce = findTransformerNonce( - '0xfa6282736af206cb4cfc5cb786d82aecdf1186f9', - '0x39dce47a67ad34344eab877eae3ef1fa2a1d50bb', - ); - const payTakerNonce = findTransformerNonce( - '0x4638a7ebe75b911b995d0ec73a81e4f85f41f24e', - '0x39dce47a67ad34344eab877eae3ef1fa2a1d50bb', - ); +blockchainTests.resets('MultiplexFeature', env => { + const POOL_FEE = 1234; let zeroEx: IZeroExContract; let multiplex: MultiplexFeatureContract; - let rfqMaker: string; let flashWalletAddress: string; + let sandbox: LiquidityProviderSandboxContract; + let liquidityProvider: TestLiquidityProviderContract; + let sushiFactory: TestUniswapV2FactoryContract; + let uniV2Factory: TestUniswapV2FactoryContract; + let uniV3Factory: TestUniswapV3FactoryContract; + let dai: TestMintableERC20TokenContract; + let shib: TestMintableERC20TokenContract; + let zrx: TestMintableERC20TokenContract; + let weth: TestWethContract; + let owner: string; + let maker: string; + let taker: string; + let transformerNonce: number; + + //////////////// Deployment utility functions //////////////// + async function migrateOtcOrdersFeatureAsync(): Promise { + const featureImpl = await OtcOrdersFeatureContract.deployFrom0xArtifactAsync( + artifacts.OtcOrdersFeature, + env.provider, + env.txDefaults, + artifacts, + zeroEx.address, + weth.address, + ); + await new IOwnableFeatureContract(zeroEx.address, env.provider, env.txDefaults) + .migrate(featureImpl.address, featureImpl.migrate().getABIEncodedTransactionData(), owner) + .awaitTransactionSuccessAsync(); + } + + async function migrateLiquidityProviderContractsAsync(): Promise { + sandbox = await LiquidityProviderSandboxContract.deployFrom0xArtifactAsync( + artifacts.LiquidityProviderSandbox, + env.provider, + env.txDefaults, + artifacts, + zeroEx.address, + ); + liquidityProvider = await TestLiquidityProviderContract.deployFrom0xArtifactAsync( + artifacts.TestLiquidityProvider, + env.provider, + env.txDefaults, + artifacts, + ); + } + + async function migrateUniswapV2ContractsAsync(): Promise { + sushiFactory = await TestUniswapV2FactoryContract.deployFrom0xArtifactAsync( + artifacts.TestUniswapV2Factory, + env.provider, + env.txDefaults, + artifacts, + ); + uniV2Factory = await TestUniswapV2FactoryContract.deployFrom0xArtifactAsync( + artifacts.TestUniswapV2Factory, + env.provider, + env.txDefaults, + artifacts, + ); + } + + async function migrateUniswapV3ContractsAsync(): Promise { + uniV3Factory = await TestUniswapV3FactoryContract.deployFrom0xArtifactAsync( + artifacts.TestUniswapV3Factory, + env.provider, + env.txDefaults, + artifacts, + ); + const featureImpl = await UniswapV3FeatureContract.deployFrom0xArtifactAsync( + artifacts.TestUniswapV3Feature, + env.provider, + env.txDefaults, + artifacts, + weth.address, + uniV3Factory.address, + await uniV3Factory.POOL_INIT_CODE_HASH().callAsync(), + ); + await new IOwnableFeatureContract(zeroEx.address, env.provider, env.txDefaults) + .migrate(featureImpl.address, featureImpl.migrate().getABIEncodedTransactionData(), owner) + .awaitTransactionSuccessAsync(); + } + + //////////////// Miscellaneous utils //////////////// + + function isWethContract(t: TestMintableERC20TokenContract | TestWethContract): t is TestWethContract { + return !!(t as any).deposit; + } + + async function mintToAsync( + token: TestMintableERC20TokenContract | TestWethContract, + recipient: string, + amount: BigNumber, + ): Promise { + if (isWethContract(token)) { + await token.depositTo(recipient).awaitTransactionSuccessAsync({ value: amount }); + } else { + await token.mint(recipient, amount).awaitTransactionSuccessAsync(); + } + } + + //////////////// Deploy Uniswap pools //////////////// + + async function createUniswapV2PoolAsync( + factory: TestUniswapV2FactoryContract, + token0: TestMintableERC20TokenContract | TestWethContract, + token1: TestMintableERC20TokenContract | TestWethContract, + balance0: BigNumber = toBaseUnitAmount(10), + balance1: BigNumber = toBaseUnitAmount(10), + ): Promise { + const r = await factory.createPool(token0.address, token1.address).awaitTransactionSuccessAsync(); + const pool = new TestUniswapV2PoolContract( + (r.logs[0] as LogWithDecodedArgs).args.pool, + env.provider, + env.txDefaults, + ); + await mintToAsync(token0, pool.address, balance0); + await mintToAsync(token1, pool.address, balance1); + if (token0.address < token1.address) { + await pool.setReserves(balance0, balance1, constants.ZERO_AMOUNT).awaitTransactionSuccessAsync(); + } else { + await pool.setReserves(balance1, balance0, constants.ZERO_AMOUNT).awaitTransactionSuccessAsync(); + } + return pool; + } + + async function createUniswapV3PoolAsync( + token0: TestMintableERC20TokenContract | TestWethContract, + token1: TestMintableERC20TokenContract | TestWethContract, + balance0: BigNumber = toBaseUnitAmount(10), + balance1: BigNumber = toBaseUnitAmount(10), + ): Promise { + const r = await uniV3Factory + .createPool(token0.address, token1.address, new BigNumber(POOL_FEE)) + .awaitTransactionSuccessAsync(); + const pool = new TestUniswapV3PoolContract( + (r.logs[0] as LogWithDecodedArgs).args.pool, + env.provider, + env.txDefaults, + ); + await mintToAsync(token0, pool.address, balance0); + await mintToAsync(token1, pool.address, balance1); + return pool; + } + + //////////////// Generate subcalls //////////////// + + function getTestRfqOrder(overrides: Partial = {}): RfqOrder { + return getRandomRfqOrder({ + maker, + verifyingContract: zeroEx.address, + chainId: 1337, + takerToken: dai.address, + makerToken: zrx.address, + makerAmount: toBaseUnitAmount(1), + takerAmount: toBaseUnitAmount(1), + txOrigin: taker, + ...overrides, + }); + } + async function getRfqSubcallAsync( + rfqOrder: RfqOrder, + sellAmount: BigNumber = rfqOrder.takerAmount, + ): Promise { + const rfqDataEncoder = AbiEncoder.create([ + { name: 'order', type: 'tuple', components: RfqOrder.STRUCT_ABI }, + { name: 'signature', type: 'tuple', components: SIGNATURE_ABI }, + ]); + const makerToken = + rfqOrder.makerToken === weth.address + ? weth + : new TestMintableERC20TokenContract(rfqOrder.makerToken, env.provider, env.txDefaults); + await mintToAsync(makerToken, rfqOrder.maker, rfqOrder.makerAmount); + return { + id: MultiplexSubcall.Rfq, + sellAmount, + data: rfqDataEncoder.encode({ + order: rfqOrder, + signature: await rfqOrder.getSignatureWithProviderAsync(env.provider), + }), + }; + } + + function getTestOtcOrder(fields: Partial = {}): OtcOrder { + return getRandomOtcOrder({ + maker, + verifyingContract: zeroEx.address, + chainId: 1337, + takerToken: dai.address, + makerToken: zrx.address, + makerAmount: toBaseUnitAmount(1), + takerAmount: toBaseUnitAmount(1), + taker, + txOrigin: taker, + ...fields, + }); + } + async function getOtcSubcallAsync( + otcOrder: OtcOrder, + sellAmount: BigNumber = otcOrder.takerAmount, + ): Promise { + const otcDataEncoder = AbiEncoder.create([ + { name: 'order', type: 'tuple', components: OtcOrder.STRUCT_ABI }, + { name: 'signature', type: 'tuple', components: SIGNATURE_ABI }, + ]); + const makerToken = + otcOrder.makerToken === weth.address + ? weth + : new TestMintableERC20TokenContract(otcOrder.makerToken, env.provider, env.txDefaults); + await mintToAsync(makerToken, otcOrder.maker, otcOrder.makerAmount); + return { + id: MultiplexSubcall.Otc, + sellAmount, + data: otcDataEncoder.encode({ + order: otcOrder, + signature: await otcOrder.getSignatureWithProviderAsync(env.provider), + }), + }; + } + + function getUniswapV2MultiHopSubcall(tokens: string[], isSushi: boolean = false): MultiHopSellSubcall { + const uniswapDataEncoder = AbiEncoder.create([ + { name: 'tokens', type: 'address[]' }, + { name: 'isSushi', type: 'bool' }, + ]); + return { + id: MultiplexSubcall.UniswapV2, + data: uniswapDataEncoder.encode({ tokens, isSushi }), + }; + } + function getUniswapV2BatchSubcall( + tokens: string[], + sellAmount: BigNumber = getRandomInteger(1, toBaseUnitAmount(1)), + isSushi: boolean = false, + ): BatchSellSubcall { + return { + ...getUniswapV2MultiHopSubcall(tokens, isSushi), + sellAmount, + }; + } + + function getUniswapV3MultiHopSubcall( + tokens_: Array, + ): MultiHopSellSubcall { + const elems: string[] = []; + tokens_.forEach((t, i) => { + if (i) { + elems.push(hexUtils.leftPad(POOL_FEE, 3)); + } + elems.push(hexUtils.leftPad(t.address, 20)); + }); + const data = hexUtils.concat(...elems); + + return { + id: MultiplexSubcall.UniswapV3, + data, + }; + } + function getUniswapV3BatchSubcall( + tokens: Array, + sellAmount: BigNumber = getRandomInteger(1, toBaseUnitAmount(1)), + ): BatchSellSubcall { + return { + ...getUniswapV3MultiHopSubcall(tokens), + sellAmount, + }; + } + + function getLiquidityProviderMultiHopSubcall(): MultiHopSellSubcall { + const plpDataEncoder = AbiEncoder.create([ + { name: 'provider', type: 'address' }, + { name: 'auxiliaryData', type: 'bytes' }, + ]); + return { + id: MultiplexSubcall.LiquidityProvider, + data: plpDataEncoder.encode({ + provider: liquidityProvider.address, + auxiliaryData: constants.NULL_BYTES, + }), + }; + } + function getLiquidityProviderBatchSubcall( + sellAmount: BigNumber = getRandomInteger(1, toBaseUnitAmount(1)), + ): BatchSellSubcall { + return { + ...getLiquidityProviderMultiHopSubcall(), + sellAmount, + }; + } + + function getTransformERC20Subcall( + inputToken: string, + outputToken: string, + sellAmount: BigNumber = getRandomInteger(1, toBaseUnitAmount(1)), + mintAmount: BigNumber = getRandomInteger(1, toBaseUnitAmount(1)), + ): BatchSellSubcall { + const transformERC20Encoder = AbiEncoder.create([ + { + name: 'transformations', + type: 'tuple[]', + components: [ + { name: 'deploymentNonce', type: 'uint32' }, + { name: 'data', type: 'bytes' }, + ], + }, + ]); + const transformDataEncoder = AbiEncoder.create([ + { + name: 'data', + type: 'tuple', + components: [ + { name: 'inputToken', type: 'address' }, + { name: 'outputToken', type: 'address' }, + { name: 'burnAmount', type: 'uint256' }, + { name: 'mintAmount', type: 'uint256' }, + { name: 'feeAmount', type: 'uint256' }, + ], + }, + ]); + return { + id: MultiplexSubcall.TransformERC20, + sellAmount, + data: transformERC20Encoder.encode({ + transformations: [ + { + deploymentNonce: transformerNonce, + data: transformDataEncoder.encode([ + { + inputToken, + outputToken, + burnAmount: constants.ZERO_AMOUNT, + mintAmount, + feeAmount: constants.ZERO_AMOUNT, + }, + ]), + }, + ], + }), + }; + } + + function getNestedBatchSellSubcall(calls: BatchSellSubcall[]): MultiHopSellSubcall { + const batchSellDataEncoder = AbiEncoder.create([ + { + name: 'calls', + type: 'tuple[]', + components: [ + { name: 'id', type: 'uint8' }, + { name: 'sellAmount', type: 'uint256' }, + { name: 'data', type: 'bytes' }, + ], + }, + ]); + return { + id: MultiplexSubcall.BatchSell, + data: batchSellDataEncoder.encode({ calls }), + }; + } + + function getNestedMultiHopSellSubcall( + tokens: string[], + calls: MultiHopSellSubcall[], + sellAmount: BigNumber = getRandomInteger(1, toBaseUnitAmount(1)), + ): BatchSellSubcall { + const multiHopSellDataEncoder = AbiEncoder.create([ + { + name: 'tokens', + type: 'address[]', + }, + { + name: 'calls', + type: 'tuple[]', + components: [ + { name: 'id', type: 'uint8' }, + { name: 'data', type: 'bytes' }, + ], + }, + ]); + return { + id: MultiplexSubcall.MultiHopSell, + sellAmount, + data: multiHopSellDataEncoder.encode({ tokens, calls }), + }; + } before(async () => { - const erc20Abis = _.mapValues(erc20Artifacts, v => v.compilerOutput.abi); - [rfqMaker] = await env.getAccountAddressesAsync(); - zeroEx = new IZeroExContract('0xdef1c0ded9bec7f1a1670819833240f027b25eff', env.provider, env.txDefaults, { - ...abis, - ...erc20Abis, - }); + [owner, maker, taker] = await env.getAccountAddressesAsync(); + zeroEx = await fullMigrateAsync(owner, env.provider, env.txDefaults, {}); 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( + + [dai, shib, zrx] = await Promise.all( + [...new Array(3)].map(async () => + TestMintableERC20TokenContract.deployFrom0xArtifactAsync( + artifacts.TestMintableERC20Token, + env.provider, + env.txDefaults, + artifacts, + ), + ), + ); + weth = await TestWethContract.deployFrom0xArtifactAsync( + artifacts.TestWeth, + env.provider, + env.txDefaults, + artifacts, + ); + + await Promise.all([ + ...[dai, shib, zrx, weth].map(t => + t.approve(zeroEx.address, constants.MAX_UINT256).awaitTransactionSuccessAsync({ from: taker }), + ), + ...[dai, shib, zrx, weth].map(t => + t.approve(zeroEx.address, constants.MAX_UINT256).awaitTransactionSuccessAsync({ from: maker }), + ), + ]); + await migrateOtcOrdersFeatureAsync(); + await migrateLiquidityProviderContractsAsync(); + await migrateUniswapV2ContractsAsync(); + await migrateUniswapV3ContractsAsync(); + transformerNonce = await env.web3Wrapper.getAccountNonceAsync(owner); + await TestMintTokenERC20TransformerContract.deployFrom0xArtifactAsync( + artifacts.TestMintTokenERC20Transformer, + env.provider, + env.txDefaults, + artifacts, + ); + + const featureImpl = await MultiplexFeatureContract.deployFrom0xArtifactAsync( artifacts.MultiplexFeature, env.provider, env.txDefaults, artifacts, zeroEx.address, - WETH_ADDRESS, - PLP_SANDBOX_ADDRESS, + weth.address, + sandbox.address, + uniV2Factory.address, + sushiFactory.address, + await uniV2Factory.POOL_INIT_CODE_HASH().callAsync(), + await sushiFactory.POOL_INIT_CODE_HASH().callAsync(), ); - 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 }); + await new IOwnableFeatureContract(zeroEx.address, env.provider, env.txDefaults) + .migrate(featureImpl.address, featureImpl.migrate().getABIEncodedTransactionData(), owner) + .awaitTransactionSuccessAsync(); + multiplex = new MultiplexFeatureContract(zeroEx.address, env.provider, env.txDefaults, abis); }); - 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), - }), - }; + describe('batch sells', () => { + describe('multiplexBatchSellTokenForToken', () => { + it('reverts if minBuyAmount is not satisfied', async () => { + const order = getTestRfqOrder(); + const rfqSubcall = await getRfqSubcallAsync(order); + await mintToAsync(dai, taker, rfqSubcall.sellAmount); - 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 tx = multiplex + .multiplexBatchSellTokenForToken( + dai.address, + zrx.address, + [rfqSubcall], + order.takerAmount, + order.makerAmount.plus(1), + ) + .awaitTransactionSuccessAsync({ from: taker }); + return expect(tx).to.revertWith('MultiplexFeature::_multiplexBatchSell/UNDERBOUGHT'); }); - 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: BALANCER_BRIDGE_SOURCE_ID, - takerTokenAmount: expiredRfqCall.sellAmount, - makerTokenAmount: expiredRfqCall.sellAmount, - bridgeData: poolEncoder.encode([BALANCER_WETH_DAI]), - }, - ], - limitOrders: [], - rfqOrders: [], - fillSequence: [FillQuoteTransformerOrderType.Bridge], - fillAmount: expiredRfqCall.sellAmount, - refundReceiver: constants.NULL_ADDRESS, + it('reverts if given an invalid subcall type', async () => { + const invalidSubcall: BatchSellSubcall = { + id: MultiplexSubcall.Invalid, + sellAmount: toBaseUnitAmount(1), + data: constants.NULL_BYTES, + }; + const tx = multiplex + .multiplexBatchSellTokenForToken( + dai.address, + zrx.address, + [invalidSubcall], + invalidSubcall.sellAmount, + constants.ZERO_AMOUNT, + ) + .awaitTransactionSuccessAsync({ from: taker }); + return expect(tx).to.revertWith('MultiplexFeature::_executeBatchSell/INVALID_SUBCALL'); }); - const payTakerData = encodePayTakerTransformerData({ - tokens: [WETH_ADDRESS], - amounts: [constants.MAX_UINT256], + it('reverts if the full sell amount is not sold', async () => { + const order = getTestRfqOrder(); + const rfqSubcall = await getRfqSubcallAsync(order); + await mintToAsync(dai, taker, rfqSubcall.sellAmount); + + const tx = multiplex + .multiplexBatchSellTokenForToken( + dai.address, + zrx.address, + [rfqSubcall], + order.takerAmount.plus(1), + order.makerAmount, + ) + .awaitTransactionSuccessAsync({ from: taker }); + return expect(tx).to.revertWith('MultiplexFeature::_executeBatchSell/INCORRECT_AMOUNT_SOLD'); }); - 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: [ + it('RFQ, fallback(UniswapV2)', async () => { + const order = getTestRfqOrder(); + const rfqSubcall = await getRfqSubcallAsync(order); + await createUniswapV2PoolAsync(uniV2Factory, dai, zrx); + await mintToAsync(dai, taker, rfqSubcall.sellAmount); + + const tx = await multiplex + .multiplexBatchSellTokenForToken( + dai.address, + zrx.address, + [rfqSubcall, getUniswapV2BatchSubcall([dai.address, zrx.address], order.takerAmount)], + order.takerAmount, + constants.ZERO_AMOUNT, + ) + .awaitTransactionSuccessAsync({ from: taker }); + verifyEventsFromLogs( + tx.logs, + [ { - deploymentNonce: fqtNonce, - data: fqtData, - }, - { - deploymentNonce: payTakerNonce, - data: payTakerData, + orderHash: order.getHash(), + maker, + taker, + makerToken: order.makerToken, + takerToken: order.takerToken, + takerTokenFilledAmount: order.takerAmount, + makerTokenFilledAmount: order.makerAmount, + pool: order.pool, }, ], - ethValue: constants.ZERO_AMOUNT, - }), - }; + IZeroExEvents.RfqOrderFilled, + ); + }); + it('OTC, fallback(UniswapV2)', async () => { + const order = getTestOtcOrder(); + const otcSubcall = await getOtcSubcallAsync(order); + await createUniswapV2PoolAsync(uniV2Factory, dai, zrx); + await mintToAsync(dai, taker, otcSubcall.sellAmount); - 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(BALANCER_BRIDGE_SOURCE_ID); - 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); + const tx = await multiplex + .multiplexBatchSellTokenForToken( + dai.address, + zrx.address, + [otcSubcall, getUniswapV2BatchSubcall([dai.address, zrx.address], order.takerAmount)], + order.takerAmount, + constants.ZERO_AMOUNT, + ) + .awaitTransactionSuccessAsync({ from: taker }); + verifyEventsFromLogs( + tx.logs, + [ + { + orderHash: order.getHash(), + maker, + taker, + makerToken: order.makerToken, + takerToken: order.takerToken, + takerTokenFilledAmount: order.takerAmount, + makerTokenFilledAmount: order.makerAmount, + }, + ], + IZeroExEvents.OtcOrderFilled, + ); + }); + it('expired RFQ, fallback(UniswapV2)', async () => { + const order = getTestRfqOrder({ expiry: constants.ZERO_AMOUNT }); + const rfqSubcall = await getRfqSubcallAsync(order); + const uniswap = await createUniswapV2PoolAsync(uniV2Factory, dai, zrx); + await mintToAsync(dai, taker, rfqSubcall.sellAmount); + + const tx = await multiplex + .multiplexBatchSellTokenForToken( + dai.address, + zrx.address, + [rfqSubcall, getUniswapV2BatchSubcall([dai.address, zrx.address], order.takerAmount)], + order.takerAmount, + constants.ZERO_AMOUNT, + ) + .awaitTransactionSuccessAsync({ from: taker }); + verifyEventsFromLogs( + tx.logs, + [ + { + orderHash: order.getHash(), + maker, + expiry: order.expiry, + }, + ], + MultiplexFeatureEvents.ExpiredRfqOrder, + ); + verifyEventsFromLogs( + tx.logs, + [ + { + token: dai.address, + from: taker, + to: uniswap.address, + value: order.takerAmount, + }, + { + token: zrx.address, + from: uniswap.address, + to: taker, + }, + ], + TestMintableERC20TokenEvents.Transfer, + ); + }); + it('expired OTC, fallback(UniswapV2)', async () => { + const order = getTestOtcOrder({ expiry: constants.ZERO_AMOUNT }); + const otcSubcall = await getOtcSubcallAsync(order); + const uniswap = await createUniswapV2PoolAsync(uniV2Factory, dai, zrx); + await mintToAsync(dai, taker, otcSubcall.sellAmount); + + const tx = await multiplex + .multiplexBatchSellTokenForToken( + dai.address, + zrx.address, + [otcSubcall, getUniswapV2BatchSubcall([dai.address, zrx.address], order.takerAmount)], + order.takerAmount, + constants.ZERO_AMOUNT, + ) + .awaitTransactionSuccessAsync({ from: taker }); + verifyEventsFromLogs( + tx.logs, + [ + { + orderHash: order.getHash(), + maker, + expiry: order.expiry, + }, + ], + MultiplexFeatureEvents.ExpiredOtcOrder, + ); + verifyEventsFromLogs( + tx.logs, + [ + { + token: dai.address, + from: taker, + to: uniswap.address, + value: order.takerAmount, + }, + { + token: zrx.address, + from: uniswap.address, + to: taker, + }, + ], + TestMintableERC20TokenEvents.Transfer, + ); + }); + it('expired RFQ, fallback(TransformERC20)', async () => { + const order = getTestRfqOrder({ expiry: constants.ZERO_AMOUNT }); + const rfqSubcall = await getRfqSubcallAsync(order); + const transformERC20Subcall = getTransformERC20Subcall(dai.address, zrx.address, order.takerAmount); + await mintToAsync(dai, taker, order.takerAmount); + + const tx = await multiplex + .multiplexBatchSellTokenForToken( + dai.address, + zrx.address, + [rfqSubcall, transformERC20Subcall], + order.takerAmount, + constants.ZERO_AMOUNT, + ) + .awaitTransactionSuccessAsync({ from: taker }); + verifyEventsFromLogs( + tx.logs, + [ + { + orderHash: order.getHash(), + maker, + expiry: order.expiry, + }, + ], + MultiplexFeatureEvents.ExpiredRfqOrder, + ); + verifyEventsFromLogs( + tx.logs, + [ + { + token: dai.address, + from: taker, + to: flashWalletAddress, + value: order.takerAmount, + }, + { + token: dai.address, + from: flashWalletAddress, + to: constants.NULL_ADDRESS, + }, + { + token: zrx.address, + from: flashWalletAddress, + to: taker, + }, + ], + TestMintableERC20TokenEvents.Transfer, + ); + verifyEventsFromLogs( + tx.logs, + [ + { + caller: zeroEx.address, + sender: zeroEx.address, + taker, + inputTokenBalance: order.takerAmount, + }, + ], + TestMintTokenERC20TransformerEvents.MintTransform, + ); + }); + it('LiquidityProvider, UniV3, Sushiswap', async () => { + const sushiswap = await createUniswapV2PoolAsync(sushiFactory, dai, zrx); + const uniV3 = await createUniswapV3PoolAsync(dai, zrx); + const liquidityProviderSubcall = getLiquidityProviderBatchSubcall(); + const uniV3Subcall = getUniswapV3BatchSubcall([dai, zrx]); + const sushiswapSubcall = getUniswapV2BatchSubcall([dai.address, zrx.address], undefined, true); + const sellAmount = BigNumber.sum( + ...[liquidityProviderSubcall, uniV3Subcall, sushiswapSubcall].map(c => c.sellAmount), + ).minus(1); + await mintToAsync(dai, taker, sellAmount); + const tx = await multiplex + .multiplexBatchSellTokenForToken( + dai.address, + zrx.address, + [liquidityProviderSubcall, uniV3Subcall, sushiswapSubcall], + sellAmount, + constants.ZERO_AMOUNT, + ) + .awaitTransactionSuccessAsync({ from: taker }); + verifyEventsFromLogs( + tx.logs, + [ + { + token: dai.address, + from: taker, + to: liquidityProvider.address, + value: liquidityProviderSubcall.sellAmount, + }, + { + token: zrx.address, + from: liquidityProvider.address, + to: taker, + }, + { + token: zrx.address, + from: uniV3.address, + to: taker, + }, + { + token: dai.address, + from: taker, + to: uniV3.address, + value: uniV3Subcall.sellAmount, + }, + { + token: dai.address, + from: taker, + to: sushiswap.address, + value: sushiswapSubcall.sellAmount.minus(1), + }, + { + token: zrx.address, + from: sushiswap.address, + to: taker, + }, + ], + TestMintableERC20TokenEvents.Transfer, + ); + }); + it('proportional fill amounts', async () => { + const order = getTestRfqOrder(); + const uniswap = await createUniswapV2PoolAsync(uniV2Factory, dai, zrx); + const sellAmount = toBaseUnitAmount(1); + await mintToAsync(dai, taker, sellAmount); + + const rfqFillProportion = 0.42; + const rfqSubcall = await getRfqSubcallAsync(order, encodeFractionalFillAmount(rfqFillProportion)); + // fractional fill amount 100% => the rest of the total sell amount is sold to Uniswap + const uniswapV2Subcall = getUniswapV2BatchSubcall( + [dai.address, zrx.address], + encodeFractionalFillAmount(1), + ); + const tx = await multiplex + .multiplexBatchSellTokenForToken( + dai.address, + zrx.address, + [rfqSubcall, uniswapV2Subcall], + sellAmount, + constants.ZERO_AMOUNT, + ) + .awaitTransactionSuccessAsync({ from: taker }); + verifyEventsFromLogs( + tx.logs, + [ + { + token: dai.address, + from: taker, + to: order.maker, + value: sellAmount.times(rfqFillProportion), + }, + { + token: zrx.address, + from: order.maker, + to: taker, + }, + { + token: dai.address, + from: taker, + to: uniswap.address, + value: sellAmount.minus(sellAmount.times(rfqFillProportion)), + }, + { + token: zrx.address, + from: uniswap.address, + to: taker, + }, + ], + TestMintableERC20TokenEvents.Transfer, + ); + }); + it('RFQ, MultiHop(UniV3, UniV2)', async () => { + const order = getTestRfqOrder(); + const rfqSubcall = await getRfqSubcallAsync(order); + const uniV3 = await createUniswapV3PoolAsync(dai, shib); + const uniV3Subcall = getUniswapV3MultiHopSubcall([dai, shib]); + const uniV2 = await createUniswapV2PoolAsync(uniV2Factory, shib, zrx); + const uniV2Subcall = getUniswapV2MultiHopSubcall([shib.address, zrx.address]); + const nestedMultiHopSubcall = getNestedMultiHopSellSubcall( + [dai.address, shib.address, zrx.address], + [uniV3Subcall, uniV2Subcall], + ); + const sellAmount = rfqSubcall.sellAmount.plus(nestedMultiHopSubcall.sellAmount); + await mintToAsync(dai, taker, sellAmount); + + const tx = await multiplex + .multiplexBatchSellTokenForToken( + dai.address, + zrx.address, + [rfqSubcall, nestedMultiHopSubcall], + sellAmount, + constants.ZERO_AMOUNT, + ) + .awaitTransactionSuccessAsync({ from: taker }); + verifyEventsFromLogs( + tx.logs, + [ + { + token: dai.address, + from: taker, + to: order.maker, + value: order.takerAmount, + }, + { + token: zrx.address, + from: order.maker, + to: taker, + value: order.makerAmount, + }, + { + token: shib.address, + from: uniV3.address, + to: uniV2.address, + }, + { + token: dai.address, + from: taker, + to: uniV3.address, + value: nestedMultiHopSubcall.sellAmount, + }, + { + token: zrx.address, + from: uniV2.address, + to: taker, + }, + ], + TestMintableERC20TokenEvents.Transfer, + ); + }); }); - 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); + describe('multiplexBatchSellEthForToken', () => { + it('RFQ', async () => { + const order = getTestRfqOrder({ takerToken: weth.address }); + const rfqSubcall = await getRfqSubcallAsync(order); - 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 tx = await multiplex + .multiplexBatchSellEthForToken(zrx.address, [rfqSubcall], constants.ZERO_AMOUNT) + .awaitTransactionSuccessAsync({ from: taker, value: order.takerAmount }); + verifyEventsFromLogs( + tx.logs, + [{ owner: zeroEx.address, value: order.takerAmount }], + TestWethEvents.Deposit, + ); + verifyEventsFromLogs( + tx.logs, + [ + { + token: weth.address, + from: zeroEx.address, + to: order.maker, + value: order.takerAmount, + }, + { + token: zrx.address, + from: order.maker, + to: taker, + value: order.makerAmount, + }, + ], + TestMintableERC20TokenEvents.Transfer, + ); + }); + it('OTC', async () => { + const order = getTestOtcOrder({ takerToken: weth.address }); + const otcSubcall = await getOtcSubcallAsync(order); - 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); + const tx = await multiplex + .multiplexBatchSellEthForToken(zrx.address, [otcSubcall], constants.ZERO_AMOUNT) + .awaitTransactionSuccessAsync({ from: taker, value: order.takerAmount }); + verifyEventsFromLogs( + tx.logs, + [{ owner: zeroEx.address, value: order.takerAmount }], + TestWethEvents.Deposit, + ); + verifyEventsFromLogs( + tx.logs, + [ + { + token: weth.address, + from: zeroEx.address, + to: order.maker, + value: order.takerAmount, + }, + { + token: zrx.address, + from: order.maker, + to: taker, + value: order.makerAmount, + }, + ], + TestMintableERC20TokenEvents.Transfer, + ); + }); + it('UniswapV2', async () => { + const uniswap = await createUniswapV2PoolAsync(uniV2Factory, weth, zrx); + const uniswapV2Subcall = getUniswapV2BatchSubcall([weth.address, zrx.address]); + + const tx = await multiplex + .multiplexBatchSellEthForToken(zrx.address, [uniswapV2Subcall], constants.ZERO_AMOUNT) + .awaitTransactionSuccessAsync({ from: taker, value: uniswapV2Subcall.sellAmount }); + verifyEventsFromLogs( + tx.logs, + [{ owner: zeroEx.address, value: uniswapV2Subcall.sellAmount }], + TestWethEvents.Deposit, + ); + verifyEventsFromLogs( + tx.logs, + [ + { + token: weth.address, + from: zeroEx.address, + to: uniswap.address, + value: uniswapV2Subcall.sellAmount, + }, + { + token: zrx.address, + from: uniswap.address, + to: taker, + }, + ], + TestMintableERC20TokenEvents.Transfer, + ); + }); + it('UniswapV3', async () => { + const uniV3 = await createUniswapV3PoolAsync(weth, zrx); + const uniswapV3Subcall = getUniswapV3BatchSubcall([weth, zrx]); + const tx = await multiplex + .multiplexBatchSellEthForToken(zrx.address, [uniswapV3Subcall], constants.ZERO_AMOUNT) + .awaitTransactionSuccessAsync({ from: taker, value: uniswapV3Subcall.sellAmount }); + verifyEventsFromLogs( + tx.logs, + [{ owner: zeroEx.address, value: uniswapV3Subcall.sellAmount }], + TestWethEvents.Deposit, + ); + verifyEventsFromLogs( + tx.logs, + [ + { + token: zrx.address, + from: uniV3.address, + to: taker, + }, + { + token: weth.address, + from: zeroEx.address, + to: uniV3.address, + value: uniswapV3Subcall.sellAmount, + }, + ], + TestMintableERC20TokenEvents.Transfer, + ); + }); + it('LiquidityProvider', async () => { + const liquidityProviderSubcall = getLiquidityProviderBatchSubcall(); + const tx = await multiplex + .multiplexBatchSellEthForToken(zrx.address, [liquidityProviderSubcall], constants.ZERO_AMOUNT) + .awaitTransactionSuccessAsync({ from: taker, value: liquidityProviderSubcall.sellAmount }); + verifyEventsFromLogs( + tx.logs, + [{ owner: zeroEx.address, value: liquidityProviderSubcall.sellAmount }], + TestWethEvents.Deposit, + ); + verifyEventsFromLogs( + tx.logs, + [ + { + token: weth.address, + from: zeroEx.address, + to: liquidityProvider.address, + value: liquidityProviderSubcall.sellAmount, + }, + { + token: zrx.address, + from: liquidityProvider.address, + to: taker, + }, + ], + TestMintableERC20TokenEvents.Transfer, + ); + }); + it('TransformERC20', async () => { + const transformERC20Subcall = getTransformERC20Subcall(weth.address, zrx.address); + const tx = await multiplex + .multiplexBatchSellEthForToken(zrx.address, [transformERC20Subcall], constants.ZERO_AMOUNT) + .awaitTransactionSuccessAsync({ from: taker, value: transformERC20Subcall.sellAmount }); + verifyEventsFromLogs( + tx.logs, + [{ owner: zeroEx.address, value: transformERC20Subcall.sellAmount }], + TestWethEvents.Deposit, + ); + verifyEventsFromLogs( + tx.logs, + [ + { + token: weth.address, + from: zeroEx.address, + to: flashWalletAddress, + value: transformERC20Subcall.sellAmount, + }, + { + token: weth.address, + from: flashWalletAddress, + to: constants.NULL_ADDRESS, + }, + { + token: zrx.address, + from: flashWalletAddress, + to: taker, + }, + ], + TestMintableERC20TokenEvents.Transfer, + ); + }); + it('RFQ, MultiHop(UniV3, UniV2)', async () => { + const order = getTestRfqOrder({ takerToken: weth.address, makerToken: zrx.address }); + const rfqSubcall = await getRfqSubcallAsync(order); + const uniV3 = await createUniswapV3PoolAsync(weth, shib); + const uniV3Subcall = getUniswapV3MultiHopSubcall([weth, shib]); + const uniV2 = await createUniswapV2PoolAsync(uniV2Factory, shib, zrx); + const uniV2Subcall = getUniswapV2MultiHopSubcall([shib.address, zrx.address]); + const nestedMultiHopSubcall = getNestedMultiHopSellSubcall( + [weth.address, shib.address, zrx.address], + [uniV3Subcall, uniV2Subcall], + ); + const sellAmount = rfqSubcall.sellAmount.plus(nestedMultiHopSubcall.sellAmount); + + const tx = await multiplex + .multiplexBatchSellEthForToken( + zrx.address, + [rfqSubcall, nestedMultiHopSubcall], + constants.ZERO_AMOUNT, + ) + .awaitTransactionSuccessAsync({ from: taker, value: sellAmount }); + verifyEventsFromLogs(tx.logs, [{ owner: zeroEx.address, value: sellAmount }], TestWethEvents.Deposit); + verifyEventsFromLogs( + tx.logs, + [ + { + token: weth.address, + from: zeroEx.address, + to: order.maker, + value: order.takerAmount, + }, + { + token: zrx.address, + from: order.maker, + to: taker, + value: order.makerAmount, + }, + { + token: shib.address, + from: uniV3.address, + to: uniV2.address, + }, + { + token: weth.address, + from: zeroEx.address, + to: uniV3.address, + value: nestedMultiHopSubcall.sellAmount, + }, + { + token: zrx.address, + from: uniV2.address, + to: taker, + }, + ], + TestMintableERC20TokenEvents.Transfer, + ); + }); + }); + describe('multiplexBatchSellTokenForEth', () => { + it('RFQ', async () => { + const order = getTestRfqOrder({ makerToken: weth.address }); + const rfqSubcall = await getRfqSubcallAsync(order); + await mintToAsync(dai, taker, order.takerAmount); + const tx = await multiplex + .multiplexBatchSellTokenForEth(dai.address, [rfqSubcall], order.takerAmount, constants.ZERO_AMOUNT) + .awaitTransactionSuccessAsync({ from: taker }); + verifyEventsFromLogs(tx.logs, [{ owner: zeroEx.address }], TestWethEvents.Withdrawal); + verifyEventsFromLogs( + tx.logs, + [ + { + token: dai.address, + from: taker, + to: order.maker, + value: order.takerAmount, + }, + { + token: weth.address, + from: order.maker, + to: zeroEx.address, + value: order.makerAmount, + }, + ], + TestMintableERC20TokenEvents.Transfer, + ); + }); + it('OTC', async () => { + const order = getTestOtcOrder({ makerToken: weth.address }); + const otcSubcall = await getOtcSubcallAsync(order); + await mintToAsync(dai, taker, order.takerAmount); + const tx = await multiplex + .multiplexBatchSellTokenForEth(dai.address, [otcSubcall], order.takerAmount, constants.ZERO_AMOUNT) + .awaitTransactionSuccessAsync({ from: taker }); + verifyEventsFromLogs(tx.logs, [{ owner: zeroEx.address }], TestWethEvents.Withdrawal); + verifyEventsFromLogs( + tx.logs, + [ + { + token: dai.address, + from: taker, + to: order.maker, + value: order.takerAmount, + }, + { + token: weth.address, + from: order.maker, + to: zeroEx.address, + value: order.makerAmount, + }, + ], + TestMintableERC20TokenEvents.Transfer, + ); + }); + it('UniswapV2', async () => { + const uniswapV2Subcall = getUniswapV2BatchSubcall([dai.address, weth.address]); + const uniswap = await createUniswapV2PoolAsync(uniV2Factory, dai, weth); + await mintToAsync(dai, taker, uniswapV2Subcall.sellAmount); + + const tx = await multiplex + .multiplexBatchSellTokenForEth( + dai.address, + [uniswapV2Subcall], + uniswapV2Subcall.sellAmount, + constants.ZERO_AMOUNT, + ) + .awaitTransactionSuccessAsync({ from: taker }); + verifyEventsFromLogs(tx.logs, [{ owner: zeroEx.address }], TestWethEvents.Withdrawal); + verifyEventsFromLogs( + tx.logs, + [ + { + token: dai.address, + from: taker, + to: uniswap.address, + value: uniswapV2Subcall.sellAmount, + }, + { + token: weth.address, + from: uniswap.address, + to: zeroEx.address, + }, + ], + TestMintableERC20TokenEvents.Transfer, + ); + }); + it('UniswapV3', async () => { + const uniswapV3Subcall = getUniswapV3BatchSubcall([dai, weth]); + const uniV3 = await createUniswapV3PoolAsync(dai, weth); + await mintToAsync(dai, taker, uniswapV3Subcall.sellAmount); + + const tx = await multiplex + .multiplexBatchSellTokenForEth( + dai.address, + [uniswapV3Subcall], + uniswapV3Subcall.sellAmount, + constants.ZERO_AMOUNT, + ) + .awaitTransactionSuccessAsync({ from: taker }); + verifyEventsFromLogs(tx.logs, [{ owner: zeroEx.address }], TestWethEvents.Withdrawal); + verifyEventsFromLogs( + tx.logs, + [ + { + token: weth.address, + from: uniV3.address, + to: zeroEx.address, + }, + { + token: dai.address, + from: taker, + to: uniV3.address, + value: uniswapV3Subcall.sellAmount, + }, + ], + TestMintableERC20TokenEvents.Transfer, + ); + }); + it('LiquidityProvider', async () => { + const liquidityProviderSubcall = getLiquidityProviderBatchSubcall(); + await mintToAsync(dai, taker, liquidityProviderSubcall.sellAmount); + + const tx = await multiplex + .multiplexBatchSellTokenForEth( + dai.address, + [liquidityProviderSubcall], + liquidityProviderSubcall.sellAmount, + constants.ZERO_AMOUNT, + ) + .awaitTransactionSuccessAsync({ from: taker }); + verifyEventsFromLogs(tx.logs, [{ owner: zeroEx.address }], TestWethEvents.Withdrawal); + verifyEventsFromLogs( + tx.logs, + [ + { + token: dai.address, + from: taker, + to: liquidityProvider.address, + value: liquidityProviderSubcall.sellAmount, + }, + { + token: weth.address, + from: liquidityProvider.address, + to: zeroEx.address, + }, + ], + TestMintableERC20TokenEvents.Transfer, + ); + }); + it('TransformERC20', async () => { + const transformERC20Subcall = getTransformERC20Subcall( + dai.address, + weth.address, + undefined, + constants.ZERO_AMOUNT, + ); + await mintToAsync(dai, taker, transformERC20Subcall.sellAmount); + + const tx = await multiplex + .multiplexBatchSellTokenForEth( + dai.address, + [transformERC20Subcall], + transformERC20Subcall.sellAmount, + constants.ZERO_AMOUNT, + ) + .awaitTransactionSuccessAsync({ from: taker }); + verifyEventsFromLogs(tx.logs, [{ owner: zeroEx.address }], TestWethEvents.Withdrawal); + verifyEventsFromLogs( + tx.logs, + [ + { + token: dai.address, + from: taker, + to: flashWalletAddress, + value: transformERC20Subcall.sellAmount, + }, + { + token: dai.address, + from: flashWalletAddress, + to: constants.NULL_ADDRESS, + }, + { + token: weth.address, + from: flashWalletAddress, + to: zeroEx.address, + }, + ], + TestMintableERC20TokenEvents.Transfer, + ); + }); + it('RFQ, MultiHop(UniV3, UniV2)', async () => { + const order = getTestRfqOrder({ takerToken: dai.address, makerToken: weth.address }); + const rfqSubcall = await getRfqSubcallAsync(order); + const uniV3 = await createUniswapV3PoolAsync(dai, shib); + const uniV3Subcall = getUniswapV3MultiHopSubcall([dai, shib]); + const uniV2 = await createUniswapV2PoolAsync(uniV2Factory, shib, weth); + const uniV2Subcall = getUniswapV2MultiHopSubcall([shib.address, weth.address]); + const nestedMultiHopSubcall = getNestedMultiHopSellSubcall( + [dai.address, shib.address, weth.address], + [uniV3Subcall, uniV2Subcall], + ); + const sellAmount = rfqSubcall.sellAmount.plus(nestedMultiHopSubcall.sellAmount); + await mintToAsync(dai, taker, sellAmount); + + const tx = await multiplex + .multiplexBatchSellTokenForEth( + dai.address, + [rfqSubcall, nestedMultiHopSubcall], + sellAmount, + constants.ZERO_AMOUNT, + ) + .awaitTransactionSuccessAsync({ from: taker }); + verifyEventsFromLogs( + tx.logs, + [ + { + token: dai.address, + from: taker, + to: order.maker, + value: order.takerAmount, + }, + { + token: weth.address, + from: order.maker, + to: zeroEx.address, + value: order.makerAmount, + }, + { + token: shib.address, + from: uniV3.address, + to: uniV2.address, + }, + { + token: dai.address, + from: taker, + to: uniV3.address, + value: nestedMultiHopSubcall.sellAmount, + }, + { + token: weth.address, + from: uniV2.address, + to: zeroEx.address, + }, + ], + TestMintableERC20TokenEvents.Transfer, + ); + verifyEventsFromLogs(tx.logs, [{ owner: zeroEx.address }], TestWethEvents.Withdrawal); + }); }); }); - 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; + describe('multihop sells', () => { + describe('multiplexMultiHopSellTokenForToken', () => { + it('reverts if given an invalid subcall type', async () => { + const invalidSubcall: MultiHopSellSubcall = { + id: MultiplexSubcall.Invalid, + data: constants.NULL_BYTES, + }; + const tx = multiplex + .multiplexMultiHopSellTokenForToken( + [dai.address, zrx.address], + [invalidSubcall], + toBaseUnitAmount(1), + constants.ZERO_AMOUNT, + ) + .awaitTransactionSuccessAsync({ from: taker }); + return expect(tx).to.revertWith('MultiplexFeature::_computeHopTarget/INVALID_SUBCALL'); + }); + it('reverts if minBuyAmount is not satisfied', async () => { + const sellAmount = getRandomInteger(1, toBaseUnitAmount(1)); + await createUniswapV2PoolAsync(uniV2Factory, dai, zrx); + const uniswapV2Subcall = getUniswapV2MultiHopSubcall([dai.address, zrx.address]); + await mintToAsync(dai, taker, sellAmount); - 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: CURVE_BRIDGE_SOURCE_ID, - 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 tx = multiplex + .multiplexMultiHopSellTokenForToken( + [dai.address, zrx.address], + [uniswapV2Subcall], + sellAmount, + constants.MAX_UINT256, + ) + .awaitTransactionSuccessAsync({ from: taker }); + return expect(tx).to.revertWith('MultiplexFeature::_multiplexMultiHopSell/UNDERBOUGHT'); }); - const payTakerData = encodePayTakerTransformerData({ - tokens: [USDC_ADDRESS], - amounts: [constants.MAX_UINT256], + it('reverts if array lengths are mismatched', async () => { + const sellAmount = getRandomInteger(1, toBaseUnitAmount(1)); + await createUniswapV2PoolAsync(uniV2Factory, dai, zrx); + const uniswapV2Subcall = getUniswapV2MultiHopSubcall([dai.address, zrx.address]); + await mintToAsync(dai, taker, sellAmount); + + const tx = multiplex + .multiplexMultiHopSellTokenForToken( + [dai.address, zrx.address], + [uniswapV2Subcall, uniswapV2Subcall], + sellAmount, + constants.ZERO_AMOUNT, + ) + .awaitTransactionSuccessAsync({ from: taker }); + return expect(tx).to.revertWith('MultiplexFeature::_multiplexMultiHopSell/MISMATCHED_ARRAY_LENGTHS'); }); - const curveFqtCall = { - selector: zeroEx.getSelector('_transformERC20'), - sellAmount, - data: transformERC20Encoder.encode({ - transformations: [ + it('UniswapV2 -> LiquidityProvider', async () => { + const sellAmount = getRandomInteger(1, toBaseUnitAmount(1)); + const buyAmount = getRandomInteger(1, toBaseUnitAmount(1)); + const uniswap = await createUniswapV2PoolAsync(uniV2Factory, dai, shib); + const uniswapV2Subcall = getUniswapV2MultiHopSubcall([dai.address, shib.address]); + const liquidityProviderSubcall = getLiquidityProviderMultiHopSubcall(); + await mintToAsync(dai, taker, sellAmount); + await mintToAsync(zrx, liquidityProvider.address, buyAmount); + + const tx = await multiplex + .multiplexMultiHopSellTokenForToken( + [dai.address, shib.address, zrx.address], + [uniswapV2Subcall, liquidityProviderSubcall], + sellAmount, + buyAmount, + ) + .awaitTransactionSuccessAsync({ from: taker }); + verifyEventsFromLogs( + tx.logs, + [ { - deploymentNonce: fqtNonce, - data: fqtData, + token: dai.address, + from: taker, + to: uniswap.address, + value: sellAmount, }, { - 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(CURVE_BRIDGE_SOURCE_ID); - 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: CURVE_BRIDGE_SOURCE_ID, - 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, + token: shib.address, + from: uniswap.address, + to: liquidityProvider.address, }, { - deploymentNonce: payTakerNonce, - data: payTakerData, + token: zrx.address, + from: liquidityProvider.address, + to: taker, + value: buyAmount, }, ], - 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 }, + TestMintableERC20TokenEvents.Transfer, ); - logUtils.log(`${tx.gasUsed} gas used`); + }); + it('LiquidityProvider -> Sushiswap', async () => { + const sellAmount = getRandomInteger(1, toBaseUnitAmount(1)); + const shibAmount = getRandomInteger(1, toBaseUnitAmount(1)); + const liquidityProviderSubcall = getLiquidityProviderMultiHopSubcall(); + const sushiswap = await createUniswapV2PoolAsync(sushiFactory, shib, zrx); + const sushiswapSubcall = getUniswapV2MultiHopSubcall([shib.address, zrx.address], true); + await mintToAsync(dai, taker, sellAmount); + await mintToAsync(shib, liquidityProvider.address, shibAmount); - 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(CURVE_BRIDGE_SOURCE_ID); - 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, + const tx = await multiplex + .multiplexMultiHopSellTokenForToken( + [dai.address, shib.address, zrx.address], + [liquidityProviderSubcall, sushiswapSubcall], + sellAmount, + constants.ZERO_AMOUNT, + ) + .awaitTransactionSuccessAsync({ from: taker }); + verifyEventsFromLogs( + tx.logs, + [ + { + token: dai.address, + from: taker, + to: liquidityProvider.address, + value: sellAmount, + }, + { + token: shib.address, + from: liquidityProvider.address, + to: sushiswap.address, + value: shibAmount, + }, + { + token: zrx.address, + from: sushiswap.address, + to: taker, + }, ], - 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 }, + TestMintableERC20TokenEvents.Transfer, ); - logUtils.log(`${tx.gasUsed} gas used`); + }); + it('UniswapV3 -> BatchSell(RFQ, UniswapV2)', async () => { + const sellAmount = getRandomInteger(1, toBaseUnitAmount(1)); + await mintToAsync(dai, taker, sellAmount); + const uniV3 = await createUniswapV3PoolAsync(dai, shib); + const uniV3Subcall = getUniswapV3MultiHopSubcall([dai, shib]); + const rfqOrder = getTestRfqOrder({ takerToken: shib.address, makerToken: zrx.address }); + const rfqFillProportion = 0.42; + const rfqSubcall = await getRfqSubcallAsync(rfqOrder, encodeFractionalFillAmount(rfqFillProportion)); + const uniV2 = await createUniswapV2PoolAsync(uniV2Factory, shib, zrx); + const uniV2Subcall = getUniswapV2BatchSubcall( + [shib.address, zrx.address], + encodeFractionalFillAmount(1), + ); + const nestedBatchSellSubcall = getNestedBatchSellSubcall([rfqSubcall, uniV2Subcall]); + + const tx = await multiplex + .multiplexMultiHopSellTokenForToken( + [dai.address, shib.address, zrx.address], + [uniV3Subcall, nestedBatchSellSubcall], + sellAmount, + constants.ZERO_AMOUNT, + ) + .awaitTransactionSuccessAsync({ from: taker }); + verifyEventsFromLogs( + tx.logs, + [ + { + token: shib.address, + from: uniV3.address, + to: zeroEx.address, + }, + { + token: dai.address, + from: taker, + to: uniV3.address, + value: sellAmount, + }, + { + token: shib.address, + from: zeroEx.address, + to: maker, + }, + { + token: zrx.address, + from: maker, + to: taker, + }, + { + token: shib.address, + from: zeroEx.address, + to: uniV2.address, + }, + { + token: zrx.address, + from: uniV2.address, + to: taker, + }, + ], + TestMintableERC20TokenEvents.Transfer, + ); + }); + it('BatchSell(RFQ, UniswapV2) -> UniswapV3', async () => { + const rfqOrder = getTestRfqOrder({ takerToken: dai.address, makerToken: shib.address }); + const rfqSubcall = await getRfqSubcallAsync(rfqOrder, rfqOrder.takerAmount); + const uniV2 = await createUniswapV2PoolAsync(uniV2Factory, dai, shib); + const uniV2Subcall = getUniswapV2BatchSubcall([dai.address, shib.address]); + const sellAmount = rfqSubcall.sellAmount.plus(uniV2Subcall.sellAmount); + await mintToAsync(dai, taker, sellAmount); + const nestedBatchSellSubcall = getNestedBatchSellSubcall([rfqSubcall, uniV2Subcall]); + + const uniV3 = await createUniswapV3PoolAsync(shib, zrx); + const uniV3Subcall = getUniswapV3MultiHopSubcall([shib, zrx]); + + const tx = await multiplex + .multiplexMultiHopSellTokenForToken( + [dai.address, shib.address, zrx.address], + [nestedBatchSellSubcall, uniV3Subcall], + sellAmount, + constants.ZERO_AMOUNT, + ) + .awaitTransactionSuccessAsync({ from: taker }); + verifyEventsFromLogs( + tx.logs, + [ + { + token: dai.address, + from: taker, + to: maker, + value: rfqOrder.takerAmount, + }, + { + token: shib.address, + from: maker, + to: zeroEx.address, + value: rfqOrder.makerAmount, + }, + { + token: dai.address, + from: taker, + to: uniV2.address, + value: uniV2Subcall.sellAmount, + }, + { + token: shib.address, + from: uniV2.address, + to: zeroEx.address, + }, + { + token: zrx.address, + from: uniV3.address, + to: taker, + }, + { + token: shib.address, + from: zeroEx.address, + to: uniV3.address, + }, + ], + TestMintableERC20TokenEvents.Transfer, + ); + }); + }); + describe('multiplexMultiHopSellEthForToken', () => { + it('reverts if first token is not WETH', async () => { + const sellAmount = getRandomInteger(1, toBaseUnitAmount(1)); + await createUniswapV2PoolAsync(uniV2Factory, weth, zrx); + const uniswapV2Subcall = getUniswapV2MultiHopSubcall([weth.address, zrx.address]); + await mintToAsync(weth, taker, sellAmount); + + const tx = multiplex + .multiplexMultiHopSellEthForToken( + [dai.address, zrx.address], + [uniswapV2Subcall], + constants.ZERO_AMOUNT, + ) + .awaitTransactionSuccessAsync({ from: taker, value: sellAmount }); + return expect(tx).to.revertWith('MultiplexFeature::multiplexMultiHopSellEthForToken/NOT_WETH'); + }); + it('UniswapV2 -> LiquidityProvider', async () => { + const sellAmount = getRandomInteger(1, toBaseUnitAmount(1)); + const buyAmount = getRandomInteger(1, toBaseUnitAmount(1)); + const uniswap = await createUniswapV2PoolAsync(uniV2Factory, weth, shib); + const uniswapV2Subcall = getUniswapV2MultiHopSubcall([weth.address, shib.address]); + const liquidityProviderSubcall = getLiquidityProviderMultiHopSubcall(); + await mintToAsync(zrx, liquidityProvider.address, buyAmount); + + const tx = await multiplex + .multiplexMultiHopSellEthForToken( + [weth.address, shib.address, zrx.address], + [uniswapV2Subcall, liquidityProviderSubcall], + buyAmount, + ) + .awaitTransactionSuccessAsync({ from: taker, value: sellAmount }); + verifyEventsFromLogs(tx.logs, [{ owner: zeroEx.address, value: sellAmount }], TestWethEvents.Deposit); + verifyEventsFromLogs( + tx.logs, + [ + { + token: weth.address, + from: zeroEx.address, + to: uniswap.address, + value: sellAmount, + }, + { + token: shib.address, + from: uniswap.address, + to: liquidityProvider.address, + }, + { + token: zrx.address, + from: liquidityProvider.address, + to: taker, + value: buyAmount, + }, + ], + TestMintableERC20TokenEvents.Transfer, + ); + }); + it('LiquidityProvider -> Sushiswap', async () => { + const sellAmount = getRandomInteger(1, toBaseUnitAmount(1)); + const shibAmount = getRandomInteger(1, toBaseUnitAmount(1)); + const liquidityProviderSubcall = getLiquidityProviderMultiHopSubcall(); + const sushiswap = await createUniswapV2PoolAsync(sushiFactory, shib, zrx); + const sushiswapSubcall = getUniswapV2MultiHopSubcall([shib.address, zrx.address], true); + await mintToAsync(shib, liquidityProvider.address, shibAmount); + + const tx = await multiplex + .multiplexMultiHopSellEthForToken( + [weth.address, shib.address, zrx.address], + [liquidityProviderSubcall, sushiswapSubcall], + constants.ZERO_AMOUNT, + ) + .awaitTransactionSuccessAsync({ from: taker, value: sellAmount }); + verifyEventsFromLogs(tx.logs, [{ owner: zeroEx.address, value: sellAmount }], TestWethEvents.Deposit); + verifyEventsFromLogs( + tx.logs, + [ + { + token: weth.address, + from: zeroEx.address, + to: liquidityProvider.address, + value: sellAmount, + }, + { + token: shib.address, + from: liquidityProvider.address, + to: sushiswap.address, + value: shibAmount, + }, + { + token: zrx.address, + from: sushiswap.address, + to: taker, + }, + ], + TestMintableERC20TokenEvents.Transfer, + ); + }); + it('UniswapV3 -> BatchSell(RFQ, UniswapV2)', async () => { + const sellAmount = getRandomInteger(1, toBaseUnitAmount(1)); + const uniV3 = await createUniswapV3PoolAsync(weth, shib); + const uniV3Subcall = getUniswapV3MultiHopSubcall([weth, shib]); + const rfqOrder = getTestRfqOrder({ takerToken: shib.address, makerToken: zrx.address }); + const rfqFillProportion = 0.42; + const rfqSubcall = await getRfqSubcallAsync(rfqOrder, encodeFractionalFillAmount(rfqFillProportion)); + const uniV2 = await createUniswapV2PoolAsync(uniV2Factory, shib, zrx); + const uniV2Subcall = getUniswapV2BatchSubcall( + [shib.address, zrx.address], + encodeFractionalFillAmount(1), + ); + const nestedBatchSellSubcall = getNestedBatchSellSubcall([rfqSubcall, uniV2Subcall]); + + const tx = await multiplex + .multiplexMultiHopSellEthForToken( + [weth.address, shib.address, zrx.address], + [uniV3Subcall, nestedBatchSellSubcall], + constants.ZERO_AMOUNT, + ) + .awaitTransactionSuccessAsync({ from: taker, value: sellAmount }); + verifyEventsFromLogs(tx.logs, [{ owner: zeroEx.address, value: sellAmount }], TestWethEvents.Deposit); + verifyEventsFromLogs( + tx.logs, + [ + { + token: shib.address, + from: uniV3.address, + to: zeroEx.address, + }, + { + token: weth.address, + from: zeroEx.address, + to: uniV3.address, + value: sellAmount, + }, + { + token: shib.address, + from: zeroEx.address, + to: maker, + }, + { + token: zrx.address, + from: maker, + to: taker, + }, + { + token: shib.address, + from: zeroEx.address, + to: uniV2.address, + }, + { + token: zrx.address, + from: uniV2.address, + to: taker, + }, + ], + TestMintableERC20TokenEvents.Transfer, + ); + }); + it('BatchSell(RFQ, UniswapV2) -> UniswapV3', async () => { + const rfqOrder = getTestRfqOrder({ takerToken: weth.address, makerToken: shib.address }); + const rfqSubcall = await getRfqSubcallAsync(rfqOrder, rfqOrder.takerAmount); + const uniV2 = await createUniswapV2PoolAsync(uniV2Factory, weth, shib); + const uniV2Subcall = getUniswapV2BatchSubcall([weth.address, shib.address]); + const sellAmount = rfqSubcall.sellAmount.plus(uniV2Subcall.sellAmount); + const nestedBatchSellSubcall = getNestedBatchSellSubcall([rfqSubcall, uniV2Subcall]); + + const uniV3 = await createUniswapV3PoolAsync(shib, zrx); + const uniV3Subcall = getUniswapV3MultiHopSubcall([shib, zrx]); + + const tx = await multiplex + .multiplexMultiHopSellEthForToken( + [weth.address, shib.address, zrx.address], + [nestedBatchSellSubcall, uniV3Subcall], + constants.ZERO_AMOUNT, + ) + .awaitTransactionSuccessAsync({ from: taker, value: sellAmount }); + verifyEventsFromLogs(tx.logs, [{ owner: zeroEx.address, value: sellAmount }], TestWethEvents.Deposit); + verifyEventsFromLogs( + tx.logs, + [ + { + token: weth.address, + from: zeroEx.address, + to: maker, + value: rfqOrder.takerAmount, + }, + { + token: shib.address, + from: maker, + to: zeroEx.address, + value: rfqOrder.makerAmount, + }, + { + token: weth.address, + from: zeroEx.address, + to: uniV2.address, + value: uniV2Subcall.sellAmount, + }, + { + token: shib.address, + from: uniV2.address, + to: zeroEx.address, + }, + { + token: zrx.address, + from: uniV3.address, + to: taker, + }, + { + token: shib.address, + from: zeroEx.address, + to: uniV3.address, + }, + ], + TestMintableERC20TokenEvents.Transfer, + ); + }); + }); + describe('multiplexMultiHopSellTokenForEth', () => { + it('reverts if last token is not WETH', async () => { + const sellAmount = getRandomInteger(1, toBaseUnitAmount(1)); + await createUniswapV2PoolAsync(uniV2Factory, zrx, weth); + const uniswapV2Subcall = getUniswapV2MultiHopSubcall([zrx.address, weth.address]); + await mintToAsync(zrx, taker, sellAmount); + + const tx = multiplex + .multiplexMultiHopSellTokenForEth( + [zrx.address, dai.address], + [uniswapV2Subcall], + sellAmount, + constants.ZERO_AMOUNT, + ) + .awaitTransactionSuccessAsync({ from: taker }); + return expect(tx).to.revertWith('MultiplexFeature::multiplexMultiHopSellTokenForEth/NOT_WETH'); + }); + it('UniswapV2 -> LiquidityProvider', async () => { + const sellAmount = getRandomInteger(1, toBaseUnitAmount(1)); + const buyAmount = getRandomInteger(1, toBaseUnitAmount(1)); + const uniswap = await createUniswapV2PoolAsync(uniV2Factory, dai, shib); + const uniswapV2Subcall = getUniswapV2MultiHopSubcall([dai.address, shib.address]); + const liquidityProviderSubcall = getLiquidityProviderMultiHopSubcall(); + await mintToAsync(dai, taker, sellAmount); + await mintToAsync(weth, liquidityProvider.address, buyAmount); + + const tx = await multiplex + .multiplexMultiHopSellTokenForEth( + [dai.address, shib.address, weth.address], + [uniswapV2Subcall, liquidityProviderSubcall], + sellAmount, + buyAmount, + ) + .awaitTransactionSuccessAsync({ from: taker }); + verifyEventsFromLogs( + tx.logs, + [ + { + token: dai.address, + from: taker, + to: uniswap.address, + value: sellAmount, + }, + { + token: shib.address, + from: uniswap.address, + to: liquidityProvider.address, + }, + { + token: weth.address, + from: liquidityProvider.address, + to: zeroEx.address, + value: buyAmount, + }, + ], + TestMintableERC20TokenEvents.Transfer, + ); + verifyEventsFromLogs(tx.logs, [{ owner: zeroEx.address, value: buyAmount }], TestWethEvents.Withdrawal); + }); + it('LiquidityProvider -> Sushiswap', async () => { + const sellAmount = getRandomInteger(1, toBaseUnitAmount(1)); + const shibAmount = getRandomInteger(1, toBaseUnitAmount(1)); + const liquidityProviderSubcall = getLiquidityProviderMultiHopSubcall(); + const sushiswap = await createUniswapV2PoolAsync(sushiFactory, shib, weth); + const sushiswapSubcall = getUniswapV2MultiHopSubcall([shib.address, weth.address], true); + await mintToAsync(dai, taker, sellAmount); + await mintToAsync(shib, liquidityProvider.address, shibAmount); + + const tx = await multiplex + .multiplexMultiHopSellTokenForEth( + [dai.address, shib.address, weth.address], + [liquidityProviderSubcall, sushiswapSubcall], + sellAmount, + constants.ZERO_AMOUNT, + ) + .awaitTransactionSuccessAsync({ from: taker }); + verifyEventsFromLogs( + tx.logs, + [ + { + token: dai.address, + from: taker, + to: liquidityProvider.address, + value: sellAmount, + }, + { + token: shib.address, + from: liquidityProvider.address, + to: sushiswap.address, + value: shibAmount, + }, + { + token: weth.address, + from: sushiswap.address, + to: zeroEx.address, + }, + ], + TestMintableERC20TokenEvents.Transfer, + ); + verifyEventsFromLogs(tx.logs, [{ owner: zeroEx.address }], TestWethEvents.Withdrawal); + }); + it('UniswapV3 -> BatchSell(RFQ, UniswapV2)', async () => { + const sellAmount = getRandomInteger(1, toBaseUnitAmount(1)); + const uniV3 = await createUniswapV3PoolAsync(dai, shib); + const uniV3Subcall = getUniswapV3MultiHopSubcall([dai, shib]); + const rfqOrder = getTestRfqOrder({ takerToken: shib.address, makerToken: weth.address }); + const rfqFillProportion = 0.42; + const rfqSubcall = await getRfqSubcallAsync(rfqOrder, encodeFractionalFillAmount(rfqFillProportion)); + const uniV2 = await createUniswapV2PoolAsync(uniV2Factory, shib, weth); + const uniV2Subcall = getUniswapV2BatchSubcall( + [shib.address, weth.address], + encodeFractionalFillAmount(1), + ); + const nestedBatchSellSubcall = getNestedBatchSellSubcall([rfqSubcall, uniV2Subcall]); + await mintToAsync(dai, taker, sellAmount); + + const tx = await multiplex + .multiplexMultiHopSellTokenForEth( + [dai.address, shib.address, weth.address], + [uniV3Subcall, nestedBatchSellSubcall], + sellAmount, + constants.ZERO_AMOUNT, + ) + .awaitTransactionSuccessAsync({ from: taker }); + verifyEventsFromLogs( + tx.logs, + [ + { + token: shib.address, + from: uniV3.address, + to: zeroEx.address, + }, + { + token: dai.address, + from: taker, + to: uniV3.address, + value: sellAmount, + }, + { + token: shib.address, + from: zeroEx.address, + to: maker, + }, + { + token: weth.address, + from: maker, + to: zeroEx.address, + }, + { + token: shib.address, + from: zeroEx.address, + to: uniV2.address, + }, + { + token: weth.address, + from: uniV2.address, + to: zeroEx.address, + }, + ], + TestMintableERC20TokenEvents.Transfer, + ); + verifyEventsFromLogs(tx.logs, [{ owner: zeroEx.address }], TestWethEvents.Withdrawal); + }); + it('BatchSell(RFQ, UniswapV2) -> UniswapV3', async () => { + const rfqOrder = getTestRfqOrder({ takerToken: dai.address, makerToken: shib.address }); + const rfqSubcall = await getRfqSubcallAsync(rfqOrder, rfqOrder.takerAmount); + const uniV2 = await createUniswapV2PoolAsync(uniV2Factory, dai, shib); + const uniV2Subcall = getUniswapV2BatchSubcall([dai.address, shib.address]); + const sellAmount = rfqSubcall.sellAmount.plus(uniV2Subcall.sellAmount); + const nestedBatchSellSubcall = getNestedBatchSellSubcall([rfqSubcall, uniV2Subcall]); + await mintToAsync(dai, taker, sellAmount); + const uniV3 = await createUniswapV3PoolAsync(shib, weth); + const uniV3Subcall = getUniswapV3MultiHopSubcall([shib, weth]); + + const tx = await multiplex + .multiplexMultiHopSellTokenForEth( + [dai.address, shib.address, weth.address], + [nestedBatchSellSubcall, uniV3Subcall], + sellAmount, + constants.ZERO_AMOUNT, + ) + .awaitTransactionSuccessAsync({ from: taker }); + verifyEventsFromLogs( + tx.logs, + [ + { + token: dai.address, + from: taker, + to: maker, + value: rfqOrder.takerAmount, + }, + { + token: shib.address, + from: maker, + to: zeroEx.address, + value: rfqOrder.makerAmount, + }, + { + token: dai.address, + from: taker, + to: uniV2.address, + value: uniV2Subcall.sellAmount, + }, + { + token: shib.address, + from: uniV2.address, + to: zeroEx.address, + }, + { + token: weth.address, + from: uniV3.address, + to: zeroEx.address, + }, + { + token: shib.address, + from: zeroEx.address, + to: uniV3.address, + }, + ], + TestMintableERC20TokenEvents.Transfer, + ); + verifyEventsFromLogs(tx.logs, [{ owner: zeroEx.address }], TestWethEvents.Withdrawal); + }); }); }); }); diff --git a/contracts/zero-ex/test/features/otc_orders_test.ts b/contracts/zero-ex/test/features/otc_orders_test.ts index e546852791..24a49c9ef8 100644 --- a/contracts/zero-ex/test/features/otc_orders_test.ts +++ b/contracts/zero-ex/test/features/otc_orders_test.ts @@ -21,6 +21,7 @@ import { blockchainTests.resets('OtcOrdersFeature', env => { const { NULL_ADDRESS, MAX_UINT256, ZERO_AMOUNT: ZERO } = constants; + const ETH_TOKEN_ADDRESS = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'; let maker: string; let taker: string; let notMaker: string; @@ -309,7 +310,7 @@ blockchainTests.resets('OtcOrdersFeature', env => { const order = getTestOtcOrder(); await testUtils.prepareBalancesForOrdersAsync([order], taker); const tx = zeroEx - .fillOtcOrder(order, await order.getSignatureWithProviderAsync(env.provider), order.takerAmount, false) + .fillOtcOrder(order, await order.getSignatureWithProviderAsync(env.provider), order.takerAmount) .awaitTransactionSuccessAsync({ from: taker, value: 1 }); // This will revert at the language level because the fill function is not payable. return expect(tx).to.be.rejectedWith('revert'); @@ -395,7 +396,7 @@ blockchainTests.resets('OtcOrdersFeature', env => { it('reverts if `unwrapWeth` is true but maker token is not WETH', async () => { const order = getTestOtcOrder(); const tx = testUtils.fillOtcOrderAsync(order, order.takerAmount, taker, true); - return expect(tx).to.revertWith('OtcOrdersFeature/INVALID_UNWRAP_WETH'); + return expect(tx).to.revertWith('OtcOrdersFeature::fillOtcOrderForEth/MAKER_TOKEN_NOT_WETH'); }); it('allows for fills on orders signed by a approved signer', async () => { @@ -415,7 +416,7 @@ blockchainTests.resets('OtcOrdersFeature', env => { .awaitTransactionSuccessAsync({ from: contractWalletOwner }); // fill should succeed const receipt = await zeroEx - .fillOtcOrder(order, sig, order.takerAmount, false) + .fillOtcOrder(order, sig, order.takerAmount) .awaitTransactionSuccessAsync({ from: taker }); verifyEventsFromLogs( receipt.logs, @@ -445,9 +446,7 @@ blockchainTests.resets('OtcOrdersFeature', env => { .registerAllowedOrderSigner(contractWalletSigner, false) .awaitTransactionSuccessAsync({ from: contractWalletOwner }); // fill should revert - const tx = zeroEx - .fillOtcOrder(order, sig, order.takerAmount, false) - .awaitTransactionSuccessAsync({ from: taker }); + const tx = zeroEx.fillOtcOrder(order, sig, order.takerAmount).awaitTransactionSuccessAsync({ from: taker }); return expect(tx).to.revertWith( new RevertErrors.NativeOrders.OrderNotSignedByMakerError( order.getHash(), @@ -465,16 +464,14 @@ blockchainTests.resets('OtcOrdersFeature', env => { // need to provide contract wallet with a balance await makerToken.mint(contractWallet.address, order.makerAmount).awaitTransactionSuccessAsync(); // fill should revert - const tx = zeroEx - .fillOtcOrder(order, sig, order.takerAmount, false) - .awaitTransactionSuccessAsync({ from: taker }); + const tx = zeroEx.fillOtcOrder(order, sig, order.takerAmount).awaitTransactionSuccessAsync({ from: taker }); return expect(tx).to.revertWith( new RevertErrors.NativeOrders.OrderNotSignedByMakerError(order.getHash(), maker, order.maker), ); }); }); describe('fillOtcOrderWithEth()', () => { - it('Can fill an order with ETH', async () => { + it('Can fill an order with ETH (takerToken=WETH)', async () => { const order = getTestOtcOrder({ takerToken: wethToken.address }); const receipt = await testUtils.fillOtcOrderWithEthAsync(order); verifyEventsFromLogs( @@ -484,7 +481,25 @@ blockchainTests.resets('OtcOrdersFeature', env => { ); await assertExpectedFinalBalancesFromOtcOrderFillAsync(order); }); - it('Can partially fill an order with ETH', async () => { + it('Can fill an order with ETH (takerToken=ETH)', async () => { + const order = getTestOtcOrder({ takerToken: ETH_TOKEN_ADDRESS }); + const makerEthBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(maker); + const receipt = await testUtils.fillOtcOrderWithEthAsync(order); + verifyEventsFromLogs( + receipt.logs, + [testUtils.createOtcOrderFilledEventArgs(order)], + IZeroExEvents.OtcOrderFilled, + ); + const takerBalance = await new TestMintableERC20TokenContract(order.makerToken, env.provider) + .balanceOf(taker) + .callAsync(); + expect(takerBalance, 'taker balance').to.bignumber.eq(order.makerAmount); + const makerEthBalanceAfter = await env.web3Wrapper.getBalanceInWeiAsync(maker); + expect(makerEthBalanceAfter.minus(makerEthBalanceBefore), 'maker balance').to.bignumber.equal( + order.takerAmount, + ); + }); + it('Can partially fill an order with ETH (takerToken=WETH)', async () => { const order = getTestOtcOrder({ takerToken: wethToken.address }); const fillAmount = order.takerAmount.minus(1); const receipt = await testUtils.fillOtcOrderWithEthAsync(order, fillAmount); @@ -495,7 +510,27 @@ blockchainTests.resets('OtcOrdersFeature', env => { ); await assertExpectedFinalBalancesFromOtcOrderFillAsync(order, fillAmount); }); - it('Can refund excess ETH is msg.value > order.takerAmount', async () => { + it('Can partially fill an order with ETH (takerToken=ETH)', async () => { + const order = getTestOtcOrder({ takerToken: ETH_TOKEN_ADDRESS }); + const fillAmount = order.takerAmount.minus(1); + const makerEthBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(maker); + const receipt = await testUtils.fillOtcOrderWithEthAsync(order, fillAmount); + verifyEventsFromLogs( + receipt.logs, + [testUtils.createOtcOrderFilledEventArgs(order, fillAmount)], + IZeroExEvents.OtcOrderFilled, + ); + const { makerTokenFilledAmount, takerTokenFilledAmount } = computeOtcOrderFilledAmounts(order, fillAmount); + const takerBalance = await new TestMintableERC20TokenContract(order.makerToken, env.provider) + .balanceOf(taker) + .callAsync(); + expect(takerBalance, 'taker balance').to.bignumber.eq(makerTokenFilledAmount); + const makerEthBalanceAfter = await env.web3Wrapper.getBalanceInWeiAsync(maker); + expect(makerEthBalanceAfter.minus(makerEthBalanceBefore), 'maker balance').to.bignumber.equal( + takerTokenFilledAmount, + ); + }); + it('Can refund excess ETH is msg.value > order.takerAmount (takerToken=WETH)', async () => { const order = getTestOtcOrder({ takerToken: wethToken.address }); const fillAmount = order.takerAmount.plus(420); const takerEthBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(taker); @@ -509,10 +544,34 @@ blockchainTests.resets('OtcOrdersFeature', env => { expect(takerEthBalanceBefore.minus(takerEthBalanceAfter)).to.bignumber.equal(order.takerAmount); await assertExpectedFinalBalancesFromOtcOrderFillAsync(order); }); - it('Cannot fill an order if taker token is not WETH', async () => { + it('Can refund excess ETH is msg.value > order.takerAmount (takerToken=ETH)', async () => { + const order = getTestOtcOrder({ takerToken: ETH_TOKEN_ADDRESS }); + const fillAmount = order.takerAmount.plus(420); + const takerEthBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(taker); + const makerEthBalanceBefore = await env.web3Wrapper.getBalanceInWeiAsync(maker); + const receipt = await testUtils.fillOtcOrderWithEthAsync(order, fillAmount); + verifyEventsFromLogs( + receipt.logs, + [testUtils.createOtcOrderFilledEventArgs(order)], + IZeroExEvents.OtcOrderFilled, + ); + const takerEthBalanceAfter = await env.web3Wrapper.getBalanceInWeiAsync(taker); + expect(takerEthBalanceBefore.minus(takerEthBalanceAfter), 'taker eth balance').to.bignumber.equal( + order.takerAmount, + ); + const takerBalance = await new TestMintableERC20TokenContract(order.makerToken, env.provider) + .balanceOf(taker) + .callAsync(); + expect(takerBalance, 'taker balance').to.bignumber.eq(order.makerAmount); + const makerEthBalanceAfter = await env.web3Wrapper.getBalanceInWeiAsync(maker); + expect(makerEthBalanceAfter.minus(makerEthBalanceBefore), 'maker balance').to.bignumber.equal( + order.takerAmount, + ); + }); + it('Cannot fill an order if taker token is not ETH or WETH', async () => { const order = getTestOtcOrder(); const tx = testUtils.fillOtcOrderWithEthAsync(order); - return expect(tx).to.revertWith('OtcOrdersFeature/INVALID_WRAP_ETH'); + return expect(tx).to.revertWith('OtcOrdersFeature::fillOtcOrderWithEth/INVALID_TAKER_TOKEN'); }); }); @@ -578,15 +637,22 @@ blockchainTests.resets('OtcOrdersFeature', env => { const order = getTestOtcOrder({ taker, txOrigin }); const tx = testUtils.fillTakerSignedOtcOrderAsync(order, txOrigin, notTaker); return expect(tx).to.revertWith( - new RevertErrors.NativeOrders.OrderNotSignedByTakerError(order.getHash(), notTaker, taker), + new RevertErrors.NativeOrders.OrderNotFillableByTakerError(order.getHash(), notTaker, taker), ); }); it('cannot fill order with bad maker signature', async () => { const order = getTestOtcOrder({ taker, txOrigin }); - // Overwrite chainId to result in a different hash and therefore different - // signature. - const tx = testUtils.fillTakerSignedOtcOrderAsync(order.clone({ chainId: 1234 })); + const anotherOrder = getTestOtcOrder({ taker, txOrigin }); + await testUtils.prepareBalancesForOrdersAsync([order], taker); + const tx = zeroEx + .fillTakerSignedOtcOrder( + order, + await anotherOrder.getSignatureWithProviderAsync(env.provider), + await order.getSignatureWithProviderAsync(env.provider, SignatureType.EthSign, taker), + ) + .awaitTransactionSuccessAsync({ from: txOrigin }); + return expect(tx).to.revertWith( new RevertErrors.NativeOrders.OrderNotSignedByMakerError(order.getHash(), undefined, order.maker), ); @@ -600,7 +666,6 @@ blockchainTests.resets('OtcOrdersFeature', env => { order, await order.getSignatureWithProviderAsync(env.provider), await order.getSignatureWithProviderAsync(env.provider, SignatureType.EthSign, taker), - false, ) .awaitTransactionSuccessAsync({ from: txOrigin, value: 1 }); // This will revert at the language level because the fill function is not payable. @@ -702,51 +767,102 @@ blockchainTests.resets('OtcOrdersFeature', env => { it('reverts if `unwrapWeth` is true but maker token is not WETH', async () => { const order = getTestOtcOrder({ taker, txOrigin }); const tx = testUtils.fillTakerSignedOtcOrderAsync(order, txOrigin, taker, true); - return expect(tx).to.revertWith('OtcOrdersFeature/INVALID_UNWRAP_WETH'); + return expect(tx).to.revertWith('OtcOrdersFeature::fillTakerSignedOtcOrder/MAKER_TOKEN_NOT_WETH'); }); + }); - it('allows for fills on orders signed by a approved signer (taker)', async () => { - const order = getTestOtcOrder({ txOrigin, taker: contractWallet.address }); - await testUtils.prepareBalancesForOrdersAsync([order], contractWallet.address); - // allow signer - await contractWallet - .registerAllowedOrderSigner(contractWalletSigner, true) - .awaitTransactionSuccessAsync({ from: contractWalletOwner }); - // fill should succeed - const receipt = await zeroEx - .fillTakerSignedOtcOrder( - order, - await order.getSignatureWithProviderAsync(env.provider), - await order.getSignatureWithProviderAsync( - env.provider, - SignatureType.EthSign, - contractWalletSigner, - ), - false, + describe('batchFillTakerSignedOtcOrders()', () => { + it('Fills multiple orders', async () => { + const order1 = getTestOtcOrder({ taker, txOrigin }); + const order2 = getTestOtcOrder({ + taker: notTaker, + txOrigin, + nonceBucket: order1.nonceBucket, + nonce: order1.nonce.plus(1), + }); + await testUtils.prepareBalancesForOrdersAsync([order1], taker); + await testUtils.prepareBalancesForOrdersAsync([order2], notTaker); + const tx = await zeroEx + .batchFillTakerSignedOtcOrders( + [order1, order2], + [ + await order1.getSignatureWithProviderAsync(env.provider), + await order2.getSignatureWithProviderAsync(env.provider), + ], + [ + await order1.getSignatureWithProviderAsync(env.provider, SignatureType.EthSign, taker), + await order2.getSignatureWithProviderAsync(env.provider, SignatureType.EthSign, notTaker), + ], + [false, false], ) .awaitTransactionSuccessAsync({ from: txOrigin }); verifyEventsFromLogs( - receipt.logs, - [testUtils.createOtcOrderFilledEventArgs(order)], + tx.logs, + [testUtils.createOtcOrderFilledEventArgs(order1), testUtils.createOtcOrderFilledEventArgs(order2)], IZeroExEvents.OtcOrderFilled, ); - await assertExpectedFinalBalancesFromOtcOrderFillAsync(order); }); - - it(`doesn't allow fills with an unapproved signer (taker)`, async () => { - const order = getTestOtcOrder({ txOrigin, taker: contractWallet.address }); - await testUtils.prepareBalancesForOrdersAsync([order], contractWallet.address); - // fill should succeed - const tx = zeroEx - .fillTakerSignedOtcOrder( - order, - await order.getSignatureWithProviderAsync(env.provider), - await order.getSignatureWithProviderAsync(env.provider, SignatureType.EthSign, notTaker), - false, + it('Fills multiple orders and unwraps WETH', async () => { + const order1 = getTestOtcOrder({ taker, txOrigin }); + const order2 = getTestOtcOrder({ + taker: notTaker, + txOrigin, + nonceBucket: order1.nonceBucket, + nonce: order1.nonce.plus(1), + makerToken: wethToken.address, + makerAmount: new BigNumber('1e18'), + }); + await testUtils.prepareBalancesForOrdersAsync([order1], taker); + await testUtils.prepareBalancesForOrdersAsync([order2], notTaker); + await wethToken.deposit().awaitTransactionSuccessAsync({ from: maker, value: order2.makerAmount }); + const tx = await zeroEx + .batchFillTakerSignedOtcOrders( + [order1, order2], + [ + await order1.getSignatureWithProviderAsync(env.provider), + await order2.getSignatureWithProviderAsync(env.provider), + ], + [ + await order1.getSignatureWithProviderAsync(env.provider, SignatureType.EthSign, taker), + await order2.getSignatureWithProviderAsync(env.provider, SignatureType.EthSign, notTaker), + ], + [false, true], ) .awaitTransactionSuccessAsync({ from: txOrigin }); - return expect(tx).to.revertWith( - new RevertErrors.NativeOrders.OrderNotSignedByTakerError(order.getHash(), notTaker, order.taker), + verifyEventsFromLogs( + tx.logs, + [testUtils.createOtcOrderFilledEventArgs(order1), testUtils.createOtcOrderFilledEventArgs(order2)], + IZeroExEvents.OtcOrderFilled, + ); + }); + it('Skips over unfillable orders', async () => { + const order1 = getTestOtcOrder({ taker, txOrigin }); + const order2 = getTestOtcOrder({ + taker: notTaker, + txOrigin, + nonceBucket: order1.nonceBucket, + nonce: order1.nonce.plus(1), + }); + await testUtils.prepareBalancesForOrdersAsync([order1], taker); + await testUtils.prepareBalancesForOrdersAsync([order2], notTaker); + const tx = await zeroEx + .batchFillTakerSignedOtcOrders( + [order1, order2], + [ + await order1.getSignatureWithProviderAsync(env.provider), + await order2.getSignatureWithProviderAsync(env.provider), + ], + [ + await order1.getSignatureWithProviderAsync(env.provider, SignatureType.EthSign, taker), + await order2.getSignatureWithProviderAsync(env.provider, SignatureType.EthSign, taker), // Invalid signature for order2 + ], + [false, false], + ) + .awaitTransactionSuccessAsync({ from: txOrigin }); + verifyEventsFromLogs( + tx.logs, + [testUtils.createOtcOrderFilledEventArgs(order1)], + IZeroExEvents.OtcOrderFilled, ); }); }); diff --git a/contracts/zero-ex/test/features/transform_erc20_test.ts b/contracts/zero-ex/test/features/transform_erc20_test.ts index 4031a13371..0322d25351 100644 --- a/contracts/zero-ex/test/features/transform_erc20_test.ts +++ b/contracts/zero-ex/test/features/transform_erc20_test.ts @@ -243,6 +243,8 @@ blockchainTests.resets('TransformERC20 feature', env => { inputTokenAmount, minOutputTokenAmount, transformations: [transformation], + useSelfBalance: false, + recipient: taker, }) .awaitTransactionSuccessAsync({ value: callValue }); verifyEventsFromLogs( @@ -281,7 +283,7 @@ blockchainTests.resets('TransformERC20 feature', env => { const inputTokenAmount = getRandomPortion(startingInputTokenBalance); const minOutputTokenAmount = getRandomInteger(1, '1e18'); const outputTokenMintAmount = minOutputTokenAmount; - const callValue = outputTokenMintAmount.times(2); + const callValue = outputTokenMintAmount; const transformation = createMintTokenTransformation({ outputTokenMintAmount, inputTokenBurnAmunt: inputTokenAmount, @@ -296,6 +298,8 @@ blockchainTests.resets('TransformERC20 feature', env => { inputTokenAmount, minOutputTokenAmount, transformations: [transformation], + useSelfBalance: false, + recipient: taker, }) .awaitTransactionSuccessAsync({ value: callValue }); verifyEventsFromLogs( @@ -352,6 +356,8 @@ blockchainTests.resets('TransformERC20 feature', env => { inputTokenAmount, minOutputTokenAmount, transformations: [transformation], + useSelfBalance: false, + recipient: taker, }) .awaitTransactionSuccessAsync({ value: callValue }); verifyEventsFromLogs( @@ -406,6 +412,8 @@ blockchainTests.resets('TransformERC20 feature', env => { inputTokenBurnAmunt: inputTokenAmount, }), ], + useSelfBalance: false, + recipient: taker, }) .awaitTransactionSuccessAsync({ value: callValue }); const expectedError = new ZeroExRevertErrors.TransformERC20.IncompleteTransformERC20Error( @@ -438,6 +446,8 @@ blockchainTests.resets('TransformERC20 feature', env => { inputTokenBurnAmunt: inputTokenAmount, }), ], + useSelfBalance: false, + recipient: taker, }) .awaitTransactionSuccessAsync({ value: callValue }); const expectedError = new ZeroExRevertErrors.TransformERC20.NegativeTransformERC20OutputError( @@ -475,6 +485,8 @@ blockchainTests.resets('TransformERC20 feature', env => { inputTokenAmount, minOutputTokenAmount, transformations, + useSelfBalance: false, + recipient: taker, }) .awaitTransactionSuccessAsync({ value: callValue }); verifyEventsFromLogs( @@ -520,6 +532,8 @@ blockchainTests.resets('TransformERC20 feature', env => { inputTokenAmount, minOutputTokenAmount, transformations, + useSelfBalance: false, + recipient: taker, }) .awaitTransactionSuccessAsync({ value: callValue }); return expect(tx).to.revertWith( @@ -549,6 +563,8 @@ blockchainTests.resets('TransformERC20 feature', env => { inputTokenAmount: MAX_UINT256, minOutputTokenAmount, transformations: [transformation], + useSelfBalance: false, + recipient: taker, }) .awaitTransactionSuccessAsync({ value: callValue }); verifyEventsFromLogs( @@ -584,6 +600,8 @@ blockchainTests.resets('TransformERC20 feature', env => { inputTokenAmount: MAX_UINT256, minOutputTokenAmount, transformations: [transformation], + useSelfBalance: false, + recipient: taker, }) .awaitTransactionSuccessAsync({ value: ethAttchedAmount }); verifyEventsFromLogs( diff --git a/contracts/zero-ex/test/full_migration_test.ts b/contracts/zero-ex/test/full_migration_test.ts index 6177247450..38dc3dee27 100644 --- a/contracts/zero-ex/test/full_migration_test.ts +++ b/contracts/zero-ex/test/full_migration_test.ts @@ -70,7 +70,7 @@ blockchainTests.resets('Full migration', env => { TransformERC20: { contractType: ITransformERC20FeatureContract, fns: [ - 'transformERC20', + // 'transformERC20', TODO '_transformERC20', 'createTransformWallet', 'getTransformWallet', @@ -148,6 +148,9 @@ blockchainTests.resets('Full migration', env => { if (item.type === 'byte') { return hexUtils.random(1); } + if (item.type === 'bool') { + return Math.random() > 0.5; + } if (/^bytes$/.test(item.type)) { return hexUtils.random(_.random(0, 128)); } diff --git a/contracts/zero-ex/test/transformers/affiliate_fee_transformer_test.ts b/contracts/zero-ex/test/transformers/affiliate_fee_transformer_test.ts index 87abfebf04..249d558db0 100644 --- a/contracts/zero-ex/test/transformers/affiliate_fee_transformer_test.ts +++ b/contracts/zero-ex/test/transformers/affiliate_fee_transformer_test.ts @@ -89,7 +89,7 @@ blockchainTests.resets('AffiliateFeeTransformer', env => { .rawExecuteTransform(transformer.address, { data, sender: randomAddress(), - taker: randomAddress(), + recipient: randomAddress(), }) .awaitTransactionSuccessAsync(); expect(await getBalancesAsync(host.address)).to.deep.eq(ZERO_BALANCES); @@ -119,7 +119,7 @@ blockchainTests.resets('AffiliateFeeTransformer', env => { .rawExecuteTransform(transformer.address, { data, sender: randomAddress(), - taker: randomAddress(), + recipient: randomAddress(), }) .awaitTransactionSuccessAsync(); expect(await getBalancesAsync(host.address)).to.deep.eq(ZERO_BALANCES); @@ -149,7 +149,7 @@ blockchainTests.resets('AffiliateFeeTransformer', env => { .rawExecuteTransform(transformer.address, { data, sender: randomAddress(), - taker: randomAddress(), + recipient: randomAddress(), }) .awaitTransactionSuccessAsync(); expect(await getBalancesAsync(host.address)).to.deep.eq({ diff --git a/contracts/zero-ex/test/transformers/pay_taker_transformer_test.ts b/contracts/zero-ex/test/transformers/pay_taker_transformer_test.ts index d357d8fcda..2dd14ac729 100644 --- a/contracts/zero-ex/test/transformers/pay_taker_transformer_test.ts +++ b/contracts/zero-ex/test/transformers/pay_taker_transformer_test.ts @@ -80,7 +80,7 @@ blockchainTests.resets('PayTakerTransformer', env => { await host .rawExecuteTransform(transformer.address, { data, - taker, + recipient: taker, sender: randomAddress(), }) .awaitTransactionSuccessAsync(); @@ -102,7 +102,7 @@ blockchainTests.resets('PayTakerTransformer', env => { await host .rawExecuteTransform(transformer.address, { data, - taker, + recipient: taker, sender: randomAddress(), }) .awaitTransactionSuccessAsync(); @@ -124,7 +124,7 @@ blockchainTests.resets('PayTakerTransformer', env => { await host .rawExecuteTransform(transformer.address, { data, - taker, + recipient: taker, sender: randomAddress(), }) .awaitTransactionSuccessAsync(); @@ -146,7 +146,7 @@ blockchainTests.resets('PayTakerTransformer', env => { await host .rawExecuteTransform(transformer.address, { data, - taker, + recipient: taker, sender: randomAddress(), }) .awaitTransactionSuccessAsync(); diff --git a/contracts/zero-ex/test/transformers/positive_slippage_fee_transformer_test.ts b/contracts/zero-ex/test/transformers/positive_slippage_fee_transformer_test.ts index 9ce122bebf..d1aaee6fc8 100644 --- a/contracts/zero-ex/test/transformers/positive_slippage_fee_transformer_test.ts +++ b/contracts/zero-ex/test/transformers/positive_slippage_fee_transformer_test.ts @@ -70,7 +70,7 @@ blockchainTests.resets('PositiveSlippageFeeTransformer', env => { .rawExecuteTransform(transformer.address, { data, sender: randomAddress(), - taker: randomAddress(), + recipient: randomAddress(), }) .awaitTransactionSuccessAsync(); expect(await getBalancesAsync(host.address)).to.deep.eq(beforeBalanceHost); @@ -92,7 +92,7 @@ blockchainTests.resets('PositiveSlippageFeeTransformer', env => { .rawExecuteTransform(transformer.address, { data, sender: randomAddress(), - taker: randomAddress(), + recipient: randomAddress(), }) .awaitTransactionSuccessAsync(); expect(await getBalancesAsync(host.address)).to.deep.eq(beforeBalanceHost); @@ -112,7 +112,7 @@ blockchainTests.resets('PositiveSlippageFeeTransformer', env => { .rawExecuteTransform(transformer.address, { data, sender: randomAddress(), - taker: randomAddress(), + recipient: randomAddress(), }) .awaitTransactionSuccessAsync(); expect(await getBalancesAsync(host.address)).to.deep.eq({ diff --git a/contracts/zero-ex/test/utils/orders.ts b/contracts/zero-ex/test/utils/orders.ts index 4f460d237b..6a6d55f941 100644 --- a/contracts/zero-ex/test/utils/orders.ts +++ b/contracts/zero-ex/test/utils/orders.ts @@ -156,14 +156,23 @@ export class NativeOrdersTestEnvironment { unwrapWeth: boolean = false, ): Promise { await this.prepareBalancesForOrdersAsync([order], taker); - return this.zeroEx - .fillOtcOrder( - order, - await order.getSignatureWithProviderAsync(this._env.provider), - new BigNumber(fillAmount), - unwrapWeth, - ) - .awaitTransactionSuccessAsync({ from: taker }); + if (unwrapWeth) { + return this.zeroEx + .fillOtcOrderForEth( + order, + await order.getSignatureWithProviderAsync(this._env.provider), + new BigNumber(fillAmount), + ) + .awaitTransactionSuccessAsync({ from: taker }); + } else { + return this.zeroEx + .fillOtcOrder( + order, + await order.getSignatureWithProviderAsync(this._env.provider), + new BigNumber(fillAmount), + ) + .awaitTransactionSuccessAsync({ from: taker }); + } } public async fillTakerSignedOtcOrderAsync( @@ -173,14 +182,23 @@ export class NativeOrdersTestEnvironment { unwrapWeth: boolean = false, ): Promise { await this.prepareBalancesForOrdersAsync([order], taker); - return this.zeroEx - .fillTakerSignedOtcOrder( - order, - await order.getSignatureWithProviderAsync(this._env.provider), - await order.getSignatureWithProviderAsync(this._env.provider, SignatureType.EthSign, taker), - unwrapWeth, - ) - .awaitTransactionSuccessAsync({ from: origin }); + if (unwrapWeth) { + return this.zeroEx + .fillTakerSignedOtcOrderForEth( + order, + await order.getSignatureWithProviderAsync(this._env.provider), + await order.getSignatureWithProviderAsync(this._env.provider, SignatureType.EthSign, taker), + ) + .awaitTransactionSuccessAsync({ from: origin }); + } else { + return this.zeroEx + .fillTakerSignedOtcOrder( + order, + await order.getSignatureWithProviderAsync(this._env.provider), + await order.getSignatureWithProviderAsync(this._env.provider, SignatureType.EthSign, taker), + ) + .awaitTransactionSuccessAsync({ from: origin }); + } } public async fillOtcOrderWithEthAsync( diff --git a/contracts/zero-ex/test/wrappers.ts b/contracts/zero-ex/test/wrappers.ts index ed85df16d8..02059e5886 100644 --- a/contracts/zero-ex/test/wrappers.ts +++ b/contracts/zero-ex/test/wrappers.ts @@ -102,6 +102,12 @@ export * from '../test/generated-wrappers/mixin_uniswap_v3'; 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/multiplex_liquidity_provider'; +export * from '../test/generated-wrappers/multiplex_otc'; +export * from '../test/generated-wrappers/multiplex_rfq'; +export * from '../test/generated-wrappers/multiplex_transform_erc20'; +export * from '../test/generated-wrappers/multiplex_uniswap_v2'; +export * from '../test/generated-wrappers/multiplex_uniswap_v3'; export * from '../test/generated-wrappers/native_orders_cancellation'; export * from '../test/generated-wrappers/native_orders_feature'; export * from '../test/generated-wrappers/native_orders_info'; @@ -149,6 +155,8 @@ export * from '../test/generated-wrappers/test_transform_erc20'; export * from '../test/generated-wrappers/test_transformer_base'; export * from '../test/generated-wrappers/test_transformer_deployer_transformer'; export * from '../test/generated-wrappers/test_transformer_host'; +export * from '../test/generated-wrappers/test_uniswap_v2_factory'; +export * from '../test/generated-wrappers/test_uniswap_v2_pool'; export * from '../test/generated-wrappers/test_uniswap_v3_factory'; export * from '../test/generated-wrappers/test_uniswap_v3_feature'; export * from '../test/generated-wrappers/test_uniswap_v3_pool'; diff --git a/contracts/zero-ex/tsconfig.json b/contracts/zero-ex/tsconfig.json index 6aaad5652b..c9049f98e7 100644 --- a/contracts/zero-ex/tsconfig.json +++ b/contracts/zero-ex/tsconfig.json @@ -135,6 +135,12 @@ "test/generated-artifacts/MixinZeroExBridge.json", "test/generated-artifacts/MooniswapLiquidityProvider.json", "test/generated-artifacts/MultiplexFeature.json", + "test/generated-artifacts/MultiplexLiquidityProvider.json", + "test/generated-artifacts/MultiplexOtc.json", + "test/generated-artifacts/MultiplexRfq.json", + "test/generated-artifacts/MultiplexTransformERC20.json", + "test/generated-artifacts/MultiplexUniswapV2.json", + "test/generated-artifacts/MultiplexUniswapV3.json", "test/generated-artifacts/NativeOrdersCancellation.json", "test/generated-artifacts/NativeOrdersFeature.json", "test/generated-artifacts/NativeOrdersInfo.json", @@ -182,6 +188,8 @@ "test/generated-artifacts/TestTransformerBase.json", "test/generated-artifacts/TestTransformerDeployerTransformer.json", "test/generated-artifacts/TestTransformerHost.json", + "test/generated-artifacts/TestUniswapV2Factory.json", + "test/generated-artifacts/TestUniswapV2Pool.json", "test/generated-artifacts/TestUniswapV3Factory.json", "test/generated-artifacts/TestUniswapV3Feature.json", "test/generated-artifacts/TestUniswapV3Pool.json", diff --git a/packages/protocol-utils/src/revert-errors/native_orders.ts b/packages/protocol-utils/src/revert-errors/native_orders.ts index 76ff4f9339..c7c8de1485 100644 --- a/packages/protocol-utils/src/revert-errors/native_orders.ts +++ b/packages/protocol-utils/src/revert-errors/native_orders.ts @@ -49,20 +49,6 @@ export class OrderNotSignedByMakerError extends RevertError { } } -export class OrderNotSignedByTakerError extends RevertError { - constructor(orderHash?: string, signer?: string, taker?: string) { - super( - 'OrderNotSignedByTakerError', - 'OrderNotSignedByTakerError(bytes32 orderHash, address signer, address taker)', - { - orderHash, - signer, - taker, - }, - ); - } -} - export class InvalidSignerError extends RevertError { constructor(maker?: string, signer?: string) { super('InvalidSignerError', 'InvalidSignerError(address maker, address signer)', { @@ -152,7 +138,6 @@ const types = [ OrderNotFillableByOriginError, OrderNotFillableError, OrderNotSignedByMakerError, - OrderNotSignedByTakerError, OrderNotFillableBySenderError, OrderNotFillableByTakerError, CancelSaltTooLowError,