SIP 120: TWAP Exchange Function Source

AuthorKain Warwick, Andre Cronje, Justin Moses, Brett Sun
Discussions-Tohttps://research.synthetix.io/t/sip-120-keep3r-twap-exchange-function/364
StatusFeasibility
Created2021-02-24

Implementors

Justin Moses (@justinjmoses), Brett Sun (@sohkai)

Simple Summary

Provide a new exchange function allowing users to atomically exchange assets without fee reclamation by pricing synths via a combination of Chainlink and DEX TWAP oracles (Uniswap V3).

Abstract

This SIP proposes a new parallel exchange function that enables atomic transactions between synths by eschewing the current fee reclamation mechanism. The price selection method is designed to be resistant against both frontrunning oracle latency and flashloan attacks by sourcing prices from Chainlink and DEX TWAP oracles (Uniswap V3).

Motivation

Fee reclamation prevents atomic transactions between synths, degrading the composability of synths in the wider DeFi ecosystem. An attempt at improving fee reclamation came with SIP-89 (Virtual Synths), which was designed to enable atomic transactions between synths without removing the frontrunning protection provided by fee reclamation. The intent was to enable cross-asset swaps between AMM pools within protocols like Curve by tokenizing the claim to an exchange requiring fee reclamation. While virtual synths have now been enabled and have proved fairly successful, they continue to present significant UX friction for implementers and users due to a second transaction still being required to settle the exchange.

On-chain TWAP price oracles were introduced with Uniswap V2, expanded upon in Uniswap V3, and have been increasingly utilized in DeFi. For Synthetix, they present an opportunity to utilize an alternative source of prices for fully atomic, one transaction exchanges of synths. These price oracles are difficult to technically frontrun, as you’d have to frontrun an active market, and as a result do not expose clean, “pure profit” frontrunning opportunities akin to those based on oracle latency. Furthermore, they have been carefully constructed to be resilient to manipulation from both flashloan and longer-window attacks.

Initially, TWAP prices will be sourced directly from Uniswap V3 pools with the expectation that the Synthetix system will only query large-liquidity markets such as WETH/WBTC and WETH/USDC. In the future, the oracle sources may be adjusted based on the assets enabled for atomic exchanges and their market conditions.

Specification

Overview

To facilitate atomic exchanges, the Synthetix and Exchanger contracts will expose a new function exchangeAtomically(). This new function will act in a similar manner to the current Exchanger.exchange() flow but with primary differences in:

  1. The execution price, detailed below
  2. Not having a fee reclamation window and therefore not minting any virtual synths
  3. Restrictions on source and destination synths, configurable by SCCPs

Unlike Exchanger.exchange(), which relies solely on Chainlink oracles, the execution price for atomic exchanges is selected between the prices given by Chainlink oracles and Uniswap V3 pools. Two distinct prices are considered, P_CLBUF and P_TWAP, with the selected execution price being the one that outputs the minimum amount of destination synths:

  • P_CLBUF: current Chainlink price, with a buffer of N bps applied against the trading direction (specified per-synth)
  • P_TWAP: current Uniswap V3 TWAP price, over a window of N seconds

P_CLBUF can be calculated internally within the current Synthetix system and P_TWAP will be provided externally via UniswapV3CrossPoolOracle (0x0F1f5A87f99f0918e6C81F16E59F3518698221Ff).

Finally, several new configuration settings are proposed:

  • SystemSettings.maxAtomicVolumePerBlock (MAX_VOLUME): the max volume for atomic exchanges accepted in a block, specified in sUSD
  • SystemSettings.atomicTwapPriceWindow (TWAP_WINDOW): the time window to use for TWAP, specified in number of seconds
  • SystemSettings.atomicEquivalentForDexPricing: a synth’s equivalent on-chain asset with higher on-chain liquidity to poll TWAP prices from. Having this specified for a synth will also allow it to be used as a source synth or destination synth in atomic exchanges.
  • SystemSettings.atomicExchangeFeeRate: the exchange fee rate to be paid to the debt pool on each atomic exchange, specified in bps and per-synth. If not set, the normal exchange fee rate will be applied.
  • SystemSettings.atomicPriceBuffer (CL_BUFFER): the buffer to be applied against the current Chainlink price in the direction detrimental to the trade, specified in bps and per-synth

