Skip to content

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

SOURCE OF TRUTH — OFF-CHAIN MONITORING & TRIGGER — NEVER SUPPLIES DATA CHAINLINK CRE ON-CHAIN FRED — DPRIME published US prime rate AWS Lambda daily · diff · pre-flight the pool bounds KMS-signed ETH-JWT · no on-chain tx CRE Gateway recovers JWT signer must be in authorizedKeys CRE workflow — DON confidential fetch · percent → RAY OCR-signed (chainId, reportedAt, rate) Chainlink Forwarder verifies DON signatures RAACPrimeRateReceiver chainId + freshness watermark report sink — no request path LendingPool enforces 0 < rate ≤ RAY and the cap baseRate 25% · optimalRate 50% ReserveLibrary accrue at old rate, then re-derive setParameter(MaxRate, …) PROTOCOL_ROLE — same off-chain sequence read to compare — discarded authoritative fetch signed HTTP trigger dispatch writeReport onReport — Forwarder only setPrimeRate() library call second, non-atomic tx getPrimeRate() · primeRateChangeBps

The trigger (AWS Lambda)

Each daily run:

  1. Reads LendingPool.getPrimeRate() (RAY) and the per-update cap parameters().primeRateChangeBps, and fetches the latest DPRIME observation from FRED, converting percent → RAY with the same integer math the workflow uses (7.50% → 7.5e25).
  2. If FRED equals the on-chain rate, it stops — no trigger.
  3. Pre-flight. It replicates the pool's setPrimeRate bounds 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.
  4. Otherwise it signs an alg:"ETH" JWT with its KMS key — the payload carries the sha256 digest of the canonical JSON-RPC body, signed EIP-191 — and POSTs workflows.execute to 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:

  1. Fetches DPRIME over Confidential HTTP — the single FRED_API_KEY is injected by the Vault DON enclave and never enters workflow memory.
  2. Converts percent → RAY with integer math, so the result is byte-identical across DON nodes.
  3. Encodes the report tuple (uint256 chainId, uint64 reportedAt, uint256 primeRate) and signs it under OCR consensus. chainId is the chain being written to; reportedAt is runtime.now(), the DON-consensus clock.
  4. 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 ChainIdMismatch unless chainId == block.chainid — a DON-signed payload cannot be replayed against a sibling receiver on another chain;
  • reverts InvalidReportTimestamp on reportedAt == 0;
  • reverts StaleReport unless reportedAt is strictly newer than lastReportedAt, 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, lastPrimeRate and lastUpdateTimestamp, 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 above 1 RAY;
  • rejects a move larger than percentMul(RAY, maxChangeBPS) in either direction, where maxChangeBPS is the pool's primeRateChangeBps parameter (PrimeRateChangeExceedsLimit);
  • accrues interest at the old rate before the change lands;
  • sets primeRate, then re-derives baseRate = 25% of prime and optimalRate = 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 PrimeRateUpdated on 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).

  • LendingPool — receives prime-rate updates via setPrimeRate
  • ReserveLibrary — enforces update bounds and recalculates derived rates
  • RAACHousePriceReceiver — sibling CRE oracle, log-triggered rather than HTTP-triggered (docs)