TL;DR: It is not secure to read pricing information from an exchange or other protocol. Always use a decentralized oracle.
You're setting yourself up to be hacked when building smart contracts. Getting price information from a decentralized oracle is ALWAYS what you should be doing. Reading pricing information is especially easy from a decentralized oracle like Chainlink.
This is a great question, and one that can be really tough for new engineers to grasp, let's break it down.
@Mikko in another answer also gives a number of helpful links for more information.
The naive approach
Reading the price from a DEX(decentralized exchange) is easy, especially from AMMs. One of the most popular DEX's Uniswap has a function called getReserves
. You can "basically" call this function and calculate the "price" of an asset using this where the assets are reserve0
and reserve1
.
function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast);
To get the price of reserve0
, you'd just divide.
price = reserve0 / reserve1
And now you have the price of the asset. However, this can run into MAJOR issues when working with smart contracts. Your "price" is now dependent on the liquidity of the exchange or said another way, your price is dependent on a centralized point of failure.
What do you mean, centralized?
Now you may be thinking "oh, a DEX is a 'decentralized exchange', so what do you mean the price is centralized?". Great question. When you get the price from a DEX, you're getting the price from 1 DEX. The trading of the assets is decentralized, but the pricing of the asset is coming from 1 DEX, a centralized location.
If the reserves of your DEX somehow dramatically change, the DEX may deliver an incredibly inaccurate price, even if it's just for a moment.
The two most common attacks in defi are reentrancy, and oracle manipulation attacks
An easy attack vector that has plagued and destroyed countless protocols is called a "flash loan attack" or more accurately "price oracle attack". Oftentimes, a hacker will see another protocol is using a DEX as their pricing information and will use a flash loan to temporarily drastically increase or decrease the reserves of an asset to inflate or deflate the price and then use that price to attack your protocol. Let's look at an example.
Let's use the same math as above, let's say a protocol has a trading pair of 100 USDT and 1 ETH. If we do the math here:
price = reserve0 / reserve1
This means, that $100 USDT = 1 ETH.
Now let's say an ETH whale or someone using a flash loan comes along and dumps 49 ETH into our exchange. Well, the pools have now changed, and the price will look like this now:
price = 100 / 50
The price of ETH is now $2. $50 -> $2, instantly. If you have liquidations, rewards, or anything based on this price, you have now screwed your users into false actions due to this manipulation.
In fact, the streets are littered with protocols who have been hit exactly by this, or some derivative.
The safer, more reliable, and more accurate approach
Using a decentralized blockchain oracle will protect you from these harms. In the scenario above, even if someone flash loan attacks the DEX, your pricing information is still secured because the data is coming from a decentralized collective. So if one or two data sources are drastically incorrect, it doesn't matter.
Additionally, it's really easy to pull from an oracle feed. The following is some valid solidity code that will pull the latest ETH / USD feed from the kovan testnet.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
contract PriceConsumerV3 {
AggregatorV3Interface internal priceFeed;
/**
* Network: Kovan
* Aggregator: ETH/USD
* Address: 0x9326BFA02ADD2366b30bacB125260Af641031331
*/
constructor() {
priceFeed = AggregatorV3Interface(0x9326BFA02ADD2366b30bacB125260Af641031331);
}
/**
* Returns the latest price
*/
function getLatestPrice() public view returns (int) {
(
uint80 roundID,
int price,
uint startedAt,
uint timeStamp,
uint80 answeredInRound
) = priceFeed.latestRoundData();
return price;
}
}
More information: