Skip to content

RAAC Real Estate Oracle

Overview

The RAAC Real Estate oracle keeps on-chain house-NFT prices in sync with the off-chain valuation database. It has three parts:

  • RAACHousePrices — the on-chain source of truth. Stores each RAAC house's latest price in USD, exposes read helpers that return prices in pmUSD, tracks timestamps, and notifies optional syncers.
  • RAACHousePriceReceiver — the updater. Accepts price-update requests, marks the affected houses pending, and writes DON-signed prices into RAACHousePrices.
  • CRE workflow + AWS driver — the off-chain half: a daily Lambda that decides which houses need repricing, and a Chainlink Runtime Environment (CRE) workflow that fetches the authoritative prices and writes them back.

This replaces the Chainlink Functions oracle

The previous RAACHousePriceOracle (Chainlink Functions, one house per request, JS source on the DON) is no longer the update path. The storage layer — RAACHousePrices, including setHousePrices — is unchanged; only the oracle layer calling into it is new. RAACHousePrices.oracle now points at RAACHousePriceReceiver.

AWS never supplies data — it only monitors and triggers

No price that lands on-chain ever originates in AWS. The Lambda reads the valuation database only to decide which houses are out of sync; the prices it saw are discarded and never sent on-chain. The CRE workflow re-fetches and re-verifies every price itself on the DON, under OCR consensus. This holds for every RAAC CRE oracle.

Price Units

  • Stored unit: USD with oracle-decimals (18 decimals)
  • Read helper: returns price converted to pmUSD using the PmUSDToUSDOracle

Architecture

The trigger is on-chain first: houses have to be flagged pendingRequest = true before the new price arrives, so the update starts with a transaction rather than an HTTP call.

SOURCE OF TRUTH — OFF-CHAIN MONITORING & TRIGGER — NEVER SUPPLIES DATA CHAINLINK CRE ON-CHAIN Valuation database auditor-signed fractional totals AWS Lambda daily · diff every id · batch by maxBatch() KMS-signed tx · holds priceUpdateRequester PriceUpdateRequested event log carrying ids re-fires on every request CRE workflow — DON confidential fetch · EIP-712 verify OCR-signed (chainId, reportedAt, ids, prices) Chainlink Forwarder verifies DON signatures RAACHousePriceReceiver pendingRequest blocks mint · deposit · withdraw per-id staleness & deviation guards RAACHousePrices canonical USD price + timestamp onlyOracle writes Syncers — max 5 RAACNFTVaultAdapterV2 → vault NAV Integrators / Protocol getLatestPrice() → pmUSD read to diff — discarded authoritative fetch EVM log trigger requestPriceUpdate(ids) emit + set pending writeReport onReport — Forwarder only setHousePrices() notify() price reads totalSupply() · on-chain prices

Key points:

  • The receiver only orchestrates updates; it does not store canonical prices.
  • RAACHousePrices converts USD → pmUSD at read time via PmUSDToUSDOracle.
  • Syncers (max 5) can subscribe to updates for indexing, caching, or off-chain services.
  • The requester key cannot influence prices — the workflow fetches and verifies them itself — it can only decide which houses get repriced.

RAACHousePrices

Purpose

  • Store canonical house prices per RAAC tokenId in USD.
  • Provide read helpers that convert USD → pmUSD using PmUSDToUSDOracle.
  • Track last update timestamps, maintain a lightweight token registry, and notify syncers on changes.

Core Concepts

  • USD storage, pmUSD readout: getLatestPrice(tokenId) converts stored USD to pmUSD using crvUSDToUSDOracle.getPrice() and its decimals().
  • Timestamps: tokenToLastUpdateTimestamp[tokenId] and lastUpdateTimestamp (global) track freshness.
  • Syncers: Optional contracts implementing IRAACHousePriceSyncer can be added (up to 5). They receive notify(tokenId, previousPrice, newPrice) on every write.

Vault NAV Syncer

One configured syncer is RAACNFTVaultAdapterV2.sol. When a house price is updated and the corresponding asset is stored in the vault, the adapter receives the notify callback and updates the vault NAV immediately.

  • Oracle-only writes: setHousePrice and setHousePrices are restricted to the configured oracle address, which is RAACHousePriceReceiver.

Conversion

