April 27, 2021
Liquity heavily depends on the ETH-USD price for many of its operations. Since LUSD is pegged to the US dollar, Liquity needs the dollar price of ETH to calculate Troves’ individual collateral ratios (ICR) and the total system collateral ratio (TCR). These ratios determine which Trove actions are allowed, when Troves can be liquidated, and when Recovery Mode kicks in.
Thus, Liquity requires a current and accurate ETH price, freshly fetched in each operation that uses it.
Liquity protocol is immutable and governance-free. No person or group has any control over the protocol — it is “set in stone” in smart contract code, and can never be changed.
If the source of Liquity’s ETH price data were to fail or freeze, no one can manually take action to set things straight. So, we had to build a system that was resilient to temporary oracle failures or freezes, without the need for human intervention.
Liquity utilizes a price feed with a dual-oracle design. The system has a primary oracle and a secondary oracle, and sophisticated logic for switching to one should the other fail.
The intent is to give Liquity a high chance of always seeing an accurate ETH price, and to swiftly return to the primary oracle when it’s working again — all without any human decision-making or action.
Chainlink is our primary oracle. They are the largest player in the oracle space, they have an excellent track record, they’re sufficiently decentralized, and they’re constantly improving their systems. You can learn more about Liquity’s use of Chainlink here.
Tellor is our secondary oracle. They’re fully decentralized — price data is requested, with a “tip” attached — and miners compete to push accurate data and win the tip. Inaccurate data is disputed, and the threat of stake slashing incentivizes honest price reporting.
We looked at many candidates for a secondary oracle, and most fell short in some way: either sorely lacking decentralization, or had poor guarantees about endpoints or (in the case of DEXs) future liquidity. Tellor, with its decentralized architecture, best fit the bill.
Both Chainlink and Tellor use a proxy-logic pattern — they each have a constant address and interface, so they can upgrade logic “under the hood”, without changing their endpoints. This is critical for the Liquity protocol, as it cannot be altered to point to new external contracts if an oracle upgrades its code.
In summary, the system applies the following algorithm:
Under normal conditions, Chainlink updates whenever the ETH price deviates by 0.5% or otherwise after three hours have passed. It typically updates several times per hour.
Liquity considers the Chainlink oracle frozen if it hasn’t updated its price for more than four hours. Historically, that has only happened in times of extreme network congestion, where price aggregation from nodes was disrupted. Chainlink has since upgraded to an off-chain aggregation method that makes it far more robust to even these extreme congestion events.
However, in the unlikely scenario where the Chainlink oracle doesn’t update the ETH price within four hours, Liquity will detect it and switch to using Tellor.
Tellor operates a little differently — the price is only updated after Tellor is “tipped” and miners submit price data.
If both oracles are frozen (no updates for more than four hours) the system uses the last good price it has seen, and awaits Tellor’s updated price. If both oracles start reporting current data again, Liquity switches back to Chainlink.
There are a few ways an oracle can outright fail:
Liquity deems the oracle to be broken if any of the above occur.
Additionally, when Liquity is using the primary Chainlink oracle, it checks the Chainlink price deviation between two price updates. If the difference is greater than 50%, and the Tellor price doesn’t match the new price, Liquity considers the primary price incorrect and switches to using Tellor. If the Tellor price does match, Liquity infers that the major price change is a legitimate market movement, and continues using Chainlink.
In the extreme and unlikely event that both oracles fail, Liquity uses the last good price seen for all operations — effectively freezing the price used by the system.
However, Liquity still does its best to recover and switch back to the primary oracle when it comes back online.
Oracles can fail or freeze temporarily. After a problem with the primary oracle, how can Liquity know when to trust it again?
Let’s imagine the primary goes live again and resumes serving valid, current price data. The mere fact that it’s live again wouldn’t guarantee that it’s serving accurate data, and there’s reason to be suspicious of new price data from an oracle that was previously found to be frozen or broken.
Of course, off-chain, humans can investigate the situation: we could check the market price, talk to the dev team and auditors, and evaluate whether the oracle is in fact working properly.
But on-chain, without an admin or governance process, there’s no way for Liquity to manually re-approve the oracle.
Here’s how we solved this in smart contract code: Liquity returns to the primary only when both oracles are live and serving a sufficiently similar price (less than 5% difference). This logic works because the chance that both oracles are serving the same incorrect price data is very low. If they’re both live with in-sync price data, Liquity infers that both oracles are in fact serving the real market price and switches back to the primary oracle.
If you want to check things out yourself, the complete oracle logic can be found in the PriceFeed smart contract.
Liquity is intended to function for many years to come, with no human control or intervention. It needs an accurate and up-to-date ETH price, and resilience to oracle problems. The dual-oracle design with fallback and recovery logic gives the system a fighting chance in the event of an oracle glitch or even critical failure.