This guide covers deploying QuorumProof to Stellar mainnet. Follow every section in order. Do not skip the security checklist.
- Rust 1.70+ with
wasm32-unknown-unknowntarget:rustup target add wasm32-unknown-unknown - Stellar CLI:
cargo install --locked stellar-cli - A funded mainnet Stellar account for the deployer key
- A separate funded mainnet Stellar account for the admin key (must be different from deployer)
cp .env.example .envEdit .env for mainnet:
STELLAR_NETWORK=mainnet
STELLAR_RPC_URL=https://mainnet.sorobanrpc.com
# Populated after deployment (step 3)
CONTRACT_QUORUM_PROOF=
CONTRACT_SBT_REGISTRY=
CONTRACT_ZK_VERIFIER=
# Frontend
VITE_STELLAR_NETWORK=mainnet
VITE_STELLAR_RPC_URL=https://mainnet.sorobanrpc.com
VITE_CONTRACT_QUORUM_PROOF=
VITE_CONTRACT_SBT_REGISTRY=
VITE_CONTRACT_ZK_VERIFIER=Import your funded mainnet deployer key:
stellar keys add deployer --secret-key
# Enter your secret key (S...) when promptedVerify the account is funded:
stellar account show deployer --network mainnetBuild all three contracts from a clean state:
./scripts/build.shVerify the WASM artifacts exist:
ls -lh target/wasm32-unknown-unknown/release/*.wasmExpected output: quorum_proof.wasm, sbt_registry.wasm, zk_verifier.wasm.
Run the full test suite before deploying:
cargo testAll tests must pass. Do not deploy with failing tests.
Deploy each contract in order. The deployer key pays the deployment fees.
# Deploy quorum_proof
CONTRACT_QUORUM_PROOF=$(stellar contract deploy \
--wasm target/wasm32-unknown-unknown/release/quorum_proof.wasm \
--source deployer \
--network mainnet)
echo "CONTRACT_QUORUM_PROOF=$CONTRACT_QUORUM_PROOF"
# Deploy sbt_registry
CONTRACT_SBT_REGISTRY=$(stellar contract deploy \
--wasm target/wasm32-unknown-unknown/release/sbt_registry.wasm \
--source deployer \
--network mainnet)
echo "CONTRACT_SBT_REGISTRY=$CONTRACT_SBT_REGISTRY"
# Deploy zk_verifier
CONTRACT_ZK_VERIFIER=$(stellar contract deploy \
--wasm target/wasm32-unknown-unknown/release/zk_verifier.wasm \
--source deployer \
--network mainnet)
echo "CONTRACT_ZK_VERIFIER=$CONTRACT_ZK_VERIFIER"Record all three contract IDs immediately. Update .env with the values.
Initialize each contract with the admin address. Use a dedicated admin account — not the deployer.
ADMIN_ADDRESS=<your-admin-stellar-address>
stellar contract invoke \
--id $CONTRACT_QUORUM_PROOF \
--source deployer \
--network mainnet \
-- initialize \
--admin $ADMIN_ADDRESS
stellar contract invoke \
--id $CONTRACT_SBT_REGISTRY \
--source deployer \
--network mainnet \
-- initialize \
--admin $ADMIN_ADDRESS \
--quorum-proof-id $CONTRACT_QUORUM_PROOF
stellar contract invoke \
--id $CONTRACT_ZK_VERIFIER \
--source deployer \
--network mainnet \
-- initialize \
--admin $ADMIN_ADDRESSThe admin account must be a multisig before any admin operations are performed. A single-key admin is a critical security risk.
Set up 2-of-3 multisig on the admin account:
stellar account set-options \
--source admin \
--network mainnet \
--master-weight 1 \
--med-threshold 2 \
--high-threshold 2 \
--signer <signer2-public-key>,1 \
--signer <signer3-public-key>,1Verify the multisig configuration:
stellar account show $ADMIN_ADDRESS --network mainnetConfirm med_threshold and high_threshold are both 2 and three signers are listed.
# Confirm admin is set correctly on each contract
stellar contract invoke \
--id $CONTRACT_QUORUM_PROOF \
--network mainnet \
-- get_adminComplete every item before going live. This checklist is a minimum — also review docs/security-audit-checklist.md.
- Deployer key and admin key are different accounts
- Admin account is configured as multisig (2-of-3 minimum)
- All signing keys are stored in hardware wallets (Ledger or equivalent)
- No secret keys are stored in
.env, version control, CI logs, or shell history - A secondary admin key is registered on-chain as a backup (see
docs/disaster-recovery.md§1.1) - Key rotation schedule is documented (recommended: every 90 days)
- All three contracts are initialized (admin set, cross-contract addresses linked)
-
verify_claim(ZK stub) is confirmed admin-gated — do not expose to public callers - Contract IDs are recorded in a team password manager and
.env.examplecomments - Deployment transaction hashes are recorded for audit trail
- RPC endpoint is authenticated (use a private RPC node or API key, not the public endpoint)
- Frontend/API layer enforces rate limiting on contract invocations
- Admin functions are not exposed via any public API endpoint
- Contract event monitoring is configured (Stellar Horizon event stream or equivalent)
- Alerts are set for:
pauseevents,revoke_credentialevents, unusual dispute volume - Logs are retained for a minimum of 2 years
- Team is aware that
verify_claimis a non-functional stub (see README warning) - No production credential decision relies on
verify_claimoutput -
verify_claimaccess is restricted to admin only
If a contract upgrade introduces a regression, roll back by re-deploying the previous WASM:
# Install the previous WASM and get its hash
stellar contract install \
--wasm <path-to-previous.wasm> \
--source deployer \
--network mainnet
# Note the returned WASM hash
# Invoke upgrade with the previous hash (requires admin multisig)
stellar contract invoke \
--id $CONTRACT_QUORUM_PROOF \
--source admin \
--network mainnet \
-- upgrade \
--admin $ADMIN_ADDRESS \
--new-wasm-hash <PREVIOUS_WASM_HASH>See docs/contract-upgrade-strategy.md for the full upgrade and rollback procedure.
If a critical vulnerability is discovered, pause the contract immediately:
stellar contract invoke \
--id $CONTRACT_QUORUM_PROOF \
--source admin \
--network mainnet \
-- pause \
--admin $ADMIN_ADDRESSPausing blocks issue_credential, attest, and revoke_credential. Read-only functions remain accessible. Unpause only after the vulnerability is resolved and a patched contract is deployed.
If contracts must be redeployed from scratch (e.g., key compromise with no backup):
- Deploy new contracts following Section 3
- Re-initialize with a new admin key (Section 4)
- Coordinate with all issuers to re-issue credentials — on-chain state from the old contracts is not migrated automatically
- Update all
.envfiles, frontend configs, and notify credential holders
See docs/disaster-recovery.md for the full recovery procedure.
Error: account not found
The deployer or admin account is not funded on mainnet. Fund the account with at least 10 XLM before deploying.
Error: contract already initialized
initialize was called twice. The contract is already set up — verify the admin address with get_admin and proceed.
Error: insufficient funds for fee
Increase the deployer account balance. Contract deployment on mainnet requires ~1–2 XLM per contract.
Error: invalid wasm
The WASM artifact is missing or corrupt. Re-run ./scripts/build.sh and verify the files exist before deploying.
Contract invocation returns HostError: unauthorized
The caller is not the admin or the admin multisig threshold was not met. Ensure all required signers have signed the transaction.
RPC timeout or connection refused
The public RPC endpoint may be rate-limiting. Switch to a private RPC node or add an API key to STELLAR_RPC_URL.
After deployment, run these checks to confirm everything is working:
# Issue a test credential (requires an issuer account)
stellar contract invoke \
--id $CONTRACT_QUORUM_PROOF \
--source issuer \
--network mainnet \
-- issue_credential \
--subject <subject-address> \
--credential-type 1 \
--metadata-hash <32-byte-hash>
# Verify the credential was stored
stellar contract invoke \
--id $CONTRACT_QUORUM_PROOF \
--network mainnet \
-- get_credential \
--credential-id 1If both calls succeed, the deployment is functional.