For a stored USD price P_usd and oracle price pmUSD/USD = Q with d = decimals(), the read helper returns: \( P_{pmUSD} = P_{usd} * 10^{d} / Q \)

Key Functions

Function Description Access
getLatestPrice(tokenId) Returns (priceInCrvUSD, lastUpdateTimestamp) view
crvUSDToUSDPrice() Returns pmUSD/USD price normalized to 18 decimals view
decimals() Mirror decimals of PmUSDToUSDOracle view
getRawPrice(tokenId) Returns (priceInUSD, lastUpdateTimestamp) view
setHousePrice(tokenId, amount) Set single USD price and timestamp; emit create/update; notify syncers onlyOracle
setHousePrices(tokenIds[], amounts[]) Batch set USD prices; emit per-item events; notify syncers onlyOracle
setOracle(addr) Point at the updater (the CRE receiver) onlyOwner
setCrvUSDToUSDOracle(addr) Set the FX oracle used for conversion onlyOwner
isPendingRequest(tokenId) Staticcalls the updater: is a request pending for tokenId view
addSyncer(addr) / removeSyncer(addr) Manage syncer list; interface-checked; max 5 onlyOwner

Events & Errors

  • Events: PriceCreated, PriceUpdated, OracleUpdated, CrvUSDToUSDOracleUpdated, SyncerAdded, SyncerRemoved
  • Errors: NotOracle, ArrayLengthMismatch, SyncerDoesNotSupportInterface, MaximumSyncers

The off-chain driver (AWS Lambda)

A daily Lambda decides which houses need repricing. Each run:

  1. Reads totalSupply() from the RAAC ERC-721 to learn the house universe (IDs 1..N).
  2. Reads the current on-chain price for every house from RAACHousePrices in one batched JSON-RPC round-trip.
  3. Fetches each house's authoritative price from the valuation database in a single query — the exact value the CRE will write.
  4. Diffs the two. Every ID that does not match exactly is queued for update.
  5. Splits the queue into batches of at most the receiver's on-chain maxBatch(), read from the contract each run rather than configured.
  6. For each batch, signs a transaction with its KMS key calling RAACHousePriceReceiver.requestPriceUpdate(ids).
  7. Posts lifecycle messages to Slack — started → requesting → outcome, with tx hashes, never per-ID prices.

The KMS key holds the receiver's priceUpdateRequester role and nothing else, and must be funded with gas. The Lambda performs no verification of the prices themselves: the workflow re-fetches and re-verifies them before any write.


RAACHousePriceReceiver

Requesting an update

requestPriceUpdate(uint256[] ids) is onlyPriceUpdateRequester. It atomically:

  • reverts on an empty batch (EmptyBatch) or one larger than maxBatch (BatchTooLarge);
  • sets pendingRequest[id] = true for each id not already pending, emitting PendingRequestSet;
  • emits PriceUpdateRequested(ids) — the log the CRE workflow's EVM trigger watches.

This is the only entry point that sets pending flags, so a pending house always has a request behind it. The trigger event fires on every call, including for ids already pending, so re-requesting after a missed or failed CRE run re-fires the workflow.

clearPendingRequests(uint256[] ids) is the onlyOwner recovery path for a run that never delivered a report — flags are otherwise cleared automatically when the price is written.

Writing the report

onReport is callable only by the Chainlink Forwarder, with the ReceiverTemplate base additionally checking the report's author and workflow id. _processReport decodes (uint256 chainId, uint64 reportedAt, uint256[] ids, uint256[] prices) and then:

  • rejects the whole report if chainId != block.chainid (ChainIdMismatch), reportedAt == 0 (InvalidReportTimestamp), the arrays disagree in length (LengthMismatch), the batch is empty (EmptyBatch), or it exceeds maxBatch (BatchTooLarge);
  • skips individual ids with a zero price (PriceRejected), with a reportedAt not strictly newer than that id's lastReportedAt (PriceStale — out-of-order delivery), or whose move exceeds maxDeviationBps against the current on-chain price (PriceRejected);
  • writes the survivors in one housePrices.setHousePrices(ids, prices) call;
  • advances lastReportedAt[id] and clears pendingRequest[id] for each written house, so consumers unblock atomically with the price update.

