EVM Smart Contracts

In the Pull model, consumers include verification data about the most recent update directly in their transaction. This data is fetched 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 (with subscription) 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(select TypeScript project)

Step 2: Write a contract that verifies an update in the transaction

Now create a new PullVerifierSub contract, which utilizes the pullMarketplace.verifyAsSubscriber function to verify the update information provided through calldata. The update contains oracle votes along with their signatures.

This contract should already be in subscription so it can have access for verification. The subscription can be purchased from the marketplace and the consumer contract needs to be added as an authorized address for the subscription.

Once the PullVerifierSub contract verifies the update against these inputs, it stores them on-chain, to later be accessed. 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/PullVerifierSub.sol
// SPDX-License-Identifier: BSL1.1
pragma solidity ^0.8.20;

interface IPullMarketplace  {
        struct PriceUpdate {
        uint256 price;
        uint256 timestamp;
    }
    function verifyAsSubscriber(
        bytes calldata updateData,
        bytes32 feedKey
    )
        external
        returns (PriceUpdate memory);
}
contract PullVerifierSub {
    IPullMarketplace public pullMarketplace;
    uint public latestPrice;
    uint public latestTimestamp;
    constructor(address _pullMarketplace){
        pullMarketplace = IPullMarketplace(_pullMarketplace);
    }
    // @notice function that verifies feed as subscriber from provided update through UDFOracle and stores update data
    function verify(bytes calldata updateData, bytes32 feedKey) external{
        IPullMarketplace.PriceUpdate memory priceUpdate = pullMarketplace.verifyAsSubscriber(updateData,feedKey);
        latestPrice = priceUpdate.price;
        latestTimestamp = priceUpdate.timestamp;
    }
}

Next, create an ignition script to help with deployment of the new PullVerifierSub contract.

./ignition/modules/PullVerifierSub.ts
import { buildModule } from "@nomicfoundation/hardhat-ignition/modules";

// PullMarketplace address on Eth Sepolia network
const PullMarketplaceAddress = "0xC6670092056e7bf56c3149FDE52A187b3e9f2e63";

const PullVerifierSubModule = buildModule("PullVerifierSubModule", (m) => {
	const consumer = m.contract("PullVerifierSub", [PullMarketplaceAddress ]);

	return { consumer };
});

export default PullVerifierSubModule;

Step 3: Deploy PullVerifierSub

To deploy your contract on testnet, you'll need ETH eth_sepolia testnet tokens. You can obtain funds from public faucets (Option 1, Option 2, Option 3, Option 4, Option 5).

Now let's modify the hardhat configuration file to add the 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 can deploy the PullVerifierSub contract by running the command below.

yarn hardhat ignition deploy ./ignition/modules/PullVerifierSub.ts --network eth_sepolia
Expected Result
✔ Confirm deploy to network eth_sepolia (11155111)? … yes
Compiled 1 Solidity file successfully (evm target: paris).
Hardhat Ignition 🚀

Deploying [ PullVerifierSubModule ]

Batch #1
  Executed PullVerifierSubModule#PullVerifierSub

[ PullVerifierSubModule] successfully deployed 🚀

Deployed Addresses

PullVerifierSubModule#PullVerifierSub - 0x7c0ad1Bb6c6dD48CA70C190B400aD56DeF61F43C

Step 4: Create a Pull Subscribtion

Now we should add the deployed PullVerifierSub contract as an allowed address to our subscription, so that it can verify Pull updates.

To do that, go to the BTC/USD pair page and select Pull Mode. Then add the address of the deployed PullVerifierSub contract under "Which addresses can read the contract?" and click Subscribe.

You may read more about subscribing to feeds at Subscribe to a Data Feed.

Step 5: Execute transaction

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

To validate that we have our price, we will write an example backend script that send transaction to execute PullVerifierSub.verifyAsSubscriber. Let's create separate directory scripts and our new script verifyAsSub.ts to do just that.

mkdir scripts && touch ./scripts/verifyAsSub.ts

Change the PullVerifierAddress to the address you got from deployment and execute the script.

