SIP-65: Decentralized Circuit Breaker
Author | |
---|---|
Status | Implemented |
Type | Governance |
Implementor | TBD |
Release | TBD |
Discussions-To | <https://discordapp.com/invite/AEdUHzt> |
Created | 2020-06-17 |
Simple Summary
Integrate a circuit breaker into each exchange, if the price of either Synth being exchanged has changed by more than a specific percentage the Synth will be suspended.
Abstract
When any exchange is attempted, prior to executing the exchange the protocol will check to see the price difference on both the source and destination synths since the last exchange of either. If the price difference on either is over a certain threshold (SCCP modifible), then the synth will be suspended (via SIP-44) and the exchange prevented. It will then be up to the protocolDAO to investigate the issue and to activate the synth once again after reviewing the incident.
Motivation
Both of the major outages of the Synthetix protocol have been the result of an incorrect price being reported via an oracle. The first was the KRW
case from the Synthetix oracle in June 2019 and was exploited by a bot. The second was the mispricing of silver (XAG
) to gold (XAU
) from the Chainlink pricing network in February 2020. Even with robust data integrity checking, both instances reflect situations where multiple failures occurred simultaneously and prices were published that were incorrect and exploitable.
Both of these should have immediately caused the synth to be suspended and unusable until the protocolDAO
had time to investigate.
Because the debt pool is shared among all SNX
stakers, and because of Synthetix's infinite liquidity feature whereby 100% of any synth can be traded into the same USD value of any other synth, a mispriced synth could have catastropic consequences for the debt pool - inflating it to a point where the SNX
collateral was not sufficient to back all outstanding debt.
This change will ensure that at the moment of exploit (trying to exchange one synth to another), a check is performed. Note that sUSD
is fixed to 1
so by tackling this problem at the exchange
action we capture the vast majority of the exploits (more on this below).
Specification
Overview
The Exchanger
contract will be amended to include a new priceDeviationThresholdFactor
parameter (configurable by SCCP, see below).
Every time an exchange occurs, we will check that both the source and destination synth prices have not changed by more than the threshold. We will then persist these rates as the lastExchangeRate
for both synths. If there is not a lastExchangeRate
for either synth, the contract will lookup the last three price changes on chain and compare each of them to the current one (which is more gas intensive but is a rare edge-case).
The function to check and potentially suspend will also be publicly available, so that anyone may invoke it without needing to attempt an exchange
.
In addition, we need to handle the settlement of a trade (see SIP-37 for more details on trade settlement). Because settlement is called to process some past event (i.e. how much is owed when the price of the oracle after the waiting period ends is taken into account) - we cannot nor would not want to do any suspension during settlement. However, we also cannot leave the trade in an unsettled state and block future user exchanges. As such, we propose to waive any reclaims or rebates in the event that the amount received deviates from the amount that should have been received by more than the priceDeviationThresholdFactor
.
There is a remote possibility that an exchange gets in before a spike, fronrunning a real rate change, but by the time the waiting period expires
N
minutes later, a spike occurs, and the exchange is settled with no fee reclaim. As such, theprotocolDAO
, when investigating suspended synths via price spikes, must also look through the unsettled exchanges performed right before the spike and determine the necessary course of action before resuming the synth in question. That being said, as long as the synth pricing is returned to normal after the outage, than the settlement occuring after that will not be cause for concern.
Finally, as the suspension is limited to the synth, even in a case of a false positive - where a synth is suspended when it shouldn't be - the only concern is increased downtime for any user to exchange or transfer that synth. It will be on the protocolDAO to investigate and resume the synth after a thorough investigation.
Rationale
A cleaner way to solve this problem would be to suspend the system on price updates, not on exchanges. However as Synthetix partially uses decentralized price feeds from Chainlink (and is planning to migrate to them fully in the near term with SIP-36), it cannot hook into actions from contracts it reads due to the limitations of smart contract interactions.
Note: a future version of this will instead incorporate upcoming changes in Chainlink Aggregators to read circuit breaker switches from them, instead of having to rely on previous prices from exchanges.
In the meantime, checking at the moment of exchange is the optimal strategy. Unfortunately this means slightly more gas for the user, which in the current climate is a difficult decision, but necessary to prevent collapse of the system.
In order to reduce gas as much as possible, this SIP proposes to store a lastExchangeRate
mapping locally on Exchanger
and use that as the source of truth, rather than looking back through ExchangeRates
for some amount of predetermined time to determine if an invalid price occurred.
Caveats
-
Imposing the suspension on a user's
exchange
will incur slightly more gas cost per successful exchange (~5%). This is unfortunate in the current Ethereum gas climate but necessary. -
If the suspension is hit, then the user who performs the action will pay the gas cost (which is much less than an exchange) and not have their exchange processed. However, this suspension action is expected to be a very rare edge case, and adding extra development work to repay them for their efforts is not worth it given the unlikeliness of this being needed. Additionally we are investigating the implementation of a keeper network to ensure that actions like this are incentivised and do not fall onto the user.
-
If we only check
exchange
actions, this does not preventSNX
stakers from issuing or burningsUSD
while an invalid price is on-chain. However, the primary risk is that a staker can pay off their debt using the exploit. That is, that the debt pool has reduced and they now need to burn far lesssUSD
to unlock theirSNX
. This exploit is only possible if one or more prices are returned much lower than is accurate (otherwise the debt pool would expand rather than contract), and that those synths consist of a large enough proportion of the debt pool. Additionally, since sUSD is always fixed to1
, the proportion of the debt pool denominated insUSD
will never change regardless of the other synths. As such, this extreme edge case does not seem worthy of also performing the check onissue
,burn
andclaim
actions given the additional gas costs per action. -
Checking the last price from an exchange isn't a perfect solution, it may miss scenarios where the price fluctuates in and out of a reasonable band - but it's a acceptable compromise until such time as Chainlink have integrated circuit breakers onto all their Aggregators and we have migrated to Chainlink completely.
Technical Specification
IExchanger
additions:
// Views
function isSynthRateInvalid(bytes32 currencyKey) external view returns (bool);
// Mutative functions
function suspendSynthWithInvalidRate(bytes32 currencyKey) external;
// requires isSynthPricingInvalid(currencyKey) is true
In order to save gas, each time a new exchange
occurs, the price of both the src
and dest
synths will be stored locally in Exchanger
(rather than looking back in ExchangeRates
for some amount of time).
Additionally, Exchanger.exchange
will be amended to perform suspendSynthWithInvalidRate(currencyKey)
for either (or both) source or destination synth when isSynthRateInvalid(currencyKey)
is true
.
Workflow
-
Synthetix.exchange(onBehalf)?
invoked from synthsrc
todest
byuser
foramount
- For both
src
anddest
synths:- Is there a previous rate for the synth?
- Yes:
- Is the factor difference in rate now compared to the previous rate >=
priceDeviationThresholdFactor
?- Yes: ✅🔚 Settle any unsettled trades into
src
as per usual (ifsrc
was the breach, then settle with no reclaim or rebate - see below), then suspend the synth and return immediately. - No: Persist the current rate as the last
- Yes: ✅🔚 Settle any unsettled trades into
- Is the factor difference in rate now compared to the previous rate >=
- No:
- For each of the last
3
rounds,- Is the factor difference in rate now compared to the rate at current round >=
priceDeviationThresholdFactor
?- Yes: ✅🔚 Suspend the synth and return immediately.
- No: Persist the current rate as the last
- Is the factor difference in rate now compared to the rate at current round >=
- For each of the last
- Yes:
- Is there a previous rate for the synth?
- Then
- ✅ Continue with exchange
- For both
-
Synthetix.settle
invoked ondest
foruser
- For each unsettled exchange from some
synth
todest
:- Is the factor difference in
amountReceived
comparedamountShouldHaveReceived
>=priceDeviationThresholdFactor
?- Yes: Settle the exchange with
0
reclaim and0
rebate - No: Settle the exchange as per usual
- Yes: Settle the exchange with
- Is the factor difference in
- For each unsettled exchange from some
Test Cases
Preconditions
- Given the
priceDeviationThresholdFactor
is set to1.5
(stored on-chain as1.5e18
) - And the
waitingPeriodSecs
is set to180
(3 minutes)
Common cases
- Given there was previously an exchange for
sUSD
(or any synth) tosETH
, recording the last sETH price as200
- And the current market price of
sETH
is returning500
- When a user attempts to exchange
sBTC
(or any other synth) intosETH
(or from any synth intosETH
)- Then as
500
is more than1.5x
away from200
(i.e. it's more than300
),sETH
will be suspended, and the exchange will be prevented.
- Then as
- When a user attempts to exchange
- And the current market price of
sETH
is returning105
- When a user attempts to exchange
sBTC
(or any other synth) intosETH
- Then as
105
is less than0.67x
away from200
(i.e. it's above100
), then the exchange will continue, and105
will be persisted as the last price.- When one minute elapses
- And a new price for
sETH
is returned at5
- And five minutes elapses (thus ending their waiting period)
- And a new price for
sETH
is returned at100
- And the
user
attempts toexchange
sETH
into any other synth- Then as the price of
5
(the price at three minutes after their exchange) is more than0.67x
away from105
(i.e. below52.5
), then the settlement will process with no reclaim or rebate.
- Then as the price of
- Then as
- When a user attempts to exchange
- And the current market price of
Edge cases
- Given there is no previous exchange recorded for
sETH
- And the protocol upgrades
Exchanger
contract- And there are 10 previous rates for
sETH
price, all within 1% of each other- When a user attempts to exchange
sETH
forsUSD
,- Then the current rate will be within 1% of the previous three rates, and the exchange will continue, persisting the current rate
- When a user attempts to exchange
- And there are 10 previous rates for
- And the protocol upgrades
ExchangeRates
contract or a ChainlinkAggregator
is replaced for a price- And there is no previous rates for
sETH
- Then the exchange will continue, persisting the current rate
- And there is no previous rates for
- And the protocol upgrades
- Given ExchangeRates returns 0 for the
sETH
rate- When a user attempts to exchange
sUSD
forsETH
- Then it fails with divide by 0 error from ExchangeRates.effectiveValue (as is current)
- When a user attempts to exchange
sETH
forsUSD
- Then it suspends the
sETH
synth as the0
rate trips the circuit breaker
- Then it suspends the
- When a user attempts to exchange
Configurable Values (Via SCCP)
priceDeviationThresholdFactor
the factor from which that price must deviate (from it's last exchange) to trigger the suspension (stored with 18 decimals). For example, a factor3
(stored as3e18
) would mean that with an last exchange rate of100
and a new rate of300
or greater, or a new rate of33.3
(recurring) and lower, would cause suspension.
Copyright
Copyright and related rights waived via CC0.