Protocol Architecture
Design thesis
Change as little as possible of a five-year, multi-audit codebase. Rayyan is Compound V3 (Comet) with exactly one thing replaced (how debt grows) and everything else untouched.
┌──────────────────────────────────────────────────────────┐
│ Rayyan Comet (Compound V3 fork, ~240 changed/added lines)│
│ • borrowMurabaha / repayMurabaha / getAmountOwed │
│ • per-position rate lock + contract-price-cap-as-debt │
│ • streaming linear accrual + FIFO expiration queue │
│ • native: collateral, HF, absorb(), oracles, pausing │
└───────────────┬──────────────────────────────────────────┘
│ whitelisted (collateral-only operator)
┌───────────────▼───────────────┐ ┌──────────────────────┐
│ RayyanCloser │ │ Keeper (off-chain) │
│ unified close via Uniswap V3 │◄──│ settlement, absorb, │
│ flash swap; protocolClose │ │ POL band │
└───────────────────────────────┘ └──────────────────────┘What changed in Comet
| Mechanism | Change |
|---|---|
| Borrowing | borrowMurabaha(amount, maxRatePerSecond): snapshots the curve rate at max(spot, EMA) utilization (the EMA uses τ 4h with folds capped at 50%, which makes crushing spot with a transient supply useless for underpricing a lock), reverts if the rate exceeds the caller's disclosed bound, computes contractPrice = amount × (1 + rate × 360d), records the contract price as the Comet-native debt, transfers only the principal. One position per address. Post-checks: collateralization + 80% utilization cap. |
| Debt growth | The native borrow index is static; per-position locked terms replace the global rate. getAmountOwed() computes principal + linear accrued markup (capped), with ibra' applied. |
| LP yield | accruedInterestIndices rewritten: the supply index advances by the LP share of aggregate streaming markup, segmented at position expirations so every view (balanceOf, totalSupply, getReserves) agrees with stored accrual exactly. |
| Expiration queue | Universal 360-day tenor ⇒ positions expire in open order ⇒ a FIFO queue with lazy deletion stops each position's stream at exactly its settlement date, amortized O(1). Late repay/absorb skip the aggregate decrement the queue already performed. |
| Repayment | repayMurabaha(borrower): pulls getAmountOwed(), zeroes the debt (the forgiven remainder is ibra'), removes the stream, clears all position state. Callable by the borrower or the whitelisted Closer. |
| Bypass guards | Native implicit borrow/repay paths are sealed: withdraw, supply, and transfer of base that would create or repay murabaha debt revert with explicit errors. borrowMurabaha/repayMurabaha are the only doors. |
| Liquidation | absorb() changes in one line plus position-state cleanup: the pledge is credited against getAmountOwed(), so a liquidated borrower keeps the ibra' rebate. With the store-front factor at 1.0 the collateral buyer's discount equals the whole haircut and the protocol keeps none of it. isLiquidatable() additionally returns true past the settlement date, enabling keeper settlement of matured positions. |
| Governance | setIbraFactor (≤ 1e18), setLpShareBps (70–99%, accrues at the old split first), setRayyanCloser, donateToSupply (a voluntary donation to all suppliers; enforcement money never flows through it). |
Untouched: collateral management, health factor math, buyCollateral, Chainlink price integration, the utilization rate-curve functions, pause guardian, ERC-20 accounting.
RayyanCloser (~220 lines, separate contract)
Comet has no native flash loan, so the unified close uses a Uniswap V3 flash swap: the pool delivers USDC output first; inside the swap callback the Closer repays the murabaha (releasing collateral), withdraws exactly the WETH owed, pays the pool, and returns everything else to the borrower. Atomic; guarded by pool-caller and in-flight borrower checks; holds zero funds at rest.
protocolClose(borrower) is keeper-only and post-maturity, with the same mechanics plus the settlement agency fee, 0.25% of the amount settled with a floor of one base unit, paid to the operator as the borrower's appointed agent. The Closer holds a collateral-only operator carve-out in Comet (it can never touch base-token/LP funds), implementing the spec's trust model (governance-set, timelocked in production) while letting settlement work with zero borrower pre-authorization.
Keeper (off-chain, ops/keeper.mjs)
Event-driven position tracking (MurabahaBorrow/Repay/PositionAbsorbed), per block:
- Settlement:
protocolCloseon matured positions; the agency fee stays with the operator. - Liquidation:
absorbon health-factor breaches. Seized collateral is sold throughbuyCollateralto whoever takes the buyer's discount; if Rayyan's keeper ever buys it, any resale profit goes to the charity fund. - POL utilization band: holds pool utilization in 65–72% using treasury deposits.
Notable implementation facts
- Runtime size 24,471 bytes, under the EVM's 24,576 limit (asset slots trimmed to 6; queue storage internal).
- Solidity 0.8.15, upstream pin
compound-finance/comet @ d5a30b0(last classic-Comet commit). All modifications marked// RAYYANin source. - Rounding is everywhere pool-favorable; per-position stream rates are stored (not recomputed) so add/remove is exactly symmetric, verified by an aggregate-consistency fuzz invariant.