Getting Prices Right

Written by alcueca | Published 2022/02/04
Tech Story Tags: blockchain | ethereum | solidity | oracles | how-to | smart-contracts | chainlink | compositional-prices

TLDRWe are used to reading prices in smart contracts. Thinking about token amounts makes things much easier.via the TL;DR App

How I Learned Not to Worry and Love Oracles

Most of us use price feeds without considering what a price actually is and how it should be represented. During the development of Yield v2 I struggled with obtaining asset prices as a combination from several sources, and solving that problem led to a breakthrough.

Now we can easily compose any number of price feeds to get the relative values of any two asset amounts.

Dan Robinson, who leads the meme division at Paradigm, summarized it best:
If you are a bit confused about all this, so was I, but read on. You are about to learn what does it mean.

Chainlink and the Original Sin

Chainlink officially launched back in 2019 and quickly became the dominant source for price data. From Chainlink, you can get the price of many tokens against ETH or USD, apart from other more exotic data feeds.

What probably none of us stop to think is what do those 18 decimals represent.

According to the docs, the return value is a fixed point number with 18 decimals for ETH data feeds and 8 decimals for USD data feeds.

A fixed-point number doesn’t have much meaning in itself; it’s not something that you can hold in your hand, even in an imaginary hand. I can imagine myself holding one DAI in the metaverse, but I can’t imagine myself holding the exchange rate of DAI to ETH.

We are used to dealing with abstract ideas, but they have their own cognitive load. If you are not careful of compounding abstractions, you will find it progressively harder to understand what it is that you are doing.

Using an exchange rate is fine when your work is simple, but as you will see, things can easily get more complicated to the point where an exchange rate is not the better option.

Compositional Prices

At Yield, we have built a collateralized debt platform that can theoretically lend any asset and theoretically accept any asset as collateral. That feature, though, is limited to the asset pairs for which you have a price feed. For lending an asset, you need to know if the posted collateral is more valuable than the debt by a certain collateralization factor.


Chainlink, though, only offers price feeds where one of the assets is ETH or USD. What do I do if I want to use DAI as collateral to lend USDC?

Easy, you get the DAI/ETH price and the ETH/USDC price and multiply them. Presto! You’ve got a DAI/USDC price feed.

You can get DAI/ETH from Chainlink and multiply it by the reverse of the USDC/ETH price, also from Chainlink.

DAI / ETH * ETH / USDC = DAI/USDC


Ah, yeah, we need to use fixed-point arithmetic. The screenshot is python but don’t forget that solidity doesn’t have native fixed-point types, either.

Knowing the price only gets you halfway there, though. Often you need to know something like whether foo DAI is worth more or less than bar USDC. To do that, you need to bring decimals into the fray, and things get a bit more complex. DAI has 18 decimals, USDC has 6 decimals.

To solve that, you multiply the amount of base by the decimals of the quote and divide the result by the decimals of the base.

amountUSDC = decimalsUSDC * amountDAI * priceDAI_USDC / decimalsDAI


So far, so good. Just be careful with the decimals, and with not getting your bases and quotes confused. It’s not so easy to remember whether the value you get from Chainlink is ETH/DAI or DAI/ETH, but with a bit of hair pulling, you’ll get there.


Complex Compositional Prices

Now you want to use cUSDC as collateral to borrow DAI. Why, you ask? Allan says something about rates, shorts, longs, no idea. He says jump, and I only ask how high.

We want to combine DAI/ETH, ETH/USDC and USDC/cUSDC. We can get a USDC/cUSDC price feed from the cToken contract, and it has… 16 decimals.

Fine, so then we convert the USDC/cUSDC exchange rate and upscale it to 18 decimals, and then we multiply the DAI/USDC price for the upscaled USDC/cUSDC. That gets us DAI/cUSDC, unless I’m getting the reverse of the exchange rate in Compound. Let me check the docs… and now, how many decimals is cUSDC… Does it matter that USDC in the middle has 6 decimals?

Fuck, I don’t even know.

