Skip to main content
Lending stack: governance owns admin surfaces; users talk to the controller only.

Contracts

The protocol is eight contracts. Five are in the diagram above; the other three sit at the edges.
The position NFT is easy to overlook and load-bearing. The controller stores no owner address — it calls owner_of(account_id) on the NFT for every authority check. Transferring that token transfers the whole position, collateral and debt together, with no protocol-side handover step. See Position NFT.

Market coordinate

All market-specific flows use:
The same token on different hubs is independent.

Account risk

Accounts bind to one spoke. The spoke decides collateral flags, borrow flags, caps, LTV, liquidation threshold, liquidation bonus, and fee for each hub asset. Prices come from the price aggregator (PriceKey::Token(asset)), not from the spoke.

Pool boundary

The controller is the pool owner. Users do not call pool mutators directly. The pool tracks internal cash, indexes, scaled supply, scaled debt, and revenue.

External boundaries

Each of these is untrusted, and each is checked in a different way:
  • Price aggregator output is validated for staleness, cross-source agreement, and sanity bounds before use. A failure reverts the operation rather than falling back.
  • Swap aggregator output is ignored as a claim. The controller measures its own balance deltas instead.
  • Flash-loan receivers run arbitrary code. The pool snapshots its balance and verifies repayment of principal plus fee before returning.
  • Token contracts may deliver less than they are sent. Every inbound credit is a measured delta, never the requested amount.

Keeper

An off-chain service, not a contract, and it holds no privileged role — the controller has no roles at all. It does two jobs:
  • Extends storage TTL and restores archived entries. On mainnet it wakes every 6 hours and extends anything expiring within 21 days. This is the one component with a real deadline; see the RPC disruption runbook.
  • Optionally calls update_indexes. That loop is disabled by default (enable_index_refresh: false), because indexes accrue from ledger time on the next interaction anyway. Nothing depends on the keeper pushing them.