SIP 120: Keep3r TWAP Exchange Function Source

AuthorKain Warwick, Andre Cronje, Justin Moses, Brett Sun

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 Keep3r TWAP oracles.


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 flash loan attacks by sourcing prices from Chainlink and Keep3r TWAP oracles.


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 UniswapV2 and present an opportunity as an alternative source of prices for fully atomic, one transaction synth exchanges. 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.

TWAP oracles have recently seen increased usage in several DeFi projects as a lagging price oracle and, importantly, there now exists a decentralized infrastructure network that is incentivized to maintain these oracles’ price-freshness via Keep3r (e.g. see the Uniswap and Sushiswap TWAP oracle jobs on Keep3r).



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 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, which relies solely on Chainlink oracles, the execution price for atomic exchanges is selected between the prices given by Chainlink oracles and Keep3r’s Uniswap and Sushiswap TWAP oracles. Three distinct prices are considered, P_CLBUF, P_TWAP, and P_SPOT, 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: minimum TWAP price between Uniswap and Sushiswap, over a window of N 30min periods
  • P_SPOT: minimum spot price between Uniswap and Sushiswap

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

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 30min periods
  • 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 with this SynthetixAMM contract.


The most important considerations are around the price selection method. Ideally, the chosen method will strike a balance between preventing both flash loan style price attacks and frontrunning oracle latency attacks while enabling low-slippage execution of atomic synth exchanges on high value cross asset swaps across multiple curve-style AMM pools.

The three prices of P_CLBUF, P_TWAP, and P_SPOT each 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 and Sushiswap TWAPs are designed to only update at the next block, preventing flashloan style attacks and making longer-term manipulation costly to the attacker. However, by their construction, TWAPs always lag behind spot.
  • P_SPOT: Spot prices derived from Uniswap and Sushiswap generally follow spot, but are easy to manipulate via a flashloan or sandwich attack.

By selecting the worst price amongst these three 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 on a fixed rate.
  • If P_TWAP or P_SPOT are used, a synth trader could be impacted by a set of sandwich transactions on Uniswap or Sushiswap that negatively impacts their exchange price. However, such attacks only grief the synth trader, not the debt pool, and come at a cost of at least 60bps (30bps per swap) for the attacker.
  • 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 through 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, P_TWAP, and P_SPOT 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 exchanger 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).
  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, 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 6.

ExchangeRates.effectiveAtomicValueAndRates() selects the execution price by:

  1. Applying CL_BUFFER to P_CL to obtain P_CLBUF
  2. Querying the Keep3rV4OracleUSD oracle aggregation contract (0x471588ffe76c39815051f264d994cb45653cfd04) to obtain P_AGG, the minimum output between P_TWAP (based on TWAP_WINDOW), P_SPOT, and P_CL
  3. Finally, outputting whichever of P_CLBUF and P_AGG provides the minimum output

For step 2, note that due to the low liquidity of synths on Uniswap and Sushiswap, queries to Keep3rV4OracleUSD 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 Keep3rV4OracleUSD needs to be replaced, the owner of ExchangeRates will have the ability to update its address.

Finally, while outside of the Synthetix system, it may be worthwhile to note the technical characteristics of the Keep3rV4OracleUSD and its underlying Keep3r TWAP oracle contracts:

  • Keep3rV4OracleUSD aggregates the Uniswap (0x73353801921417F465377c8d898c6f4C0270282C) and Sushiswap (0xf67Ab1c914deE06Ba0F264031885Ea7B276a7cDa) Keep3r TWAP oracles, providing the minimum output from each source’s TWAP and spot, and the current Chainlink price
  • Keep3rV4OracleUSD uses ETH as the routing mechanism between assets, such that a query not involving ETH requires two hops. For example, USDC:BTC is the combination of two separate observations for USDC:ETH and ETH:BTC.
  • Each Keep3r TWAP oracle maintains its own whitelist of query-able assets, generally limited to a few high-liquidity pools which may be expanded through governance
  • Each Keep3r TWAP oracle is incentivized by the Keep3r network to be “data fresh” to the last 30min
  • In cases of “data staleness”, either due to network congestion or incentive failures, queries may result in reverts. Reverts are ensured if the last observation saved is outside the desired window (“immediately stale”), but further observations in the past are allowed to be stale if multiple periods are specified.
  • All quoted prices are returned in the output asset’s decimals (which may not be 18, e.g. USDC, USDT, WBTC)

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_SPOT of 0.011, 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_SPOT of 0.0098, P_CL of 0.0099, and CL_BUFFER of 50bps
    • Choose 0.00098 (P_SPOT) to output 9.8 sETH
  • Given P_TWAP of 0.01, P_SPOT of 0.011, 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_SPOT 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.01, P_SPOT of 0.01, P_CL of 0.01, and CL_BUFFER of 0bps
    • Choose 0.01 (P_TWAP/P_SPOT/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_SPOT of 110, P_CL of 110, and CL_BUFFER of 50bps:
    • Choose 100 (P_TWAP) to output 1000 sUSD
  • Given P_TWAP of 100, P_SPOT of 98, P_CL of 99, and CL_BUFFER of 50bps
    • Choose 98 (P_SPOT) to output 980 sUSD
  • Given P_TWAP of 100, P_SPOT of 110, 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_SPOT of 100, P_CL of 100, and CL_BUFFER of 0bps
    • Choose 100 (P_TWAP/P_SPOT/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 30min periods
  • Synth equivalents for TWAP price look ups, specified in token addresses
  • Synths allowed, specified with a synth having an equivalent mapped (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: 2 (i.e. 60min)
  • SystemSettings.atomicEquivalentForDexPricing (and thereby also allowing these synths to be exchanged atomically):
    • sUSD: USDC
    • sETH: ETH
    • sBTC: WBTC
  • SystemSettings.atomicExchangeFeeRate: TBD
  • SystemSettings.atomicPriceBuffer: TBD

Copyright and related rights waived via CC0.