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

Search for a command to run...

No comments yet. Be the first to comment.
Here we will go through the process of contract upgrades. I assume you are already familiar with proxy patterns in Ethereum. No matter which libraries or frameworks you use, every upgrade ends with the same critical action: executing the method that ...

This article isn’t about cryptocurrency or decentralized finance. Instead, we’ll explore public EVM blockchains and how they can be used in your next project, depending on your specific needs and goals. I’ll delve into the pros, cons, and practical e...

* to write in the local and forked development network In the previous article, we looked into how the EVM storage is arranged and how we can access all the contract's data relatively easily when the contract is validated or we have the source code. ...

One of the greatest features of Web3 is the Open Data Principle. The Data doesn't belong just to companies, governments, groups, and individuals - it belongs to all of them and you at once. Anyone can create a twig (the contract) in global storage (t...

A multi-signature account as middleware between you and the dApps

After trying to answer the question at stackexchange I've decided to write this short article, as the topic is indeed quite interesting.
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.
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.
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
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.
When you perform
READactions at blockchain you always get the result back which refers to someblockNumber. 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.
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.
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 foreth.
cli$ 0xweb contract read chainlink/oracle-eth latestAnswer
apiimport { 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.
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.
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.
cli0xweb 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
apiimport 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
UniswapV2 interfacesinterface 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);
}
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).
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;
.fromBlock(opts?.block ?? opts?.date) method - so yes, you can specify the BlockNumber or Date(which will be resolved to the BlockNumber).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)
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.
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 pairsFollowing points you have to think about:
1$🏁