Fetch Data via Pull Model

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:

  1. 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.

  2. 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.

$ yarn init -y
// yarn initialization flow
$ yarn add --dev hardhat
$ yarn hardhat init
// Hardhat initialization flow

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: UNLICENSED
pragma solidity ^0.8.28;

interface IUDFOracle {

    struct LatestUpdate {
        // @notice The price for asset from latest update
        uint256 latestPrice;
        // @notice The timestamp of latest update
        uint256 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 ids
    function getOraclePriceUpdateFromMsg(
        bytes calldata updateMsg,
        bytes32 dataFeedId
    ) external payable returns (LatestUpdate memory update);

    // @notice Returns the necessary fee to be attached to getOraclePriceUpdateFromMsg function
    // @return required fee to verify an update
    function getUpdateFee(uint256 nUpdates) external view returns (uint256);
}

contract PullConsumer {
    bytes32 public constant BTC_KEY = bytes32("BTC/USD");

    // @notice UDFOracle address on specific chain
    IUDFOracle public oracle;

    constructor(address _udfOracle) {
        oracle = IUDFOracle(_udfOracle);
    }

    event PriceVerified(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 error
    function verifyPrice(
        bytes calldata updateMsg
    ) public {

        // Calculate required fee
        uint256 fee = oracle.getUpdateFee(1);

        // Verify the update through UDFOracle
        IUDFOracle.LatestUpdate memory update = oracle.getOraclePriceUpdateFromMsg{value: fee}(updateMsg, BTC_KEY);

        // Emit event with verified update
        emit PriceVerified(BTC_KEY, update.latestPrice, update.latestTimestamp);
    }

    // @notice deposit native tokens for fee coverage
    receive() payable external {}
}

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 network
const UDFOracleAddress = "0xa22Cb39480D660c1C68e3dEa2B9b4e3683773035";

const PullConsumerModule = buildModule("PullConsumerModule", (m) => {
	const consumer = m.contract("PullConsumer", [UDFOracleAddress]);

	return { consumer };
});

export default 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";

const config: 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
    },
  },
};

export default config;

Now we're ready to deploy PullConsumer contract.

$ yarn hardhat ignition deploy ./ignition/modules/PullConsumer.ts --network eth_sepolia

 Confirm deploy to network eth_sepolia (11155111)? … yes
Hardhat Ignition 🚀

Resuming existing deployment from ./ignition/deployments/chain-11155111

Deploying [ PullConsumerModule ]

Batch #1
  Executed PullConsumerModule#PullConsumer

[ PullConsumerModule ] successfully deployed 🚀

Deployed Addresses

PullConsumerModule#PullConsumer - 0xAf84DEF16E25b6722aE9ADBd29eBf1573b6569e7

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).

$ curl https://udfsnaptest.ent-dx.com/last_votes?feedKeys=NGL/USD

{
  "update_call_data": "0x..",
  "feeds": [
    {
      "feed_key": "NGL/USD",
      "votes": [
        {
          "Value": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASXMH4q/zZY=",
          "Timestamp": 1737047589,
          "Publisher": "0x2e1f4AE4195f25bA62351f9Dc8A7F7D65743B196",
          "Signature": {
            "R": "0xdee67f0dc44369a8d3f894560710c7a01083c57c801ca86893f4dfccdf0e413f",
            "S": "0x030bc8af5a265644007acd41b2668f9d78c575bad53a37b3df087d0d6b7a0400",
            "V": 27
          }
        },
        {
          "Value": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASXMH4q/zZY=",
          "Timestamp": 1737047590,
          "Publisher": "0x5a598D1d71b932ed3bF880AE00c9d44494bd75d0",
          "Signature": {
            "R": "0x443bf1153015f65a64541043cb46664d67d2183552c2c18f2807d3a1250ba494",
            "S": "0x6882828d918cef53b3814482a1128628fc29b70ee4a81371f627e3e0f9f135fc",
            "V": 28
          }
        },
        {
          "Value": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASXMH4q/zZY=",
          "Timestamp": 1737047589,
          "Publisher": "0x6d5da0218C26E4B9cec27931ECC3D7DF58cC7750",
          "Signature": {
            "R": "0xcb646121d87fb9bbabec54a0709bbd10e8cb27f1660dc892b88f5c06da256339",
            "S": "0x486647bca396d35cff273103ff54bbbbf6710aae7abc9a52846efa27e653dd3e",
            "V": 27
          }
        }
      ]
    }
  ]
}

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 Sepolia
const PullConsumerAddress = "0x06fAdf55c689Da5d17472FE604302e595Bd257c0";

