SIP-120: Atomic Exchange Function

Author
StatusImplemented
TypeGovernance
ImplementorJJ
ReleaseAlkaid
Discussions-Tohttps://research.synthetix.io/t/sip-120-keep3r-twap-exchange-function/364
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 oracles (Uniswap V3). This functionality will only be available on Ethereum's base layer (L1).

Abstract

This SIP proposes a new exchange function for L1 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 oracles.

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.

DEX-based oracles have come a long way in DeFi, with on-chain TWAP oracles introduced in Uniswap V2, expanded upon in Uniswap V3, and seen increasing adoption by DeFi protocols. For Synthetix, they present an opportunity to utilize an alternative source of prices for fully atomic, one transaction exchanges of synths. Used correctly, these TWAP oracles are difficult to technically frontrun, as one would have to frontrun an active market, and thereby do not expose clean, “pure profit” frontrunning opportunities akin to those based on oracle latency. Furthermore, these TWAP oracles have been carefully constructed to be resilient to manipulation from both flashloan and longer-window attacks.

Initially, the Synthetix system will source DEX-based pricing from Uniswap V3 with the expectation that only large-liquidity markets such as WETH/WBTC and WETH/USDC will be queried. The reported DEX-based price of a synth will be an aggregation of Uniswap V3's latest price, "spot", and a TWAP based on a configurable window, and then compared against the current Chainlink rate. In the future, the DEX-based price may be re-configured to include other sources or pricing models based on the assets enabled for atomic exchanging and evolving market conditions.

The proposed functionality in this SIP is intended only for L1. The current reality, and ongoing hope, of the L2 Synthetix system is to mitigate oracle latency frontrunning, and thereby avoid non-atomic transactions altogether (i.e. no fee reclamation), with the much faster transaction processing capabilities of L2.

Specification

Overview

Taken altogether, this SIP introduces three major changes to the current system:

  1. A refactoring of the Exchanger and ExchangeRates contracts, splitting each into a shared base class and environment-specific (L1) variant
  2. A new exchangeAtomically() function, implementing atomic exchanges
  3. A new IDexPriceAggregator concept, to plug in DEX-based pricing of synths

And additionally, a number of new configurable values in SystemSettings.

Environment-specific Exchanger and ExchangeRates

L1-specific functionality in Exchanger and ExchangeRates will be spun out into ExchangerWithFeeReclamationAlternatives and ExchangeRatesWithDexPricing, respectively. ExchangerWithFeeReclamationAlternatives will amalgamate and replace the current ExchangerWithVirtualSynth variant of Exchanger.

On L1, these variants will be deployed instead of the base Exchanger and ExchangeRates contracts. To the system (via AddressResolver), these variants will still be known as Exchanger and ExchangeRates.

exchangeAtomically()

To facilitate atomic exchanges, the Synthetix and ExchangerWithFeeReclamationAlternatives 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 DEX-based oracles. Two distinct prices are considered, P_CLBUF and P_DEX, 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_DEX: aggregated DEX price, over a window of N seconds (if a TWAP is considered)

P_CLBUF can be calculated internally within the current Synthetix system. P_DEX is provided externally from an oracle contract implementing IDexPriceAggregator.

IDexPriceAggregator

To source DEX-based prices, a new IDexPriceAggregator interface is introduced. Its interface consists of a single pricing function, assetToAsset():

function assetToAsset(
    address tokenIn,
    uint amountIn,
    address tokenOut,
    uint twapPeriod
) external view returns (uint amountOut);

The external oracle contract implementing IDexPriceAggregator can source prices from any DEX and use any internal measure to aggregate multiple pricing methods. A complex example of an IDexPriceAggregator oracle contract would source prices from Uniswap V3 and Sushiswap, and aggregate their spot and TWAP prices together via a liquidity-weighted mean.

Initially, this SIP proposes to use an IDexPriceAggregator that sources prices solely from Uniswap V3, returning the worst price between the "spot" and TWAP. This initial oracle has been deployed to 0xf120F029Ac143633d1942e48aE2Dfa2036C5786c (DexPriceAggregatorUniswapV3).

SystemSettings