A simplified proof-of-concept of this exchange mechanism can be found at this SynthetixAMM contract (sourcing from defunct Keep3r TWAP oracles).

Rationale

The most important considerations are with the price selection method. Ideally, the chosen method will strike a balance between preventing both flashloan style price attacks and frontrunning oracle latency attacks while enabling low-slippage execution of atomic synth exchanges for high-value, cross-asset swaps across multiple equivalent-asset AMM pools (e.g. Curve).

The prices of P_CLBUF and P_TWAP have their own strengths and weaknesses:

  • P_CLBUF: P_CL, the current Chainlink price, is the official “internal” price used for all other exchanges and system debt calculations. However, its update latency is easily gamed via technical frontrunning in today’s circumstances. P_CLBUF provides a buffer of N bps (CL_BUFFER) from P_CL to provide a safety net from the deviation threshold for Chainlink oracle updates. Note that CL_BUFFER is configurable per-synth and P_CLBUF is calculated using the larger CL_BUFFER of the source and destination synth.
  • P_TWAP: Uniswap V3 TWAPs are designed to only update based on the state at the beginning of the block, preventing flashloan style attacks and making longer-term manipulation costly to the attacker. However, by their construction, TWAPs always lag behind spot.

By selecting the worst price between these two at any given time, the hope is that a “good enough but not exploitable” price can be obtained for the vast majority of situations. In the remainder, there may be periods of high volatility where one price dramatically lags behind, whereby traders will likely forgo atomic execution to obtain a better price through the fee reclamation route.

To show that the price selection method of choosing the price that gives the minimum output is safe, we note various potential market and exploit situations:

  • If any price provides better output than P_CL, a trader can immediately arbitrage back through a fee reclamation exchange
  • If P_CL is about to be updated, traders will, at best, receive an output that is dampened by the CL_BUFFER rate. This essentially provides the same defense as fee reclamation, but taken in advance at a fixed rate.
  • If P_TWAP is used, a synth trader’s execution price could be negatively impacted by a price manipulation attack across multiple blocks on Uniswap. However, such attacks only grief the synth trader, not the debt pool, and come at large risk for the griefer due to the need to hold a position across multiple blocks and a lack of atomic execution (i.e. no Flashbots). This scenario would also be similar to a griefer risking capital in one or more CEXes to briefly grief participants by changing P_CL, but even costlier to run due to the TWAP period.
  • On-chain prices usually follow CEX, so a trader could “frontrun the on-chain market” at the expense of the debt pool in periods of clear market directionality. However, it could be argued that this also applies with fee reclamation, only that it requires more careful timing.

The final concern was backtested with front-running strategies over a few periods of clear market directionality, showing that traders were likely to obtain positive results–at the expense of the debt pool–in such conditions if no price dampeners were included. Adding exchange fees into the model effectively hindered most strategies from taking profit, although it was observed that the necessary rates would be different between BTC and ETH, which prompted for per-synth configuration of the CL_BUFFER and exchange fee rates.

To reduce the risk of CL_BUFFER not providing adequate protection in multi-hop exchanges, this SIP proposes to initially require sUSD as the source or destination synth in an atomic exchange. For example, exchanging between sETH and sBTC involves two reads from Chainlink (ETH:USD and BTC:USD), increasing the potential for oracle latency abuse when updates to both prices are expected.

Finally, as further backstops to decrease the risk associated with this new exchange mechanism, the proposed configuration parameters allow the system to be gradually eased in by increasing per-block volume limits, asset whitelisting, and pricing-related parameter tweaks.

