In the Pull model, consumers include verification data about the most recent update directly in their transaction. This data is fetched directly from the oracle and verified within the same transaction before being used. This scheme comes with the following 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 their 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, which utilizes the UDFOracle.getOraclePriceUpdateFromMsg function to verify the update information provided through calldata. The update contains oracle votes along with their signatures.
Once the PullConsumer contract verifies the update against these inputs, it emits an event indicating successful verification. If the verification fails (e.g., due to mismatched data, invalid signatures, or other inconsistencies) the contract reverts the entire transaction, ensuring that only valid data is accepted.
./contractrs/PullConsumer.sol
// SPDX-License-Identifier: UNLICENSEDpragmasolidity ^0.8.28;interface IUDFOracle {structLatestUpdate {// @notice The price for asset from latest updateuint256 latestPrice;// @notice The timestamp of latest updateuint256 latestTimestamp; }// @notice Accept updates encoded to bytes, verifies the updates and returns update from calldata. Reverts if the update is not in message// @param updateMsg Encoded message from oracle// @param dataFeedId Array of data feed idsfunctiongetOraclePriceUpdateFromMsg(bytescalldata updateMsg,bytes32 dataFeedId ) externalpayablereturns (LatestUpdatememory update);// @notice Returns the necessary fee to be attached to getOraclePriceUpdateFromMsg function// @return required fee to verify an updatefunctiongetUpdateFee(uint256 nUpdates) externalviewreturns (uint256);}contract PullConsumer {bytes32publicconstant BTC_KEY =bytes32("BTC/USD");// @notice UDFOracle address on specific chain IUDFOracle public oracle;constructor(address_udfOracle) { oracle =IUDFOracle(_udfOracle); }eventPriceVerified(bytes32,uint256,uint256);// @notice function that verifies BTC/USD feed from provided update through UDFOracle and// emits an event if the update is valid, otherwise reverts with a custom errorfunctionverifyPrice(bytescalldata updateMsg ) public {// Calculate required feeuint256 fee = oracle.getUpdateFee(1);// Verify the update through UDFOracle IUDFOracle.LatestUpdate memory update = oracle.getOraclePriceUpdateFromMsg{value: fee}(updateMsg, BTC_KEY);// Emit event with verified updateemitPriceVerified(BTC_KEY, update.latestPrice, update.latestTimestamp); }// @notice deposit native tokens for fee coveragereceive() payableexternal {}}
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";// UDFOracle address on Eth Sepolia networkconstUDFOracleAddress="0xa22Cb39480D660c1C68e3dEa2B9b4e3683773035";constPullConsumerModule=buildModule("PullConsumerModule", (m) => {constconsumer=m.contract("PullConsumer", [UDFOracleAddress]);return { consumer };});exportdefault PullConsumerModule;
Acquire Tokens For Testnet
To deploy your contract on the testnet, you'll need ETH Sepolia testnet tokens. If you don’t have any ETH on eth_sepolia, follow the Hardhat Network guide to spin up a local development node forked from eth_sepolia.
Step #3: Deploy the contract
Modify hardhat config to add eth_sepolia network.
./hardhat.config.ts
import { HardhatUserConfig } from"hardhat/config";import"@nomicfoundation/hardhat-toolbox";constconfig:HardhatUserConfig= { solidity:"0.8.28", networks: { eth_sepolia: { chainId:11155111,// or http://127.0.0.1:8545, if you forked it locally url:"https://ethereum-sepolia-rpc.publicnode.com", accounts: [ "0x" ],// TODO: Set deployer private key }, },};exportdefault config;
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 backend service. This service constantly synchronizes it's state with oracle's state and provides oracle data via convenient APIs (REST, Websocket).
This response provides all necessary details to verify the NGL/USD update, specifically:
feeds: An array containing parsed update data for requested feeds.
votes: Publisher's latest votes on the specific feed. This aggregated array of votes is necessary to obtain agreed-upon value on-chain.
Value: The data that Publisher observed.
Timestamp: Timestamp at which the Publisher observed that data.
Publisher: Publisher's public key.
Signature: ECDSA signature for the observed value. This signature represents commitment from publisher that this data is what he observed.
This data enables executing the 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 scripts to demonstrate update verification.
First of all we need to fund the PullConsumer contract, so it will be able to pay the verification fee. Copy the following code to scripts/fundConsumer.ts.
./scripts/fundConsumer.ts
import { ethers } from"hardhat";// PullConsumer contract address on ETH SepoliaconstPullConsumerAddress="0x06fAdf55c689Da5d17472FE604302e595Bd257c0";asyncfunctionmain() {// Use default hardhat signer to fund the PullConsumer contractconst [funder] =awaitethers.getSigners();// 1e-15 is the exact cost for 10 updates on eth_seopliaconstdepositAmount=ethers.parseEther("0.000000000000001");// Send funding transactionconsttx=awaitfunder.sendTransaction({ to: PullConsumerAddress, value: depositAmount });awaittx.wait();console.log(`sent ${depositAmount} to PullConsumer, tx: ${tx.hash}`);}main().then(() =>process.exit(0)).catch(error => {console.error(error);process.exit(1); });
Change the PullConsumerAddress to the address you got from deployment and execute the script to fund deployed PullConsumer.
Now we will need to 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. The contract can then validate the update and incorporate the verified price into its on-chain logic.
First, add the UDF SDK library. We will use it to fetch the update using the REST API and encode it for on-chain usage.
$yarnadd--dev@entangle-labs/udf-sdk
Copy the script below to scripts/fetchAndVerify.ts.
./scripts/fetchAndVerify.ts
import { ethers } from"hardhat";import { UdfSdk } from'@entangle-labs/udf-sdk';// PullConsumer contract address on ETH SepoliaconstPullConsumerAddress="0x06fAdf55c689Da5d17472FE604302e595Bd257c0";asyncfunctionmain() {// Fetch the update data from finalized-data-snapconstsdk=newUdfSdk();constupdateData=awaitsdk.getCallData(["BTC/USD"]);// Bind PullConsumer contract on the networkconstconsumer=awaitethers.getContractAt("PullConsumer", PullConsumerAddress );// Send verify transactionlet tx =awaitconsumer.verifyPrice( updateData );awaittx.wait();console.log("sent tx:",tx.hash);}main().then(() =>process.exit(0)).catch(error => {console.error(error);process.exit(1); });