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.
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 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.
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)
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
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.
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:
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.idMultiplexSubcall.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 _computeHopTargetif 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.
ZEIP for MetaTransactionsFeatureV2 and Multiplex Upgrades
Summary & Motivation
This ZEIP introduces 2 sets of changes
(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: https://github.com/0xProject/protocol/pull/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:
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.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)_executeMetaTransactionPrivate
inMetaTransactionsFeatureV2
to allow for calls intoMultiplexFeature
along with functions to create and execute the calls:_executeMultiplexBatchSellTokenForTokenCall
_executeMultiplexBatchSellTokenForEthCall
_executeMultiplexMultiHopSellTokenForTokenCall
_executeMultiplexMultiHopSellTokenForEthCall
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
UniswapV3
, and 70% of the trade fills through a market makerOtcOrder
MultiHop
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.Multiplex
andMultiHop
can also be chained together, potentially in any combination through the following functions:_nestedMultiHopSell
in_multiplexBatchSell
multihop
within amultiplex
WETH→DAI
swap :OtcOrder
(WETH→DAI)MultiHop
(UniswapV3 WETH→USDC
→UniswapV3 USDC→DAI
)_nestedBatchSell
in_multiplesMultiHopSell
multiplex
within amultihop
WETH→DAI
swap :OtcOrder
(WETH→USDC)Multiplex
( 50%OtcOrder
, 50%OtcOrder
)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 fillOtcOrders
through themultiplexMultiHopSellPrivate
path. To achieve this functionality we are adding thesubcall.id
MultiplexSubcall.OTC
and internal function call_multiHopSellOtcOrder
to the if statement within_executeMultiHopSell
within_multiplexMultiHopSellPrivate
.The functionality of
_multiHopSellOtcOrder
is as follows:_multiHopSellOtcOrder
is the FIRST subcall within amultiplexMultiHopSellPrivate
:taker
of theOtcOrder
to thestate.from
(determined by_computeHopTarget
)state.from = params.payer
recipient
of theOtcOrder
tostate.to
(determined by_computeHopTarget
)state.from = address(this)
ExchangeProxy
get the tokens from the resulting fill of theOtcOrder
so we can use its own balance to fill the next subcall._multiHopSellOtcOrder
is the not the FIRST or LAST subcall within amultiplexMultiHopSellPrivate
:taker
of theOtcOrder
to thestate.from
(determined by_computeHopTarget
)state.from = address(this)
ExchangeProxy
balance to fill theOtcOrder
as previous hops will have built up theExchangeProxy
balance.recipient
of theOtcOrder
tostate.to
(determined by_computeHopTarget
)state.from = address(this)
ExchangeProxy
get the tokens from the resulting fill of theOtcOrder
so we can use its own balance to fill the next subcall._multiHopSellOtcOrder
is the LAST subcall within amultiplexMultiHopSellPrivate
:taker
of theOtcOrder
to thestate.from
(determined by_computeHopTarget
)state.from = address(this)
ExchangeProxy
balance to fill theOtcOrder
as previous hops will have built up theExchangeProxy
balance.recipient
of theOtcOrder
tostate.to
(determined by_computeHopTarget
)state.from = params.payer
params.payer
get the resulting fill from theOtcOrder
to finish off the trade.params.payer
will always bemsg.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 theExchangeProxy
balance during aMultiplex
. To accurately determine which balance to use, we needed to add thesubcall.id == MultiplexSubcall.OTC
case to the_computeHopTarget
if
statement.This function has 3 main states (in the context of
MultiplexSubcall.OTC
):i == 0
MultiplexSubcall.OTC
is first in the list of subcallsparams.useSelfbalance == false
params.payer
params.payer
is alwaysmsg.sender
params.useSelfbalance == true
ExchangeProxy
akaaddress(this)
i != 0 && i < subcalls.length
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.i == subcalls.length
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.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