Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
337 changes: 337 additions & 0 deletions docs/cookbook/tutorials/reliable-price-feeds
Original file line number Diff line number Diff line change
@@ -0,0 +1,337 @@

# Reliable Price Feeds: Pyth "Pull" vs. Chainlink "Push"

### **Objective**

Implement a "Pull" oracle architecture using Pyth Network on Base Sepolia, then compare it with the traditional Chainlink "Push" model.

### **Architecture: Pull vs. Push**

* **Pull (Pyth):** Prices are not updated on-chain automatically. Your frontend fetches a signed price update from an off-chain service (Hermes) and submits it to the contract *in the same transaction* as your trade/logic.
* *Pro:* Sub-second latency, lower gas (you only pay when you need it).
* *Con:* Requires off-chain infrastructure (Hermes client) in your frontend.


* **Push (Chainlink):** Node operators update the on-chain price periodically (e.g., every hour or 0.5% deviation).
* *Pro:* Simple to read (just a `view` function).
* *Con:* Higher latency, potentially "stale" during low volatility.



---

## **Prerequisites**

* **Foundry:** `curl -L https://foundry.paradigm.xyz | bash`
* **Node.js & pnpm/yarn**
* **Base Sepolia ETH:** [Coinbase Faucet](https://portal.cdp.coinbase.com/products/faucet)
* **RPC URL:** Base Sepolia (public or private).

---

## **Part 1: Implementing Pyth (The Pull Model)**

### **1. Smart Contract Setup**

We will build a contract that accepts a `priceUpdate` blob, verifies it, and then executes logic using that fresh price.

**Setup:**

```bash
forge init pyth-oracle
cd pyth-oracle
forge install pyth-network/pyth-sdk-solidity --no-commit

```

**`foundry.toml` Remappings:**
Add this to your `foundry.toml` to map the import correctly.

```toml
remappings = [
"@pythnetwork/pyth-sdk-solidity/=lib/pyth-sdk-solidity/",
"forge-std/=lib/forge-std/src/"
]

```

**Contract: `src/PythPriceReader.sol**`

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@pythnetwork/pyth-sdk-solidity/IPyth.sol";
import "@pythnetwork/pyth-sdk-solidity/PythStructs.sol";

contract PythPriceReader {
IPyth public pyth;

// ETH/USD Price ID (Standard across EVM chains for Pyth)
bytes32 constant ETH_PRICE_ID = 0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace;

event PriceUpdated(uint64 publishTime, int64 price, int32 expo);

// Base Sepolia Pyth Contract: 0xA2aa501b19aff244D90cc15a4Cf739D2725B5729
// Base Mainnet Pyth Contract: 0x8250f4aF4B972684F7b336503E2D6dFeDeB1487a
constructor(address _pythContract) {
pyth = IPyth(_pythContract);
}

/**
* @notice Updates the price on-chain and then reads it.
* @param priceUpdateData The binary data fetched from Hermes API.
*/
function updateAndReadPrice(bytes[] calldata priceUpdateData) public payable {
// 1. Get the fee required to verify the signature
uint fee = pyth.getUpdateFee(priceUpdateData);

// 2. Check if user sent enough ETH
require(msg.value >= fee, "Insufficient fee for Pyth update");

// 3. Update the price feed (reverts if signature is invalid)
pyth.updatePriceFeeds{value: fee}(priceUpdateData);

// 4. Read the fresh price
// getPriceNoOlderThan will revert if the data is older than 60 seconds
PythStructs.Price memory price = pyth.getPriceNoOlderThan(ETH_PRICE_ID, 60);

emit PriceUpdated(price.publishTime, price.price, price.expo);
}

/**
* @notice View function to get the last stored price (might be stale).
*/
function getLastPrice() public view returns (PythStructs.Price memory) {
return pyth.getPrice(ETH_PRICE_ID);
}
}

```

### **2. Deployment Script**

Create `script/DeployPyth.s.sol`.

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "forge-std/Script.sol";
import "../src/PythPriceReader.sol";

contract DeployPyth is Script {
function run() external {
// Load private key
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");

vm.startBroadcast(deployerPrivateKey);

// Base Sepolia Pyth Address
address pythAddress = 0xA2aa501b19aff244D90cc15a4Cf739D2725B5729;

PythPriceReader oracle = new PythPriceReader(pythAddress);

console.log("PythPriceReader deployed at:", address(oracle));

vm.stopBroadcast();
}
}

```

**Deploy:**

```bash
# Create .env file with PRIVATE_KEY=... and BASE_SEPOLIA_RPC=...
source .env
forge script script/DeployPyth.s.sol --rpc-url $BASE_SEPOLIA_RPC --broadcast --verify --etherscan-api-key $BASESCAN_API_KEY

```

---

### **3. Frontend Integration (The "Pull" Mechanism)**

This is where Pyth differs. You cannot just call `writeContract`. You must first fetch the "update blob" from the Hermes API.

**Dependencies:**

```bash
npm install @pythnetwork/hermes-client viem wagmi

```

**Frontend Component (`components/PythUpdater.tsx`):**

```tsx
'use client';

import { useState } from 'react';
import { HermesClient } from "@pythnetwork/hermes-client";
import { useWriteContract, useWaitForTransactionReceipt } from 'wagmi';
import { parseEther } from 'viem';

// The ID for ETH/USD
const PRICE_ID = ["0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace"];
// Your deployed contract address
const CONTRACT_ADDRESS = "0xYourDeployedContractAddress";

// ABI Fragment for updateAndReadPrice
const ABI = [{
"inputs": [{"internalType": "bytes[]", "name": "priceUpdateData", "type": "bytes[]"}],
"name": "updateAndReadPrice",
"outputs": [],
"stateMutability": "payable",
"type": "function"
}] as const;

export default function PythUpdater() {
const [loading, setLoading] = useState(false);
const { data: hash, writeContract } = useWriteContract();
const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({ hash });

const handleUpdate = async () => {
setLoading(true);
try {
// 1. Initialize Hermes Client (Public Endpoint)
const connection = new HermesClient("https://hermes.pyth.network");

// 2. Fetch the latest price update data (binary blob)
const priceUpdates = await connection.getLatestPriceUpdates(PRICE_ID);

// 3. Extract the binary data payload
const updateData = priceUpdates.binary.data.map((d) => `0x${d}` as `0x${string}`);

// 4. Get the required fee (simulated here, but usually ~1 wei or minimal value on Base)
// In production, call contract.getUpdateFee(updateData) to be precise.
// Pyth fees are very low; hardcoding a small buffer often works for tutorials.
const fee = parseEther("0.0001");

// 5. Send transaction
writeContract({
address: CONTRACT_ADDRESS,
abi: ABI,
functionName: 'updateAndReadPrice',
args: [updateData],
value: fee, // IMPORTANT: You must send value to pay the Pyth protocol fee
});

} catch (err) {
console.error("Failed to fetch price:", err);
} finally {
setLoading(false);
}
};

return (
<div className="p-4 border rounded-lg bg-base-100">
<h2 className="text-xl font-bold mb-4">Pyth Pull Oracle</h2>
<button
onClick={handleUpdate}
disabled={loading || isConfirming}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"
>
{loading ? "Fetching from Hermes..." : isConfirming ? "Updating On-chain..." : "Pull & Update Price"}
</button>

{isSuccess && <p className="mt-2 text-green-500">Price updated successfully on-chain!</p>}
</div>
);
}

```

---

## **Part 2: Comparison with Chainlink (The Push Model)**

To illustrate the difference, here is how you would implement the same requirement using Chainlink. Note the absence of off-chain fetching in the frontend.

**Install Chainlink Contracts:**

```bash
forge install smartcontractkit/chainlink --no-commit

```

**`foundry.toml` Remappings:**

```toml
remappings = [
"@chainlink/=lib/chainlink/",
# ... previous mappings
]

```

**Contract: `src/ChainlinkPriceReader.sol**`

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol";

contract ChainlinkPriceReader {
AggregatorV3Interface internal dataFeed;

/**
* Network: Base Sepolia
* Aggregator: ETH/USD
* Address: 0x694AA1769357215DE4FAC081bf1f309aDC325306
*/
constructor() {
dataFeed = AggregatorV3Interface(0x694AA1769357215DE4FAC081bf1f309aDC325306);
}

/**
* @notice Read the latest price. No arguments, no payment, no off-chain fetch.
*/
function getLatestPrice() public view returns (int) {
(
/* uint80 roundID */,
int answer,
/* uint startedAt */,
/* uint timeStamp */,
/* uint80 answeredInRound */
) = dataFeed.latestRoundData();
return answer;
}
}

```

### **Comparison Summary**

| Feature | Pyth (Pull) | Chainlink (Push) |
| --- | --- | --- |
| **Data Freshness** | Real-time (Sub-second) | Heartbeat (e.g., 1 hour) or Deviation (0.5%) |
| **Gas Cost** | Paid by **user** (Update + Execution) | Paid by **Oracle Node** (User reads for free) |
| **Architecture** | Fetch Off-chain -> Submit On-chain | Read On-chain directly |
| **Complexity** | High (Needs frontend fetcher) | Low (Plug and play) |
| **Best For** | High-frequency trading, Derivatives | Lending protocols, simple swaps |

---

## **Common Pitfalls**

1. **Stale Price Reverts (Pyth):**
* If you call `getPriceNoOlderThan(id, 60)` and the user's transaction takes 61 seconds to confirm (unlikely on Base, but possible), it will revert.
* *Fix:* Increase the tolerance or ensure your frontend fetching logic is tight.


2. **Insufficient Fee (Pyth):**
* `pyth.updatePriceFeeds` is **payable**. If you forget to pass `value: fee` in your frontend `writeContract` call, the transaction will revert with `InsufficientFee`.


3. **Wrong Address/Network:**
* Oracles have different addresses for Sepolia vs Mainnet. Hardcoding Mainnet addresses while testing on Sepolia will cause your contract deployment to fail or return 0.


4. **Decimals:**
* Pyth returns price and `expo` (exponent). E.g., Price `30000000000` and Expo `-8` = $300.00.
* Chainlink ETH/USD usually returns 8 decimals fixed.
* *Always normalize* your decimals before doing math in Solidity.