Several new configuration settings are proposed:

  • SystemSettings.atomicMaxVolumePerBlock: the max volume for atomic exchanges accepted in a block, specified in sUSD
  • SystemSettings.atomicTwapWindow: the time window to consider for a DEX-based TWAP, specified in number of seconds
  • SystemSettings.atomicEquivalentForDexPricing: a synth's equivalent on-chain asset with higher on-chain liquidity to poll DEX-based 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 regular 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
  • SystemSettings.atomicVolatilityConsiderationWindow: the time window to evaluate whether a synth is too volatile to atomically exchange, specified in number of seconds
  • SystemSettings.atomicVolatilityUpdateThreshold: the maximum number of Chainlink updates in the consideration window before a synth is deemed too volatile to atomically exchange, specified in number of updates

Rationale

The most important considerations concern 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 exchanges for high-value, cross-asset swaps involving synths across multiple equivalent-asset AMM pools (e.g. Curve).

The prices of P_CLBUF and P_DEX 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 on L1. 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. CL_BUFFER is configurable per-synth and P_CLBUF is calculated using the larger CL_BUFFER of the source and destination synth.
  • P_DEX: P_DEX is the worst price between two separate pricing methods from Uniswap V3:
    • P_DEX_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.
    • P_DEX_SPOT: Uniswap V3 spot prices generally follow CEX spot but can be easy to manipulate via flashloan or sandwich style attacks when used improperly. The proposed IDexPriceAggregator's implementation sources spot from the prior block's observed price rather than the current block's, mitigating intra-block flashloan and sandwich style attacks. However, this measurement is still subject to short-term volatility, such as a large trade occurring in the prior block.

By selecting the worst price between P_CLBUF, P_DEX_TWAP, and P_DEX_SPOT 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--or be prevented from atomic exchanges altogether via the volatility circuit breaker detailed below.

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 (i.e. oracle latency), 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_DEX_TWAP or P_DEX_SPOT 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.
  • 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 further decrease the risk to the debt pool during periods of clear market directionality, a volatility circuit breaker is included to automatically disable and re-enable atomic exchanges for a given synth based on its perceived volatility. This circuit breaker uses the number of Chainlink updates over the past N seconds as a proxy for volatility; if there are more updates than the configured threshold, the synth is deemed too volatile.

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

First, refactor Exchanger, ExchangerWithVirtualSynth, and ExchangeRates into their shared base versions and L1-variants.

Then, add the following interfaces and storage variables:

  • ExchangerWithFeeReclamationAlternatives.exchangeAtomically() and Synthetix.exchangeAtomically()
  • ExchangeRatesWithDexPricing.effectiveAtomicValueAndRates() and related setters to manage the IDexPriceAggregator to use
  • ExchangerWithFeeReclamationAlternatives.lastAtomicVolume (struct { uint64 time, uint192 volume }), SystemSettings.atomicMaxVolumePerBlock (uint256), and related setter SystemSettings.setAtomicMaxVolumePerBlock(), configurable by SCCPs
  • SystemSettings.atomicTwapWindow (uint256) and related setter SystemSettings.setAtomicTwapWindow(), 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
  • SystemSettings.atomicVolatilityConsiderationWindow (mapping (bytes32 => uint256)) and related setter SystemSettings.setAtomicVolatilityConsiderationWindow(), configurable by SCCPs
  • SystemSettings.atomicVolatilityUpdateThreshold (mapping (bytes32 => uint256)) and related setter SystemSettings.setAtomicVolatilityUpdateThreshold(), configurable by SCCPs

In detail, ExchangerWithFeeReclamationAlternatives.exchangeAtomically() will:

  1. Use the onlySynthetixorSynth modifier and other user-input related sanity checks
  2. Ensure the source and destination synths are not deemed too volatile
  3. Settle any previous fee reclamation exchanges on the source synth
  4. Select the execution price between P_CLBUF and P_DEX via ExchangeRatesWithDexPricing.effectiveAtomicValueAndRates()
  5. 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
  6. Ensure both the source synth and destination synth can be atomically exchanged and that one of them is sUSD
  7. Update the volume counter, ExchangerWithFeeReclamationAlternatives.lastAtomicVolume, for the current exchange’s sUSD value and ensure the per-block volume limit for atomic exchanges is not exceeded
  8. 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.
  9. 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.
  10. Update internal bookkeeping with the new exchange and debt snapshot (priced via internal Chainlink rate), and emit related events
  11. Process trading rewards

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 4.

