Skip to main content
The controller mediates lending flows. The pool is controller-owned and stores accounting by HubAssetKey. External contracts are treated as untrusted at the boundary where their output enters protocol logic.

Trust boundaries

Supply

  1. User calls supply(caller, account_id, spoke_id, assets).
  2. account_id = 0 creates an account bound to spoke_id and mints its position NFT to caller.
  3. Controller authenticates the caller, checks the spoke listing and halt flags, and enforces the spoke supply cap and position limits.
  4. Controller transfers the tokens to the pool and credits the measured amount.
  5. Pool syncs the market row and mints scaled supply shares (floor-rounded).
  6. Controller writes SupplyPositions(account_id) and emits the position batch; the pool emits its market-state batch.

Borrow

  1. User calls borrow(caller, account_id, borrows, to).
  2. Controller authenticates owner/delegate and loads account state.
  3. Controller resolves a complete price snapshot, then checks the spoke borrow flags, the spoke borrow cap, LTV, and health factor.
  4. Pool syncs the hub-asset row, mints scaled debt shares, checks cash and utilization, then transfers tokens to to or caller.
  5. Controller writes BorrowPositions(account_id) and emits position events.

Withdraw

  1. User calls withdraw(caller, account_id, withdrawals, to).
  2. Amount 0 means close that hub-asset supply position.
  3. If the account has debt, the controller re-checks the LTV gate, health factor, and the minimum-borrow-collateral floor after the withdrawal.
  4. Pool burns supply shares and transfers tokens. Withdraw is not pause-gated, and a frozen listing still permits exit.

Repay

  1. Any payer calls repay(caller, account_id, payments).
  2. Controller transfers payment tokens to pool.
  3. Pool burns debt shares and refunds overpayment.
  4. Controller updates debt positions.

Liquidation

  1. Liquidator calls liquidate(liquidator, account_id, debt_payments, seize_mode).
  2. Controller resolves a complete price snapshot and requires health factor below one WAD.
  3. Debt payments repay pool debt; credit is measured, not requested.
  4. Collateral is seized pro-rata across the account’s whole collateral set. In SeizeMode::Transfer the pool pays underlying and withholds the protocol fee; in SeizeMode::Credit the seized supply shares move to a controller account and no collateral tokens move.
  5. Eligible residual bad debt is socialized into the market’s supply index, clamped at the supply-index floor.

Flash loan

  1. User calls flash_loan(caller, asset, amount, receiver, data).
  2. Controller validates asset: HubAssetKey and flash-loan availability; the pool computes the fee and returns it.
  3. Pool snapshots balance, transfers the amount, invokes receiver callback, pulls repayment, and verifies final balance.
  4. Fee is recorded as protocol revenue.

Strategy routes

Strategy entrypoints take opaque aggregator route bytes. The controller does not decode them. It checks its own balance deltas and the account’s final risk. Slippage protection lives inside the route payload, and only the aggregator enforces it. The controller will not catch a bad price for you — see Strategies.

Who enforces what

The same operation is gated in two places, and the split is easy to get wrong: Caps are a controller concern because they are per spoke. The pool knows nothing about spokes — it only tracks per-market totals.