Technical Specification

Add the following interfaces and storage variables:

  • Exchanger.exchangeAtomically() and Synthetix.exchangeAtomically()
  • ExchangeRates.effectiveAtomicValueAndRates()
  • Exchanger.lastAtomicVolume (struct { uint64 block, uint192 volume }), SystemSettings.maxAtomicVolumePerBlock (uint256), and related setter SystemSettings.setMaxAtomicVolumePerBlock(), configurable by SCCPs
  • SystemSettings.atomicTwapPriceWindow (uint256) and related setter SystemSettings.setAtomicPriceWindow(), configurable by SCCPs
  • SystemSettings.atomicEquivalentForDexPricing (mapping (bytes32 => address)) and related setter SystemSettings.setAtomicEquivalentForDexPricing(), configurable by SCCPs
  • SystemSettings.atomicExchangeFeeRate (mapping (bytes32 => uint256)) and related setter SystemSettings.setAtomicExchangeFeeRate(), configurable by SCCPs
  • SystemSettings.atomicPriceBuffer (mapping (bytes32 => uint256)) and related setter SystemSettings.setAtomicPriceBuffer(), configurable by SCCPs

In detail, Exchanger.exchangeAtomically() will:

  1. Use the onlySynthetixorSynth modifier and other user-input related sanity checks
  2. Settle any previous fee reclamation exchanges on the source synth
  3. Select the execution price between P_CLBUF and P_TWAP via ExchangeRates.effectiveAtomicValueAndRates()
  4. Sanity check the current exchange’s Chainlink rates against the internal circuit breaker (SIP-65) as well as the obtained atomic rate against the current Chainlink rate
  5. Ensure both the source synth and destination synth can be atomically exchanged and that one of them is sUSD
  6. Update the volume counter, Exchanger.lastAtomicVolume, for the current exchange’s sUSD value and check that the per-block volume limit for atomic exchanges is not exceeded
  7. Execute the exchange by burning source synth, issuing destination synth, and collecting fees. Crucially, this step issues destination synths directly to the account executing the exchange and does not create new virtual synths, bypassing fee reclamation. Fees collected will be derived from the amount of destination synths issued at this step.
  8. If required, remit the fee with any required conversions back to sUSD (priced via internal Chainlink rate) using either the default fee rate or atomic fee rate.
  9. Update internal bookkeeping with the new exchange and debt snapshot (priced via internal Chainlink rate), and emit related events
  10. Process trading rewards

When diffed against the current Exchanger.exchange(), only steps 3 through 8 should present meaningful differences in Exchanger.exchangeAtomically()’s implementation.

Note that internal system updates arising from the exchange, e.g. updating the debt cache or circuit breaker, will continue to use the current Chainlink rate rather than the selected execution price in 3.

ExchangeRates.effectiveAtomicValueAndRates() selects the execution price by:

  1. Applying CL_BUFFER to P_CL to obtain P_CLBUF
  2. Querying the UniswapV3CrossPoolOracle contract (0xeAAFD7547B781C60c71F0854a7dA2c1FF23a7dd0) to obtain P_TWAP
  3. Outputting whichever of P_CLBUF and P_TWAP provides the minimum output

For step 2, note that due to the low liquidity of synths on Uniswap, queries to UniswapV3CrossPoolOracle require synths to be mapped to an equivalent non-synth version with high liquidity instead. This can be configured via SCCPs by calling SystemSettings.setAtomicEquivalentForDexPricing().

In the event UniswapV3CrossPoolOracle needs to be replaced, for example to use a more complicated oracle that aggregates multiple DEXes, the owner of ExchangeRates will have the ability to do so.

Test Cases

Included with implementation.

Of interest may be the price selection method, so several examples are included in this SIP.


The cases below assume no trading fees or rebates are applied. Other configuration, such as the volume limit and TWAP window, are also ignored.

