Developer Guide
Documentation
How the Particula oracle integrates with a lending protocol — from on-chain rating to collateral factor to health factor.
Architecture Overview
Particula is a cross-chain credit rating oracle. Ratings are attested on a Hub chain (LUKSO Testnet), bridged via Hyperlane to Ethereum Sepolia, and consumed by lending protocols through a lightweight oracle interface.
In production the OAppReceiverLZ validates EIP-712 signatures and stores ratings. In this demo, a MockOracleReceiver mirrors the same interface so the lending logic is identical.
How a Lending Protocol Fetches Ratings
Integration requires a single view call on the oracle receiver. The Spoke calls this every time it needs to compute collateral limits:
interface ILendingOracle {
// Returns the raw rating score (0–10 000) for a given asset key.
function getRating(string calldata key) external view returns (uint256);
}
// Inside MockLendingSpoke:
function getRatingCF(uint256 reserveId) public view returns (uint256) {
Reserve storage r = reserves[reserveId];
if (bytes(r.oracleKey).length == 0) return 0;
uint256 rating = ILendingOracle(oracleReceiver).getRating(r.oracleKey);
uint256 cf = (rating * CF_MULTIPLIER) / 10_000; // CF_MULTIPLIER = 9000 (90%)
return cf > r.maxCF ? r.maxCF : cf; // capped at governance maxCF
}The oracle key is a string identifier like "Ethereum-0xAAAA0001" that uniquely maps to an asset across chains. It is set once when the reserve is registered via addReserve().
Collateral Factor (CF)
The CF determines how much borrowing power one unit of collateral provides.
CF = min(maxCF, floor(rating × 9000 / 10 000))
Where:
rating Oracle score, range 0–10 000
9 800 = AAA, 7 500 = BBB, 3 500 = CCC
9000/10000 Fixed 90% scaling constant (built into protocol)
maxCF Governance cap — default 8 500 bps (85%)
CF unit Basis points (bps) — 8 500 bps = 85%Health Factor (HF)
HF measures the safety of an open position. It is recomputed on every oracle update — no user action needed.
HF = weightedCollateral / totalDebt weightedCollateral = Σ (supply_i × CF_i / 10 000) HF < 1.0 → Position is underwater → liquidatable HF ≥ 1.0 → Position is solvent HF ≥ 1.1 → Healthy buffer (recommended minimum)
// Inside MockLendingSpoke:
function getHealthFactor(address user) external view returns (uint256) {
uint256 weighted = 0;
for (uint256 i = 0; i < reserves.length; i++) {
uint256 supply = supplies[user][i];
if (supply > 0) {
uint256 cf = getRatingCF(i); // live oracle read
weighted += (supply * cf) / 10_000;
}
}
uint256 debt = debts[user][stableReserveId];
if (debt == 0) return type(uint256).max; // ∞ — no debt
return (weighted * 1e18) / debt; // scaled 18 decimals
}Cross-Chain Rating Flow
Each rating update triggers two writes: one directly on Sepolia (for immediate availability) and one dispatched from LUKSO Testnet via Hyperlane (mirroring the production attestor flow).
- 1Attestor (Sepolia) — Calls setOracleData(key, rating, timestamp) on MockOracleReceiver. Instant — no bridge needed.
- 2Attestor (LUKSO Testnet) — Calls dispatchRatingTo(receiver, key, rating) on MinimalRatingBridge. Hyperlane delivers the message cross-chain.
- 3MinimalRatingReceiver (Sepolia) — Receives Hyperlane message, validates sender, calls setOracleData() on the lending oracle. Same result as step 1 via bridge.
- 4MockLendingSpoke (Sepolia) — Next call to getRatingCF() / getHealthFactor() reads the new rating. CF and HF update automatically — no governance vote, no user action.
Integration Cost for a Lending Protocol
Adding Particula ratings to an existing lending protocol requires minimal changes:
Deployed Contracts (Ethereum Sepolia)
Each scenario has isolated contracts. All addresses are in src/config/contracts.ts.
| Scenario | Oracle | Spoke |
|---|---|---|
| S1 — Happy Path | 0xebE096a2… | 0xCa00c0C2… |
| S2 — Downgrade | 0xfcE6155c… | 0x3AC48Ce2… |
| S3 — Instability | 0xc3Ca2c71… | 0x06635978… |
| S4 — Multi-Asset | 0x86591253… | 0xb3b0A00B… |
| S5 — Gov CF Cap | 0x9de4961a… | 0x64ffc4f4… |
| S6 — Gov Delist | 0x363557f0… | 0xd8F8AA11… |
| S7 — Horizon RWA | 0xe2b4a10f… | 0x0931E8D4… |
Full addresses in contracts.ts. Explorer: sepolia.etherscan.io
Cost Analysis — Onchain vs Offchain
Each rating update triggers both onchain and offchain operations. The table below shows representative costs per operation type based on testnet data. Offchain values are mocked — swap for real provider billing before mainnet.
| Operation | Layer | Provider | Est. cost | Frequency |
|---|---|---|---|---|
| Oracle rating write | Onchain | Sepolia gas | ~$0.002 | Per rating update |
| Hyperlane bridge dispatch | Onchain | LUKSO gas + Hyperlane | ~$0.003 | Per cross-chain sync |
| Risk data API fetch | Offchain | Particula API | ~$0.0001 | Per rating update |
| Passport metadata pin | Offchain | Pinata IPFS | ~$0.0003 | Per metadata update |
| User supply/borrow | Onchain | Sepolia gas | ~$0.001 | Per user action |
| Liquidation | Onchain | Sepolia gas | ~$0.003 | Per liquidation |
Optimization Recommendations
- 1.Batch oracle updates. Combining multiple asset rating writes into one transaction reduces per-update gas by ~60% at scale (shared base tx cost).
- 2.IPFS content-addressing. Identical metadata payloads share the same CID — skip re-pinning if the rating tier hasn't changed (e.g. AA → AA with same subscores).
- 3.Lazy bridge dispatch. Only bridge to spoke chains when a position on that chain is active. Avoid syncing to idle chains unnecessarily.
- 4.API call deduplication. Cache oracle API responses for up to 60 seconds — multiple downstream consumers can share one fetch when rating updates are infrequent.
Note: Offchain cost values (API, IPFS) are mocked with realistic estimates. Before mainnet, wire in real billing data from your API provider and Pinata webhook — the Cost Tracker UI is already structured to accept real values by replacing the mock generators in src/lib/mockOffchainCosts.ts.