Skip to content

RAAC Real Estate Oracle

Overview

The RAAC Real Estate oracle stack consists of two cooperating smart contracts:

  • RAACHousePrices: The on-chain source of truth that stores each RAAC house's latest price in USD and exposes read helpers to return prices in crvUSD. It also maintains timestamps, notifies optional syncers, and handles batch updates.
  • RAACHousePriceOracle: A Chainlink Functions-based updater. It validates requests, fetches off-chain house prices from RAAC's API, and calls RAACHousePrices to persist the results.

Price Units

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

Architecture

flowchart TD
    A[Owner/Operator] -- sendRequest(args) --> B[RAACHousePriceOracle<br/>Chainlink Functions]
    B -- validate args, map requestId->tokenId --> C[Pending Map]
    B -- off-chain fetch --> D[Chainlink DON]
    D -- response (USD price) --> B
    B -- setHousePrice(tokenId, price) --> E[RAACHousePrices]
    E -- notify --> F[Syncers]
    E -- getLatestPrice(tokenId) --> G[Integrators / Protocol]

Key points:

  • RAACHousePriceOracle only orchestrates updates; it does not store canonical prices.
  • RAACHousePrices performs conversion to crvUSD at read time via CrvUSDToUSDOracle.
  • Syncers (max 5) can subscribe to updates for indexing, caching, or off-chain services.

RAACHousePrices

Purpose

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

Core Concepts

  • USD storage, crvUSD readout: getLatestPrice(tokenId) converts stored USD to crvUSD 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.

Conversion

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

Key Functions

Function Description Access
getLatestPrice(tokenId) Returns (priceInCrvUSD, lastUpdateTimestamp) view
crvUSDToUSDPrice() Returns crvUSD/USD price normalized to 18 decimals view
decimals() Mirror decimals of CrvUSDToUSDOracle 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) Set the updater oracle address onlyOwner
setCrvUSDToUSDOracle(addr) Set the FX oracle used for conversion onlyOwner
isPendingRequest(tokenId) Ask updater whether a request is 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

Purpose

  • Validate update requests and parameters (chainId, RAAC NFT address, tokenId).
  • Trigger off-chain fetch via Chainlink Functions and receive the priced response.
  • Persist prices on-chain by calling RAACHousePrices.setHousePrice.

Request Model

  • Arguments: [chainId, raacNFTAddress, tokenId]
  • Validation: strict equality checks for chainId and raacNFTAddress; tokenId non-empty.
  • Tracking: requestId → tokenId; pendingRequest[tokenId] = true until fulfillment.

The JavaScript source used by the Chainlink Functions DON to fetch prices is published here: IPFS (Pinata gateway).

Batching

The on-chain store supports both single and batch updates. The Chainlink Functions oracle example here demonstrates a single-house update per request. A higher-level operator can fan out multiple requests or extend source code to return a list and call setHousePrices.

In the future, a dedicated batching oracle will be introduced to improve scalability. When this batching oracle is deployed, the current oracle will be replaced by updating the oracle address via RAACHousePrices::setOracle. This upgrade will enable efficient batch price updates directly from the new oracle.

Fulfillment Flow

  1. Operator calls sendRequest(args, bytesArgs) on RAACHousePriceOracle.
  2. Contract validates arguments, prepares the Functions request, and records requestId → tokenId.
  3. Chainlink DON executes the JavaScript source, calls RAAC API, and returns encoded uint256 priceUSD.
  4. _processResponse(requestId, response) decodes priceUSD and calls RAACHousePrices.setHousePrice(tokenId, priceUSD).
  5. Clears tracking and emits HousePriceUpdated.

Key Functions

Function Description Access
sendRequest(args, bytesArgs) Prepare and send a Chainlink Functions request onlyOwner
isPendingRequest(houseId) Returns if a request is in-flight for houseId view

Events & Errors

  • Events: HousePriceRequestSent, HousePriceUpdated, plus base OracleRequestSent
  • Errors: InvalidPrice (non-zero enforcement), base errors from BaseChainlinkFunctionsOracle

Pending State & Frontrun Protection

The oracle maintains a pending state for each house (tokenId) while a price update request is in flight. This is used in protocol components such as RAACNFT.sol and during NFT deposit in the RWA Vault. Deposits are paused for a given asset while its price is being updated, preventing any frontrunning or manipulation based on stale or soon-to-change prices.

If a request is not fulfilled (e.g., due to timeout or failure), another request can be sent for the same house. However, the protocol does not allow forcibly resetting the pending state—this ensures that a house price cannot be skipped or left in an ambiguous state, which would otherwise open the door to frontrunning attacks. Only successful fulfillment or a new request (after timeout) can clear the pending state and allow further actions on the asset.

Multiple Requests Allowed

The oracle allows multiple price update requests for the same house (tokenId) even while a previous request is still pending. Each new request will overwrite the previous pending state for that house, and the last fulfilled response will be recorded as the official price. This design enables operators to retry or reissue requests without waiting for timeouts, but only the most recent successful fulfillment will update the price and clear the pending state.


BaseChainlinkFunctionsOracle (Shared Base)

Provides reusable plumbing for Chainlink Functions oracles:

  • Configuration: donId, s_source (JS), secrets location/reference, subscriptionId, callbackGasLimit.
  • Lifecycle hooks: _validateRequest, _processRequest, _emitRequestEvents, _beforeFulfill, _processResponse.
  • Last-results storage: s_lastRequestId, s_lastResponse, s_lastError.

Operations

Function Description
setSubscriptionId(uint64) Update billing subscription
setCallbackGasLimit(uint32) Tune fulfillment gas
setEncryptedSecretsReference(location, ref) Update secrets config

Owner-Gated Triggers

sendRequest is onlyOwner. Operators (keepers/schedulers) should hold the owner role on the oracle instance, not on RAACHousePrices.


Integration Notes

  • To read a house price for lending and health factor calculations, always query RAACHousePrices.getLatestPrice(tokenId) which returns the price denominated in crvUSD and its update timestamp.
  • To show the raw USD price and timestamp, use getRawPrice(tokenId).
  • The crvUSD conversion relies on CrvUSDToUSDOracle; 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.