Skip to content

Repository files navigation

CRE Price Snapshot

A Chainlink Runtime Environment (CRE) workflow that, in a single HTTP-triggered execution:

  1. Accepts { "token": "ETH" } (or BTC, LINK, BNB, SOL).
  2. Reads the current USD price of that token from a Chainlink Data Feed on Sepolia via EVM Read.
  3. Resolves the block number at which the Data Feed answer was last updated by querying AnswerUpdated event logs on the underlying OCR aggregator.
  4. Writes a Record { token, price, blockNumber, timestamp } to a deployed smart contract via EVM Write.

Proof of execution

I ran the workflow with --broadcast and it successfully wrote the ETH/USD price to my contract on Sepolia.

Live transaction: 0x1a2171c7eb5202ea24c8d6d5abb3b6b4fc1fb2f00e3ebbd46e569ffa4b46065a

Simulation terminal output

Etherscan transaction confirmation


Repository layout

cre-price-snapshot/
├── contracts/
│   ├── interfaces/
│   │   └── ISnapshot.sol          # ISnapshot interface (Record struct)
│   ├── abi/
│   │   ├── AggregatorV3Interface.ts
│   │   ├── PriceSnapshot.ts
│   │   └── index.ts
│   └── PriceSnapshot.sol          # Consumer contract
├── deploy/
│   └── scripts/
│       └── deploy.ts              # Deploy script (pure ethers v6)
├── price-snapshot-workflow/
│   ├── main.ts                    # CRE workflow logic
│   ├── package.json
│   ├── tsconfig.json
│   ├── workflow.yaml
│   ├── config.staging.json        # Staging config (simulation)
│   └── config.production.json     # Production config
├── hardhat.config.ts              # Hardhat 3 config (compile)
├── package.json                   # Hardhat 3 deploy dependencies
├── tsconfig.json                  # TypeScript config for deploy
├── project.yaml                   # CRE project / RPC config
├── secrets.yaml                   # Secrets template (no real values)
├── .env.example                   # Environment variable template
├── .gitignore
└── README.md

Step 1 — Clone & configure secrets

git clone https://github.com/<your-username>/cre-price-snapshot
cd cre-price-snapshot

# Copy the env template and fill in your private key
cp .env.example .env
# Edit .env: set CRE_ETH_PRIVATE_KEY (64 hex chars, no 0x prefix)

Step 2 — Deploy the smart contract to Sepolia

From the project root (cre-price-snapshot/):

npm install
npm run deploy:sepolia

Expected output:

✅ PriceSnapshot deployed successfully!
Contract address: 0x80AF3cea8aa1C86753b33508FcEe33E6C099EC28

The contract is already deployed at 0x80AF3cea8aa1C86753b33508FcEe33E6C099EC28 on Sepolia — you only need to re-deploy if you want your own instance.

Forwarder addresses for Ethereum Sepolia

Purpose Address
Simulation (cre ... --broadcast) 0x15fC6ae953E024d975e77382eEeC56A9101f9F88
Production (live DON) 0xF8344CFd5c43616a4366C34E3EEE75af79a74482

The deploy script uses the simulation forwarder so the workflow can immediately broadcast transactions during cre workflow simulate --broadcast. To migrate to production, call setForwarder("0xF8344…") (owner-only) on the contract.

Update the config if you deployed a new instance:

# In price-snapshot-workflow/config.staging.json
# Set "snapshotContractAddress" to your new address

Step 3 — Install workflow dependencies

cd price-snapshot-workflow
bun install
cd ..

Step 4 — Authenticate with the CRE CLI

cre login
cre whoami   # verify authentication

Step 5 — Run the workflow simulation

From the project root (cre-price-snapshot/):

Dry run (no broadcast — default)

cre workflow simulate price-snapshot-workflow \
  --target staging-settings \
  --http-payload '{"token":"ETH"}'

With broadcast (sends a real Sepolia transaction)