On a sUSD -> sETH trade of 1000 sUSD (prices reported in sUSD:sETH):

  • Given P_TWAP of 0.01, P_CL of 0.011, and CL_BUFFER of 50bps
    • Choose 0.01 (P_TWAP) to output 10 sETH
  • Given P_TWAP of 0.01, P_CL of 0.0099, and CL_BUFFER of 50bps
    • Choose 0.0098505 (P_CLBUF) to output 9.8505 sETH
  • Given P_TWAP of 0.01, P_CL of 0.01, and CL_BUFFER of 50bps
    • Choose 0.00995 (P_CLBUF) to output 9.95 sETH
  • Given P_TWAP of 0.0099, P_CL of 0.01, and CL_BUFFER of 200bps
    • Choose 0.0098 (P_CLBUF) to output 9.8 sETH
  • Given P_TWAP of 0.0099, P_CL of 0.01, and CL_BUFFER of 0bps
    • Choose 0.0099 (P_TWAP) to output 9.9 sETH
  • Given P_TWAP of 0.01, P_CL of 0.01, and CL_BUFFER of 0bps
    • Choose 0.01 (P_TWAP/P_CLBUF) to output 10 sETH

Conversely, on a sETH -> sUSD trade of 10 sETH (prices reported in sETH:sUSD):

  • Given P_TWAP of 100, P_CL of 110, and CL_BUFFER of 50bps:
    • Choose 100 (P_TWAP) to output 1000 sUSD
  • Given P_TWAP of 100, P_CL of 99, and CL_BUFFER of 50bps
    • Choose 98.505 (P_CLBUF) to output 985.05 sUSD
  • Given P_TWAP of 100, P_CL of 100, and CL_BUFFER of 50bps
    • Choose 99.5 (P_CLBUF) to output 995 sUSD
  • Given P_TWAP of 99, P_CL of 100, and CL_BUFFER of 200bps
    • Choose 98 (P_CLBUF) to output 980 sUSD
  • Given P_TWAP of 99, P_CL of 100, and CL_BUFFER of 0bps
    • Choose 99 (P_TWAP) to output 990 sUSD
  • Given P_TWAP of 100, P_CL of 100, and CL_BUFFER of 0bps
    • Choose 100 (P_TWAP/P_CLBUF) to output 1000 sUSD

Configurable Values (Via SCCP)

Relevant only for atomic exchanges:

  • Per-block volume limit, specified in sUSD
  • TWAP time window, specified in number of seconds
  • Synth equivalents for TWAP price look ups, specified in token addresses
  • Synths allowed, specified with a synth having a mapped equivalent (above)
  • Fee override for atomic exchanges, specified in bps and per-synth
  • Price buffer against Chainlink, specified in bps and per-synth

Initially, this SIP proposes the following system configuration:

  • SystemSettings.maxAtomicVolumePerBlock: TBD
  • SystemSettings.atomicTwapPriceWindow: 1800 (i.e. 30min)
  • SystemSettings.atomicEquivalentForDexPricing (and thereby also allowing these synths to be exchanged atomically):
    • sUSD: USDC
    • sETH: WETH9
    • sBTC: WBTC
  • SystemSettings.atomicExchangeFeeRate: TBD
  • SystemSettings.atomicPriceBuffer: TBD

Historical context

SIP-120 was initially proposed with Keep3r’s TWAP oracles, whose liveliness and correctness relied on an external network of incentivized actors to provide rates of whitelisted assets from Uniswap V2 and Sushiswap liquidity pools.

However, near the end of this SIP’s initial development, Uniswap V3 was revealed and, important to this SIP, included expanded oracle functionality that allowed one to retrieve TWAP rates directly from a Uniswap V3 pool. This was a critical improvement that allows the Synthetix system to both save gas and minimize external dependencies by not relying on an external network of actors to maintain a TWAP oracle.

Copyright and related rights waived via CC0.