Skip to main content
Trust boundaries and authorization across governance, controller, and pool: what crosses each edge and what gets checked there. Validate once at the boundary, then rely on checked inputs downstream. Caller auth, oracle responses, and balance deltas are checked at entry; downstream code assumes those checks already ran. That design does not guarantee solvency, oracle correctness, or loss prevention.

Trust boundaries

Authorization

The controller is paused by default after construction. User and strategy entrypoints carry #[when_not_paused].

Governance boundary

Controller boundary

Several permissionless flows are intentional: repay does not require the account owner (anyone may repay any account; overpayment is refunded), and liquidate is open to any liquidator on an eligible account — including the account’s own owner. The only self-reference rejected is SeizeMode::Credit(id) naming the liquidated account itself (#133), which would hand its collateral straight back. clean_bad_debt and recapitalize are likewise permissionless, because both reduce risk. supply is partly open: a third party may supply into an existing account, but only into hub assets it already holds a supply position in, so a third party cannot consume the account’s supply-position slots. Ownership transfer is two-step on both contracts: transfer_ownership(new_owner, live_until_ledger), then accept_ownership() by the incoming owner.
The controller has no roles. It holds no access-control role set at all: admin entry points are owner-gated, and user flows are caller-authenticated. accept_ownership on the controller renews its instance TTL and completes the ownership handover — there is nothing else to sync. Roles exist only on governance.

Position ownership

Each account is one token in the position-NFT collection: account_id is the token_id. The controller stores no owner address and calls owner_of on every authority check, so transferring the token transfers the whole position — collateral and debt together, atomically. Account deletion always routes through remove_account_and_burn_nft, so a deleted account can never leave a live token behind. Owners may grant delegates, but only to addresses governance has already registered as active position managers. A grant is bound to the granting owner, so an NFT transfer deactivates the previous owner’s grants immediately.

Reentry guard

A shared single-flight guard, FlashLoanOngoing, blocks reentry during external code execution:
  • Flash-loan callbacks.
  • Strategy swaps through the aggregator.
  • Blend submit calls during migration.
Mutating controller entrypoints reject calls while the guard is set, raising #400 FlashLoanOngoing.

Oracle risk

The oracle path is fail-closed: a provider trap reverts the whole transaction. Every valuation-dependent mutation consumes a complete price snapshot and reverts on staleness, unsafe deviation, a sanity-band violation, or a missing leg — there is no permissive mode and no risk-decreasing exemption. A two-source price is the midpoint of its two accepted legs or it fails; one readable leg is an incomplete observation, not a fallback. The only surface that degrades instead of panicking is the quotes view, which returns valid: false. See Oracles.

Token assumptions

Listed tokens are SAC / SEP-41 contracts. The protocol does not assume 1:1 transfer semantics: amounts are always measured, never assumed, so a fee-on-transfer or short-delivering token credits the delivered amount and a liquidation’s seizure scales down to match the measured receipt. Three things are still assumed:
  • Decimals are 3 to 18. Outside that range, listing fails.
  • A balance only changes through transfers the protocol makes or observes. That rules out rebasing tokens — they are out of scope.
  • Governance approved the token by listing it. Every token address the controller hands to a token contract is checked against a spoke listing first. That includes the flash_position refund step, which runs after the flash guard has closed.

Pool trust

The pool is deliberately narrow:
  • It performs no account-level solvency decisions.
  • Its mutating methods are controller-only.
  • It never calls another pool.
  • It does not infer risk from token balances: it keeps its own cash book and market share totals.
get_reserves(hub_asset) returns protocol-tracked cash, not the live token balance, so a direct donation to the pool address is not borrowable. Live balance is read only for flash-loan settlement checks. The pool does emit three events — PoolMarketStateBatchEvent, PoolMarketParamsBatchEvent, and StrategyFeeEvent. Indexers must subscribe to it, not only to the controller.

Upgrade model

Controller and pool upgrades are governance-timelocked in production. The controller’s upgrade auto-pauses first. Pool rate-model updates accrue at the old rate before applying new parameters. There is no generic storage-migration framework; layout-changing upgrades require a coordinated migration. Recommended procedure: governance pause → schedule upgrade → execute after delay → re-run required configuration → smoke-test views → unpause.

Strategy router and accumulator risk

The aggregator is governance-set and external. Slippage (total_min_out) is enforced inside the aggregator, not the controller. The accumulator is address-only trust: claimed revenue forwards to it, so verify the address before enabling claims.

Operational monitoring

Watch for assets with no oracle configured on the price aggregator; oracle staleness and deviation errors; get_revenue rising while claim_revenue returns zero; pool cash below withdrawal demand; supply or borrow caps near saturation; account and shared-state TTL approaching archival; token-issuer upgrades; aggregator contract changes; timelock operations approaching expiry; and a borrow index approaching MAX_BORROW_INDEX_RAY, which clamps silently and stops both debt growth and supplier earnings with no revert and no event. See the operational runbooks for oracle, RPC, and indexer disruption.

Assurance

Selected core invariants are covered by in-repo tests, fuzz targets, and Certora formal specifications (scoped; not a blanket product audit). Operational controls include global pause, per-spoke pause/freeze, timelocked admin changes, and fail-closed pricing on every action that needs a price. These controls do not eliminate smart-contract or market risk.

Next

Governance

Timelock roles and emergency controls.

Deployment

Build, deploy, and bring-up sequence.

Configuration

Validation rules for risk, oracle, and spoke parameters.