ExchangeRatesWithDexPricing.effectiveAtomicValueAndRates() selects the execution price by:

  1. Applying CL_BUFFER to P_CL to obtain P_CLBUF
  2. Querying the IDexPriceAggregator contract (initially DexPriceAggregatorUniswapV3 (0xf120F029Ac143633d1942e48aE2Dfa2036C5786c)) to obtain P_DEX
  3. Outputting whichever of P_CLBUF and P_DEX provides the minimum output

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

Additional considerations

It is worth noting that the debt cache may be adversely impacted by this SIP due to synths being priced differently (lower) than their internal, Chainlink-based price during atomic exchanges. Over time, it is likely extended usage of atomic exchanges will produce a skew in the debt cache opposite the direction of the majority of atomic exchanges.

However, as of writing this SIP, it is expected that the current debt cache mechanism will be replaced by an off-chain snapshotting mechanism in the near future.

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_DEX of 0.01, P_CL of 0.011, and CL_BUFFER of 50bps
    • Choose 0.01 (P_DEX) to output 10 sETH
  • Given P_DEX 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_DEX 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_DEX 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_DEX of 0.0099, P_CL of 0.01, and CL_BUFFER of 0bps
    • Choose 0.0099 (P_DEX) to output 9.9 sETH
  • Given P_DEX of 0.01, P_CL of 0.01, and CL_BUFFER of 0bps
    • Choose 0.01 (P_DEX or P_CLBUF) to output 10 sETH

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

  • Given P_DEX of 100, P_CL of 110, and CL_BUFFER of 50bps:
    • Choose 100 (P_DEX) to output 1000 sUSD
  • Given P_DEX of 100, P_CL of 99, and CL_BUFFER of 50bps
    • Choose 98.505 (P_CLBUF) to output 985.05 sUSD
  • Given P_DEX of 100, P_CL of 100, and CL_BUFFER of 50bps
    • Choose 99.5 (P_CLBUF) to output 995 sUSD
  • Given P_DEX of 99, P_CL of 100, and CL_BUFFER of 200bps
    • Choose 98 (P_CLBUF) to output 980 sUSD
  • Given P_DEX of 99, P_CL of 100, and CL_BUFFER of 0bps
    • Choose 99 (P_DEX) to output 990 sUSD
  • Given P_DEX of 100, P_CL of 100, and CL_BUFFER of 0bps
    • Choose 100 (P_DEX or 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 DEX-basd price queries, 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
  • Volatility threshold based on Chainlink updates, specified in number of updates over a number of seconds and per-synth

Initially, this SIP proposes the following system configuration:

  • SystemSettings.atomicMaxVolumePerBlock: 200,000 sUSD
  • SystemSettings.atomicTwapWindow: 1800 (i.e. 30min)
  • SystemSettings.atomicEquivalentForDexPricing (and thereby also allowing these synths to be exchanged atomically):
    • sUSD: USDC
    • sETH: WETH9
    • sBTC: WBTC
  • SystemSettings.atomicExchangeFeeRate:
    • sETH: 30bps
    • sBTC: 30bps
  • SystemSettings.atomicPriceBuffer:
    • sETH: 15bps
    • sBTC: 15bps
  • SystemSettings.atomicVolatilityConsiderationWindow:
    • sETH: 600 (i.e. 10min)
    • sBTC: 600 (i.e. 10min)
  • SystemSettings.atomicVolatilityUpdateThreshold:
    • sETH: 3
    • sBTC: 3

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 announced and, important to this SIP, included expanded oracle functionality that simplified the retrieval of TWAP rates by allowing other contracts to directly retrieve TWAP rates from a Uniswap V3 pool. Upgrading to this new version allowed the Synthetix system to both save gas and minimize external dependencies.

A simplified proof-of-concept of this earlier version of the exchange mechanism can be found in this SynthetixAMM contract which sourced DEX-based prices from (now defunct) Keep3r TWAP oracles.

Closer towards deployment, a sandbox version of the updated exchange mechanism in this SIP was deployed to another SynthetixAMM contract. This version piggybacked on the existing L1 Synthetix deployment to be as close to a real deployment as possible.

Copyright and related rights waived via CC0.