stellar CLI, usually wrapped by make targets. The same
sequence applies to testnet and mainnet. Substitute testnet or mainnet for
<network> below.
What you need before deploy
Prerequisites
- The
stellarCLI andjq. - A compatible Rust toolchain for Soroban contracts.
- A funded signing identity (default
deployer), orSIGNER=ledgerfor a ledger-backed signer.
Build artifacts
make deploy-artifacts produces governance.wasm, controller.wasm, and
pool.wasm for upload. The position NFT and price aggregator are deployed by
governance from hashes it already holds, so they need no separate staging step.
Deploy and bring-up order
The order is load-bearing. Governance deploys first, because it owns everything after it. The controller starts paused. The pool deploys once for the whole protocol, and every asset registers on that one pool.make <network> deploy runs phases 1–8 below; make <network> setup runs the
whole thing including markets and spokes.
1
Upload WASM and deploy governance
Uploads
pool.wasm and controller.wasm, then deploys governance with
__constructor(admin, min_delay).That constructor sets admin as owner, sets it as the access-control admin,
grants it all five roles (PROPOSER, EXECUTOR, CANCELLER, GUARDIAN,
ORACLE), and stores min_delay. A zero delay is rejected with
#39 InvalidTimelockDelay.Record the governance id and the delay — later steps need both.2
Deploy the controller
Governance calls
deploy_controller(controller_wasm_hash) immediately. It is
one-time and owner-gated; a second call reverts #5 PoolAlreadyDeployed.The controller’s __constructor(admin) does four things:- sets governance as owner;
- sets both position limits to
POSITION_LIMIT_MAX = 5; - sets the minimum-borrow floor to
$5WAD; - pauses the contract.
The controller grants no roles at all. It has no access-control role set
— every admin entry point is owner-gated, and every user flow is
caller-authenticated. Only governance has roles.
3
Deploy the price aggregator
Governance calls
deploy_price_aggregator(wasm_hash). This also wires the
controller atomically in the same call, so there is no separate
“set price aggregator” step at bring-up.Skip this and every priced call fails with #27 AggregatorNotSet.To re-point a live aggregator later, use the timelocked
SetPriceAggregator operation.4
Deploy the central pool
Schedule and execute
DeployPool(wasm_hash) through the timelock. The hash
is an argument — there is no template-registration step.Run it once; a repeat reverts #5 PoolAlreadyDeployed. Day-2 code changes
use the same shape, UpgradePool(hash).5
Deploy the position NFT
Schedule and execute
DeployPositionNft(...) through the timelock.This is not optional. The account_id is the NFT token_id, and the
controller asks the NFT contract who owns an account on every authority
check. Without it, every account-touching call fails with
#53 PositionNftNotSet.6
Wire the swap aggregator and accumulator
make <network> configure-controller sets the swap aggregator and the
revenue accumulator.Both are checked, not skipped silently. Without an accumulator,
claim_revenue fails with #211 NoAccumulator. Set
ALLOW_MISSING_AGGREGATOR=1 or ALLOW_MISSING_ACCUMULATOR=1 only if you
deliberately want to defer one.7
Register each market on the shared pool
Governance schedules a typed
AdminOperation per step through
propose(proposer, op, salt), then execute after the delay. Per asset:AdminOperation::CreateLiquidityPool— registers the hub asset on the existing central pool.AdminOperation::ConfigureAssetOracle— price reads work only after this.AdminOperation::AddAssetToSpoke(orEditAssetInSpoke) per spoke.
create_liquidity_pool reverts with #30 PoolNotInitialized if
deploy_pool has not run, #2 AssetAlreadySupported if the market already
exists, #43 HubNotActive if the hub is missing or deactivated, and
#8 WrongToken if params.asset_id does not match the asset.8
Configure spokes and Blend pools
Add spokes and memberships through governance:Approve Blend migration pools through governance:
9
Unpause
Turn the protocol live only after the pool and position NFT are deployed,
every market has pool rows and a working oracle, and the spokes list them.Unpause is timelocked — it is
AdminOperation::Unpause, scheduled and
executed like any other governance change. Only pause is immediate. On
mainnet the setup target deliberately leaves the protocol paused so the
timelock can be raised to its production floor first:make <network> setup chains: deploy → configure-controller → market, spoke,
and Blend setup → rent prepay → status. On testnet it also unpauses; on
mainnet it deliberately does not. make <network> resume re-runs everything
from configure-controller onward. Run individual targets only for partial
deploys or recovery.Pre-flight checks
Before enabling a network for users, confirm:- Workspace tests, integration harness, and fuzz targets pass for the release
- Certora profiles for the release tag
- Deployed addresses, governance ownership, timelock roles, markets, spokes, oracles, and caps
- Keeper config covers launched
HubAssetKeymarkets - Pause / unpause drill on that network
Upgrades
Governance self-upgrade uses
AdminOperation::UpgradeGov(hash) scheduled with
propose and applied with execute_self (make <network> upgradeGovernance).
Governance-self operations cannot go through execute, because Soroban does not
allow the generic self-call pattern used for controller calls.
Smoke tests
make <network> getAllMarkets,
make <network> getAllIndexes, and make <network> info.
Next
Governance
Timelock roles and the propose/execute lifecycle.
Configuration
Validation rules for risk, oracle, and spoke parameters.
Addresses
Deployed contract addresses per network.

