Oracles No More: A Guide to Obtaining Onchain Historical Data for Smart Contracts on Ethereum

AlexEuler
6 min readMay 9, 2023

--

If you’ve spent a few weeks developing smart contracts on the Ethereum network, you’ve likely encountered a frustrating limitation: the lack of historical onchain data. Without this information, it’s difficult to make informed decisions or develop strategies based on past performance.

Oracles, such as Chainlink and Uniswap aim to provide this data. However, these solutions are not without their own drawbacks. For one, they can be expensive to maintain due to the gas costs associated with their usage. Additionally, each oracle has a unique set of inaccuracies that can further complicate matters.

For instance, Chainlink’s update frequency is generally limited to every 30 minutes with a minimum price deviation of 1%, and Uniswap only displays the mean geometric price over a certain period. Furthermore, the feeds themselves are limited in their scope, providing only basic price information, neglecting other crucial onchain data. Nevertheless, oracles have become the go-to solution for obtaining onchain data in the DeFi space.

But what if I told you that there’s a way to access ANY onchain data with the EXACT PRECISION for ANY past block? If this sounds intriguing, then stay with me and read on this article.

Setting the stage

To illustrate the approach, let’s consider an example. Imagine we’re providing liquidity on Uniswap V3 and we want to initiate a rebalance only if there has been a significant price movement beyond the current position in the last 24–48 hours.

So, how do we solve this problem? The answer is to supply all the necessary onchain data required for the check using an external caller, which we’ll refer to as the “Prover”, along with merkle proofs that confirm the accuracy of this data. These proofs can then be validated by a smart contract, which we’ll call the “Verifier”, and used to calculate invariants.

As an example, consider the following function that can validate whether the price falls outside of the expected range on Uniswap

It’s important to note that the variables lowTick, upperTick, and currentTick are simply storage slots within the Ethereum network. Specifically, lowTick and upperTick represent the boundaries of a position in the Uniswap pool, while currentTick represents the current price of the pool.

This means that we can generate a proof to demonstrate that a specific block in the past, falling within the 24-48 hour timeframe from the current timestamp, had these storage slots with specific values. If the Verifier can successfully verify the accuracy of these proofs, then we can use past on-chain data within a smart contract without relying on any kind of oracle.

This approach allows us to obtain historical data in a trustless manner, eliminating the need for expensive oracles and reducing the potential for inaccuracies.

The algorithm

Here’s a more detailed breakdown of our algorithm for obtaining historical on-chain data without relying on oracles:

  1. First, we need to generate and verify a proof that there exists a block within the last 24–48 hours that has a specific blockhash. This proof can be generated using an external caller, or the “Prover”, and verified by the smart contract, or the “Verifier”.
  2. Next, we need to generate and verify a proof that this block is indeed part of our current chain. This “block validity proof” can be generated and verified in a similar manner to the blockhash proof.
  3. Once we have confirmed the existence and validity of the desired block, we can generate and verify a proof that the storage slots containing lowTick, upperTick, and currentTick have specific values, as supplied by the external caller. This "storage slots proof" can again be generated using a Prover and verified by the Verifier.
  4. Finally, with these values in hand, we can call checkRebalance and verify that its result is true, indicating that a rebalance decision is based on the historical data we've obtained

The blockhash proof

Here’s how the Ethereum block header looks. The blockhash is essentially the keccak256 hash of all its elements:

The process for verifying the accuracy of a blockhash is relatively straightforward. The Prover simply needs to supply the block header as bytes, along with the blockhash. The Verifier can then make a keccak256 hash of the header and confirm that it matches the hash provided by the Prover.

Once this verification is complete, we can extract crucial information such as the block number, timestamp, and stateRoot from the header and consider it to be trusted.

It’s worth noting that the gas cost of keccak256 is very low — a mere 30 gas. It is significantly cheaper than the 2100 gas required for reading a single storage slot, for example.

The block validity proof

To verify that the block in question is part of our chain, we can use the blockhash(uint blockNumber) returns (bytes32) function in Solidity if the block is relatively recent (i.e., one of the last 256 blocks). We can extract the blockNumber from the header and confirm that it has the expected block hash in Solidity.

If the block is older than 256 blocks, we can ask the Prover to supply all block headers up to the verified block. We can then verify each header using the process outlined in step 1, and extract the parentHash field from each header. By confirming that each parentHash matches the previous header's block hash, we can establish that this is indeed a valid chain. The last parentHash in this sequence must be the block hash we're checking.

By following this multi-step process, we can verify that the desired block is part of our chain.

The storage slots proof

Now that we’ve confirmed the accuracy of the provided block header, we can extract the stateRoot, which is a merkle hash of all storage slots. The Prover can supply merkle proofs for the storage slots associated with this stateRoot, along with any other required proofs, and the Verifier can confirm their accuracy.

By following this process, we can confirm the exact values of lowTick, upperTick, and currentTick at the specific timestamp of interest, without relying on oracles or trusting any external parties.

With these values in hand, we can confidently call checkRebalance and ensure that it produces the correct result. This process allows us to obtain exact onchain data without relying on oracles and eliminates any potential for inaccuracies or manipulation.

Are there any existing implementations?

Currently, I have not been able to find any tooling specifically designed to obtain historical on-chain data for smart contracts without relying on oracles. If you are aware of any such contracts or projects, please let me know in the comments.

One project that is exploring this space is Axiom, which aims to provide an alternative to oracles by using zero-knowledge proofs to verify on-chain data. Their approach involves generating proofs of specific data values at specific times, and allowing contracts to validate the proofs and use the data in their operations. While Axiom is not live yet, it’s an interesting example of how this approach could be implemented in practice.

For more insights and updates on the latest DeFi strategies and trends, follow me on Twitter at https://twitter.com/0xAlexEuler.

--

--

AlexEuler

Building Mellow protocol | Chartered Financial Analyst | Smart Contracts & Full Stack Dev | Math & Stochastic Analysis https://alexeuler.dev