There must be a better way.

Partially Applied Multiplications

The breakthrough came when I stopped thinking about prices, and started thinking about amounts.

Our Oracles take a parameter for the amount of the base asset, and return the amount of the quote asset that you would get if you trade them.

In plain terms, I don’t ask the oracle: “Hey, what’s the DAI/ETH price?”

I ask: “Hey, how much ETH can I get for 100 DAI?”**

Now, that can be easily composed. If I want to know the DAI/USDC price, and I have the DAI/ETH and the USDC/ETH oracles, I can ask:

  • “Hey, how much ETH do I get for 100 DAI?” - “You get 0.04 ETH, fren“
  • “Cool, I got 0.04 ETH. Now, how much USDC do I get from 0.04 ETH?” - “Easy, that’s 100.1 USDC“

Notice that I’m not worrying about decimals anymore. I’m not wondering what the number I’m getting means, either. That number has a concrete meaning. It’s a handful of coins.


Let’s try DAI/cUSDC, which I couldn’t do in my mind before:

  • “I’ve got 100 DAI; how much ETH is that?” - “0.04”
  • “Of course, so how much USDC is 0.04 ETH?” - “100.1”
  • “Awesome, then how much cUSDC is 100.1 USDC?” - “102.3”
  • “Righto, so 100 DAI is 102.3 cUSDC, I’m liquidating this guy”

How Do We Do It At Yield

The oracle infrastructure at Yield is not permissionless, but if you are ok with that, you can use our contracts to get price feeds, or you can copy what we’ve done and deploy new oracles. In this section I’ll explain how it works.

We have a simple IOracle interface with two functions, peek and get. In many cases, they both return the same, but if a price feed has a view value and a mutable value, peek will get you the view one, and get will get you the mutable. From off-chain, you probably want to use peek. From another smart contract, inside a mutable function, you probably want to use get.

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

interface IOracle {
   /**
    * @notice Doesn't refresh the price, but returns the latest value available without doing any transactional operations:
    * @return value in wei
    */
   function peek(bytes32 base, bytes32 quote, uint256 amount) external view returns (uint256 value, uint256 updateTime);

   /**
    * @notice Does whatever work or queries will yield the most up-to-date price, and returns it.
    * @return value in wei
    */
   function get(bytes32 base, bytes32 quote, uint256 amount) external returns (uint256 value, uint256 updateTime);
}


These two functions take a pair of asset identifiers and an amount of the base asset, and will return the equivalent amount of the quote asset and a timestamp to indicate how fresh the price is.

This interface abstracts all the complexity of getting a price from a given provider, allowing us to ask any of our oracles about the relative values of two assets in terms of amounts. So far, we are pulling data from Chainlink, Compound, Uniswap, Lido, Convex, and Yearn. Pulling data from ERC4626 vaults is trivial as well since they already implement this pattern.

To combine data feeds from different sources, we have a Composite Oracle that conforms to the same interface, but that stores paths on how to get from A to B, pulling prices from IOracle contracts.


So, for example, to get the DAI/ENS price, we have this:

  • Chainlink IOracle for DAI/ETH (0x3031, 0x3030)
  • Uniswap V3 IOracle for ENS/ETH (0x3037, 0x3030)
  • Composite IOracle, that knows that to get DAI/ENS (0x3031, 0x3037), you ask the Chainlink IOracle for how much ETH is the DAI worth, and then you ask the Uniswap V3 IOracle how much ENS is that ETH worth.

Conclusion

It’s about time to stop reporting prices as fixed-point numbers, unless that’s exactly what you need.

Implementing oracles that return amounts is easy and allows for easier oracle composition.

At Yield we have implemented oracles that read from Chainlink, Compound, Uniswap V3, Lido, Yearn, and Convex, and we can combine their data to retrieve the relative values of any two assets. Adding an additional data provider is a trivial affair.



Written by alcueca | Hello. I design and build blockchain solutions. I like to make the complex simple.
Published by HackerNoon on 2022/02/04