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 theCrvUSDToUSDOracle
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 tocrvUSD
at read time viaCrvUSDToUSDOracle
.- 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
usingCrvUSDToUSDOracle
. - 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 tocrvUSD
usingcrvUSDToUSDOracle.getPrice()
and itsdecimals()
. - Timestamps:
tokenToLastUpdateTimestamp[tokenId]
andlastUpdateTimestamp
(global) track freshness. - Syncers: Optional contracts implementing
IRAACHousePriceSyncer
can be added (up to 5). They receivenotify(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
andsetHousePrices
are restricted to the configuredoracle
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
RAACHousePriceOracle (Chainlink Functions)¶
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
andraacNFTAddress
;tokenId
non-empty. - Tracking:
requestId → tokenId
;pendingRequest[tokenId] = true
until fulfillment.
Off-chain Source (Chainlink Functions)¶
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¶
- Operator calls
sendRequest(args, bytesArgs)
onRAACHousePriceOracle
. - Contract validates arguments, prepares the Functions request, and records
requestId → tokenId
. - Chainlink DON executes the JavaScript source, calls RAAC API, and returns encoded
uint256 priceUSD
. _processResponse(requestId, response)
decodespriceUSD
and callsRAACHousePrices.setHousePrice(tokenId, priceUSD)
.- 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 baseOracleRequestSent
- Errors:
InvalidPrice
(non-zero enforcement), base errors fromBaseChainlinkFunctionsOracle
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 incrvUSD
and its update timestamp. - To show the raw USD price and timestamp, use
getRawPrice(tokenId)
. - The
crvUSD
conversion relies onCrvUSDToUSDOracle
; ensure it is configured and healthy. Switching it is owner-controlled viasetCrvUSDToUSDOracle
.
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.