async function main() {

  // Use default hardhat signer to fund the PullConsumer contract
  const [funder] = await ethers.getSigners();

  // 1e-15 is the exact cost for 10 updates on eth_seoplia
  const depositAmount = ethers.parseEther("0.000000000000001");

  // Send funding transaction
  const tx = await funder.sendTransaction({
    to: PullConsumerAddress,
    value: depositAmount
  });
  await tx.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.

$ yarn hardhat run ./scripts/fundConsumer.ts --network eth_sepolia

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.

$ yarn add --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 Sepolia
const PullConsumerAddress = "0x06fAdf55c689Da5d17472FE604302e595Bd257c0";

async function main() {

  // Fetch the update data from finalized-data-snap
  const sdk = new UdfSdk();
  const updateData = await sdk.getCallData(["BTC/USD"]);

  // Bind PullConsumer contract on the network
  const consumer = await ethers.getContractAt(
    "PullConsumer",
    PullConsumerAddress
  );

  // Send verify transaction
  let tx = await consumer.verifyPrice(
    updateData
  );
  await tx.wait();
  console.log("sent tx:", tx.hash);
}

main()
  .then(() => process.exit(0))
  .catch(error => {
    console.error(error);
    process.exit(1);
  });

Now let's execute the script.

$ yarn hardhat run ./scripts/fetchAndVerify.ts --network eth_sepolia
sent tx: 0x32f0d7c271dd5becb0829c3a22ecbc10b130fabc68ed84761fdd52795371e057

You can use cast to examine the transaction calltrace and logs. The emitted event shows the latest update that we verified.

Note that gas usage may vary depending on the result and the version of the library in use.

$ cast run 0x32f0d7c271dd5becb0829c3a22ecbc10b130fabc68ed84761fdd52795371e057
Executing previous transactions from the block.
Traces:
  [104051] 0x06fAdf55c689Da5d17472FE604302e595Bd257c0::a330ab1a(000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001a955444646014254432f55534400000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000015ae906a9bb68703c13300000000000000000000000000000000000000000000000000000000678a3da3ee0ac7ca70eeec26c1248a2685b5f771f4bc5cd79e0d5c415a9db83cdf9bce4c5ea5769a4390c93372e77377885c9b79611fa42620211ac87f525610072590961b0000000000000000000000000000000000000000000015aed621212a7521cb6800000000000000000000000000000000000000000000000000000000678a3da8d2a95cafa29293d2063227591d7bf0dcf075b9869d391695b4b8239c0a58257f00d337bb063543f8f8b86cd2cc3a4f756f4c568b34d333ef19dc21beef02bb7b1b0000000000000000000000000000000000000000000015aed72aa4d7f06d60b000000000000000000000000000000000000000000000000000000000678a3daad66baf2f23e08d961cfb64d2d091b7de183cd6b774c9428ecf554497803b5cb3695d1e2ae1153fdce40489d710747a48b94c7bc802b6a7eff6398a0136b9db451b0000000000000000000000000000000000000000000000)
    ├─ [8321] 0xa22Cb39480D660c1C68e3dEa2B9b4e3683773035::getUpdateFee(1) [staticcall]
       ├─ [3428] 0xE9698AE915c8D813929A89087607cb4771E15bB4::getUpdateFee(1) [delegatecall]
          └─  [Return] 0x0000000000000000000000000000000000000000000000000000000000000064
       └─  [Return] 0x0000000000000000000000000000000000000000000000000000000000000064
    ├─ [79487] 0xa22Cb39480D660c1C68e3dEa2B9b4e3683773035::b228abee{value: 100}(00000000000000000000000000000000000000000000000000000000000000404254432f5553440000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001a955444646014254432f55534400000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000015ae906a9bb68703c13300000000000000000000000000000000000000000000000000000000678a3da3ee0ac7ca70eeec26c1248a2685b5f771f4bc5cd79e0d5c415a9db83cdf9bce4c5ea5769a4390c93372e77377885c9b79611fa42620211ac87f525610072590961b0000000000000000000000000000000000000000000015aed621212a7521cb6800000000000000000000000000000000000000000000000000000000678a3da8d2a95cafa29293d2063227591d7bf0dcf075b9869d391695b4b8239c0a58257f00d337bb063543f8f8b86cd2cc3a4f756f4c568b34d333ef19dc21beef02bb7b1b0000000000000000000000000000000000000000000015aed72aa4d7f06d60b000000000000000000000000000000000000000000000000000000000678a3daad66baf2f23e08d961cfb64d2d091b7de183cd6b774c9428ecf554497803b5cb3695d1e2ae1153fdce40489d710747a48b94c7bc802b6a7eff6398a0136b9db451b0000000000000000000000000000000000000000000000)
    │   ├─ [78998] 0xE9698AE915c8D813929A89087607cb4771E15bB4::b228abee{value: 100}(00000000000000000000000000000000000000000000000000000000000000404254432f5553440000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001a955444646014254432f55534400000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000015ae906a9bb68703c13300000000000000000000000000000000000000000000000000000000678a3da3ee0ac7ca70eeec26c1248a2685b5f771f4bc5cd79e0d5c415a9db83cdf9bce4c5ea5769a4390c93372e77377885c9b79611fa42620211ac87f525610072590961b0000000000000000000000000000000000000000000015aed621212a7521cb6800000000000000000000000000000000000000000000000000000000678a3da8d2a95cafa29293d2063227591d7bf0dcf075b9869d391695b4b8239c0a58257f00d337bb063543f8f8b86cd2cc3a4f756f4c568b34d333ef19dc21beef02bb7b1b0000000000000000000000000000000000000000000015aed72aa4d7f06d60b000000000000000000000000000000000000000000000000000000000678a3daad66baf2f23e08d961cfb64d2d091b7de183cd6b774c9428ecf554497803b5cb3695d1e2ae1153fdce40489d710747a48b94c7bc802b6a7eff6398a0136b9db451b0000000000000000000000000000000000000000000000) [delegatecall]
    │   │   ├─ [70088] 0xD2DbaeDdeAd06a2f8CA3DFd497918b0f04D655DA::40366475(00000000000000000000000000000000000000000000000000000000000000404254432f5553440000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001a955444646014254432f55534400000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000015ae906a9bb68703c13300000000000000000000000000000000000000000000000000000000678a3da3ee0ac7ca70eeec26c1248a2685b5f771f4bc5cd79e0d5c415a9db83cdf9bce4c5ea5769a4390c93372e77377885c9b79611fa42620211ac87f525610072590961b0000000000000000000000000000000000000000000015aed621212a7521cb6800000000000000000000000000000000000000000000000000000000678a3da8d2a95cafa29293d2063227591d7bf0dcf075b9869d391695b4b8239c0a58257f00d337bb063543f8f8b86cd2cc3a4f756f4c568b34d333ef19dc21beef02bb7b1b0000000000000000000000000000000000000000000015aed72aa4d7f06d60b000000000000000000000000000000000000000000000000000000000678a3daad66baf2f23e08d961cfb64d2d091b7de183cd6b774c9428ecf554497803b5cb3695d1e2ae1153fdce40489d710747a48b94c7bc802b6a7eff6398a0136b9db451b0000000000000000000000000000000000000000000000) [staticcall]
    │   │   │   ├─ [65099] 0x0B801785fd2e2c6DeaA8f25272a6A3a9477Acb0A::40366475(00000000000000000000000000000000000000000000000000000000000000404254432f5553440000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001a955444646014254432f55534400000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000015ae906a9bb68703c13300000000000000000000000000000000000000000000000000000000678a3da3ee0ac7ca70eeec26c1248a2685b5f771f4bc5cd79e0d5c415a9db83cdf9bce4c5ea5769a4390c93372e77377885c9b79611fa42620211ac87f525610072590961b0000000000000000000000000000000000000000000015aed621212a7521cb6800000000000000000000000000000000000000000000000000000000678a3da8d2a95cafa29293d2063227591d7bf0dcf075b9869d391695b4b8239c0a58257f00d337bb063543f8f8b86cd2cc3a4f756f4c568b34d333ef19dc21beef02bb7b1b0000000000000000000000000000000000000000000015aed72aa4d7f06d60b000000000000000000000000000000000000000000000000000000000678a3daad66baf2f23e08d961cfb64d2d091b7de183cd6b774c9428ecf554497803b5cb3695d1e2ae1153fdce40489d710747a48b94c7bc802b6a7eff6398a0136b9db451b0000000000000000000000000000000000000000000000) [delegatecall]
                ├─ [3000] PRECOMPILES::ecrecover(0x3984bb7e1ef55d2f7141db8f518ffa36f1697116742df01c7db0655da99e65bf, 27, 107669505338790686031453094908890618571439522988596554118113281685317712858700, 42809756097531666972552580785672452814689850063670443074905288637454785810582) [staticcall]
                   └─  [Return] 0x0000000000000000000000005a598d1d71b932ed3bf880ae00c9d44494bd75d0
                ├─ [7722] 0x2aD3e0F3EAcf63B28C446216C3B7904f264dDDEB::55b5190b(7564662d76312e310000000000000000000000000000000000000000000000000000000000000000000000005a598d1d71b932ed3bf880ae00c9d44494bd75d0) [staticcall]
                   ├─ [2826] 0xa4175a584CBdd3338d224a43D5402cE1535bdBE5::55b5190b(7564662d76312e310000000000000000000000000000000000000000000000000000000000000000000000005a598d1d71b932ed3bf880ae00c9d44494bd75d0) [delegatecall]
                      └─  [Return] 0x0000000000000000000000000000000000000000000000000000000000000001
                   └─  [Return] 0x0000000000000000000000000000000000000000000000000000000000000001
                ├─ [3000] PRECOMPILES::ecrecover(0xf55bf7f55e02869990875c6eead1f5c3febf5db6dfbe973ef1db071315036fae, 27, 95284935052208949698212341092569313062112225970066620437216927335440704873855, 373189368881125244784677836006706524848972752981708289204010184273373084539) [staticcall]
                   └─  [Return] 0x0000000000000000000000006d5da0218c26e4b9cec27931ecc3d7df58cc7750
                ├─ [3222] 0x2aD3e0F3EAcf63B28C446216C3B7904f264dDDEB::55b5190b(7564662d76312e310000000000000000000000000000000000000000000000000000000000000000000000006d5da0218c26e4b9cec27931ecc3d7df58cc7750) [staticcall]
                   ├─ [2826] 0xa4175a584CBdd3338d224a43D5402cE1535bdBE5::55b5190b(7564662d76312e310000000000000000000000000000000000000000000000000000000000000000000000006d5da0218c26e4b9cec27931ecc3d7df58cc7750) [delegatecall]
                      └─  [Return] 0x0000000000000000000000000000000000000000000000000000000000000001
                   └─  [Return] 0x0000000000000000000000000000000000000000000000000000000000000001
                ├─ [3000] PRECOMPILES::ecrecover(0x6e42178e6d93a2228b7f57dba4a7c623d60508837d4015a625a77f9a572a5507, 27, 96985211309256781924049281191977154112961078301840483286110354376573052476595, 47657374086679531407092229775505437011821392514610355149874632795749179054917) [staticcall]
                   └─  [Return] 0x0000000000000000000000002e1f4ae4195f25ba62351f9dc8a7f7d65743b196
                ├─ [3222] 0x2aD3e0F3EAcf63B28C446216C3B7904f264dDDEB::55b5190b(7564662d76312e310000000000000000000000000000000000000000000000000000000000000000000000002e1f4ae4195f25ba62351f9dc8a7f7d65743b196) [staticcall]
                   ├─ [2826] 0xa4175a584CBdd3338d224a43D5402cE1535bdBE5::55b5190b(7564662d76312e310000000000000000000000000000000000000000000000000000000000000000000000002e1f4ae4195f25ba62351f9dc8a7f7d65743b196) [delegatecall]
                      └─  [Return] 0x0000000000000000000000000000000000000000000000000000000000000001
                   └─  [Return] 0x0000000000000000000000000000000000000000000000000000000000000001
                └─  [Return] 0x0000000000000000000000000000000000000000000015aed621212a7521cb6800000000000000000000000000000000000000000000000000000000678a3daa
             └─  [Return] 0x0000000000000000000000000000000000000000000015aed621212a7521cb6800000000000000000000000000000000000000000000000000000000678a3daa
          └─  [Return] 0x0000000000000000000000000000000000000000000015aed621212a7521cb6800000000000000000000000000000000000000000000000000000000678a3daa
       └─  [Return] 0x0000000000000000000000000000000000000000000015aed621212a7521cb6800000000000000000000000000000000000000000000000000000000678a3daa
    ├─  emit topic 0: 0x306259b6674493dd28eb1001985ff18fc3fd9890824711cc7c173223eec959af
               data: 0x4254432f555344000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000015aed621212a7521cb6800000000000000000000000000000000000000000000000000000000678a3daa
    └─  [Stop]


Transaction successfully executed.
Gas used: 130187

Last updated

Was this helpful?