Documentation Index
Fetch the complete documentation index at: https://tech.ramses.xyz/llms.txt
Use this file to discover all available pages before exploring further.
Introduction
This guide will cover how to fetch price observations from a V3 pool to get onchain asset prices. It is based on the Price Oracle example, found in the Uniswap code examples repository. To run this example, check out the guide’s README and follow the setup instructions.If you need a briefer on the SDK and to learn more about how these guides connect to the examples repository, please visit our background page!
- Understanding observations
- Fetching observations
- Computing TWAP
- Computing TWAL
- Why prefer observe over observations
oracle.ts
Understanding Observations
First, we need to create a Pool contract to fetch data from the blockchain. Check out the Pool data guide to learn how to compute the address and create an ethers Contract to interact with.65535.
If the Pool cannot store an additional Observation, it overwrites the oldest one.
We create an interface to map our data to:
Observations from our pool contract, we will use the observe function:
slot0 function.
observationCardinalityNext is the maximum number of Observations the Pool can store at the moment.
The observationCardinality is the actual number of Observations the Pool has currently stored.
Observations are only stored when the swap() function is called on the Pool or when a Position is modified, so it can take some time to write the Observations after the observationCardinalityNext was increased.
If the number of Observations on the Pool is not sufficient, we need to call the increaseObservationCardinalityNext() function and set it to the value we desire.
This is a write function as the contract needs to store more data on the blockchain.
We will need a wallet or signer to pay the corresponding gas fee.
In this example, we want to fetch 10 observations.
swap() and the modifyPosition() function of the Pool.
Saving an Observation is a write operation on the blockchain and therefore costs gas.
This means that the pool will only be able to save observations for blocks where write calls are executed on the Pool contract.
If no Observation is stored for a block, it is calculated as the time weighted arithmetic mean between the two closest Observations.
Because of this, we can be sure the oldest Observation is at least 10 blocks old.
It is very likely that the number of blocks covered is bigger than 10.
Fetching Observations
We are now sure that at least 10 observations exist, and can safely fetch observations for the last 10 blocks. We call theobserve function with an array of numbers, representing the timestamps of the Observations in seconds ago from now.
In this example, we calculate averages over the last ten blocks so we fetch 2 observations with 9 times the blocktime in between.
Fetching an Observation 0s ago will return the most recent Observation interpolated to the current timestamp as observations are written at most once a block.
Calculating the average Price
To calculate the time weighted average price (TWAP) in the period we fetched, we first need to understand what the values we fetched mean. ThetickCumulative value is a snapshot of the tick accumulator at the timestamp we fetched. The Tick Accumulator stores the sum of all current ticks at every second since the Pool was initialised. Its value is therefore increasing with every second.
We cannot directly use the value of a single Observation for anything meaningful. Instead we need to compare the difference between two Observations and calculate the time weighted arithmetic mean.
tickToPrice function, which returns a Price Object. Check out the Pool data guide to understand how to construct a Pool Object and access its properties. We don’t need the full Tick Data for this guide.
Calculating the average Liquidity
To understand the term active Liquidity, check out the previous guide. Similar to thetick accumulator, the liquidity accumulator stores a sum of values for every second since the Pool was initialized and increases with every second.
Because of the size of the active liquidity value, it is impractical to just add up the active liquidity. Instead the seconds per liquidity are summed up.
The secondsPerLiquidityX128 value is calculated by shifting the seconds since the last Observation by 128 bits and dividing that value by the active liquidity. It is then added to the accumulator.
last is the most recent Observation in this illustrative code snippet. Consider taking a look at the Solidity Oracle library to see the actual implementation.
Let’s invert this calculation and find the average active liquidity over our observed time period.
The costs associated with manipulating/ changing the liquidity of a Pool are orders of magnitude smaller than with manipulating the price of the assets, as prices will be arbitraged for assets with more than one market.
Adding massive amounts of liquidity to a Pool and withdrawing them after a block has passed more or less only costs gas fees.Use the TWAP with care and consider handling outliers.
Why prefer observe over observations?
As touched on previously, theobserve function calculates Observations for the timestamps requested from the nearest observations stored in the Pool.
It is also possible to directly fetch the stored observations by calling the observations function with the index of the Observation that we are interested in.
Let’s fetch all observations stored in our Pool. We already made sure the observationCardinality is 10.
The solidity struct Observation looks like this:
65535, but indices equal to or greater than the observationCardinality will return uninitialized Observations.
The full code to the following code snippets can be found in oracle.ts
observe function here.
While observe creates an array onchain in the smart contract and returns it, calling observations requires us to make multiple RPC calls.
Depending on our setup and the Node we are using, either option can be faster, but making multiple RPC calls always has the danger of the blockchain state changing between our calls.
While it is extremely unlikely, it is still possible that our Node updates with a new block and new Observation in between our calls.
Because we access indices of an array, this would give us an unexpected result that we need to handle as an edge case in our implementation.
Because Observations are stored in a fixed size array with always the oldest Observation overwritten if a new one is stored, they are not sorted.
We need to sort the result by the timestamp.
observations than with observe, and we need to consider multiple edge cases.
For this reason, it is recommended to use the observe function.
