RAAC US Prime Rate Oracle¶
Overview¶
The prime-rate feed is built on the Chainlink Runtime Environment (CRE). It replaces the legacy Chainlink Functions contract RAACPrimeRateOracle, which is no longer the update path.
The protocol's borrowing curve derives from the US Prime Rate — baseRate is 25% of prime, optimalRate is 50% — so the feed's job is to keep LendingPool.primeRate aligned with the published rate under controlled, capped updates.
It has two halves, in separate repos:
| Half | Role |
|---|---|
Trigger — AWS Lambda (aws/raac-prime-rate) |
Daily EventBridge run. Diffs FRED against the on-chain rate, pre-flights the pool's bounds, and — only if an update is needed and would be accepted — signs a JWT with its KMS key and POSTs an HTTP trigger to the CRE gateway. Sends no on-chain transaction. |
Fetch + write — CRE workflow + RAACPrimeRateReceiver (chainlink/cre-prime-rate-oracle) |
Re-fetches the rate authoritatively on the DON, signs it under OCR consensus, and writes it to the pool through the Chainlink Forwarder. |
AWS never supplies data — it only monitors and triggers
No value that lands on-chain ever originates in AWS. Every CRE workflow fetches and computes its own data on the DON, under OCR consensus. The Lambda reads the same source only to decide whether an update is needed; the number it saw is discarded and never sent. An unauthorized trigger is therefore harmless in value terms — the gateway auth exists to prevent spam and cost, not to protect the rate. This holds for every RAAC CRE oracle.
Architecture¶
The trigger (AWS Lambda)¶
Each daily run:
- Reads
LendingPool.getPrimeRate()(RAY) and the per-update capparameters().primeRateChangeBps, and fetches the latestDPRIMEobservation from FRED, converting percent → RAY with the same integer math the workflow uses (7.50% → 7.5e25). - If FRED equals the on-chain rate, it stops — no trigger.
- Pre-flight. It replicates the pool's
setPrimeRatebounds exactly: positive,≤ 1 RAY, and within±percentMul(RAY, primeRateChangeBps)of the current rate. If the update would be rejected on-chain, it does not trigger — it posts a loud Slack error (current, target, cap, allowed window) and throws, tripping the CloudWatch alarm. Without this, the revert would happen inside the CRE write with no obvious signal. - Otherwise it signs an
alg:"ETH"JWT with its KMS key — the payload carries thesha256digest of the canonical JSON-RPC body, signed EIP-191 — and POSTsworkflows.executeto the CRE gateway.
The signing key is a KMS secp256k1 key that is never exported; the Lambda's EVM address is derived at runtime from GetPublicKey. Because the Lambda sends no transaction, that key needs no gas.
The cap can stall an update
primeRateChangeBps (default 2%) limits each update to ±2 percentage points of one RAY. If FRED moves further than that in one step, the update cannot land in a single write — either raise primeRateChangeBps via setParameter, or let the rate catch up in capped steps across daily runs.
Gateway auth¶
The CRE HTTP trigger capability validates each request's signature against the workflow's configured authorizedKeys (type ECDSA_EVM) at the gateway, before the workflow runs. The gateway recovers the JWT signer and checks it equals the configured authorized EVM address. Only the holder of that KMS key can trigger the workflow — there is no shared bearer secret to leak.
The workflow (CRE)¶
On an authorized trigger the workflow:
- Fetches
DPRIMEover Confidential HTTP — the singleFRED_API_KEYis injected by the Vault DON enclave and never enters workflow memory. - Converts percent → RAY with integer math, so the result is byte-identical across DON nodes.
- Encodes the report tuple
(uint256 chainId, uint64 reportedAt, uint256 primeRate)and signs it under OCR consensus.chainIdis the chain being written to;reportedAtisruntime.now(), the DON-consensus clock. - Calls
writeReport, which lands via the Chainlink Forwarder.
RAACPrimeRateReceiver¶
The receiver is a report sink — there is no on-chain request method and no pending machinery, because a single global rate has no per-item state to coordinate.
onReport is callable only by the Forwarder, and the ReceiverTemplate base additionally checks the report's author and workflow id against the configured expectations. _processReport then:
- decodes
(chainId, reportedAt, primeRate); - reverts
ChainIdMismatchunlesschainId == block.chainid— a DON-signed payload cannot be replayed against a sibling receiver on another chain; - reverts
InvalidReportTimestamponreportedAt == 0; - reverts
StaleReportunlessreportedAtis strictly newer thanlastReportedAt, so a delayed run cannot roll the rate backward; - calls
lendingPool.setPrimeRate(primeRate)— the pool, not the receiver, enforces the value bounds and the per-update cap, and a violation reverts the whole report; - only then advances
lastReportedAt,lastPrimeRateandlastUpdateTimestamp, so a rejected rate never consumes an observation slot; - emits
PrimeRateUpdated(primeRate).
| Member | Description |
|---|---|
getPrimeRate() |
Last rate written by this receiver (RAY). Parity with the legacy oracle |
lastPrimeRate / lastUpdateTimestamp |
Last written value and its block timestamp |
lastReportedAt |
Freshness watermark (DON-consensus clock) |
setForwarderAddress / setExpectedAuthor / setExpectedWorkflowId |
Owner-only workflow-identity configuration |
Errors: InvalidLendingPoolAddress, ChainIdMismatch, InvalidReportTimestamp, StaleReport, plus the base InvalidSender / InvalidAuthor / InvalidWorkflowId.
Integration with LendingPool¶
LendingPool.setPrimeRate is onlyPrimeRateOracle — primeRateOracle holds the receiver's address (set by the owner via setPrimeRateOracle). It delegates to ReserveLibrary.setPrimeRate, which:
- rejects a non-positive rate (
PrimeRateMustBePositive) and anything above1 RAY; - rejects a move larger than
percentMul(RAY, maxChangeBPS)in either direction, wheremaxChangeBPSis the pool'sprimeRateChangeBpsparameter (PrimeRateChangeExceedsLimit); - accrues interest at the old rate before the change lands;
- sets
primeRate, then re-derivesbaseRate = 25% of primeandoptimalRate = 50% of prime; - recomputes interest rates and liquidity, and emits
PrimeRateUpdated(oldPrimeRate, newPrimeRate).
maxRate moves in parallel¶
maxRate is not re-derived on-chain, but it is derived from the prime rate. Any prime-rate change therefore triggers a matching maxRate update as a separate PROTOCOL_ROLE transaction (setParameter(MaxRate, …)) in the same off-chain sequence. The formula and the caveat that the two writes are not atomic are in Lending Pool — the prime rate is not a setParameter value.
Operational notes¶
- Cadence: daily EventBridge schedule; a run that finds no change is a no-op with a Slack notice.
- Observability: Slack lifecycle messages per run; CloudWatch alarms on Lambda errors (including a blocked pre-flight); on-chain, track
PrimeRateUpdatedon both the receiver and the pool. - Keys — keep them distinct: the workflow owner (deploys/updates the workflow and manages DON secrets), the receiver owner (Forwarder and workflow-identity configuration), and the trigger key (the Lambda's KMS key, which can only cause a re-fetch of the true rate).
Related components¶
LendingPool— receives prime-rate updates viasetPrimeRateReserveLibrary— enforces update bounds and recalculates derived ratesRAACHousePriceReceiver— sibling CRE oracle, log-triggered rather than HTTP-triggered (docs)