Ethereum: Get token price at a specific block number (onchain)

Ethereum: Get token price at a specific block number (onchain)

·

6 min read

After trying to answer the question at stackexchange I've decided to write this short article, as the topic is indeed quite interesting.

1. Archive Node

The most simple is the last part of the question: at a specific block (onchain). As you may know, ethereum's geth node can be launched with various --gcmode values: full or archive. The archive mode doesn't remove any historical state data, it means - we can perform read queries specifying the blockNumber and getting the result back, as it was at that block.

Node-as-a-Service

All popular node providers, like infura.io, quicknode.com, alchemy.com can give you access to the archive node, but it won't be free of charge.

Self Hosting

As already mentioned, if you use geth include additionally --syncmode full --gcmode archive flags. But you have to be patient - it takes time to full sync the node, and it will occupy >10.2TB storage: etherscan/chainarchive

Erigon 🔗

I use erigon as it allows us to specify the number of blocks we want to keep with the historical state data

./erigon.exe --datadir D:/Erigon --chain mainnet --private.api.addr=127.0.0.1:9090 --prune=hrtc --prune.h.older=400000 --prune.r.older=400000 --prune.t.older=400000 --prune.c.older=400000

This will keep 400000 blocks, which with the average blocktime of 13s will keep the data for 2 months, and it takes 680GB of storage.

Intermediate summary

When you perform READ actions at blockchain you always get the result back which refers to some blockNumber. Even if you don't specify the block number, you get the result just for the latest block, but you can set any previous block your node supports.

2. Token Price

To retrieve the price onchain, there are 2 options - a) oracles: 3rd party contracts, which accumulate the prices in the correct way, and b) direct DEX sources - like Uniswap.

2.1 Oracles

The most convenient way, as it provides direct TOKEN/USD pair access, and most correct way, as it provides the most accurate price information at a given time point (blockNumber).

I suggest the ⬡ Chainlink price feed contracts: data.chain.link/crypto-usd. For an example, lets get the ETH price from the feed:

data.chain.link/eth-usd (0x5f4ec3df9cbd43714fe2740f5e3616155c5b8419)

Here I will use 0xweb 📦 package manager to create contract classes and to read data from the blockchain.

# install 0xweb as global util
$ npm i 0xweb -g
# initialize dependencies in the current folder, for API usage. For CLI not required 
$ 0xweb init

# download and generate classes for the contract
$ 0xweb install 0x5f4ec3df9cbd43714fe2740f5e3616155c5b8419 --name chainlink/oracle-eth

After the contract classes are generated, you can access onchain data via CLI or API

❗❣️❗ There are default KEYs for etherscan/co and infura. They are rate-limited. Please, create and insert your keys: 0xweb config -e, replace Node URLs for eth.

  • cli
$ 0xweb contract read chainlink/oracle-eth latestAnswer
  • api
import { ChainlinkOracleEth } from './0xweb/eth/chainlink/oracle-eth/oracle-eth';
import { Config } from '@dequanto/Config';
import { $bigint } from '@dequanto/utils/$bigint';

async function example () {
    await Config.fetch();

    const oracle = new ChainlinkOracleEth();
    const decimals = await oracle.decimals();
    const price: bigint = await oracle.latestAnswer();

    console.log(`ETH Price: ${ $bigint.toEther(price, decimals) } USD`);
}
example();

These examples return current ETH price. How to get the price at a specific block? Easy, by defining the block number.

$ 0xweb contract read chainlink/oracle-eth latestAnswer --block 14450000
 const price: bigint = await oracle.forBlock(14450000).latestAnswer();

⚠️ Ethereum 🗄️node URLs in default configuration are not archive nodes, so it won't work, you should replace them with archive node URLs.

Intermediate summary

Though ⬡ Chainlink is the most accurate and easy way to get the prices, but it doesn't contain all those thousands of tokens out there. That's why let's have a look at DEXes.

2.2 DEX

If a token is traded, that means we can get the price from there. Looks simple, but has its caveats. First of all, I'll show how to get the prices from UniswapV2 using 0xweb cli and dequanto libraries, and afterward, I briefly explain how it works under the hood.

  • cli

