Skip to content

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 ​

MechanismChange
BorrowingborrowMurabaha(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 growthThe native borrow index is static; per-position locked terms replace the global rate. getAmountOwed() computes principal + linear accrued markup (capped), with ibra' applied.
LP yieldaccruedInterestIndices 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 queueUniversal 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.
RepaymentrepayMurabaha(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 guardsNative 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.
Liquidationabsorb() 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.
GovernancesetIbraFactor (≤ 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:

  1. Settlement: protocolClose on matured positions; the agency fee stays with the operator.
  2. Liquidation: absorb on health-factor breaches. Seized collateral is sold through buyCollateral to whoever takes the buyer's discount; if Rayyan's keeper ever buys it, any resale profit goes to the charity fund.
  3. 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 // RAYYAN in 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.

Unaudited, pre-launch software on testnet. Nothing here is an offer of financial products.