ORACLE
Uniswap v4 does not include built-in oracle functionality. If you're unfamiliar with oracles, check out the Ethereum Foundation's oracle overview.
HOOKED! pools can implement custom oracles depending on their specific needs. This flexibility allows pool creators to choose oracle implementations that best match their use case, whether prioritizing gas efficiency, manipulation resistance, or specific data requirements.
TruncatedOracle
One default oracle implementation available in HOOKED! is the TruncatedOracle. This oracle provides price and liquidity data similar to Uniswap v3's oracle system, but with an important modification: it truncates extreme tick movements to prevent manipulation attacks.
Key Features
The TruncatedOracle includes a maximum absolute tick movement limit (MAX_ABS_TICK_MOVE = 9116). If the current tick moves more than this limit from the previous tick, the movement is truncated. This mechanism protects against flash loan attacks and other manipulation attempts that could distort price data.
Observation Structure
Observations in TruncatedOracle take the following form:
struct Observation {
// the block timestamp of the observation
uint32 blockTimestamp;
// the previous printed tick to calculate the change from time to time
int24 prevTick;
// the tick accumulator, i.e. tick * time elapsed since the pool was first initialized
int48 tickCumulative;
// the seconds per liquidity, i.e. seconds elapsed / max(1, liquidity) since the pool was first initialized
uint144 secondsPerLiquidityCumulativeX128;
// whether or not the observation is initialized
bool initialized;
}
The key differences from standard Uniswap v3 oracles are:
- prevTick: Tracks the previous tick value to enable truncation logic
- int48 tickCumulative: Uses a smaller data type (vs int56) for gas efficiency
- uint144 secondsPerLiquidityCumulativeX128: Also uses a smaller data type (vs uint160) for optimization
How It Works
Historical data is stored as an array of observations. Initially, each pool tracks only a single observation, overwriting it as blocks elapse. Anyone can pay transaction fees to increase the number of tracked observations (up to a maximum of 65535), expanding the period of data availability to approximately 9 days or more.
When updating observations, the oracle checks if the tick movement exceeds the maximum allowed change. If it does, the tick is truncated to stay within bounds, preventing sudden price spikes from corrupting historical data.
Storing price and liquidity history directly in the pool contract reduces integration costs and potential errors. The oracle's maximum length makes manipulation significantly more difficult, as calling contracts can cheaply construct time-weighted averages over any arbitrary range.
Using the Oracle
The observe function allows you to retrieve accumulator values for specific points in time:
function observe(
Observation[65535] storage self,
uint32 time,
uint32[] memory secondsAgos,
int24 tick,
uint16 index,
uint128 liquidity,
uint16 cardinality
) internal view returns (
int48[] memory tickCumulatives,
uint144[] memory secondsPerLiquidityCumulativeX128s
);
Each time observe is called, you specify an array of "seconds ago" values to retrieve observations from. If the requested times don't correspond exactly to a block where an observation was written, a counterfactual observation is constructed automatically, eliminating the need for manual interpolation.
Note that because the oracle updates at most once per block, calling observe with a secondsAgo value of 0 returns the most recently written observation, which can only be as recent as the beginning of the current block.
Tick Accumulator
The tick accumulator stores the cumulative sum of the active tick at the time of observation. The value increases monotonically, growing by the current tick value per second.
To derive the arithmetic mean tick over an interval, retrieve two observations, calculate the difference between their accumulator values, and divide by the time elapsed. This allows you to calculate time-weighted average prices (TWAPs). Note that using an arithmetic mean tick to derive a price corresponds to a geometric mean price.
Liquidity Accumulator
The liquidity accumulator stores the value of seconds divided by in-range liquidity at the time of observation. Like the tick accumulator, this value increases monotonically.
To derive the harmonic mean liquidity over an interval, retrieve two observations, calculate the difference between their accumulator values, then divide the time elapsed by this difference. This provides time-weighted average liquidity (TWAL) calculations.
The in-range liquidity accumulator should be used with care. Because the current tick and current in-range liquidity can be uncorrelated, taking the arithmetic mean tick and harmonic mean liquidity over the same interval may not accurately characterize pool behavior. For example, a pool with tick 0 for 5 seconds and tick 100 for 5 seconds will have the same accumulator values as a pool with tick 50 and matching liquidity for 10 seconds, even though the underlying behavior differs.
Deriving Price From A Tick
When referring to the "active tick" of a pool, we mean the lower tick boundary closest to the current price.
When a pool is created, each token is assigned to either token0 or token1 based on contract address. This assignment is arbitrary but fixed for the pool's lifetime, used only for relative valuation and internal logic.
Deriving an asset price from the current tick uses the fixed expression of token0 in terms of token1.
Example: Finding the price of WETH in a WETH/USDC pool where WETH is token0 and USDC is token1:
If an oracle reading shows tickCumulative as [70_000, 1_070_000] with 10 seconds elapsed:
- Calculate the difference: 1_070_000 - 70_000 = 1_000_000
- Divide by time elapsed: 1_000_000 / 10 = 100_000 (average tick)
- Calculate price using p(i) = 1.0001^i: 1.0001^100_000
The formula p(i) = 1.0001^i calculates the price ratio where i is the tick value. This gives the price of token1 in terms of token0.
Ticks are signed integers and can be negative. When token0 has lower value than token1, tickCumulative returns negative values, and price calculations reflect this relationship.
Custom Oracle Implementations
While TruncatedOracle serves as a default option, pool creators can implement custom oracle solutions tailored to their specific requirements. This might include different truncation strategies, alternative data structures, or specialized manipulation resistance mechanisms. The flexibility of HOOKED!'s hook architecture enables this customization while maintaining compatibility with the broader ecosystem.