0xweb has already the built-in token command, which can retrieve prices from UniswapV2

$ 0xweb token price LINK
# or with block number (archive node required)
$ 0xweb token price LINK --block 14450000
# or by address
$ 0xweb token price 0x514910771af9ca656af840dff83e8264ecf986ca
# example of the command return  
Symbol   LINK
Address  0x514910771af9ca656af840dff83e8264ecf986ca
Decimals 18
Price    15.715912
  • api
import di from 'a-di';
import { Config } from '@dequanto/Config';
import { PlatformFactory } from '@dequanto/chains/PlatformFactory';
import { TokenPriceService } from '@dequanto/tokens/TokenPriceService';

async function example () {
    // we need this once: to find and read all configurations
    await Config.fetch();

    let token = 'LINK';
    // any unknown tokens are also supported
    let token = { address: "0x12345abcd", decimals: 18 };
    let chain = await di.resolve(PlatformFactory).get('eth')
    let service = new TokenPriceService(chain.client, chain.explorer);
    let { price } = await service.getPrice(token, {
        block: 14450000
    });
    console.log(price);
}
example();

A couple of words about how to run the scripts from api examples. I use atma global package to run the scripts. It supports file middlewares, in particular the atma-loader-ts, it will compile, cache and execute the TS files on the fly: atma run ./example.ts. All required packages and loader configuration will be created on 0xweb init. But you can use any other loader/builder you already use for TypeScript.


⚙️ How it works

2.2.1 UniswapV2 interfaces

interface IUniswapV2Factory {
    function getPair(address tokenA, address tokenB) external view returns (address pair);
}
interface IUniswapV2Pair {
    function token0() external view returns (address);
    function token1() external view returns (address);
    function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast);
}

1. How to get the price of a token e.g.WETH from the pair WETH/USDC?

This step is trivial, the pair, has reserves of each token - amount of tokens in the pool. And the price is just a ratio of these amounts. price(WETH) = totalEther(USDC)/totalEther(WETH).

  • ⚠️ Be aware, that each ERC20 token can have different values for decimals, so just convert values to ether
  • ✨ By having two tokens, e.g. 0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48(USDC)/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2(WETH) which one is reserve0 and reserve1? Uniswap stores the tokens in a sorted way: token0 < token1

      const USDC = '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48';
      const WETH = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2';
      const lpReserves = await poolPair
          .forBlock(opts?.block ?? opts?.date)
          .getReserves();
    
      let reserves = [lpReserves.reserve0, lpReserves.reserve1];
      let sorted = BigInt(USDC) < BigInt(WETH);
      if (sorted === false) {
          reserves.reverse();
      }
      let [ usdc, weth ] = reserves;
    
  • ✨ From the previous example, you have noticed the .fromBlock(opts?.block ?? opts?.date) method - so yes, you can specify the BlockNumber or Date(which will be resolved to the BlockNumber).

2. Okay, but how do I find out the address of the pair contract?

The UniswapV2Factory contract has the method getPair, which accepts any two tokens, and returns the pair contract (if exists ❗).

// tokens order doesn't matter here
factoryContract.getPair(USDC, WETH)

3. What if I want to get the price for a token, but there is no TOKEN/USDC pair?

If we stick to on-chain solution only, you could index all pairs in the Factory, then you have the list of all pairs for a token. But this is unnecessary, as it is enough to check all major stable coins and the wrapped eth with that token: USDC, USDT, DAI, WETH.

  • ⚠️ Often only TOKEN/WETH pair is present, or stable pairs have small liquidity, but as we already know - we can get the price for WETH from Chainlink or any of WETH/STABLE pairs

Following points you have to think about:

  • ✨ to get the price from the pool with the most liquidity
  • ✨ to get the price for one or two nearby blocks and take the average
  • ✨ this gives the price in stable coins, those are not always equal to 1$

🏁

Did you find this article valuable?

Support Alex Kit by becoming a sponsor. Any amount is appreciated!