Skipped ids stay pending and keep their previous watermark. isPendingRequest(houseId) matches the old IRAACHousePriceOracle shape, so RAACHousePrices.isPendingRequest — which staticcalls the configured oracle — keeps working unchanged.

Configuration

Function Description Access
requestPriceUpdate(ids) Mark pending and fire the CRE trigger onlyPriceUpdateRequester
clearPendingRequests(ids) Force-clear pending flags (recovery) onlyOwner
setPriceUpdateRequester(addr) Rotate the trigger key onlyOwner
setMaxBatch(n) Cap ids per request and per report onlyOwner
setMaxDeviationBps(bps) Per-house move cap; 0 disables the check onlyOwner
setForwarderAddress / setExpectedAuthor / setExpectedWorkflowId Workflow-identity configuration onlyOwner
isPendingRequest(houseId) Whether a house is awaiting a price view

Events: PriceUpdateRequested, PendingRequestSet, PendingRequestCleared, PriceRejected, PriceStale, PriceUpdateRequesterUpdated, MaxDeviationBpsUpdated.


The workflow (CRE)

The workflow is deployed to the DON and triggered by the PriceUpdateRequested log. It:

  1. Fetches the authoritative prices over Confidential HTTP — the database credentials are injected by the Vault DON enclave and never enter workflow code or memory.
  2. Verifies the auditor's EIP-712 signature and ties out the global fractional totals before trusting any row.
  3. Encodes (chainId, reportedAt, ids, prices) into a report and signs it under OCR consensus. reportedAt is runtime.now(), the DON-consensus clock; chainId binds the report to one chain so it cannot be replayed against a receiver elsewhere.
  4. Calls writeReport, which lands through the Forwarder.

Pending state, frontrun protection, and the trust model

Pending State & Frontrun Protection

A house flagged pendingRequest has mint, deposit and withdraw blocked on it — in RAACNFT.sol and on NFT deposit into the RWA Vault. This closes the window in which a soon-to-change price could be traded against. The flag is set by the request and cleared only when that house's price is actually written; a report that skips the house leaves it pending.

Pending flags never expire

Nothing caps how many ids may be outstanding and nothing clears them on a timeout. A compromised trigger key cannot lie about prices — the workflow re-fetches them — but it can freeze the protocol by marking every tokenId pending. Recovery is an owner (multisig) action: setPriceUpdateRequester(newKey), then clearPendingRequests.

Price information leaks before it is on-chain

This is a push oracle. Read access to the valuation database reveals the next price hours to weeks early, and the Forwarder transaction is visible in the mempool before it confirms. No key is exposed — this is an information-flow risk. Mitigations: least-privilege, rotated DB read tokens; a settlement guard in consuming protocols; workflow logs kept free of per-ID prices.

Three keys — never conflate them

Key Powers If compromised
Workflow owner Deploy/update the workflow, manage DON secrets Can push a malicious workflow and rotate the DB key
Receiver owner setMaxBatch, setMaxDeviationBps, setForwarderAddress, setExpectedAuthor/setExpectedWorkflowId, setPriceUpdateRequester, transferOwnership Can disable the receiver's workflow-identity check
Trigger key (priceUpdateRequester, held by the Lambda) requestPriceUpdate only Cannot influence prices, but can freeze mint/deposit/withdraw protocol-wide until rotated

The workflow owner and the receiver owner must be different keys — otherwise one compromise both deploys a malicious workflow and disables the check that guards against it.


Integration Notes

  • To read a house price for lending and health factor calculations, always query RAACHousePrices.getLatestPrice(tokenId) which returns the price denominated in pmUSD and its update timestamp.
  • To show the raw USD price and timestamp, use getRawPrice(tokenId).
  • The pmUSD conversion relies on PmUSDToUSDOracle; ensure it is configured and healthy. Switching it is owner-controlled via setCrvUSDToUSDOracle.

Circuit-Breakers & Staleness

The contracts are designed so that staleness and fallback handling live in the FX oracle used for conversion. Operational procedures should include monitoring and replacing the FX oracle if needed.


  • RAACPrimeRateReceiver — sibling CRE oracle, HTTP-triggered rather than log-triggered (docs)
  • RAACNFTVaultAdapterV2 — syncer that updates vault NAV on every price write (docs)