./scripts/verifyAsSub.ts
import {
  PullVerifierSub,
} from "../typechain-types";
import { ethers } from "hardhat";
import { UdfSdk } from '@entangle-labs/udf-sdk';

// PullVerifierSub address we got from the deployment
const PullVerifierAddress = "0x7c0ad1Bb6c6dD48CA70C190B400aD56DeF61F43C";

async function main() {  
  const pullVerifier = await ethers.getContractAt(
    "PullVerifierSub",
    PullVerifierAddress 
  ) as PullVerifierSub;

  // Fetch the update data from finalized-data-snap
  const sdk = new UdfSdk();
  const updateData = await sdk.getCallData(["BTC/USD"]);
  const feedKey = ethers.encodeBytes32String("BTC/USD");
  let tx = await pullVerifier.verify(updateData,feedKey);
  await tx.wait();

  console.log("sent PullVerifierSub.verify tx", tx.hash);
  let price = await pullVerifier.latestPrice();
  let timestamp = await pullVerifier.latestTimestamp();
  console.log("price:", price );
  console.log("timestamp:", timestamp );
}

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

Now let's run the script using the command below to ensure it's working.

yarn hardhat run ./scripts/verifyAsSub.ts --network eth_sepolia
Expected Results
sent PullVerifierSub.verify tx 0x7d202c67c1d3f04606743a406979eb4dd4a435be37eb0490c502895c28ecc0ec
price: 84087090417798546633122n
timestamp: 1743492361n

You can use cast to examine the transaction call trace 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 0x7d202c67c1d3f04606743a406979eb4dd4a435be37eb0490c502895c28ecc0ec -r https://ethereum-sepolia-rpc.publicnode.com
Expected Results
Executing previous transactions from the block.
Traces:
  [99231] PullVerifierSub::verify(0x55444646014254432f55534400000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000011ce4ebc2338797454e80000000000000000000000000000000000000000000000000000000067eba567c157813327ab23012c0c51d803a60fdeb2db3460e19840849b99b5e4996ee90853a0d1ad5e5be5304cb76990ffd1e04680f7f2f85680ac62f529b5614031062e1c0000000000000000000000000000000000000000000011ce5ecc567d2b3ab1a20000000000000000000000000000000000000000000000000000000067eba567cd0d22ac7bc0820ebcbc11b087c059c2d36b207d020a3987b21b56b11c2ca0982796fc953c1ebf14434ee0dffc225d87e12ca4506f07f0971e4fcd72bcbab2241c0000000000000000000000000000000000000000000011ce6ed6df09a64f08420000000000000000000000000000000000000000000000000000000067eba567c82350a2e16b70134ec057193550fd49b18e793604b1e4743fc01c2cae01f9b7631cdfa9033e2c414cf29451f916c482316673fd8be956a1d5ea5dc1791115c01b, 0x4254432f55534400000000000000000000000000000000000000000000000000)
	├─ [82339] ERC1967Proxy::fallback(0x55444646014254432f55534400000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000011ce4ebc2338797454e80000000000000000000000000000000000000000000000000000000067eba567c157813327ab23012c0c51d803a60fdeb2db3460e19840849b99b5e4996ee90853a0d1ad5e5be5304cb76990ffd1e04680f7f2f85680ac62f529b5614031062e1c0000000000000000000000000000000000000000000011ce5ecc567d2b3ab1a20000000000000000000000000000000000000000000000000000000067eba567cd0d22ac7bc0820ebcbc11b087c059c2d36b207d020a3987b21b56b11c2ca0982796fc953c1ebf14434ee0dffc225d87e12ca4506f07f0971e4fcd72bcbab2241c0000000000000000000000000000000000000000000011ce6ed6df09a64f08420000000000000000000000000000000000000000000000000000000067eba567c82350a2e16b70134ec057193550fd49b18e793604b1e4743fc01c2cae01f9b7631cdfa9033e2c414cf29451f916c482316673fd8be956a1d5ea5dc1791115c01b, 0x4254432f55534400000000000000000000000000000000000000000000000000)
	│   ├─ [77350] PullMarketplace::verifyAsSubscriber(0x55444646014254432f55534400000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000011ce4ebc2338797454e80000000000000000000000000000000000000000000000000000000067eba567c157813327ab23012c0c51d803a60fdeb2db3460e19840849b99b5e4996ee90853a0d1ad5e5be5304cb76990ffd1e04680f7f2f85680ac62f529b5614031062e1c0000000000000000000000000000000000000000000011ce5ecc567d2b3ab1a20000000000000000000000000000000000000000000000000000000067eba567cd0d22ac7bc0820ebcbc11b087c059c2d36b207d020a3987b21b56b11c2ca0982796fc953c1ebf14434ee0dffc225d87e12ca4506f07f0971e4fcd72bcbab2241c0000000000000000000000000000000000000000000011ce6ed6df09a64f08420000000000000000000000000000000000000000000000000000000067eba567c82350a2e16b70134ec057193550fd49b18e793604b1e4743fc01c2cae01f9b7631cdfa9033e2c414cf29451f916c482316673fd8be956a1d5ea5dc1791115c01b, 0x4254432f55534400000000000000000000000000000000000000000000000000) [delegatecall]
	│   │   ├─ [60369] 0xc0931aEE1064BD5245fEe76A2d740eab8436621e::40366475(00000000000000000000000000000000000000000000000000000000000000404254432f5553440000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001a955444646014254432f55534400000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000011ce4ebc2338797454e80000000000000000000000000000000000000000000000000000000067eba567c157813327ab23012c0c51d803a60fdeb2db3460e19840849b99b5e4996ee90853a0d1ad5e5be5304cb76990ffd1e04680f7f2f85680ac62f529b5614031062e1c0000000000000000000000000000000000000000000011ce5ecc567d2b3ab1a20000000000000000000000000000000000000000000000000000000067eba567cd0d22ac7bc0820ebcbc11b087c059c2d36b207d020a3987b21b56b11c2ca0982796fc953c1ebf14434ee0dffc225d87e12ca4506f07f0971e4fcd72bcbab2241c0000000000000000000000000000000000000000000011ce6ed6df09a64f08420000000000000000000000000000000000000000000000000000000067eba567c82350a2e16b70134ec057193550fd49b18e793604b1e4743fc01c2cae01f9b7631cdfa9033e2c414cf29451f916c482316673fd8be956a1d5ea5dc1791115c01b0000000000000000000000000000000000000000000000) [staticcall]
	│   │   │   ├─ [55380] 0xB2F863B68d85b198DDe2fE7da1D8baFdCFf199c0::40366475(00000000000000000000000000000000000000000000000000000000000000404254432f5553440000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001a955444646014254432f55534400000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000011ce4ebc2338797454e80000000000000000000000000000000000000000000000000000000067eba567c157813327ab23012c0c51d803a60fdeb2db3460e19840849b99b5e4996ee90853a0d1ad5e5be5304cb76990ffd1e04680f7f2f85680ac62f529b5614031062e1c0000000000000000000000000000000000000000000011ce5ecc567d2b3ab1a20000000000000000000000000000000000000000000000000000000067eba567cd0d22ac7bc0820ebcbc11b087c059c2d36b207d020a3987b21b56b11c2ca0982796fc953c1ebf14434ee0dffc225d87e12ca4506f07f0971e4fcd72bcbab2241c0000000000000000000000000000000000000000000011ce6ed6df09a64f08420000000000000000000000000000000000000000000000000000000067eba567c82350a2e16b70134ec057193550fd49b18e793604b1e4743fc01c2cae01f9b7631cdfa9033e2c414cf29451f916c482316673fd8be956a1d5ea5dc1791115c01b0000000000000000000000000000000000000000000000) [delegatecall]
	│   │   │   │   ├─ [3000] PRECOMPILES::ecrecover(0x0a68e8365be00d0cd98a1171d18af4e3e1566fd09a59d61fe23743c2dbe50c12, 28, 87450987175619729345224437904190378668378471047311994984055329008011973683464, 37826109101770063310129024824192616189507182989479589014137121493233374922286) [staticcall]
	│   │   │   │   │   └─ ← [Return] 0x0000000000000000000000006733d110a59dc160b5c8093066a7f5103885196f
	│   │   │   │   ├─ [8142] ERC1967Proxy::55b5190b(7564662d76312e310000000000000000000000000000000000000000000000000000000000000000000000006733d110a59dc160b5c8093066a7f5103885196f) [staticcall]
	│   │   │   │   │   ├─ [3246] 0xA7a8eAAA131dc4D2f70636F5F8796e640B119926::55b5190b(7564662d76312e310000000000000000000000000000000000000000000000000000000000000000000000006733d110a59dc160b5c8093066a7f5103885196f) [delegatecall]
	│   │   │   │   │   │   └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000001
	│   │   │   │   │   └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000001
	│   │   │   │   ├─ [3000] PRECOMPILES::ecrecover(0x14e205399af2fdf39cd5ba0d6d33d50ffbb0007a622f772edc16663e7de9cd57, 28, 92747342280930951330331743016924826142985862405571157083665267571049381011608, 17906971417906977260169118529732908076796855624976136117544900421598137987620) [staticcall]
	│   │   │   │   │   └─ ← [Return] 0x000000000000000000000000be524616e96bb4b62cce8034ab6bea8f2505b55a
	│   │   │   │   ├─ [3642] ERC1967Proxy::55b5190b(7564662d76312e31000000000000000000000000000000000000000000000000000000000000000000000000be524616e96bb4b62cce8034ab6bea8f2505b55a) [staticcall]
	│   │   │   │   │   ├─ [3246] 0xA7a8eAAA131dc4D2f70636F5F8796e640B119926::55b5190b(7564662d76312e31000000000000000000000000000000000000000000000000000000000000000000000000be524616e96bb4b62cce8034ab6bea8f2505b55a) [delegatecall]
	│   │   │   │   │   │   └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000001
	│   │   │   │   │   └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000001
	│   │   │   │   ├─ [3000] PRECOMPILES::ecrecover(0xca613c8b1a20618b010d94a62a700c6e09917c2a8778a82c881f2be28d25a2b3, 27, 90524965894879087420297465183903263448359497350110207873450031945407891896759, 44829987373565001565186606953511927342046667061064660196284399196770407617984) [staticcall]
	│   │   │   │   │   └─ ← [Return] 0x00000000000000000000000093b502d3eb45b9eae948f8fca01e64d9c0ba538a
	│   │   │   │   ├─ [3642] ERC1967Proxy::55b5190b(7564662d76312e3100000000000000000000000000000000000000000000000000000000000000000000000093b502d3eb45b9eae948f8fca01e64d9c0ba538a) [staticcall]
	│   │   │   │   │   ├─ [3246] 0xA7a8eAAA131dc4D2f70636F5F8796e640B119926::55b5190b(7564662d76312e3100000000000000000000000000000000000000000000000000000000000000000000000093b502d3eb45b9eae948f8fca01e64d9c0ba538a) [delegatecall]
	│   │   │   │   │   │   └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000001
	│   │   │   │   │   └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000001
	│   │   │   │   └─ ← [Return] 0x0000000000000000000000000000000000000000000011ce5ecc567d2b3ab1a20000000000000000000000000000000000000000000000000000000067eba567
	│   │   │   └─ ← [Return] 0x0000000000000000000000000000000000000000000011ce5ecc567d2b3ab1a20000000000000000000000000000000000000000000000000000000067eba567
	│   │   ├─ emit VerifyAsSubscriberCalled(verifier: PullVerifierSub: [0x9Cd59BEbE8daBbb4a79739619a99DF890498305e], feedKey: 0x4254432f55534400000000000000000000000000000000000000000000000000)
	│   │   └─ ← [Return] PriceUpdate({ price: 84087090417798546633122 [8.408e22], timestamp: 1743496551 [1.743e9] })
	│   └─ ← [Return] PriceUpdate({ price: 84087090417798546633122 [8.408e22], timestamp: 1743496551 [1.743e9] })
	└─ ← [Stop]


Transaction successfully executed.
Gas used: 125591

Last updated

Was this helpful?