In production, governance owns the controller. Owner-gated configuration is
reached through governance proposal, timelock, and execution.
Reading the tables
Every state-changing entrypoint takes an explicitcaller: Address and calls
require_auth() on it. The Auth column describes the additional authority
required beyond that signature:
Pause —
gated means the entrypoint carries #[when_not_paused] and
reverts while the controller is paused. open means it stays callable, so a
paused protocol never traps a solvent user’s exit or blocks a liquidation.
Flash-loan reentrancy — every monetary verb routes through
require_authorized_caller, which combines caller.require_auth() with the
flash-loan guard. A call made while a flash loan is in flight reverts with
#400 FlashLoanOngoing.
Core lending
account_id = 0onsupplycreates the account, mints its position NFT tocaller, binds it tospoke_idinPositionMode::Normal, and returns the new id. There is no separate account-creation entrypoint.- Third-party supply is narrowed. A caller who is neither owner nor delegate may only add to hub assets the account already holds a supply position in. New accounts skip the check because the caller becomes the owner.
toseparates borrower from recipient onborrowandwithdraw; it defaults tocaller. Debt is always booked toaccount_id.- On
withdraw, a zero amount withdraws the entire position for that asset. The return value is the resolved figure — the only way a caller learns what a withdraw-all actually paid. repayis fully permissionless. Anyone may repay anyone’s debt; the caller’s signature is needed only to pull the tokens.- Amounts credited are measured, not requested. A token that delivers less than it is sent credits the delivered amount.
Liquidation
seize_mode is SeizeMode::Transfer (pool pays underlying, withholding the
protocol fee) or SeizeMode::Credit(account_id) (seized supply shares move to a
controller account). Credit(0) creates the receiving account and liquidate
returns its id; Transfer returns 0. See
Liquidations.
Leverage and strategies
All are pause-gated and end instrategy_finalize: restamp listed collateral
risk parameters, apply post-pool risk gates, persist, emit the position batch.
multiply and flash_position accept account_id = 0 to create the account,
and otherwise assert the passed mode equals the account’s stored mode
(#25 AccountModeMismatch). See
Strategies and
Flash loans.
Account and delegation
add_delegaterequires the delegate to be an active, governance-approved position manager at the moment of the grant. Delegates are capped at 16 (#45 RegistryCapReached).remove_delegateis deliberately not pause-gated — revoking authority must never be blocked.- A delegate grant is bound to the owner who made it. Transferring the position NFT deactivates the previous owner’s grants immediately.
renew_accountextends both the account’s storage TTL and the NFTOwnerentry.
Permissionless maintenance
Caller-signed; none carries a privileged role.Owner-gated configuration
Every mutating entrypoint inControllerAdmin carries #[only_owner], and the
owner is the governance contract. Reaching them requires a governance proposal,
the applicable timelock tier, and execution.
- Wiring:
set_swap_aggregator,set_price_aggregator,set_accumulator - Limits:
set_position_limits,set_min_borrow_collateral_usd - Delegation:
set_position_manager(manager, is_active) - Blend:
approve_blend_pool,revoke_blend_pool - Topology:
create_hub,add_spoke,remove_spoke,set_spoke_liquidation_curve - Listings:
add_asset_to_spoke,edit_asset_in_spoke,set_spoke_asset_flags,remove_asset_from_spoke - Deployment:
deploy_pool,deploy_position_nft,upgrade_pool,upgrade_position_nft - Markets:
create_liquidity_pool,upgrade_liquidity_pool_params - Emergency:
pause,unpause,force_socialize_bad_debt - Lifecycle:
upgrade,migrate,transfer_ownership,accept_ownership
get_app_version, a permissionless read of the stored
migration version, and accept_ownership, callable only by the pending owner.
Emergency asymmetry: the guardian can pause and tighten listing flags
immediately, but reopening requires timelocked governance
(
#317 SpokeAssetFlagRelaxation).Views
Read-only; none mutates state. Views that accept a list are bounded byMAX_VIEW_INPUTS = 256.
Account risk
Account state
Simulation
get_liquidation_estimate(account_id, debt_payments, seize_mode) -> LiquidationEstimate
returns seized_collaterals, protocol_fees, refunds, max_payment_wad, and
bonus_rate_bps.
Market and configuration
There is no
max_withdraw, max_borrow, or max_supply view on the
controller. Size an action against get_collateral_amount,
get_ltv_collateral_usd, get_spoke_usage, and the pool’s get_reserves, or
simulate the call.Cross-cutting semantics
account_id = 0means “create”. It applies tosupply,multiply,flash_position,migrate_from_blend, and asSeizeMode::Credit(0)inliquidate. Each returns the resolved account id.- Spoke binding is permanent. Every later call rechecks it
(
#310 SpokeMismatch). - Position mode is fixed at creation.
- Zero is overloaded by direction. On entry legs (
supply,borrow,repay) zero is rejected. Onwithdrawit means “the entire position”. - Batching is uniform. Multi-asset calls sum duplicate legs per asset and
preserve first-appearance ordering. Overflow reverts
#33 MathOverflow. - Pause protects exits.
withdraw,repay,liquidate,clean_bad_debt,recapitalize,renew_account, andremove_delegatestay callable while the protocol is paused.
Notes
HubAssetKey { hub_id, asset }is the market coordinate used by user flows, pool rows, keeper config, and most views.- Account creation uses
spoke_id, not an account category. - The
account_idis the position NFTtoken_id. The controller stores no owner address; it callsowner_of(account_id)on every authority check, so transferring the token transfers the whole position. Its own ABI is on Position NFT. - Prices are resolved by the price aggregator, keyed by
PriceKey::Token(asset).

