Skip to main content

StEthPriceFeed

Lido intends to provide secure and reliable price feed for stETH for protocols that intend to integrate it. Unfortunately, Chainlik is not available for stETH and Uniswap TWAP is not feasible at the moment: we'd want deep liquidity on stETH/ETH pair for this price, but Uni v2 doesn't allow tight curves for similaraly-priced coins.

stETH has deep liquidity in the Curve pool but it doesn't have a TWAP capability, so that's out, too. In the moment Curve price is flashloanable, if not easily. We decided that in a pinch we can provide a "price anchor" that would attest that "stETH/ETH price on Curve used to be around in recent past" (implemented using the StableSwapStateOracle) and a price feed that could provide a reasonably safe estimation of current stETH/ETH price.

Vocabulary#

  • Current price—current price of stETH on Curve pool. Flashloanable.
  • Historical price—the price of stETH on Curve pool that was at least 15 blocks ago. May be older than 15 blocks: in that case, the pool price that was 15 blocks ago differs from the "historical price" by no more than N%.
  • Safe price range—the range from historical price - N% to min(historical price + N%, 1).
  • Safe price—the price that's within the safe price range.

The parameter N is configured by the price feed admin; we're planning to initially set it to 5%.

stETH price feed specification#

The feed is used to fetch stETH/ETH pair price in a safe manner. By "safe" we mean that the price should be expensive to significantly manipulate in any direction, e.g. using flash loans or sandwich attacks.

The feed interfaces with two contracts:

  1. Curve stETH/ETH pool: source, deployed contract.
  2. StableSwapStateOracle

The pool is used as the main price source, and the oracle provides time-shifted price from the same pool used to establish a safe price range.

The price is defined as the amount of ETH wei needed to buy 1 stETH. For example, a price equal to 10**18 would mean that stETH is pegged 1:1 to ETH.

The safe price is defined as the one that satisfies all of the following conditions:

  • The absolute value of percentage difference between the safe price and the time-shifted price fetched from the Merkle oracle is at most max_safe_price_difference.
  • The safe price is at most 10**18, meaning that stETH cannot be more expensive than ETH.

Fail conditions#

Price feed can give incorrect data in, as far as we can tell, three situations:

  • stETH/ETH price moving suddenly and very quickly. There is at least 15 blocks delay between price drop and offchain oracle feed providers submitting a new historical price, and likely more bc tx are not mined instanteously. That should not happen normally: while stETH/ETH is volatile, it's not 5%-in-four-minutes volatile.
  • oracle feed going stale because feed providers go offline. This is mitigated by the fact it's operated by several very experienced professionals (all of which, e.g., are Chainlink operators too) - and we only need one operational provider to maintain the feed. The only realistic scenario where this feed goes offline is deprecating the oracle alltogether.
  • Multi-block flashloan attack. An block producer who is able to reliably get 2 blocks in a row can treat two blocks as an atomic transaction, leading to what is essentially a multiblock flashloan attack to manipulate price. That can lead to a short period of time (a few blocks) where stETH/ETH price feed is artificially manipulated. This attack is not mitigated, but in our opinion, not very realistic. It's very hard to pull off.

View Methods#

safe_price()#

Returns the cached safe price and its timestamp.

safe_price() -> (price: uint256, timestamp: uint256)

current_price()#

Returns the current pool price and whether the price is safe.

current_price() -> (price: uint256, is_safe: bool)

Methods#

update_safe_price()#

Sets the cached safe price to the max(current pool price, 1) given that the latter is safe.

update_safe_price() -> uint256

fetch_safe_price()#

Returns the cached safe price and its timestamp. Calls update_safe_price() prior to that if the cached safe price is older than max_age seconds.

fetch_safe_price(max_age: uint256) -> (price: uint256, timestamp: uint256)

Parameters:#

NameTypeDescription
max_ageuint256Amount of seconds last value of safe price considered to be valid