Deployment Runbook
Deploy protocol hooks, markets, LI.FI executors, and the quote-server service with a verifiable handoff trail.
Scope
Use this runbook when deploying or replacing:
- the protocol
HybridCollateralSettlementHookas part of a market stack. - a registered-issuer or native-stablecoin market.
- the LI.FI
LifiHookExecutor. - the quote-server Railway/Docker service that signs quotes, fetches M1 material, runs oracle updates, and optionally operates the LI.FI loop.
Do not use this page for routine policy changes after deployment. Use Risk Policy Change or the narrower admin runbook for that action.
Authority Required
Deployment private key, RPC access, repository write access for generated manifests, and custody of any service secrets required by the quote server.
Before Any Broadcast
From the repository root:
make install
make test-deploy
forge test
make size
make working-tree-secret-checkFor a docs/interface release that exposes the new market:
pnpm --filter docs types:check
make interface-secret-check
git diff --checkRecord the chain id, config path, intended manifest path, deployer address, and rollback owner before running a broadcast command.
Deployment Surfaces
| Surface | Source of truth | Notes |
|---|---|---|
| Protocol hook | market deployment script | The hook address is mined for Uniswap v4 permission bits. Do not deploy it with an arbitrary script. |
| Registered-issuer market | script/production/DeployRegisteredIssuerStack.s.sol | Deploys oracle, risk config, manager, issuer source/controller/adapter, hook, router, and manifest. |
| Native-stablecoin market | script/production/DeploySampleStablecoinStack.s.sol | Deploys native/sample stablecoin path, manager, controller, hook, router, and optional registry/liquidation pieces. |
| LI.FI hook executor | script/production/DeployLifiHookExecutor.s.sol | Deploys the executor contract used as the v1 LI.FI solver identity. |
| Quote server | quote-server/Dockerfile and root railway.json | Runs the private signing/M1/oracle/LI.FI service. |
| Public gateway | gateway/Dockerfile and gateway/railway.json | Serves interface/docs and proxies /quote/* to the private quote-server service. |
Protocol Hook Deployment
The protocol hook is deployed by the market stack scripts. The scripts mine the
hook salt, deploy HybridCollateralSettlementHook, verify the deployed address
matches expectedHook, and write hook, expectedHook, and hookSalt into
the manifest.
Registered-issuer dry run:
export DEPLOY_CONFIG=deployments/config/sepolia-m1-staging-active.json
make deploy-dry-run DEPLOY_CONFIG="$DEPLOY_CONFIG"Registered-issuer broadcast:
DEPLOY_CONFIG="$DEPLOY_CONFIG" \
forge script script/production/DeployRegisteredIssuerStack.s.sol \
--tc DeployRegisteredIssuerStackScript \
--rpc-url "$SEPOLIA_RPC_URL" \
--broadcast \
--private-key "$SEPOLIA_DEPLOYER_PRIVATE_KEY"Native-stablecoin dry run:
export DEPLOY_CONFIG=deployments/config/sepolia-native-stablecoin-registry-v3.json
forge script script/production/DeploySampleStablecoinStack.s.sol \
--tc DeploySampleStablecoinStackScript \
--rpc-url "$SEPOLIA_RPC_URL" \
--sender "$SEPOLIA_DEPLOYER_ADDRESS"Native-stablecoin broadcast:
make deploy-native-stablecoin DEPLOY_CONFIG="$DEPLOY_CONFIG"Hook gotchas:
adminin the config must match the broadcasting deployer.chainIdin the config must match the RPC chain.- Mainnet configs must not use fixed-oracle rehearsal mode.
- The hook address is not negotiable after mining. If
hookandexpectedHookdiverge, stop instead of patching the manifest by hand. - The deployed router must remain allowlisted on the hook by codehash and address.
Registered Issuer Stack
Use this path for M1/registered-issuer collateral markets.
export DEPLOY_CONFIG=deployments/config/sepolia-m1-staging-active.json
make deploy-dry-run DEPLOY_CONFIG="$DEPLOY_CONFIG"DEPLOY_CONFIG="$DEPLOY_CONFIG" \
forge script script/production/DeployRegisteredIssuerStack.s.sol \
--tc DeployRegisteredIssuerStackScript \
--rpc-url "$SEPOLIA_RPC_URL" \
--broadcast \
--private-key "$SEPOLIA_DEPLOYER_PRIVATE_KEY"After broadcast:
export MANIFEST=deployments/manifests/11155111-registered-issuer-m1-staging-active.json
make env-check MANIFEST="$MANIFEST"
make registered-issuer-payload MANIFEST="$MANIFEST" AMOUNT=1000000
make live-qa-check MANIFEST="$MANIFEST"Add initial liquidity only after the manifest and payload checks pass:
make add-registered-issuer-liquidity \
MANIFEST="$MANIFEST" \
DEPLOY_CONFIG="$DEPLOY_CONFIG" \
LP_AMOUNT0=1000000 \
LP_AMOUNT1=1000000Native Stablecoin Stack
export DEPLOY_CONFIG=deployments/config/sepolia-native-stablecoin-registry-v3.json
make deploy-native-stablecoin DEPLOY_CONFIG="$DEPLOY_CONFIG"After broadcast:
export MANIFEST=deployments/manifests/11155111-native-stablecoin-registry-v3.json
make env-check MANIFEST="$MANIFEST"
make seed-native-liquidity MANIFEST="$MANIFEST" DEPLOY_CONFIG="$DEPLOY_CONFIG"If the market needs liquidation support:
make deploy-liquidation-controller MANIFEST="$MANIFEST"Native gotchas:
- Production multi-market native stablecoin deployments should use
StableDebtRegistry; isolated direct-manager minter authority is testnet-only unless explicitly approved. - Backfilled liquidation support mutates the manifest. Review the manifest diff before handing the market to operators or bots.
- If a deployment was interrupted, prefer a checked resume config and verified on-chain reads over trusting stale local broadcast output.
LI.FI Hook Executor Deployment
Deploy the executor before running live LI.FI fill mode:
export DEPLOY_CONFIG=deployments/config/11155111-lifi-hook-executor.json
DEPLOY_CONFIG="$DEPLOY_CONFIG" \
forge script script/production/DeployLifiHookExecutor.s.sol \
--tc DeployLifiHookExecutorScript \
--rpc-url "$SEPOLIA_RPC_URL" \
--broadcast \
--private-key "$SEPOLIA_DEPLOYER_PRIVATE_KEY"Then verify the executor manifest and LI.FI readiness:
pnpm --filter @steerprotocol/collateral-rail-quote-server lifi:readiness -- --strictExecutor gotchas:
- The manifest
solverIdis derived from the executor address. DEFAULT_ADMIN_ROLE,OPERATOR_ROLE,SOLVER_SIGNER_ROLE, input settler, router, and output settler are verified by the deploy script.- Stop order opening before replacing an executor that live orders may reference.
Quote Server Deployment
Build and test the image locally from the repository root:
pnpm install --frozen-lockfile --filter @steerprotocol/collateral-rail-sdk
pnpm --filter @steerprotocol/collateral-rail-sdk build
pnpm install --frozen-lockfile --filter @steerprotocol/collateral-rail-quote-server
pnpm --filter @steerprotocol/collateral-rail-quote-server build
pnpm --filter @steerprotocol/collateral-rail-quote-server test
docker build -f quote-server/Dockerfile -t collateral-rail-quote-server .
docker run --rm -p 8080:8080 --env-file quote-server.env collateral-rail-quote-serverIn another shell:
make quote-health QUOTE_SERVER_URL=http://127.0.0.1:8080The root railway.json deploys the quote server with
quote-server/Dockerfile and health check path /healthz.
Minimum Railway environment:
| Variable | Notes |
|---|---|
CHAIN_ID | target chain id |
COLLATERAL_MANAGER | active market manager |
SETTLEMENT_CONTROLLER | required for registered-issuer paths |
SPONSOR_SIGNER_PRIVATE_KEY or QUOTE_SIGNER_PRIVATE_KEY | quote signer; must match on-chain config |
QUOTE_RPC_URL, RPC_URL, or SEPOLIA_RPC_URL | required for risk-aware reads and fixed oracle updater |
M1_JWT | required for M1-backed deposit/redeem endpoints |
M1_IS_TESTNET | true for testnet M1, false for production M1 |
Optional Railway environment for fixed oracle updates:
ORACLE_UPDATER_PRIVATE_KEY=0x...
QUOTE_FIXED_ORACLE_UPDATES_FILE=interface/public/manifests/catalog.json
QUOTE_FIXED_ORACLE_UPDATE_INTERVAL_SECONDS=21600
QUOTE_FIXED_ORACLE_PRICE_API_BASE=https://api.coinbase.comOptional Railway environment for LI.FI operation:
LI_FI_INTENTS_API_KEY=...
LI_FI_ORDER_SERVER_BASE=https://order-dev.li.fi
LI_FI_INVENTORY_ROUTES_JSON=[...]
LI_FI_TRUST_COMPONENTS_JSON=[...]
LI_FI_REPLAY_PGLITE_DATA_DIR=/data/lifi-replay
LI_FI_SERVE_VERIFY_IDENTITY=true
LI_FI_SERVE_FILL_ORDERS=false
LI_FI_SERVE_SUBMIT_FINALISE=falseAfter Railway deploy, run readiness from an environment that matches the service. If LI.FI operation is enabled, run one bounded loop before unattended mode:
pnpm --filter @steerprotocol/collateral-rail-quote-server lifi:readiness -- --strict
pnpm --filter @steerprotocol/collateral-rail-quote-server lifi:serve -- --live --onceQuote-server gotchas:
/healthzonly proves the HTTP process responds. It does not prove quote issuance, fixed oracle updates, M1 material, LI.FI quote publishing, fills, or finalise safety.- Server secrets must not appear in
VITE_*browser variables. QUOTE_FIXED_ORACLE_UPDATESandQUOTE_FIXED_ORACLE_UPDATES_FILEare mutually exclusive.- Fixed oracle updates require both RPC and
ORACLE_UPDATER_PRIVATE_KEY. - Blank
fixedOraclePriceE18values in manifests should be treated as unset; if a service crashes while parsing catalog data, inspect manifest/catalog fields before rotating keys. - For M1 registered-issuer quotes, publish preview inventory only; fetch final executable M1 permit material just in time for build/fill.
Public Gateway And Catalog
If the deployment changes the public interface or market list, regenerate the interface catalog before building the gateway:
INTERFACE_DEPLOYMENT_MANIFESTS=deployments/manifests/11155111-registered-issuer-m1-staging-active.json,deployments/manifests/11155111-native-stablecoin-registry-v3.json \
INTERFACE_DEFAULT_DEPLOYMENT_MANIFEST=deployments/manifests/11155111-registered-issuer-m1-staging-active.json \
node gateway/scripts/write-interface-env.mjs
make interface-checkThe gateway Railway service uses gateway/railway.json, serves the interface
and docs, and proxies /quote/* plus /healthz to the private quote-server
service. Set this only if the private Railway DNS name changes:
QUOTE_SERVER_UPSTREAM=http://<quote-service>.railway.internal:8080Gateway gotchas:
- The public catalog is generated at build time from configured manifest paths.
If the live site shows stale markets, check
gateway/scripts/write-interface-env.mjs,interface/public/manifests/catalog.json, and the copied deployment manifests. PUBLIC_SEPOLIA_RPC_URLandPUBLIC_WALLETCONNECT_PROJECT_IDare public browser values. Do not use quote signer, deployer, M1, or LI.FI secrets here.
Verification
Deployment is not done until:
- Manifest exists under
deployments/manifests/and hasverified: true. hook,expectedHook, andhookSaltare present and consistent.make env-check MANIFEST=...passes.- A small quote or payload path succeeds against the new market.
- Quote-server
/healthzresponds and any enabled M1, oracle updater, or LI.FI path has its own readiness evidence. - Interface catalog changes, if any, point at the same manifest facts.
Handoff Notes
Record:
- deployer, chain id, config path, manifest path, and commit hash.
- broadcast transaction hashes and generated manifest diff.
- hook address, expected hook address, hook salt, router, manager, controller, oracle, risk config, quote signer, and executor if applicable.
- quote-server service URL, Railway service name, and configured health path.
- readiness command outputs.
- any follow-up admin steps such as signer rotation, liquidation controller backfill, catalog update, or gateway redeploy.
Rollback
- Stop quote issuance and LI.FI fill submission first.
- Revert manifest/catalog changes if clients are reading the wrong deployment.
- Restore the previous quote-server environment or image if the service is bad.
- Disable signed quote mode or enter emergency pause if deployed on-chain state is unsafe.
- Resume with canary quotes or
lifi:serve -- --live --once, not unattended traffic.
Source Files
script/production/DeployRegisteredIssuerStack.s.solscript/production/DeploySampleStablecoinStack.s.solscript/production/DeployLifiHookExecutor.s.solquote-server/Dockerfilerailway.jsongateway/Dockerfilegateway/railway.jsongateway/scripts/write-interface-env.mjsdeployments/config/deployments/manifests/