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:
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.
To break this down further, the pull request to add this functionality breaks down into four major sets of changes:
- Allow
executeMetaTransaction
to pay out multiple fees by modifying the data structureMetaTransactionData
to add an array ofMetaTransactionFeeData
. To accomplish this, we created a newMetaTransactionsFeatureV2
contract andMetaTransactionV2Data
struct (copied from the V1MetaTransactionsFeature
andMetaTransactionData
) and added code to_executeMetaTransactionPrivate
to pay out these fees in the specifiedfeeToken
. This change also required the creation of theLibMetaTransactionsV2Storage
contract to create a new storage bucket to store the block numbers for executed V2 MetaTransactions. - Clean up
MetaTransactionsFeatureV2
andMetaTransactionV2Data
by removing fields and corresponding logic that are no longer needed from theMetaTransactionV2Data
struct:minGasPrice
maxGasPrice
value
feeAmount
(unneeded due to the addition of the new fees array)
- Add four new selectors to
_executeMetaTransactionPrivate
inMetaTransactionsFeatureV2
to allow for calls intoMultiplexFeature
along with functions to create and execute the calls:_executeMultiplexBatchSellTokenForTokenCall
_executeMultiplexBatchSellTokenForEthCall
_executeMultiplexMultiHopSellTokenForTokenCall
_executeMultiplexMultiHopSellTokenForEthCall
- Modify Multiplex to account for the scenario where the
MultiplexFeature
is entered viaMetaTransactionsFeatureV2
. Specifically, we refactor references tomsg.sender
into new parametersBatchSellParams.payer
andMultiHopSellParams.payer
, which we set tostate.mtx.signer
for metatransactions. This is because for a metatransaction,msg.sender
will be some third party andstate.mtx.signer
will be the taker, whereasmsg.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 makerOtcOrder
- i.e. 30% of the trade fills through
- The ability to split a given trade amount between multiple liquidity sources at varying weights.
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 tradeWETH→USDC→DAI
to access a more liquidWETH→USDC
market, and the lower slippage of stable→stable trades.
- i.e. A user want to trade
- The ability to chain a sequence of liquidity sources together to amplify liquidity.
Multiplex
and MultiHop
can also be chained together, potentially in any combination through the following functions:
_nestedMultiHopSell
in_multiplexBatchSell
- embed a
multihop
within amultiplex
- i.e.
WETH→DAI
swap :- 50%
OtcOrder
(WETH→DAI) - 50%
MultiHop
(UniswapV3 WETH→USDC
→UniswapV3 USDC→DAI
)
- 50%
- i.e.
- embed a
_nestedBatchSell
in_multiplesMultiHopSell
- embed a
multiplex
within amultihop
- i.e.
WETH→DAI
swap :- 100%
OtcOrder
(WETH→USDC) - 100%
Multiplex
( 50%OtcOrder
, 50%OtcOrder
)
- 100%
- i.e.
- embed a
Below is a map of the functions and the paths each one can take.
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 _multiHopSellOtcOrder
to the if statement within _executeMultiHopSell
within _multiplexMultiHopSellPrivate
.
The functionality of _multiHopSellOtcOrder
is as follows:
- If
_multiHopSellOtcOrder
is the FIRST subcall within amultiplexMultiHopSellPrivate
:- set the
taker
of theOtcOrder
to thestate.from
(determined by_computeHopTarget
)- in this case
state.from = params.payer
- in this case
- set the
recipient
of theOtcOrder
tostate.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 theOtcOrder
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.
- we want to have the
- in this case
- set the
- If
_multiHopSellOtcOrder
is the not the FIRST or LAST subcall within amultiplexMultiHopSellPrivate
:- Set the
taker
of theOtcOrder
to thestate.from
(determined by_computeHopTarget
)- in this case
state.from = address(this)
- we want to pull tokens from the
ExchangeProxy
balance to fill theOtcOrder
as previous hops will have built up theExchangeProxy
balance.
- we want to pull tokens from the
- in this case
- Set the
recipient
of theOtcOrder
tostate.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 theOtcOrder
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.
- we want to have the
- in this case
- Set the
- If
_multiHopSellOtcOrder
is the LAST subcall within amultiplexMultiHopSellPrivate
:- set the
taker
of theOtcOrder
to thestate.from
(determined by_computeHopTarget
)- in this case
state.from = address(this)
- we want to pull tokens from the
ExchangeProxy
balance to fill theOtcOrder
as previous hops will have built up theExchangeProxy
balance.
- we want to pull tokens from the
- in this case
- Set the
recipient
of theOtcOrder
tostate.to
(determined by_computeHopTarget
)- in this case
state.from = params.payer
- we want to have the
params.payer
get the resulting fill from theOtcOrder
to finish off the trade.params.payer
will always bemsg.sender
- we want to have the
- in this case
- set the
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 alwaysmsg.sender
- we want to use the balance of the
params.useSelfbalance == true
- we want to use the balance of the
ExchangeProxy
akaaddress(this)
- we want to use the balance of the
- If a
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 ofaddress(this)
.params.useSelfbalance
is ignored here as we always want to use theExchangeProxy
balance for intermediate trades.
- If a
i == subcalls.length
- If a
MultiplexSubcall.OTC
is the last in the list of subcalls, we want to use the balance ofaddress(this)
and set the hoptarget
toparams.recipient
params.recipient
can be any contract or EOA.
params.useSelfbalance
is ignored here as we always want to use theExchangeProxy
balance to fill the final leg of the trade.
- If a
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