In the Pull model, consumers include verification data about the most recent update directly in their transaction. This data is fetched from finalized-data-snap and verified within the same transaction before being used. This scheme comes with some very nice properties:
Consumers always use the latest update in their transaction, so the price used in the transaction is as close to real time as protocol update latency.
The cost for maintaining update relevance on-chain relies on end-consumers. This allows the oracle protocol to have much larger set of assets.
This guide will help developers who want to use UDF Pull model within thier contracts. It uses Hardhat for contract compilation, although similar methods apply with other frameworks.
Step #1: Initialize new hardhat project
In the first step, we are establishing a basic solidity development setup.
Step #2: Write a contract that verifies an update in the transaction
Now create a new PullConsumer contract, it utilizes the PullOracle.getLastPrice function to verify the update information that we provide through transaction data (calldata). After it verifies the update, it emits an event indicating the successful verification, otherwise it reverts the whole transaction.
./contractrs/PullConsumer.sol
// SPDX-License-Identifier: UNLICENSEDpragmasolidity ^0.8.19;interface IPullOracle {/// @dev Represents a digital signaturestructSignature {bytes32 r; // The first 32 bytes of the signaturebytes32 s; // The second 32 bytes of the signatureuint8 v; // The recovery byte }// @notice Verifies that the update was emitted on EOB. It does so by checking// @dev following properties:// * Calculated merkle root of (Update + merkle proofs) is equal to the provided merkle root// * Validate the signatures of EOB agents on the Merkle root to ensure// merkle root integrity. The consensus check passes only if the number of valid// unique signatures meets or exceeds the protocol's consensus rate threshold.functiongetLastPrice(bytes32 merkleRoot,bytes32[] calldata merkleProof,Signature[] calldata signatures,bytes32 dataKey,uint256 price,uint256 timestamp ) externalreturns (uint256);}contract PullConsumer {// @notice PullOracle address on specific chain IPullOracle public pullOracle;constructor(address_pullOracle) { pullOracle =IPullOracle(_pullOracle); }eventPriceVerified(bytes32,uint256,uint256);// @notice function that verifies the provided update through PullOracle and// emits an event if the update is valid, otherwise reverts with a custom errorfunctionverifyPrice(bytes32 merkleRoot,bytes32[] calldata merkleProof,IPullOracle.Signature[] calldata signatures,bytes32 dataKey,uint256 updatePrice,uint256 updateTimestamp ) public {// Verify the update through PullOracleuint256 verifiedPrice = pullOracle.getLastPrice( merkleRoot, merkleProof, signatures, dataKey, updatePrice, updateTimestamp );emitPriceVerified(dataKey, verifiedPrice, updateTimestamp); }}
Now that we have a contract, let's write hardhat ignition script to deploy it.
./ignition/modules/PullConsumer.ts
import { buildModule } from"@nomicfoundation/hardhat-ignition/modules";// PullOracle address on Eth Sepolia networkconstPullOracleAddress="0x0b2d8Ef1D9104c4Df5C89F00B645Ce8bAa56DeB5";constPullConsumerModule=buildModule("PullConsumerModule", (m) => {constconsumer=m.contract("PullConsumer", [PullOracleAddress]);return { consumer };});exportdefault PullConsumerModule;
Optional: Spin up local development node forked from eth_sepolia.
To deploy your contract on testnet, you'll need ETH eth_sepolia testnet tokens. In case you don't have ETH on eth_sepolia, you can spin up a local development fork of eth_sepolia by using anvil node like this:
Step #4: Understanding update format and Finalized Data API
At this point, PullConsumer contract is ready to verify updates that we send it. But before doing that, let's quickly go through the update format and the data that it expects.
As an example, let’s imagine you need NGL/USD price in your contract. First we need to fetch the latest update from the Finalized Data API.
This response provides all necessary details to verify the NGL/USD update, including:
merkleRoot: The resulting merkle root from a merkle tree containing the latest updates for our asset collection (prices-feed1).
signatures: Transmitter signatures on the merkleRoot bytes, confirming its emission on the EOB.
feeds: An array containing updates and verification data for requested assets.
key: Asset data key.
value: The price value and its timestamp.
merkleProofs: An array of merkle proofs to verify the presence of the NGL/USD asset in the merkle tree.
This data enables executing the getLastPrice transaction on the PullOracleConsumer contract. To begin, fetch the latest update from the Finalized Data API for the NGL/USD price.
Step #5: Send a transaction with the update data
Now that we understand the Finalized Data API, we can create a script that retrieves an update from the API, parses the response, encodes it to EVM calldata, and sends a transaction to verify the update.
Create a script scripts/fetchAndVerify.ts, note that it imports some utility functions to help with fetching and encoding, you can get them from udf-examples repository or in the attachment below the code snippet.
./scripts/fetchAndVerify.ts
import { ethers } from"hardhat";import { fetchVerifyArgs } from"./utils";// PullConsumer contract address on ETH SepoliaconstPullConsumerAddress="0xAf84DEF16E25b6722aE9ADBd29eBf1573b6569e7";asyncfunctionmain() {// Fetch the update data from finalized-data-snapconstasset="NGL/USD";constverifyArgs=awaitfetchVerifyArgs(asset);// Bind PullConsumer contract on the networkconstconsumer=awaitethers.getContractAt("PullConsumer", PullConsumerAddress );// Send verify transactionlet tx =awaitconsumer.verifyPrice(verifyArgs.merkleRoot,verifyArgs.merkleProof,verifyArgs.signatures,verifyArgs.dataKey,verifyArgs.price,verifyArgs.timestamp, );awaittx.wait();console.log("sent tx:",tx.hash);}main().then(() =>process.exit(0)).catch(error => {console.error(error);process.exit(1); });