Covers realistic failure modes during or after a mainnet Soroban contract deployment and the concrete mitigation paths available, separate from the general deployment runbook.
| Role | Authority | Contact |
|---|---|---|
| On-call engineer | Declare incident, invoke circuit breaker (pause) |
PagerDuty / Slack |
| Lead maintainer | Authorise proxy redeploy or timelocked upgrade | GitHub issue /cc |
| Project admin (multisig) | Emergency key rotation, on-chain admin actions | Defined in key-ceremony doc |
Escalation path: On-call → Lead maintainer → Project admin multisig.
A declared incident is tracked via a GitHub issue tagged incident and P0, with a post-mortem required within 5 business days.
What happens: Deploy transaction succeeds but the uploaded WASM is the wrong bytecode (wrong hash, stale build, or a non-production artifact).
Detection:
- WASM hash printed in deploy log differs from the CI-signed hash
soroban contract invoke --id <CID> -- versionreturns unexpected value- On-chain
IMPL_HASHdoes not match the reference hash in the release manifest
Mitigation path:
-
Pause via circuit breaker (#1126)
- Invoke
runtime-guard-wrapper.pause()with the multisig admin key - This halts all guarded executions; state is frozen but readable
- Expected duration: ~2 minutes
- Invoke
-
Redeploy corrected WASM behind the same proxy (contracts/proxy)
- Build the correct WASM, obtain its hash from CI
- Invoke
UupsProxy.upgrade(new_wasm_hash)with the admin key - Verify new
IMPL_HASHmatches the expected release hash
-
Verify & unpause
- Run
health_checkon the redeployed contract - Invoke
runtime-guard-wrapper.unpause() - Confirm normal operation via
get_stats
- Run
If proxy is not available: Deploy a fresh contract with corrected WASM, update all downstream consumers to point at the new contract ID.
What happens: The initialize call (or constructor-equivalent) fails mid-transaction, or succeeds but leaves storage in an inconsistent state. Soroban contract init is atomic, but if the init logic writes to multiple storage keys and one write fails silently (e.g. storage limit), the contract may be in a partially-configured state.
Detection:
health_checkreturnsfalseimmediately after deployget_statsreturns zero invariants checked despite callsget_wrapped_contractreturns an unexpected address or fails
Mitigation path:
-
Freeze interactions — the runtime guard (#1126) should already reject calls if
INITIALISEDis not set. Verify:- Invoke
runtime-guard-wrapper.pause()if not already paused - All external calls return "Contract not initialised" error
- Invoke
-
Diagnose — inspect persistent storage keys via
soroban contract read:- Check
WRAPPED_CONTRACT_ADDRESS,guard_config,CALL_LOG - Compare against expected initial state from the deployment manifest
- Check
-
Redeploy corrected contract — if the proxy pattern is in use:
- Deploy a corrected implementation WASM (with idempotent or fixed
initialize) - Invoke
UupsProxy.upgrade(new_wasm_hash) - Invoke the corrected
initializepath
- Deploy a corrected implementation WASM (with idempotent or fixed
-
If proxy is not in use — the broken contract is irrecoverable. Deploy an entirely new contract, update all registries/downstream consumers, and deprecate the old contract ID via an on-chain announcement.
What happens: Hours or days after a successful deployment, a security vulnerability is reported (internally or via bug bounty) that affects the live contract.
Detection:
- Bug report filed via SECURITY.md disclosure path
- Internal audit or fuzzing run discovers a finding applicable to the deployed WASM
- On-chain events show anomalous guard failures (
guard_failureevents spiking)
Mitigation path:
-
Emergency pause — invoke
runtime-guard-wrapper.pause()immediately- Authority: On-call engineer can invoke pause without further approval
- This freezes all guarded state transitions
-
Assess severity & impact
- Determine if funds/protected state are at immediate risk
- If yes: keep paused, proceed to step 3
- If no: consider timelocked upgrade path (#1127) for a more measured fix
-
Deploy fix via timelock (#1127) — the timelocked upgrade path requires a minimum delay (e.g. 24-48 hours) between proposal and execution:
- Push a patched implementation WASM
- Submit upgrade proposal to the
Timelockcontract - Wait for the delay period (allows watchers to review)
- Execute the upgrade
-
Emergency bypass — if the vulnerability is actively exploited and timelock delay is unacceptably risky:
- Authority required: Lead maintainer + 1 additional admin (2-of-3 multisig)
- Invoke
UupsProxy.upgrade()directly (bypassing timelock) - Document the emergency bypass in an incident report within 24 hours
-
Unpause after fix is verified, resume normal operations
What happens: The contract's storage grows beyond the Soroban per-contract storage limit, or the transaction fee spikes unexpectedly due to mainnet congestion, causing writes to fail mid-operation.
Detection:
soroban contract invokereturns a storage limit error- Deployment log shows fee-estimation warnings
health_checkfails due toHEALTHY_STORAGE_LIMITbeing exceeded
Mitigation path:
- Pause contract to prevent further storage writes
- Analyze storage usage via
soroban contract readandget_stats - Prune stale data if the contract supports data cleanup functions
- Redeploy with increased limits or storage-optimised logic via proxy upgrade
- Unpause after verification
What happens: The admin key (deployer or upgrade authority) is leaked or compromised.
Detection:
- Unexpected
upgradeortransfer_adminevents in the event log - Unauthorised
pause/unpausetransitions - GitHub secret
SOROBAN_MAINNET_SECRET_KEYis found in logs or external sources
Mitigation path:
- Rotate the admin key — generate a new key pair via Soroban key generation
- Transfer admin on all deployed contracts to the new key:
UupsProxy.transfer_admin(new_admin_address)new_admin_addressinvokesUupsProxy.accept_admin()
- Revoke old key — remove the compromised secret from GitHub Secrets
- Audit event history for any unauthorised transactions made with the compromised key
- Incident report with timeline and remediation steps
After any rollback mitigation is applied:
-
health_checkreturnstruefor all affected contracts -
get_statsshows expected invariants-checked count - Guard events emit normally (test with a known-good transaction)
- Deployment manifest is updated with new contract IDs / WASM hashes
- GitHub Secrets are rotated if keys were exposed
- Incident post-mortem is drafted with root cause and preventive measures
- Mainnet Deployment Runbook (#1134) — happy-path deployment
contracts/proxy/— UUPS proxy pattern for WASM upgradescontracts/runtime-guard-wrapper/— on-chain circuit breaker (#1126)contracts/timelock/— timelocked upgrade governance (#1127)- RELEASE_CHECKLIST.md — pre-release verification
- BRANCH_PROTECTION.md — branch and key protection policy