cre workflow simulate price-snapshot-workflow \
  --target staging-settings \
  --http-payload '{"token":"ETH"}' \
  --broadcast

You can substitute any supported token symbol:

# BTC
cre workflow simulate price-snapshot-workflow --target staging-settings --http-payload '{"token":"BTC"}' --broadcast

# LINK
cre workflow simulate price-snapshot-workflow --target staging-settings --http-payload '{"token":"LINK"}' --broadcast

Contract details

ISnapshot interface

interface ISnapshot {
    struct Record {
        string  token;       // e.g. "ETH"
        uint256 price;       // raw Chainlink answer (8 decimal places)
        uint256 blockNumber; // block at which the feed answer was last updated
        uint256 timestamp;   // Unix timestamp of the last update
    }
}

PriceSnapshot consumer contract

Function Description
onReport(bytes metadata, bytes report) Entry-point called by the KeystoneForwarder after verifying report signatures (the forwarder check). Decodes the report bytes into a Record and stores it.
latestRecord() Returns the most-recently stored record (any token).
records(string token) Returns the latest record for a specific token.
setForwarder(address) Owner-only. Update the trusted forwarder (e.g., simulation → production).

Security

The forwarder check is enforced in onReport:

if (msg.sender != s_forwarder) revert InvalidSender(msg.sender, s_forwarder);

Without this check, anyone could call onReport directly with fake data and write arbitrary prices to the contract. The check ensures only Chainlink's verified execution path can write here — every stored price went through the CRE workflow's BFT-consensus-verified signing process before the contract accepted it.


Workflow architecture

HTTP POST  →  HTTP Trigger  →  onHttpTrigger callback
                                   │
                                   ├─ EVM Read: latestRoundData() on Chainlink proxy
                                   ├─ EVM Read: aggregator() on proxy  (get underlying OCR address)
                                   ├─ EVM Read: headerByNumber()        (get current block)
                                   ├─ EVM Read: filterLogs()            (AnswerUpdated event → block #)
                                   └─ EVM Write: writeReport()
                                                    │
                                              KeystoneForwarder
                                                    │
                                             PriceSnapshot.onReport()
                                                    │
                                             snapshot(Record) stored

How blockNumber is obtained

This was the trickiest part of the assignment. latestRoundData() gives you a Unix timestamp for the last update but not a block number. To get the actual block:

  1. Call aggregator() on the proxy — the proxy is a facade, the real price logic lives in the underlying OCR aggregator contract behind it.
  2. Search that aggregator's AnswerUpdated(int256,uint256,uint256) event logs over the last 5000 blocks.
  3. The blockNumber of the most recent log is the exact block where the price was last written to the chain.

This satisfies the requirement: "The blockNumber stored on-chain must be the block number at which the Data Feed answer was last updated."

Note: one gotcha I hit — the CRE SDK uses protobuf internally, so the addresses and topics fields in filterLogs must be base64-encoded, not plain hex strings. encodeCallMsg handles this automatically for contract calls, but for filterLogs you have to call hexToBase64() yourself.


Supported Chainlink Data Feeds (Sepolia)

Token Feed Address
ETH/USD 0x694AA1769357215DE4FAC081bf1f309aDC325306
BTC/USD 0x1b44F3514812d835EB1BDB0acB33d3fA3351Ee43
LINK/USD 0xc59E3633BAAC79493d908e63626716e204A45EdF
BNB/USD 0x14866185B1962B63C3Ea9E03Bc1da838bab34C19
SOL/USD 0xc6AEad5D74Ff33FfAFb9C77a3b1b2B7cF72FF32e

To add more tokens, extend priceFeeds in config.staging.json. Feed addresses are listed at docs.chain.link/data-feeds/price-feeds/addresses.


Known limitation

The AnswerUpdated log search covers the last 5000 blocks (~17 hours on Sepolia at 12s/block). If a feed hasn't emitted an event in that window the workflow throws. A production version would paginate backward in chunks until it finds the most recent log, rather than failing with a fixed window.


References

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages