0xProject/ZEIPs

MetaTransactionsFeatureV2 and Multiplex Upgrades

Opened this issue · 0 comments

ZEIP for MetaTransactionsFeatureV2 and Multiplex Upgrades

Summary & Motivation

This ZEIP introduces 2 sets of changes

  • Tx Relay related changes
  • Multiplex changes allowing for OTC orders to be filled through multihop

(I) The Tx Relay related changes will improve the all-in gas costs of 0x Labs’ Tx Relay product, which is currently in beta with Robinhood. The changes will bring the product towards a general launch.

Tx Relay offers users the ability to complete transactions - swaps and approvals - without holding a chain’s native token (ie: gasless transactions). These gasless transactions work by ‘wrapping’ the gas costs into the all-in price the user pays, and then leveraging the metatransaction feature of the 0x protocol. Via metatransactions, 0x Labs pays the actual native token costs of the transaction, and recoups the gas costs by collecting a portion of the tokens involved in the actual swap, onchain. Tx Relay also offers integrators the ability to collect fees on-chain.

The all-in gas costs of Tx Relay, however, are close to 350k, which makes the all-in pricing for Tx Relay trades unattractive. The changes proposed in this ZEIP will bring the all-in gas costs for Tx Relay closer to 225k.

(II) The multiplex changes will improve pricing for end-users of the 0x API by allowing professional market-makers to participate in multihop trades. The latest version of the 0x Labs RFQ system uses the 0x protocol OTC Order format, which has superior gas efficiency to the legacy RFQ Order format. However, the current multiplex implementation in the protocol does not allow OTC Orders to be included in multihop trades.

This implementation excludes professional-market makers from multihop trades, which we estimate compose nearly 50% of 0x API activity. On single-hop trades, market-makers typically fill 50% of orders in popular pairs, such as WETH-USDC. By including professional market makers, using OTC Orders, in multihop trades, we stand to improve the overall pricing of the 0x API.

Type

CORE

Github Pull Request: 0xProject/protocol#665

Specifications

Tx-Relay related changes

The goal for these changes is to reduce the gas usage for gasless metatransactions and to add the ability to transfer multiple fees inside of MetaTransactionsFeature.

The diagram below shows the current state of gasless metatransactions:

metaTxn1

This flow is particularly gas heavy because the sellToken is transferred to the flash wallet using the TransformERC20 feature, and then manipulated via the 0x transformers.

With the proposed changes, we will enable gasless metatransactions to use multiplex to transfer the sellToken directly to the AMM or access RFQ directly. This will result in a flow that uses a lot less gas. The diagram below shows the token flow through this proposed pathway, along with how we can transfer the sellToken directly as fees to integrators.

metaTxn2

To break this down further, the pull request to add this functionality breaks down into four major sets of changes:

  1. Allow executeMetaTransaction to pay out multiple fees by modifying the data structure MetaTransactionData to add an array of MetaTransactionFeeData. To accomplish this, we created a new MetaTransactionsFeatureV2 contract and MetaTransactionV2Data struct (copied from the V1 MetaTransactionsFeature and MetaTransactionData) and added code to _executeMetaTransactionPrivate to pay out these fees in the specified feeToken. This change also required the creation of the LibMetaTransactionsV2Storage contract to create a new storage bucket to store the block numbers for executed V2 MetaTransactions.
  2. Clean up MetaTransactionsFeatureV2 and MetaTransactionV2Data by removing fields and corresponding logic that are no longer needed from the MetaTransactionV2Data struct:
    • minGasPrice
    • maxGasPrice
    • value
    • feeAmount (unneeded due to the addition of the new fees array)
  3. Add four new selectors to _executeMetaTransactionPrivate in MetaTransactionsFeatureV2 to allow for calls into MultiplexFeature along with functions to create and execute the calls:
    • _executeMultiplexBatchSellTokenForTokenCall
    • _executeMultiplexBatchSellTokenForEthCall
    • _executeMultiplexMultiHopSellTokenForTokenCall
    • _executeMultiplexMultiHopSellTokenForEthCall
  4. Modify Multiplex to account for the scenario where the MultiplexFeature is entered via MetaTransactionsFeatureV2. Specifically, we refactor references to msg.sender into new parameters BatchSellParams.payer and MultiHopSellParams.payer, which we set to state.mtx.signer for metatransactions. This is because for a metatransaction, msg.sender will be some third party and state.mtx.signer will be the taker, whereas msg.sender will often times be the taker in normal Multiplex flows.

File Collections

File

contracts/zero-ex/contracts/src/IZeroEx.sol
contracts/zero-ex/contracts/src/features/MetaTransactionsFeatureV2.sol
contracts/zero-ex/contracts/src/features/UniswapV3Feature.sol
contracts/zero-ex/contracts/src/features/interfaces/IMetaTransactionsFeatureV2.sol
contracts/zero-ex/contracts/src/features/interfaces/IMultiplexFeature.sol
contracts/zero-ex/contracts/src/features/interfaces/IUniswapV3Feature.sol
contracts/zero-ex/contracts/src/features/multiplex/MultiplexFeature.sol
contracts/zero-ex/contracts/src/features/multiplex/MultiplexLiquidityProvider.sol
contracts/zero-ex/contracts/src/features/multiplex/MultiplexOtc.sol
contracts/zero-ex/contracts/src/features/multiplex/MultiplexRfq.sol
contracts/zero-ex/contracts/src/features/multiplex/MultiplexTransformERC20.sol
contracts/zero-ex/contracts/src/features/multiplex/MultiplexUniswapV2.sol
contracts/zero-ex/contracts/src/features/multiplex/MultiplexUniswapV3.sol
contracts/zero-ex/contracts/src/storage/LibMetaTransactionsV2Storage.sol
contracts/zero-ex/contracts/src/storage/LibStorage.sol

