This guide covers the deployment and initialization of Mainstay contracts on Stellar networks (Testnet, Mainnet).
Note: scripts/deploy_testnet.sh hard-requires STELLAR_NETWORK=testnet (from .env) and explicitly passes --network testnet to all Stellar CLI calls to prevent accidentally deploying to the wrong network.
- Stellar CLI installed and configured.
- A functional identity (
deployer) with enough lumens.
Mainstay handles real industrial asset records used as DeFi collateral. A formal Soroban security audit is required before Mainnet deployment.
The Stellar Development Foundation (SDF) maintains the Soroban Security Audit Bank, a curated list of pre-approved audit firms. SCF-funded projects may qualify for subsidized audits.
Recommended firms (see docs/audit-report.md for full details):
| Firm | Soroban Expertise | Key Credential |
|---|---|---|
| Veridise | Audited Soroban Core | Proprietary AuditHub tooling; deepest Soroban experience |
| Halborn | Enterprise-grade assessments | Audited Soroban zkCrossDex |
| Hacken | Bridge accounting; trust boundaries | Audited Soroban intent bridges |
| Certora | Formal verification | Mathematical correctness proofs |
SCF-funded projects: Contact the Stellar Community Fund team to access subsidized audit services through the Soroban Security Audit Bank.
- Complete threat model review (
docs/threat-model.md) - Run full test suite with
cargo test --workspace— all tests must pass - Run
cargo clippy --workspace --all-targets -- -D warnings— zero warnings - Run
cargo audit— no high-severity advisories - Verify all TTL extension coverage per
docs/ttl-strategy.md - Verify
scripts/deploy_testnet.shcompletes successfully on testnet - Tag a release candidate commit for the auditor
- Engage a Soroban-specialized audit firm (see §0.1)
- Provide code snapshot (commit hash) and documentation bundle:
docs/architecture.mddocs/threat-model.mddocs/ttl-strategy.mddocs/access-control.mddocs/collateral-scoring.mddocs/credentialing.mddocs/asset-lifecycle.md
- Address all findings from the audit report
- Obtain auditor sign-off on all Critical and High findings
- Publish the final audit report in
docs/audit-report.md - Complete this deployment checklist after the audit is finished
- All Critical-severity findings resolved and verified
- All High-severity findings resolved and verified
- All Medium-severity findings resolved or documented with deferral justification
- Auditor sign-off letter received
- Final audit report published to
docs/audit-report.md - Regression tests added for all resolved findings
See docs/audit-report.md for the full list of SDF-vetted Soroban audit firms, including:
- Certora — Formal verification via Certora Sunbeam for Soroban WASM bytecode
- OtterSec — Premier Rust/WASM security ($36B+ TVL secured)
- Veridise — Audited Soroban Core; advanced static analysis via AuditHub
- Runtime Verification, ChainSecurity, Halborn, Oak Security, Zellic
SDF's Soroban Security Audit Bank may cover up to 100% of audit costs for eligible projects.
- Finalize and freeze the contract codebase (tag a release candidate).
- Run full test suite with coverage:
./scripts/test.sh. - Run
cargo clippywith all lints and resolve warnings. - Run
cargo auditto check dependency vulnerabilities. - Complete internal threat modeling (STRIDE framework).
- Verify deployment runbook initialization on testnet.
- Engage a Soroban-specialized audit firm (see
docs/audit-report.md§Recommended Audit Firms). - Address all audit findings before mainnet deployment.
- Publish the final audit report in
docs/audit-report.md. - Complete this deployment checklist after the audit is finished.
Compile all contracts to optimized WASM:
./scripts/build.shDeploy contracts in order and store their IDs.
stellar contract deploy --wasm target/wasm32-unknown-unknown/release/asset_registry.wasm --network testnet --source deployerNote the Asset Registry ID (AR_ID).
stellar contract deploy --wasm target/wasm32-unknown-unknown/release/engineer_registry.wasm --network testnet --source deployerNote the Engineer Registry ID (ER_ID).
stellar contract deploy --wasm target/wasm32-unknown-unknown/release/lifecycle.wasm --network testnet --source deployerNote the Lifecycle Contract ID (LC_ID).
stellar contract deploy --wasm target/wasm32-unknown-unknown/release/lending.wasm --network testnet --source deployerNote the Lending Contract ID (LN_ID).
Security: deployer-only initialization Each
initialize_admin/initializecall now requires thedeployerargument to sign the transaction. The--source deployerflag on the Stellar CLI satisfies this requirement. Complete all four initialization steps in the same block as deployment (or immediately after) to eliminate the window in which an observer could front-run initialization with their own address.
stellar contract invoke --id AR_ID --network testnet --source deployer -- initialize_admin \
--deployer <DEPLOYER_ADDRESS> \
--admin <ADMIN_ADDRESS>stellar contract invoke --id ER_ID --network testnet --source deployer -- initialize_admin \
--deployer <DEPLOYER_ADDRESS> \
--admin <ADMIN_ADDRESS>Connect Lifecycle to AR and ER:
stellar contract invoke --id LC_ID --network testnet --source deployer -- initialize \
--deployer <DEPLOYER_ADDRESS> \
--asset_registry AR_ID \
--engineer_registry ER_ID \
--admin <ADMIN_ADDRESS> \
--max_history 200stellar contract invoke --id LN_ID --network testnet --source deployer -- initialize \
--deployer <DEPLOYER_ADDRESS> \
--admin <ADMIN_ADDRESS> \
--token <TOKEN_ADDRESS> \
--yield_bps 500 \
--slash_bps 1000Once initialized, verify the contract state and availability.
Confirm the registry is responsive and the admin is correctly set:
stellar contract invoke --id AR_ID --network testnet --source any -- get_adminConfirm the registry is responsive and the admin is correctly set:
stellar contract invoke --id ER_ID --network testnet --source any -- get_adminConfirm that Lifecycle can reach the Asset Registry (this triggers a cross-contract call):
# Attempt to get a non-existent asset; should return a contract error (not a panic)
stellar contract invoke --id LC_ID --network testnet --source any -- get_collateral_score --asset_id 999stellar contract invoke --id LN_ID --network testnet --source any -- get_configMainstay contracts are critical for asset financing. Active monitoring is recommended.
Subscribe to contract events to track lifecycle transitions:
REG_AST: Asset registration.MAINT: Maintenance record submissions.DECAY: Score decay updates.DEPRECATED: Asset deprecation.XFER: Asset transfer sentinel in Lifecycle.
The project relies on persistent storage for all metadata and histories.
Verify that the instance storage for all four contracts is extended past 30 days:
stellar contract storage extend --id LC_ID --network testnet --durability instance --ledgers-to-extend 518400If a contract remains inactive for long periods (near 30 days), persistent entries must be manually extended using the stellar contract storage extend command to prevent data loss.
Refer to docs/ttl-strategy.md for a full mapping of storage keys.
| Aspect | Testnet | Mainnet |
|---|---|---|
--network flag |
testnet |
mainnet |
| RPC URL | https://soroban-testnet.stellar.org |
https://soroban-mainnet.stellar.org (or your own node) |
| Lumens required | Funded via Friendbot (stellar keys fund) |
Real XLM; obtain before deployment |
| Key management | Generated key (stellar keys generate) |
Hardware wallet or multisig key ceremony |
| Deployment script | ./scripts/deploy_testnet.sh |
No equivalent script; use the manual steps in this runbook with --network mainnet |
⚠️ Mainnet deployment is gated by a completed formal security audit. Do NOT skip this step.
The Mainstay system manages real industrial asset collateral that integrates with DeFi lending protocols. A vulnerability in any contract could result in:
- Loss or corruption of maintenance records
- Manipulation of collateral scores
- Unauthorized loan issuance
- Permanent data loss due to TTL expiry
- Compromise of the admin role
Gate checklist — all items must be checked before proceeding to §6.3:
- Audit firm selected and engaged (see §0.1 and
docs/audit-report.mdfor recommendations) - Audit kickoff completed with full documentation bundle
- All Critical-severity findings resolved and verified by the auditor
- All High-severity findings resolved and verified by the auditor
- All Medium-severity findings resolved or documented with deferral justification
- Auditor sign-off letter received and filed
- Final audit report published to
docs/audit-report.md - Regression tests for all resolved findings merged to
main -
cargo test --workspacepasses with zero failures -
cargo clippy --workspace --all-targets -- -D warningspasses with zero warnings -
cargo auditpasses with zero high-severity advisories -
.gitleaks.tomlscan passes with zero findings - Threat model reviewed and updated (
docs/threat-model.md) - Admin multisig ceremony completed (for mainnet admin key)
- Deployer cold wallet secured
- Emergency response plan documented and distributed
Replace every --network testnet flag with --network mainnet. Do not use ./scripts/deploy_testnet.sh — that script hard-rejects non-testnet networks.
# 1. Build (same as testnet)
./scripts/build.sh
# 2. Deploy Asset Registry
stellar contract deploy \
--wasm target/wasm32-unknown-unknown/release/asset_registry.wasm \
--network mainnet \
--source deployer
# Save as AR_ID
# 3. Deploy Engineer Registry
stellar contract deploy \
--wasm target/wasm32-unknown-unknown/release/engineer_registry.wasm \
--network mainnet \
--source deployer
# Save as ER_ID
# 4. Deploy Lifecycle (must come after AR and ER)
stellar contract deploy \
--wasm target/wasm32-unknown-unknown/release/lifecycle.wasm \
--network mainnet \
--source deployer
# Save as LC_ID
# 5. Deploy Lending
stellar contract deploy \
--wasm target/wasm32-unknown-unknown/release/lending.wasm \
--network mainnet \
--source deployer
# Save as LN_IDDeployment order is mandatory: Lifecycle initialize requires both registry contract IDs, so asset-registry and engineer-registry must be deployed and their IDs noted before lifecycle is deployed. Lending can be deployed independently.
Initialize all contracts in the same transaction block as deployment to eliminate front-run risk on initialization.
# Initialize Asset Registry
stellar contract invoke --id AR_ID --network mainnet --source deployer -- initialize_admin \
--deployer <DEPLOYER_ADDRESS> \
--admin <MULTISIG_ADMIN_ADDRESS>
# Initialize Engineer Registry
stellar contract invoke --id ER_ID --network mainnet --source deployer -- initialize_admin \
--deployer <DEPLOYER_ADDRESS> \
--admin <MULTISIG_ADMIN_ADDRESS>
# Initialize Lifecycle (bind to registries)
stellar contract invoke --id LC_ID --network mainnet --source deployer -- initialize \
--deployer <DEPLOYER_ADDRESS> \
--asset_registry AR_ID \
--engineer_registry ER_ID \
--admin <MULTISIG_ADMIN_ADDRESS> \
--max_history 200
# Initialize Lending
stellar contract invoke --id LN_ID --network mainnet --source deployer -- initialize \
--deployer <DEPLOYER_ADDRESS> \
--admin <MULTISIG_ADMIN_ADDRESS> \
--token <TOKEN_ADDRESS> \
--yield_bps 500 \
--slash_bps 1000Run these checks immediately after initialization. Do not hand off to operations until every item is confirmed.
Registry checks:
-
stellar contract invoke --id AR_ID --network mainnet --source any -- get_adminreturns the expected multisig admin address. -
stellar contract invoke --id ER_ID --network mainnet --source any -- get_adminreturns the expected multisig admin address.
Cross-contract binding check:
-
stellar contract invoke --id LC_ID --network mainnet --source any -- get_collateral_score --asset_id 999returns a contract error (AssetNotFound), not a panic orNotInitializederror. ANotInitializederror means the binding was not saved correctly.
Config check:
-
stellar contract invoke --id LC_ID --network mainnet --source any -- get_configreturnsmax_history: 200and the expected admin address.
Lending contract check:
-
stellar contract invoke --id LN_ID --network mainnet --source any -- get_configreturns the expected configuration.
TTL extension:
- Extend instance storage for all four contracts immediately after initialization:
for ID in AR_ID ER_ID LC_ID LN_ID; do stellar contract storage extend --id $ID --network mainnet --durability instance --ledgers-to-extend 518400 done
Smoke test (required):
- Register one asset type and one test asset via AR_ID.
- Register one engineer via ER_ID.
- Authorize engineer for test asset.
- Submit one maintenance record via LC_ID and confirm
get_collateral_scorereturns a non-zero value. - Remove/deregister the test data if the contract supports it, or note the test asset IDs for auditing.
- Verify lending contract functions (request, check status) with test data.
- Verify pause/unpause works correctly on all four contracts.
- Verify admin timelock operations (propose + wait + execute).
On testnet, generated keys (stellar keys generate) are acceptable. On mainnet:
- Use a hardware wallet (Ledger) or a dedicated signing key stored in a secrets manager (e.g., HashiCorp Vault).
- The
deployeridentity should be a cold wallet used exclusively for deployment; transfer admin rights to a multisig account before handing off to operations. - Store AR_ID, ER_ID, LC_ID, and LN_ID in a configuration management system (e.g., environment-specific
.env.mainnet) immediately after deployment — these IDs cannot be recovered once lost without re-deployment. - Document the multisig threshold and signer set in the emergency response plan.
- Emergency contacts documented and accessible to all multisig signers
- Pause procedure documented and tested on testnet
- Key rotation procedure documented
- Incident response runbook created (separate document)
- Monitoring and alerting configured for all four contracts