6. On-chain prices
Let's use Uniswap V2 to track on-chain prices
Goal
What's the price of ether? It's a central question to so many blockchain apps, but it's difficult to answer. It can be traded at different prices in different places, both off-chain and on-chain. Believers in efficient markets would hope arbitrage would align them, and, indeed, this was one of the earliest crypto arbitrage strategies. But there's enough complexity in pricing that many outsource it away to centralized third-party pricing APIs. That's a pity.
In this doc, we show you how to index your own prices using on-chain state. To do this, we'll track Uniswap V2 liquidity pools (LP). Each LP corresponds to a pair of tokens, and the price of one token in the pair relative to the other is a simple function of the ratio of the reserves. In tracking all pairs, we'll be able to express the spot price of any token relative to any other so long as an Uniswap V2 LP exists for the pair. There are many! You could even go further with multi-hop comparisons.
Finally, we'll check our work against the Uniswap V2 LP for WETH and USDC to get a current price of ether in US dollars.
If you want to skip to the end, here's a canvas that already has everything built.
Steps
Set up the Lambda
-
Add an EVM Lambda to the canvas. Under Hook type select ABI and then Address for selection type. Input:
0x3356c9A8f40F8E9C1d192A4347A76D18243fABC5
. This is a Uniswap V2 LP. Since we're using the ABI hook, we're hooking on all Uniswap V2 LPs. Select theswap
function hook--don't confuse it for theSwap
log hook--and add it as a Post hook with a callback namedpostSwap
. We don't need an automatically generated schema here--we'll create our own. -
Click the + button next to schemas and create a new schema called
uniswap_v2_prices
. Add these fields:Name Type chain_id uint64 block_number uint64 block_timestamp uint64 token0 address token0_symbol string token0_decimals uint64 token1 address token1_symbol string token1_decimals uint64 reserve0 uint256 reserve1 uint256 -
When you're done editing the schema, hit
Save
. Also add a Block hook with Post position and name the callbackpostBlock
. No need for a new schema here. -
As part of our execution, we're going to call UniswapV2 LPs. To do that, we need the interface. Click + next to Interfaces on the left side bar and select address. Input the address for any Uniswap V2 LP (e.g.,
0x3356c9A8f40F8E9C1d192A4347A76D18243fABC5
) and search. Once you've done this, you'll see the added interface both in the sidebar and the Interface code tab within the code editor. Rename the interface by replacingI0x3356c9A8f40F8E9C1d192A4347A76D18243fABC5
withIUniswapV2
. -
Now we need to write our Lambda code:
contract UserProbe is BaseDeclarativeProbe {
address[] public poolsWithReservesUpdated;
mapping(address => bool) poolVisited;
function postBlock() public {
for (uint256 i = poolsWithReservesUpdated.length; i > 0; i--) {
address pool = poolsWithReservesUpdated[i-1];
poolsWithReservesUpdated.pop();
delete poolVisited[pool];
(uint112 reserve0, uint112 reserve1,) = IUniswapV2(pool).getReserves();
address token0 = IUniswapV2(pool).token0();
string memory token0_symbol = IUniswapV2(token0).symbol();
uint8 token0_decimals = IUniswapV2(token0).decimals();
address token1 = IUniswapV2(pool).token1();
string memory token1_symbol = IUniswapV2(token1).symbol();
uint8 token1_decimals = IUniswapV2(token1).decimals();
simEmitToSchema_uniswap_v2_prices(SchemaUniswap_v2_pricesColumns({
chain_id: uint64(block.chainid),
block_number: uint64(block.number),
block_timestamp: uint64(block.timestamp),
token0: token0,
token0_symbol: token0_symbol,
token0_decimals: uint64(token0_decimals),
token1: token1,
token1_symbol: token1_symbol,
token1_decimals: uint64(token1_decimals),
reserve0: reserve0,
reserve1: reserve1
}));
}
}
function postSwap() public {
postSwapContext storage ctx = getPostSwapContext();
address pool = ctx.txn.to();
if (!poolVisited[pool]) {
poolsWithReservesUpdated.push(pool);
poolVisited[pool] = true;
}
}
}
- Test your code with the Test button at the top-right of the IDE. Try 17000000 to 17000010
Build the pipeline
-
Close the IDE. Click on the left handle of the Lambda component to add a
Data source
. If you wanted all the data, you would set From to 10000835, as this is when Uniswap V2 was first deployed on Ethereum. If you're just creating this canvas for learning, set the From to some block a few thousand blocks before the current tip block as it'll compute faster and reduce load on our systems. -
Leave the To block blank as we'll let our code catch up to the tip and then continue to execute.
-
Click on the right handle of the Schema component to add a
Persistence
. Name the persistenceuniswap_v2_prices
and set it. -
Hit the play button on the execution edge. The code will start executing at the tip and backfilling from the
From
block. You can see its status by mousing over the(?)
.
Querying the data
Let's query the data! Uniswap V2 LPs work with ERC20 tokens. Neither ether nor USD are natively ERC20 tokens, of course, but each has an ERC20 pegged (in various ways) to it: Wrapped Ether and USDC, respectively. To get the latest price of Ether, all we need to do is find the latest reserve ratio, normalizing for their differing decimals (WETH has 18, whereas USDC has 6).
select block_number,
token0_symbol,
token1_symbol,
(1.0 * reserve0 / POWER(10, token0_decimals)) / (1.0 * reserve1 / POWER(10, token1_decimals))
from $org.uniswap_v2_prices
where token0 = lower('0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48')
and token1 = lower('0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2')
order by block_number desc
limit 1
Key takeaways
- With block hooks and variable declaration, it's easy to perform some execution at the beginning or end of every block.
- You can condition the execution in one callback with state from another, as we did here with the postSwap callback.
- With interfaces, it's easy to interact with contracts directly within your execution.
- On-chain prices are easy!
While you're here
You could quite easily iterate this canvas further into a full backend for a pricing dApp:
- Query other prices.
- Query a history of price changes for a specific pair.
- Make prices queryable via an API.
- Push out price updates via a webhook.
- Add coverage for Uniswap V3 and/or other protocols.
Updated about 1 month ago