Multiplex changes allowing OTC orders to be filled through multihop

Currently the Exchange Proxy MultiplexFeature allows the exchange proxy to fill trades that need to pass through multiple liquidity sources. MultiplexFeature defines two overarching trade types.

  • Multiplex
    • The ability to split a given trade amount between multiple liquidity sources at varying weights.
      • i.e. 30% of the trade fills through UniswapV3, and 70% of the trade fills through a market maker OtcOrder
  • MultiHop
    • The ability to chain a sequence of liquidity sources together to amplify liquidity.
      • i.e. A user want to trade WETH→DAI . At higher trade sizes, splitting orders across different liquidity pools allows us to give a better price. We can trade WETH→USDC→DAI to access a more liquid WETH→USDC market, and the lower slippage of stable→stable trades.

Multiplex and MultiHop can also be chained together, potentially in any combination through the following functions:

  • _nestedMultiHopSell in _multiplexBatchSell
    • embed a multihop within a multiplex
      • i.e. WETH→DAI swap :
        • 50% OtcOrder (WETH→DAI)
        • 50% MultiHop ( UniswapV3 WETH→USDCUniswapV3 USDC→DAI)
  • _nestedBatchSell in _multiplesMultiHopSell
    • embed a multiplex within a multihop
      • i.e. WETH→DAI swap :
        • 100% OtcOrder (WETH→USDC)
        • 100% Multiplex ( 50% OtcOrder, 50% OtcOrder)

Below is a map of the functions and the paths each one can take.

multiplex1

multiplex2

The changes proposed for the multiplexFeature are to allow the feature to be able to fill OtcOrders through the multiplexMultiHopSellPrivate path. To achieve this functionality we are adding the subcall.id MultiplexSubcall.OTC and internal function call _multiHopSellOtcOrderto the if statement within _executeMultiHopSell within _multiplexMultiHopSellPrivate.

The functionality of _multiHopSellOtcOrder is as follows:

  • If _multiHopSellOtcOrder is the FIRST subcall within a multiplexMultiHopSellPrivate :
    • set the taker of the OtcOrder to the state.from (determined by _computeHopTarget)
      • in this case state.from = params.payer
    • set the recipient of the OtcOrder to state.to (determined by _computeHopTarget)
      • in this case state.from = address(this)
        • we want to have the ExchangeProxy get the tokens from the resulting fill of the OtcOrder so we can use its own balance to fill the next subcall.
        • doing this allows us to hop through multiple tokens without the user having to approve each token.
  • If _multiHopSellOtcOrder is the not the FIRST or LAST subcall within a multiplexMultiHopSellPrivate :
    • Set the taker of the OtcOrder to the state.from (determined by _computeHopTarget)
      • in this case state.from = address(this)
        • we want to pull tokens from the ExchangeProxy balance to fill the OtcOrder as previous hops will have built up the ExchangeProxy balance.
    • Set the recipient of the OtcOrder to state.to (determined by _computeHopTarget)
      • in this case state.from = address(this)
        • we want to have the ExchangeProxy get the tokens from the resulting fill of the OtcOrder so we can use its own balance to fill the next subcall.
        • doing this allows us to hop through multiple tokens without the user having to approve each token.
  • If _multiHopSellOtcOrder is the LAST subcall within a multiplexMultiHopSellPrivate :
    • set the taker of the OtcOrder to the state.from (determined by _computeHopTarget)
      • in this case state.from = address(this)
        • we want to pull tokens from the ExchangeProxy balance to fill the OtcOrder as previous hops will have built up the ExchangeProxy balance.
    • Set the recipient of the OtcOrder to state.to (determined by _computeHopTarget)
      • in this case state.from = params.payer
        • we want to have the params.payer get the resulting fill from the OtcOrder to finish off the trade.
          • params.payer will always be msg.sender

The new functionality of _computeHopTarget is as follows:

We want to enforce a certain order in which to use the params.payer balance, and when to use the ExchangeProxy balance during a Multiplex. To accurately determine which balance to use, we needed to add the subcall.id == MultiplexSubcall.OTC case to the _computeHopTarget if statement.

This function has 3 main states (in the context of MultiplexSubcall.OTC):

  • i == 0
    • If a MultiplexSubcall.OTC is first in the list of subcalls
    • params.useSelfbalance == false
      • we want to use the balance of the params.payer
        • params.payer is always msg.sender
    • params.useSelfbalance == true
      • we want to use the balance of the ExchangeProxy aka address(this)
  • i != 0 && i < subcalls.length
    • If a MultiplexSubcall.OTC is not the first or the last in the list of subcalls, we want to use the balance of address(this).
      • params.useSelfbalance is ignored here as we always want to use the ExchangeProxy balance for intermediate trades.
  • i == subcalls.length
    • If a MultiplexSubcall.OTC is the last in the list of subcalls, we want to use the balance of address(this) and set the hop target to params.recipient
      • params.recipientcan be any contract or EOA.
    • params.useSelfbalance is ignored here as we always want to use the ExchangeProxy balance to fill the final leg of the trade.

File Collections

File

contracts/zero-ex/contracts/src/features/MultiplexFeature.sol
contracts/zero-ex/contracts/src/features/MultiplexOtc.sol

Designated Team

0x Labs

Audits

The change was audited by ABDK. Final report is available below abdk_audit