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 from finalized-data-snap and verified within the same transaction before being used. This scheme comes with some very nice 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 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.

$ yarn init
// yarn initialization flow
$ yarn install --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, 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: UNLICENSED
pragma solidity ^0.8.19;

interface IPullOracle {
    /// @dev Represents a digital signature
    struct Signature {
        bytes32 r; // The first 32 bytes of the signature
        bytes32 s; // The second 32 bytes of the signature
        uint8 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.
    function getLastPrice(
        bytes32 merkleRoot,
        bytes32[] calldata merkleProof,
        Signature[] calldata signatures,
        bytes32 dataKey,
        uint256 price,
        uint256 timestamp
    ) external returns (uint256);
}

contract PullConsumer {
    // @notice PullOracle address on specific chain
    IPullOracle public pullOracle;

    constructor(address _pullOracle) {
        pullOracle = IPullOracle(_pullOracle);
    }

    event PriceVerified(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 error
    function verifyPrice(
        bytes32 merkleRoot,
        bytes32[] calldata merkleProof,
        IPullOracle.Signature[] calldata signatures,
        bytes32 dataKey,
        uint256 updatePrice,
        uint256 updateTimestamp
    ) public {
    
        // Verify the update through PullOracle
        uint256 verifiedPrice = pullOracle.getLastPrice(
            merkleRoot,
            merkleProof,
            signatures,
            dataKey,
            updatePrice,
            updateTimestamp
        );

        emit PriceVerified(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 network
const PullOracleAddress = "0x0b2d8Ef1D9104c4Df5C89F00B645Ce8bAa56DeB5";

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

	return { consumer };
});

export default 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:

$ anvil --fork-url https://ethereum-sepolia-rpc.publicnode.com

Copy one of private keys from stdout to use it for deployment.

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.27",
  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 API.

$ curl https://pricefeed.entangle.fi/spotters/prices-feed1?assets=NGL/USD

{
  "calldata": {
    "merkleRoot": "0xa4d6d7594888f1fe8675d033737baf6106819f7b0e59f3d794f5e1adcec70022",
    "signatures": [
      {
        "R": "0x6ddc9b92d85143cca3676e99ec80542aeda2be4dddc86cdfe51eaa7d5282463b",
        "S": "0x4a2a2f401289b63516722fd1ee0e82f8f602a7341239986a2e8ed469ec3efde8",
        "V": 28
      },
      {
        "R": "0x23b500c294ec06b1882e4a7ddae72a9293c15ed5478d847ec32a993a2e98f7a9",
        "S": "0x6b75f9bb6277737ce73ef963d304c0633e075c449314bbe009000aeefe2535cd",
        "V": 28
      },
      {
        "R": "0x16a9849fe9fad62d9fda9065029ddfc8af50436282709dddf2ea1c4695cad6a6",
        "S": "0x46553e9ac51b03f18af4e71c383fa44af6fbfb9daa986aebe10769f7b7e55f0e",
        "V": 28
      }
    ],
    "feeds": [
      {
        "key": "NGL/USD",
        "value": {
          "data": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcHqjZobCo0=",
          "timestamp": 1722866387
        },
        "merkleProofs": [
          "wg/sd9GqECY5zFKdsHRThUdPda0Parv24Npcj15EqJ4=",
          "CK5qUUFgAaZBYE/Aq3x+Y61HHAQnW7q6A2K1obw55ZE="
        ]
      }
    ]
  },
  "error": ""
}

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

async function main() {

  // Fetch the update data from finalized-data-snap
  const asset = "NGL/USD";
  const verifyArgs = await fetchVerifyArgs(asset);

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

  // Send verify transaction
  let tx = await consumer.verifyPrice(
    verifyArgs.merkleRoot,
    verifyArgs.merkleProof,
    verifyArgs.signatures,
    verifyArgs.dataKey,
    verifyArgs.price,
    verifyArgs.timestamp,
  );
  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 we wrote.

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

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

$ cast run 0x3f6abc216fb4930636ae3954bc912e7156903a2db659b70c49e128a417e18591
Executing previous transactions from the block.
Traces:
  [77193] 0xAf84DEF16E25b6722aE9ADBd29eBf1573b6569e7::60d1632b(dc8c4a19d82fb9f957de9c24a266b72ebd40cff1bd9a4b8b7a6bab8428f9aea500000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001804e474c2f55534400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000012d2f2e3c7d52040000000000000000000000000000000000000000000000000000000066f36e3a00000000000000000000000000000000000000000000000000000000000000057e97a26dd5d30ebb101fb45d4ae60d02c47457971003d3de9d099eb028cbd1e70979269a8c493b343b9fa54fc4a62485500ad54b92969fe6924a917b926f9032fbe2163edb6572159eec75bff0b9b01dee8e25e91d1f3adbade0153ca14835372c6c195c3d5a63da16fb76ac73505f10198f9186b6150bc1ae930bbb7e8079f5db92d5e4a67e686aaa20e67f31d837f176b2a50b4127da404af0e8b4de3cbbd500000000000000000000000000000000000000000000000000000000000000034239305fe134af2756899a2ba2157e7ae3a6c18ff45204e525c9244e58553b967b27016b382e76210b9ed0fc77b2b01ebf51861c5ecfadd58b638316a34384fb000000000000000000000000000000000000000000000000000000000000001c91e2e33976ba9c271cbc8db5e9012f9e20f8b375c95f71fe47eb0ed8cd572daf352470a62f65c091f180963161a3529962f50f0cd376e4965927cf409c8e34f4000000000000000000000000000000000000000000000000000000000000001cc681d06eaee22bb0d6d54c21a2c1531ddebbc1c995e095868e2df31b174c9c841378295f164bf88090a29b1050211cb159267930ffc5c4db9d3b69bc368d39dd000000000000000000000000000000000000000000000000000000000000001b)
    ├─ [64139] 0x0b2d8Ef1D9104c4Df5C89F00B645Ce8bAa56DeB5::e4f6d9bb(dc8c4a19d82fb9f957de9c24a266b72ebd40cff1bd9a4b8b7a6bab8428f9aea500000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001804e474c2f55534400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000012d2f2e3c7d52040000000000000000000000000000000000000000000000000000000066f36e3a00000000000000000000000000000000000000000000000000000000000000057e97a26dd5d30ebb101fb45d4ae60d02c47457971003d3de9d099eb028cbd1e70979269a8c493b343b9fa54fc4a62485500ad54b92969fe6924a917b926f9032fbe2163edb6572159eec75bff0b9b01dee8e25e91d1f3adbade0153ca14835372c6c195c3d5a63da16fb76ac73505f10198f9186b6150bc1ae930bbb7e8079f5db92d5e4a67e686aaa20e67f31d837f176b2a50b4127da404af0e8b4de3cbbd500000000000000000000000000000000000000000000000000000000000000034239305fe134af2756899a2ba2157e7ae3a6c18ff45204e525c9244e58553b967b27016b382e76210b9ed0fc77b2b01ebf51861c5ecfadd58b638316a34384fb000000000000000000000000000000000000000000000000000000000000001c91e2e33976ba9c271cbc8db5e9012f9e20f8b375c95f71fe47eb0ed8cd572daf352470a62f65c091f180963161a3529962f50f0cd376e4965927cf409c8e34f4000000000000000000000000000000000000000000000000000000000000001cc681d06eaee22bb0d6d54c21a2c1531ddebbc1c995e095868e2df31b174c9c841378295f164bf88090a29b1050211cb159267930ffc5c4db9d3b69bc368d39dd000000000000000000000000000000000000000000000000000000000000001b)
    │   ├─ [59122] 0x6d88F714E488bFB551022eC77933dC0469527b46::e4f6d9bb(dc8c4a19d82fb9f957de9c24a266b72ebd40cff1bd9a4b8b7a6bab8428f9aea500000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001804e474c2f55534400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000012d2f2e3c7d52040000000000000000000000000000000000000000000000000000000066f36e3a00000000000000000000000000000000000000000000000000000000000000057e97a26dd5d30ebb101fb45d4ae60d02c47457971003d3de9d099eb028cbd1e70979269a8c493b343b9fa54fc4a62485500ad54b92969fe6924a917b926f9032fbe2163edb6572159eec75bff0b9b01dee8e25e91d1f3adbade0153ca14835372c6c195c3d5a63da16fb76ac73505f10198f9186b6150bc1ae930bbb7e8079f5db92d5e4a67e686aaa20e67f31d837f176b2a50b4127da404af0e8b4de3cbbd500000000000000000000000000000000000000000000000000000000000000034239305fe134af2756899a2ba2157e7ae3a6c18ff45204e525c9244e58553b967b27016b382e76210b9ed0fc77b2b01ebf51861c5ecfadd58b638316a34384fb000000000000000000000000000000000000000000000000000000000000001c91e2e33976ba9c271cbc8db5e9012f9e20f8b375c95f71fe47eb0ed8cd572daf352470a62f65c091f180963161a3529962f50f0cd376e4965927cf409c8e34f4000000000000000000000000000000000000000000000000000000000000001cc681d06eaee22bb0d6d54c21a2c1531ddebbc1c995e095868e2df31b174c9c841378295f164bf88090a29b1050211cb159267930ffc5c4db9d3b69bc368d39dd000000000000000000000000000000000000000000000000000000000000001b) [delegatecall]
          ├─ [7969] 0x2aD3e0F3EAcf63B28C446216C3B7904f264dDDEB::e20aeb2d(756e6976657273616c2d646174612d6665656473330000000000000000000000) [staticcall]
             ├─ [3076] 0xa4175a584CBdd3338d224a43D5402cE1535bdBE5::e20aeb2d(756e6976657273616c2d646174612d6665656473330000000000000000000000) [delegatecall]
                └─  0x0000000000000000000000000000000000000000000000000000000000000003
             └─  0x0000000000000000000000000000000000000000000000000000000000000003
          ├─ [5083] 0x2aD3e0F3EAcf63B28C446216C3B7904f264dDDEB::43304c0f(756e6976657273616c2d646174612d6665656473330000000000000000000000) [staticcall]
             ├─ [4687] 0xa4175a584CBdd3338d224a43D5402cE1535bdBE5::43304c0f(756e6976657273616c2d646174612d6665656473330000000000000000000000) [delegatecall]
                └─  0x00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000001d4c
             └─  0x00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000001d4c
          ├─ [3000] PRECOMPILE::ecrecover(0x2d2b8a34999f452c2e9c8c496574b5a734c1f2f60cced0d5e546484b0e4bb8d6, 28, 29953692157924467511555369120011444243754965704359747436131081496971770674070 [2.995e76], 55703397203645364396421772722909038562063878156662274012771193357837715866875 [5.57e76]) [staticcall]
             └─  0xEfCF6f62254F76d9eF9bc06C843EAc97F0aA1723
          ├─ [3222] 0x2aD3e0F3EAcf63B28C446216C3B7904f264dDDEB::55b5190b(756e6976657273616c2d646174612d6665656473330000000000000000000000000000000000000000000000efcf6f62254f76d9ef9bc06c843eac97f0aa1723) [staticcall]
             ├─ [2826] 0xa4175a584CBdd3338d224a43D5402cE1535bdBE5::55b5190b(756e6976657273616c2d646174612d6665656473330000000000000000000000000000000000000000000000efcf6f62254f76d9ef9bc06c843eac97f0aa1723) [delegatecall]
                └─  0x0000000000000000000000000000000000000000000000000000000000000001
             └─  0x0000000000000000000000000000000000000000000000000000000000000001
          ├─ [3000] PRECOMPILE::ecrecover(0x2d2b8a34999f452c2e9c8c496574b5a734c1f2f60cced0d5e546484b0e4bb8d6, 28, 65986238726854839964835718487361910560562535202532817199043045962945513598383 [6.598e76], 24036964945178663856501994355795580653552149725259988561766297198145706013940 [2.403e76]) [staticcall]
             └─  0xE8a5D7DE6c51ae39326f93180F610F5cb8f0B4CC
          ├─ [3222] 0x2aD3e0F3EAcf63B28C446216C3B7904f264dDDEB::55b5190b(756e6976657273616c2d646174612d6665656473330000000000000000000000000000000000000000000000e8a5d7de6c51ae39326f93180f610f5cb8f0b4cc) [staticcall]
             ├─ [2826] 0xa4175a584CBdd3338d224a43D5402cE1535bdBE5::55b5190b(756e6976657273616c2d646174612d6665656473330000000000000000000000000000000000000000000000e8a5d7de6c51ae39326f93180f610f5cb8f0b4cc) [delegatecall]
                └─  0x0000000000000000000000000000000000000000000000000000000000000001
             └─  0x0000000000000000000000000000000000000000000000000000000000000001
          ├─ [3000] PRECOMPILE::ecrecover(0x2d2b8a34999f452c2e9c8c496574b5a734c1f2f60cced0d5e546484b0e4bb8d6, 27, 89787305838094802266463694692248217055094399282269066413640309071054992350340 [8.978e76], 8806251305998742245877693401520295194206986436347327694944725052504547342813 [8.806e75]) [staticcall]
             └─  0xBC506a4af4d452c2908CE6c590028EFD8EEC7962
          ├─ [3222] 0x2aD3e0F3EAcf63B28C446216C3B7904f264dDDEB::55b5190b(756e6976657273616c2d646174612d6665656473330000000000000000000000000000000000000000000000bc506a4af4d452c2908ce6c590028efd8eec7962) [staticcall]
             ├─ [2826] 0xa4175a584CBdd3338d224a43D5402cE1535bdBE5::55b5190b(756e6976657273616c2d646174612d6665656473330000000000000000000000000000000000000000000000bc506a4af4d452c2908ce6c590028efd8eec7962) [delegatecall]
                └─  0x0000000000000000000000000000000000000000000000000000000000000001
             └─  0x0000000000000000000000000000000000000000000000000000000000000001
          └─  0x000000000000000000000000000000000000000000000000012d2f2e3c7d5204
       └─  0x000000000000000000000000000000000000000000000000012d2f2e3c7d5204
    ├─  emit topic 0: 0x306259b6674493dd28eb1001985ff18fc3fd9890824711cc7c173223eec959af
               data: 0x4e474c2f55534400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000012d2f2e3c7d52040000000000000000000000000000000000000000000000000000000066f36e3a
    └─  ()


Transaction successfully executed.
Gas used: 106005

Last updated