Contract ID (Testnet): CB7PSJZALNWNX7NLOAM6LOEL4OJZMFPQZJMIYO522ZSACYWXTZIDEDSS
Network: Stellar Testnet — replace --network testnet with --network mainnet for production
Last updated: 2026-04-26
Classification: CONFIDENTIAL — DAO Core Team Only
- Roles and Responsibilities
- Pre-Incident Checklist
- Scenario A — Active Exploit / Hack in Progress
- Scenario B — Protocol Pause (Planned or Precautionary)
- Scenario C — Wasm Hash Upgrade
- Scenario D — Migrating Trapped State
- Scenario E — Multi-Sig Withdrawal Freeze
- Scenario F — Legal Freeze
- Scenario G — Gas Buffer Exhaustion
- Scenario H — Admin Key Compromise
- Scenario I — Oracle Failure
- Scenario J — Velocity Limit Breach / Flash Drain
- Post-Incident Procedures
- Multi-Sig Signer Reference Card
- Contact Tree
| Role | On-chain Key / Storage | Duty |
|---|---|---|
| DAO Admin | DataKey::CurrentAdmin |
Propose/finalize Wasm upgrades, set compliance officer, grant provider verification, set velocity limits |
| Compliance Officer | DataKey::ComplianceOfficer |
Trigger and release legal freezes |
| Finance Wallet (×3–5) | MultiSigConfig.finance_wallets |
Propose, approve, revoke, and cancel large withdrawal requests; quorum = required_signatures |
| Oracle / Resolver | DataKey::Oracle |
Resolve service challenges (resolve_challenge) |
| Provider | Per-meter provider field |
Pause/shutdown individual meters, initiate firmware updates, manage gas buffer |
| Compliance Council | Off-chain multi-sig (≥2) | Release legal freezes |
Any action requiring required_signatures approvals must be coordinated off-chain first (Signal group, emergency Telegram, or PagerDuty). Confirm quorum is available before submitting the first on-chain transaction. The contract enforces the threshold — a request with insufficient approvals will revert on execution.
DataKey::CurrentAdmin → DAO Admin address
DataKey::ComplianceOfficer → Compliance Officer address
DataKey::Oracle → Oracle/Resolver address
DataKey::MultiSigConfig(addr) → Per-provider multi-sig config
DataKey::VetoDeadline → Active upgrade veto deadline (Unix timestamp)
DataKey::ProposedUpgrade → Active UpgradeProposal struct
Run every check before executing any emergency command. Do not skip steps.
# 1. Confirm Stellar CLI is installed and on PATH
stellar --version
# 2. Confirm you are targeting the correct network
stellar network ls
# 3. Export the contract address
export CONTRACT=CB7PSJZALNWNX7NLOAM6LOEL4OJZMFPQZJMIYO522ZSACYWXTZIDEDSS
# 4. Export signing identities for your role
export ADMIN_KEY=<admin-secret-key-or-identity-alias>
export PROVIDER_KEY=<provider-secret-key-or-identity-alias>
export FINANCE_KEY=<finance-wallet-secret-key-or-identity-alias>
# 5. Verify the contract is responsive
stellar contract invoke \
--id $CONTRACT \
--network testnet \
-- \
get_count
# 6. Check the current meter count and note it
export METER_COUNT=$(stellar contract invoke \
--id $CONTRACT \
--network testnet \
-- \
get_count)
echo "Total meters: $METER_COUNT"
# 7. Verify your key matches the expected admin address
stellar keys address $ADMIN_KEY
# Compare output against the address stored in DataKey::CurrentAdmin
# 8. Check block explorer for any anomalous recent transactions
# https://stellar.expert/explorer/testnet/contract/$CONTRACTIf the contract is unresponsive: The Stellar network may be congested or the contract TTL may have expired. Check https://status.stellar.org and the block explorer before proceeding.
Trigger: Anomalous withdrawals detected, funds draining faster than expected, or a known vulnerability is being actively exploited.
Goal: Stop all outflows immediately and preserve remaining funds.
Time budget: Act within 5 minutes of detection. Every ledger (~5 seconds) is a potential loss.
challenge_service sets is_disputed = true and is_paused = true, blocking all claim and deduct_units calls immediately.
# Run once per affected meter. Replace METER_ID with each affected ID.
stellar contract invoke \
--id $CONTRACT \
--network testnet \
--source $PROVIDER_KEY \
-- \
challenge_service \
--meter_id <METER_ID>To pause all meters in a loop:
for i in $(seq 1 $METER_COUNT); do
stellar contract invoke \
--id $CONTRACT \
--network testnet \
--source $PROVIDER_KEY \
-- \
challenge_service \
--meter_id $i
echo "Challenged meter $i"
doneIf challenge_service is insufficient (e.g., the exploit bypasses the dispute flag), use the unconditional hard stop:
stellar contract invoke \
--id $CONTRACT \
--network testnet \
--source $PROVIDER_KEY \
-- \
emergency_shutdown \
--meter_id <METER_ID>emergency_shutdown sets is_active = false regardless of balance or dispute state.
# Pause each stream by setting flow rate to 0
stellar contract invoke \
--id $CONTRACT \
--network testnet \
--source $PROVIDER_KEY \
-- \
pause_continuous_flow \
--stream_id <STREAM_ID>If an attacker obtained an admin override to bypass velocity limits:
# Revoke global override (meter_id = 0)
stellar contract invoke \
--id $CONTRACT \
--network testnet \
--source $ADMIN_KEY \
-- \
revoke_velocity_override \
--admin <ADMIN_ADDRESS> \
--meter_id 0
# Revoke per-meter override
stellar contract invoke \
--id $CONTRACT \
--network testnet \
--source $ADMIN_KEY \
-- \
revoke_velocity_override \
--admin <ADMIN_ADDRESS> \
--meter_id <METER_ID>Cap all outflows system-wide while the incident is investigated:
stellar contract invoke \
--id $CONTRACT \
--network testnet \
--source $ADMIN_KEY \
-- \
set_velocity_limit_config \
--admin <ADMIN_ADDRESS> \
--global_limit 1000000 \
--per_stream_limit 100000 \
--is_enabled trueAdjust global_limit and per_stream_limit (in stroops) to the minimum needed for legitimate operations.
# Get the total request count first
stellar contract invoke \
--id $CONTRACT \
--network testnet \
-- \
get_withdrawal_request_count \
--provider <PROVIDER_ADDRESS>
# Cancel each pending request
stellar contract invoke \
--id $CONTRACT \
--network testnet \
--source $PROVIDER_KEY \
-- \
cancel_multisig_withdrawal \
--provider <PROVIDER_ADDRESS> \
--request_id <REQUEST_ID>See Section 13.
Trigger: Scheduled maintenance, oracle outage, or precautionary halt before a known vulnerability is patched.
stellar contract invoke \
--id $CONTRACT \
--network testnet \
--source $PROVIDER_KEY \
-- \
set_meter_pause \
--meter_id <METER_ID> \
--paused truestellar contract invoke \
--id $CONTRACT \
--network testnet \
--source $PROVIDER_KEY \
-- \
pause_continuous_flow \
--stream_id <STREAM_ID>stellar contract invoke \
--id $CONTRACT \
--network testnet \
--source $ADMIN_KEY \
-- \
set_velocity_limit_config \
--admin <ADMIN_ADDRESS> \
--global_limit 1000000 \
--per_stream_limit 100000 \
--is_enabled true# Resume a meter
stellar contract invoke \
--id $CONTRACT \
--network testnet \
--source $PROVIDER_KEY \
-- \
set_meter_pause \
--meter_id <METER_ID> \
--paused false
# Resume a continuous flow stream with the original flow rate
stellar contract invoke \
--id $CONTRACT \
--network testnet \
--source $PROVIDER_KEY \
-- \
resume_continuous_flow \
--stream_id <STREAM_ID> \
--flow_rate_per_second <ORIGINAL_RATE>
# Disable velocity limiting once normal operations resume
stellar contract invoke \
--id $CONTRACT \
--network testnet \
--source $ADMIN_KEY \
-- \
set_velocity_limit_config \
--admin <ADMIN_ADDRESS> \
--global_limit 1000000000 \
--per_stream_limit 100000000 \
--is_enabled falseTrigger: A critical bug is patched and a new Wasm binary is ready to deploy.
Timelock: The contract enforces a veto window (UPGRADE_VETO_PERIOD_SECONDS). Users may veto during this window. The upgrade only finalizes if the veto count stays below the threshold (VETO_THRESHOLD_BPS). There is no on-chain bypass of the timelock — it is a safety feature.
# Build the contract (from repo root)
cd contracts/utility_contracts
cargo build --target wasm32-unknown-unknown --release
# Verify the binary size is reasonable (Soroban limit is 64 KB)
ls -lh target/wasm32-unknown-unknown/release/utility_contracts.wasm
# Upload the Wasm to the network — this registers the binary but does NOT deploy it
stellar contract upload \
--network testnet \
--source $ADMIN_KEY \
--wasm target/wasm32-unknown-unknown/release/utility_contracts.wasm
# The command prints a 32-byte hex Wasm hash. Save it immediately.
export NEW_WASM_HASH=<printed-hash>
echo "New Wasm hash: $NEW_WASM_HASH"Verify the hash independently. Every signer should compute
sha256of the Wasm file locally and compare it toNEW_WASM_HASHbefore approving the proposal.sha256sum target/wasm32-unknown-unknown/release/utility_contracts.wasm
stellar contract invoke \
--id $CONTRACT \
--network testnet \
--source $ADMIN_KEY \
-- \
propose_upgrade \
--new_wasm_hash $NEW_WASM_HASHThe contract emits an UpgrdPrp event and stores the proposal at DataKey::ProposedUpgrade. The veto window starts immediately. Announce the proposal to the DAO governance forum and all stakeholders now.
Post the following information to the DAO forum:
- New Wasm hash (
NEW_WASM_HASH) - SHA-256 of the Wasm file (for independent verification)
- Link to the audited diff / changelog
- Veto deadline (read from
DataKey::VetoDeadline) - Instructions for users who wish to veto (see below)
# Read the veto deadline from contract storage (via block explorer or CLI)
# DataKey::VetoDeadline stores the Unix timestamp of the deadline.
# If veto count exceeds VETO_THRESHOLD_BPS of total meters, the upgrade is blocked.
# Check the block explorer for VetoSubmt events:
# https://stellar.expert/explorer/testnet/contract/$CONTRACTDo NOT call finalize_upgrade before the deadline expires.
Only after the veto window has passed and the veto count is below the threshold:
stellar contract invoke \
--id $CONTRACT \
--network testnet \
--source $ADMIN_KEY \
-- \
finalize_upgradeThe contract emits UpgrdFin, clears the proposal, and the contract now runs the new Wasm.
# Confirm the contract is responsive under the new Wasm
stellar contract invoke \
--id $CONTRACT \
--network testnet \
-- \
get_count
# Check a known meter to confirm state was preserved
stellar contract invoke \
--id $CONTRACT \
--network testnet \
-- \
get_meter \
--meter_id 1If the veto window is too long for a zero-day patch:
- The DAO must vote off-chain (governance forum + Signal) to accept the risk.
- Document the decision with a timestamped record before calling
finalize_upgrade. - There is no on-chain bypass — the timelock must expire naturally.
- If the window is truly unacceptable, consider pausing all meters (Scenario B) while waiting for the window to expire.
If the new Wasm introduces a regression:
- Build and upload the previous known-good Wasm binary.
- Repeat Steps 1–5 with the rollback hash.
- The same veto window applies to rollbacks.
Trigger: A bug causes state to become inaccessible or corrupted, and a migration contract is needed to rescue funds or re-initialize storage.
Warning: State migration is the highest-risk operation in this runbook. Require DAO approval and an independent audit of the migration contract before proceeding.
Soroban contracts cannot iterate all storage keys natively. Migration must be performed key-by-key using known meter IDs and stream IDs obtained from the Count storage key.
# Pause every meter
for i in $(seq 1 $METER_COUNT); do
stellar contract invoke \
--id $CONTRACT \
--network testnet \
--source $PROVIDER_KEY \
-- \
set_meter_pause \
--meter_id $i \
--paused true
echo "Paused meter $i"
done# Get the total meter count
export METER_COUNT=$(stellar contract invoke \
--id $CONTRACT \
--network testnet \
-- \
get_count)
# Dump each meter to a JSON file
mkdir -p migration_state
for i in $(seq 1 $METER_COUNT); do
stellar contract invoke \
--id $CONTRACT \
--network testnet \
-- \
get_meter \
--meter_id $i > migration_state/meter_$i.json
echo "Dumped meter $i"
done# Stream IDs share the same counter as meters (DataKey::Count)
for i in $(seq 1 $METER_COUNT); do
stellar contract invoke \
--id $CONTRACT \
--network testnet \
-- \
get_continuous_flow \
--stream_id $i > migration_state/stream_$i.json 2>/dev/null || true
done# Collect unique provider addresses from the meter dumps, then:
stellar contract invoke \
--id $CONTRACT \
--network testnet \
-- \
get_gas_buffer \
--provider <PROVIDER_ADDRESS> > migration_state/gas_buffer_<PROVIDER_ADDRESS>.jsonThe migration contract must be:
- Pre-audited by an independent security firm
- Approved by DAO governance vote
- Able to accept the old contract address and re-register state on the new contract
# Deploy the migration contract
stellar contract deploy \
--network testnet \
--source $ADMIN_KEY \
--wasm migration_contract.wasm
export MIGRATION_CONTRACT=<deployed-migration-contract-id>
# Initialize the migration
stellar contract invoke \
--id $MIGRATION_CONTRACT \
--network testnet \
--source $ADMIN_KEY \
-- \
initialize \
--old_contract $CONTRACT \
--new_contract <NEW_CONTRACT_ADDRESS>for i in $(seq 1 $METER_COUNT); do
stellar contract invoke \
--id $MIGRATION_CONTRACT \
--network testnet \
--source $ADMIN_KEY \
-- \
migrate_meter \
--meter_id $i
echo "Migrated meter $i"
doneFor each meter, compare the balance and key fields between the state dump and the new contract:
for i in $(seq 1 $METER_COUNT); do
stellar contract invoke \
--id <NEW_CONTRACT_ADDRESS> \
--network testnet \
-- \
get_meter \
--meter_id $i > migration_state/new_meter_$i.json
# Diff the old and new state (balance, user, provider must match)
diff <(jq '{balance,user,provider}' migration_state/meter_$i.json) \
<(jq '{balance,user,provider}' migration_state/new_meter_$i.json)
done
echo "Verification complete"Do not decommission the old contract until all diffs are clean.
Token balances held by the old contract must be transferred to the new contract. This requires a separate token transfer transaction authorized by the old contract's admin:
# Transfer the full token balance from old contract to new contract
stellar contract invoke \
--id <TOKEN_CONTRACT_ADDRESS> \
--network testnet \
--source $ADMIN_KEY \
-- \
transfer \
--from $CONTRACT \
--to <NEW_CONTRACT_ADDRESS> \
--amount <TOTAL_BALANCE>Trigger: A suspicious large withdrawal request is detected, a finance wallet is compromised, or a request was submitted with incorrect parameters.
propose_multisig_withdrawal → approve_multisig_withdrawal (×N) → execute_multisig_withdrawal
↕
revoke_multisig_approval (undo one approval)
↕
cancel_multisig_withdrawal (cancel entire request)
A request expires after WITHDRAWAL_REQUEST_EXPIRY seconds. Expired requests cannot be executed.
# Get total request count for a provider
stellar contract invoke \
--id $CONTRACT \
--network testnet \
-- \
get_withdrawal_request_count \
--provider <PROVIDER_ADDRESS>
# Inspect a specific request via block explorer events (MSigProp, MSigAppr)
# https://stellar.expert/explorer/testnet/contract/$CONTRACTstellar contract invoke \
--id $CONTRACT \
--network testnet \
--source $PROVIDER_KEY \
-- \
cancel_multisig_withdrawal \
--provider <PROVIDER_ADDRESS> \
--request_id <REQUEST_ID>If a finance wallet was compromised and already approved a request:
stellar contract invoke \
--id $CONTRACT \
--network testnet \
--source <COMPROMISED_FINANCE_KEY> \
-- \
revoke_multisig_approval \
--provider <PROVIDER_ADDRESS> \
--request_id <REQUEST_ID>After revoking, the approval count drops below the threshold and the request cannot be executed until re-approved.
# Step 1: Disable the current multi-sig config
stellar contract invoke \
--id $CONTRACT \
--network testnet \
--source $PROVIDER_KEY \
-- \
disable_multisig \
--provider <PROVIDER_ADDRESS>
# Step 2: Re-configure with new wallet set (replace compromised wallet)
stellar contract invoke \
--id $CONTRACT \
--network testnet \
--source $PROVIDER_KEY \
-- \
configure_multisig_withdrawal \
--provider <PROVIDER_ADDRESS> \
--finance_wallets '["<WALLET_1>","<WALLET_2>","<WALLET_3>","<WALLET_4>","<WALLET_5>"]' \
--required_signatures 3 \
--threshold_amount 100000Note:
configure_multisig_withdrawalwill revert if a config already exists andis_active = true. You must calldisable_multisigfirst.
See Section 14 — Multi-Sig Signer Reference Card for the complete step-by-step guide for finance wallet holders.
Trigger: Regulatory order, court injunction, AML/KYC flag, or law enforcement request.
stellar contract invoke \
--id $CONTRACT \
--network testnet \
--source <COMPLIANCE_OFFICER_KEY> \
-- \
legal_freeze \
--meter_id <METER_ID> \
--reason "Regulatory order #<CASE_NUMBER> — <JURISDICTION>"Funds are transferred to the LegalVault address. The meter is paused immediately. The LegalFreeze struct is stored at DataKey::LegalFreeze(meter_id).
stellar contract invoke \
--id $CONTRACT \
--network testnet \
-- \
get_legal_freeze \
--meter_id <METER_ID>Confirm is_released = false and frozen_amount matches expectations.
Both council members must coordinate off-chain before submitting. The transaction requires require_auth from each address in council_signatures.
stellar contract invoke \
--id $CONTRACT \
--network testnet \
--source <COUNCIL_MEMBER_1_KEY> \
-- \
release_legal_freeze \
--meter_id <METER_ID> \
--council_signatures '["<COUNCIL_ADDR_1>","<COUNCIL_ADDR_2>"]'Funds are returned from the LegalVault to the meter's user. The meter is unpaused.
If the compliance officer role needs to be rotated:
stellar contract invoke \
--id $CONTRACT \
--network testnet \
--source $ADMIN_KEY \
-- \
set_compliance_officer \
--officer <NEW_COMPLIANCE_OFFICER_ADDRESS>Trigger: Provider withdrawals are failing due to network congestion and the gas buffer is depleted or below the minimum threshold (100 XLM).
stellar contract invoke \
--id $CONTRACT \
--network testnet \
-- \
get_gas_buffer_balance \
--provider <PROVIDER_ADDRESS>stellar contract invoke \
--id $CONTRACT \
--network testnet \
-- \
get_gas_buffer \
--provider <PROVIDER_ADDRESS>stellar contract invoke \
--id $CONTRACT \
--network testnet \
--source $PROVIDER_KEY \
-- \
top_up_gas_buffer \
--provider <PROVIDER_ADDRESS> \
--token <XLM_TOKEN_ADDRESS> \
--amount 500- Minimum buffer: 100 XLM
- Maximum buffer: 10,000 XLM
- Auto-top-up trigger threshold: 200 XLM
- Recommended top-up during congestion: 500–1,000 XLM
stellar contract invoke \
--id $CONTRACT \
--network testnet \
--source $PROVIDER_KEY \
-- \
initialize_gas_buffer \
--provider <PROVIDER_ADDRESS> \
--token <XLM_TOKEN_ADDRESS> \
--initial_amount 500# Minimum of 100 XLM must remain after withdrawal
stellar contract invoke \
--id $CONTRACT \
--network testnet \
--source $PROVIDER_KEY \
-- \
withdraw_from_gas_buffer \
--provider <PROVIDER_ADDRESS> \
--token <XLM_TOKEN_ADDRESS> \
--amount <AMOUNT_TO_WITHDRAW>Trigger: The DAO Admin private key is suspected or confirmed to be compromised.
Time budget: Initiate the admin transfer immediately. The 48-hour timelock means you have a window — but so does the attacker.
stellar contract invoke \
--id $CONTRACT \
--network testnet \
--source $ADMIN_KEY \
-- \
initiate_admin_transfer \
--proposed_admin <NEW_ADMIN_ADDRESS>The contract stores an AdminTransferProposal with a 48-hour execution window. An AdminXfer event is emitted.
Post to the governance forum immediately with:
- The new admin address
- Reason for the transfer
- Veto instructions (users can call
veto_admin_transferif they object)
stellar contract invoke \
--id $CONTRACT \
--network testnet \
--source $ADMIN_KEY \
-- \
execute_admin_transferThe transfer is blocked if the veto count reaches the threshold (10% of active users). If vetoed, coordinate with the DAO to resolve the dispute before retrying.
After the admin transfer, rotate:
- Compliance Officer (
set_compliance_officer) - Oracle address (
set_oracle) - Any finance wallets that shared infrastructure with the compromised key
If the attacker uses the compromised key to initiate their own admin transfer:
- Mobilize the DAO to call
veto_admin_transferimmediately — 10% of active users vetoing will block the transfer. - Simultaneously, if the attacker has not yet changed the admin, use the legitimate key to cancel by initiating a competing transfer.
- Contact Stellar Foundation Security (see Section 15).
Trigger: The price oracle is returning stale data, returning zero, or is unreachable, causing USD/XLM conversions to fail or produce incorrect billing amounts.
top_uporwithdraw_earningscalls reverting withOracleNotSetorPriceConversionFailed- Billing amounts that are orders of magnitude too high or too low
get_current_ratereturningNone
stellar contract invoke \
--id $CONTRACT \
--network testnet \
-- \
get_current_rateIf this returns None, the oracle address is not set. If it returns stale data, check the last_updated field in the PriceData struct.
stellar contract invoke \
--id $CONTRACT \
--network testnet \
--source $ADMIN_KEY \
-- \
set_oracle \
--oracle_address <NEW_ORACLE_CONTRACT_ADDRESS>If meters were challenged due to incorrect billing from bad oracle data, resolve them after the oracle is fixed:
stellar contract invoke \
--id $CONTRACT \
--network testnet \
--source <ORACLE_KEY> \
-- \
resolve_challenge \
--meter_id <METER_ID> \
--restored trueTrigger: The velocity limit circuit breaker fires, blocking legitimate withdrawals, or a flash drain is detected that is consuming the daily withdrawal allowance.
stellar contract invoke \
--id $CONTRACT \
--network testnet \
-- \
get_velocity_limitsstellar contract invoke \
--id $CONTRACT \
--network testnet \
--source $ADMIN_KEY \
-- \
apply_velocity_override \
--admin <ADMIN_ADDRESS> \
--meter_id <METER_ID> \
--expires_at <UNIX_TIMESTAMP> \
--reason "maintenance"Set meter_id = 0 for a global override. Set expires_at to the minimum time needed — do not leave overrides open indefinitely.
stellar contract invoke \
--id $CONTRACT \
--network testnet \
--source $ADMIN_KEY \
-- \
set_velocity_limit_config \
--admin <ADMIN_ADDRESS> \
--global_limit 100000 \
--per_stream_limit 10000 \
--is_enabled truestellar contract invoke \
--id $CONTRACT \
--network testnet \
--source $ADMIN_KEY \
-- \
revoke_velocity_override \
--admin <ADMIN_ADDRESS> \
--meter_id <METER_ID>Complete these steps for every incident, regardless of severity.
Export all relevant transaction hashes, ledger numbers, and event logs from the block explorer before they age out of the horizon. Save to a timestamped file:
# Example: export events for the contract from the block explorer API
curl "https://horizon-testnet.stellar.org/accounts/$CONTRACT/transactions?limit=200&order=desc" \
> incident_$(date +%Y%m%d_%H%M%S)_transactions.jsonAfter the incident is contained, the Oracle must resolve any meters left in is_disputed = true:
stellar contract invoke \
--id $CONTRACT \
--network testnet \
--source <ORACLE_KEY> \
-- \
resolve_challenge \
--meter_id <METER_ID> \
--restored true # or false if service was not restoredOnce the all-clear is given, unpause each affected meter:
for i in $(seq 1 $METER_COUNT); do
stellar contract invoke \
--id $CONTRACT \
--network testnet \
--source $PROVIDER_KEY \
-- \
set_meter_pause \
--meter_id $i \
--paused false
echo "Resumed meter $i"
donestellar contract invoke \
--id $CONTRACT \
--network testnet \
--source $ADMIN_KEY \
-- \
set_velocity_limit_config \
--admin <ADMIN_ADDRESS> \
--global_limit 1000000000 \
--per_stream_limit 100000000 \
--is_enabled falseThe DAO Admin must publish a post-mortem to the governance forum within 72 hours. Include:
- Incident timeline (UTC timestamps)
- Root cause analysis
- Funds at risk and funds recovered
- Actions taken and by whom
- Remediation steps and timeline
- Changes to this runbook
If any signing key was exposed, initiate an admin transfer with the 48-hour timelock (see Scenario H).
If any procedure was unclear, missing, or failed, update this document and submit a PR before closing the incident ticket.
This section is written for Finance Wallet holders who may not be familiar with the full contract. Print this section and keep it accessible offline.
You are one of 3–5 authorized Finance Department wallet holders for your provider. Large withdrawals (above threshold_amount in USD cents) require required_signatures approvals from this group before they can execute. Your job is to:
- Verify that a withdrawal request is legitimate before approving it.
- Revoke your approval immediately if you suspect fraud.
- Cancel the request if you are the provider and the request is fraudulent.
- You received the request notification through the agreed secure channel (not email alone).
- The
amount_usd_centsmatches the amount discussed off-chain. - The
destinationaddress is the known treasury address — verify character by character. - The
meter_idis a meter you recognize as belonging to your provider. - The
expires_attimestamp gives you enough time to coordinate with other signers. - At least one other signer has independently verified the above.
If any item is unchecked, do not approve. Contact the DAO Admin immediately.
stellar contract invoke \
--id $CONTRACT \
--network testnet \
--source <YOUR_FINANCE_WALLET_KEY> \
-- \
approve_multisig_withdrawal \
--provider <PROVIDER_ADDRESS> \
--request_id <REQUEST_ID>stellar contract invoke \
--id $CONTRACT \
--network testnet \
--source <YOUR_FINANCE_WALLET_KEY> \
-- \
revoke_multisig_approval \
--provider <PROVIDER_ADDRESS> \
--request_id <REQUEST_ID>Revoking drops the approval count. If it falls below required_signatures, the request cannot execute until re-approved.
Only the provider key can cancel. If you are the provider:
stellar contract invoke \
--id $CONTRACT \
--network testnet \
--source $PROVIDER_KEY \
-- \
cancel_multisig_withdrawal \
--provider <PROVIDER_ADDRESS> \
--request_id <REQUEST_ID>Once approval_count >= required_signatures, any party can trigger execution:
stellar contract invoke \
--id $CONTRACT \
--network testnet \
--source <YOUR_FINANCE_WALLET_KEY> \
-- \
execute_multisig_withdrawal \
--provider <PROVIDER_ADDRESS> \
--request_id <REQUEST_ID>| Constant | Value | Meaning |
|---|---|---|
MIN_FINANCE_WALLETS |
3 | Minimum wallets in a multi-sig config |
MAX_FINANCE_WALLETS |
5 | Maximum wallets in a multi-sig config |
WITHDRAWAL_REQUEST_EXPIRY |
See contract | Seconds before a request auto-expires |
threshold_amount |
Configured per provider | USD cents below which multi-sig is not required |
- Immediately call
revoke_multisig_approvalfor any pending requests your key has approved. - Contact the DAO Admin and other finance wallet holders via the emergency Signal group.
- The provider must call
disable_multisigand thenconfigure_multisig_withdrawalwith a replacement wallet. - Do not use the compromised key for any other purpose.
| Priority | Role | Contact Method |
|---|---|---|
| 1 | DAO Admin | Signal / PagerDuty (primary) |
| 2 | Finance Wallet Holders (×3–5) | Signal group |
| 3 | Compliance Officer | Signal + Email |
| 4 | Oracle Operator | PagerDuty |
| 5 | Stellar Foundation Security | security@stellar.org |
Fill in actual names, handles, and contact details before deploying to mainnet. This table is a template.
| Severity | Criteria | Response time | Escalate to |
|---|---|---|---|
| P1 — Critical | Active exploit, funds draining | < 5 minutes | All roles simultaneously |
| P2 — High | Suspected exploit, oracle down, key compromise | < 15 minutes | DAO Admin + Finance Wallets |
| P3 — Medium | Planned pause, upgrade, legal freeze | < 1 hour | DAO Admin |
| P4 — Low | Gas buffer low, velocity limit false positive | < 4 hours | Provider |
This runbook covers the contract as deployed at commit main. Re-validate all commands after any Wasm upgrade. Last reviewed: 2026-04-26.