From 7f240ffc8918d4253ead352e1c0cac939ebdeb56 Mon Sep 17 00:00:00 2001 From: Michael Zhu Date: Tue, 4 May 2021 18:27:18 -0700 Subject: [PATCH] Update Multiplex and NativeOrders contracts to fill RFQ orders using ETH --- .../src/features/MetaTransactionsFeature.sol | 2 +- .../src/features/MultiplexFeature.sol | 71 ++++++++++----- .../src/features/NativeOrdersFeature.sol | 3 +- .../interfaces/INativeOrdersFeature.sol | 18 ++++ .../native_orders/NativeOrdersSettlement.sol | 90 +++++++++++++++---- .../src/fixins/FixinProtocolFees.sol | 4 +- .../src/fixins/FixinTokenSpender.sol | 54 +++++++++++ 7 files changed, 201 insertions(+), 41 deletions(-) diff --git a/contracts/zero-ex/contracts/src/features/MetaTransactionsFeature.sol b/contracts/zero-ex/contracts/src/features/MetaTransactionsFeature.sol index 07a0ca4bb7..1325eeb4fa 100644 --- a/contracts/zero-ex/contracts/src/features/MetaTransactionsFeature.sol +++ b/contracts/zero-ex/contracts/src/features/MetaTransactionsFeature.sol @@ -478,7 +478,7 @@ contract MetaTransactionsFeature is /// @dev Execute a `INativeOrdersFeature.fillRfqOrder()` meta-transaction call /// by decoding the call args and translating the call to the internal - /// `INativeOrdersFeature._fillRfqOrder()` variant, where we can overrideunimpleme + /// `INativeOrdersFeature._fillRfqOrder()` variant, where we can override /// the taker address. function _executeFillRfqOrderCall(ExecuteState memory state) private diff --git a/contracts/zero-ex/contracts/src/features/MultiplexFeature.sol b/contracts/zero-ex/contracts/src/features/MultiplexFeature.sol index a4d90ae987..1a80ed6d02 100644 --- a/contracts/zero-ex/contracts/src/features/MultiplexFeature.sol +++ b/contracts/zero-ex/contracts/src/features/MultiplexFeature.sol @@ -55,7 +55,7 @@ contract MultiplexFeature is /// @dev Name of this feature. string public constant override FEATURE_NAME = "MultiplexFeature"; /// @dev Version of this feature. - uint256 public immutable override FEATURE_VERSION = _encodeVersion(1, 0, 1); + uint256 public immutable override FEATURE_VERSION = _encodeVersion(1, 1, 0); /// @dev The WETH token contract. IEtherTokenV06 private immutable weth; @@ -231,26 +231,55 @@ contract MultiplexFeature is ); 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 {} + if (fillData.inputToken.isTokenETH()) { + require( + order.takerToken == weth && + order.makerToken == fillData.outputToken, + "MultiplexFeature::_batchFill/RFQ_ORDER_INVALID_TOKENS" + ); + inputTokenAmount = LibSafeMathV06.min256( + inputTokenAmount, + remainingEth + ); + // Try filling the RFQ order. Swallows reverts. + try + INativeOrdersFeature(address(this))._fillRfqOrderWithEth + {value: inputTokenAmount} + ( + 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); + remainingEth = remainingEth.safeSub(takerTokenFilledAmount); + } catch {} + } else { + 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, diff --git a/contracts/zero-ex/contracts/src/features/NativeOrdersFeature.sol b/contracts/zero-ex/contracts/src/features/NativeOrdersFeature.sol index bb51defe36..1a673fe160 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, @@ -69,6 +69,7 @@ contract NativeOrdersFeature is _registerFeatureFunction(this.fillOrKillRfqOrder.selector); _registerFeatureFunction(this._fillLimitOrder.selector); _registerFeatureFunction(this._fillRfqOrder.selector); + _registerFeatureFunction(this._fillRfqOrderWithEth.selector); _registerFeatureFunction(this.cancelLimitOrder.selector); _registerFeatureFunction(this.cancelRfqOrder.selector); _registerFeatureFunction(this.batchCancelLimitOrders.selector); diff --git a/contracts/zero-ex/contracts/src/features/interfaces/INativeOrdersFeature.sol b/contracts/zero-ex/contracts/src/features/interfaces/INativeOrdersFeature.sol index b85130b0a8..6a138dc4b2 100644 --- a/contracts/zero-ex/contracts/src/features/interfaces/INativeOrdersFeature.sol +++ b/contracts/zero-ex/contracts/src/features/interfaces/INativeOrdersFeature.sol @@ -137,6 +137,24 @@ interface INativeOrdersFeature is external returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount); + /// @dev Fill an RFQ order using ETH. Taker token must be WETH. + /// 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. + /// @return takerTokenFilledAmount How much maker token was filled. + /// @return makerTokenFilledAmount How much maker token was filled. + function _fillRfqOrderWithEth( + LibNativeOrder.RfqOrder calldata order, + LibSignature.Signature calldata signature, + uint128 takerTokenFillAmount, + address taker + ) + external + payable + returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount); + /// @dev Cancel a single limit order. The caller must be the maker or a valid order signer. /// Silently succeeds if the order has already been cancelled. /// @param order The limit order. 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 ea7e832a44..b04ad1efaa 100644 --- a/contracts/zero-ex/contracts/src/features/native_orders/NativeOrdersSettlement.sol +++ b/contracts/zero-ex/contracts/src/features/native_orders/NativeOrdersSettlement.sol @@ -66,6 +66,8 @@ abstract contract NativeOrdersSettlement is uint128 takerTokenFillAmount; // How much taker token amount has already been filled in this order. uint128 takerTokenFilledAmount; + + bool useEthBalance; } /// @dev Params for `_fillLimitOrderPrivate()` @@ -158,7 +160,8 @@ abstract contract NativeOrdersSettlement is order, signature, takerTokenFillAmount, - msg.sender + msg.sender, + false ); (takerTokenFilledAmount, makerTokenFilledAmount) = ( results.takerTokenFilledAmount, @@ -224,7 +227,8 @@ abstract contract NativeOrdersSettlement is order, signature, takerTokenFillAmount, - msg.sender + msg.sender, + false ); // Must have filled exactly the amount requested. if (results.takerTokenFilledAmount < takerTokenFillAmount) { @@ -273,9 +277,7 @@ abstract contract NativeOrdersSettlement is ); } - /// @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. @@ -298,7 +300,8 @@ abstract contract NativeOrdersSettlement is order, signature, takerTokenFillAmount, - taker + taker, + false ); (takerTokenFilledAmount, makerTokenFilledAmount) = ( results.takerTokenFilledAmount, @@ -306,6 +309,49 @@ abstract contract NativeOrdersSettlement is ); } + /// @dev Fill an RFQ order using ETH. Taker token must be WETH. + /// 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. + /// @return takerTokenFilledAmount How much maker token was filled. + /// @return makerTokenFilledAmount How much maker token was filled. + function _fillRfqOrderWithEth( + LibNativeOrder.RfqOrder memory order, + LibSignature.Signature memory signature, + uint128 takerTokenFillAmount, + address taker + ) + public + virtual + payable + onlySelf + returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount) + { + require( + msg.value == takerTokenFillAmount, + "NativeOrdersFeature/ETH_FILL_AMOUNT_MISMATCH" + ); + FillNativeOrderResults memory results = + _fillRfqOrderPrivate( + order, + signature, + takerTokenFillAmount, + taker, + true + ); + (takerTokenFilledAmount, makerTokenFilledAmount) = ( + results.takerTokenFilledAmount, + results.makerTokenFilledAmount + ); + if (takerTokenFilledAmount < msg.value) { + uint256 refundAmount = msg.value.safeSub(takerTokenFilledAmount); + (bool success,) = msg.sender.call{value: refundAmount}(""); + require(success, "NativeOrdersFeature/ETH_REFUND_FAILED"); + } + } + /// @dev Mark what tx.origin addresses are allowed to fill an order that /// specifies the message sender as its txOrigin. /// @param origins An array of origin addresses to update. @@ -393,7 +439,8 @@ abstract contract NativeOrdersSettlement is makerAmount: params.order.makerAmount, takerAmount: params.order.takerAmount, takerTokenFillAmount: params.takerTokenFillAmount, - takerTokenFilledAmount: orderInfo.takerTokenFilledAmount + takerTokenFilledAmount: orderInfo.takerTokenFilledAmount, + useEthBalance: false }) ); @@ -437,7 +484,8 @@ abstract contract NativeOrdersSettlement is LibNativeOrder.RfqOrder memory order, LibSignature.Signature memory signature, uint128 takerTokenFillAmount, - address taker + address taker, + bool useEthBalance ) private returns (FillNativeOrderResults memory results) @@ -498,7 +546,8 @@ abstract contract NativeOrdersSettlement is makerAmount: order.makerAmount, takerAmount: order.takerAmount, takerTokenFillAmount: takerTokenFillAmount, - takerTokenFilledAmount: orderInfo.takerTokenFilledAmount + takerTokenFilledAmount: orderInfo.takerTokenFilledAmount, + useEthBalance: useEthBalance }) ); @@ -549,13 +598,22 @@ abstract contract NativeOrdersSettlement is // function if the order is cancelled. settleInfo.takerTokenFilledAmount.safeAdd128(takerTokenFilledAmount); - // Transfer taker -> maker. - _transferERC20Tokens( - settleInfo.takerToken, - settleInfo.taker, - settleInfo.maker, - takerTokenFilledAmount - ); + if (settleInfo.useEthBalance) { + require( + settleInfo.takerToken == WETH, + "NativeOrdersFeature/USE_ETH_BALANCE_INVALID" + ); + WETH.deposit{value: takerTokenFilledAmount}(); + WETH.transfer(settleInfo.maker, takerTokenFilledAmount); + } else { + // Transfer taker -> maker. + _transferERC20Tokens( + settleInfo.takerToken, + settleInfo.taker, + settleInfo.maker, + takerTokenFilledAmount + ); + } // Transfer maker -> taker. _transferERC20Tokens( diff --git a/contracts/zero-ex/contracts/src/fixins/FixinProtocolFees.sol b/contracts/zero-ex/contracts/src/fixins/FixinProtocolFees.sol index 6437b27a97..1b2b4e15d9 100644 --- a/contracts/zero-ex/contracts/src/fixins/FixinProtocolFees.sol +++ b/contracts/zero-ex/contracts/src/fixins/FixinProtocolFees.sol @@ -32,12 +32,12 @@ abstract contract FixinProtocolFees { /// @dev The protocol fee multiplier. uint32 public immutable PROTOCOL_FEE_MULTIPLIER; + /// @dev The WETH token contract. + IEtherTokenV06 internal immutable WETH; /// @dev The `FeeCollectorController` contract. FeeCollectorController private immutable FEE_COLLECTOR_CONTROLLER; /// @dev Hash of the fee collector init code. bytes32 private immutable FEE_COLLECTOR_INIT_CODE_HASH; - /// @dev The WETH token contract. - IEtherTokenV06 private immutable WETH; /// @dev The staking contract. IStaking private immutable STAKING; diff --git a/contracts/zero-ex/contracts/src/fixins/FixinTokenSpender.sol b/contracts/zero-ex/contracts/src/fixins/FixinTokenSpender.sol index e411f58eb8..c7a9058dd4 100644 --- a/contracts/zero-ex/contracts/src/fixins/FixinTokenSpender.sol +++ b/contracts/zero-ex/contracts/src/fixins/FixinTokenSpender.sol @@ -87,6 +87,60 @@ abstract contract FixinTokenSpender { } } + /// @dev Transfers ERC20 tokens from `address(this)` to `to`. + /// @param token The token to spend. + /// @param to The recipient of the tokens. + /// @param amount The amount of `token` to transfer. + function _transferERC20Tokens( + IERC20TokenV06 token, + address to, + uint256 amount + ) + internal + { + require(address(token) != address(this), "FixinTokenSpender/CANNOT_INVOKE_SELF"); + + assembly { + let ptr := mload(0x40) // free memory pointer + + // selector for transfer(address,uint256) + mstore(ptr, 0xa9059cbb00000000000000000000000000000000000000000000000000000000) + mstore(add(ptr, 0x04), and(to, ADDRESS_MASK)) + mstore(add(ptr, 0x24), amount) + + let success := call( + gas(), + and(token, ADDRESS_MASK), + 0, + ptr, + 0x44, + ptr, + 32 + ) + + let rdsize := returndatasize() + + // Check for ERC20 success. ERC20 tokens should return a boolean, + // but some don't. We accept 0-length return data as success, or at + // least 32 bytes that starts with a 32-byte boolean true. + success := and( + success, // call itself succeeded + or( + iszero(rdsize), // no return data, or + and( + iszero(lt(rdsize, 32)), // at least 32 bytes + eq(mload(ptr), 1) // starts with uint256(1) + ) + ) + ) + + if iszero(success) { + returndatacopy(ptr, 0, rdsize) + revert(ptr, rdsize) + } + } + } + /// @dev Gets the maximum amount of an ERC20 token `token` that can be /// pulled from `owner` by this address. /// @param token The token to spend.