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
submitcalls during migration.
#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 thequotes 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_positionrefund 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’supgrade 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.

