Procedures for responding to security incidents and operational failures on ElcareHub.
For the disclosure process and contacts, see SECURITY.md.
For the threat surface these procedures address, see THREAT_MODEL.md.
- Emergency contract pause
- Admin key rotation
- Indexer recovery from re-org
- Compromised secret rotation
Use when: active exploit is detected on-chain, critical bug found in contract logic, or at the direction of the security contact.
Who can execute: The current admin wallet (the key stored in ADMIN_SECRET).
# 1. Confirm you hold the admin key
stellar keys public-key ELCARE-HUB-admin
# 2. Confirm the admin address matches what the contract has stored
stellar contract invoke \
--id "$MARKETPLACE_CONTRACT_ID" \
--rpc-url "$STELLAR_RPC_URL" \
--network-passphrase "Test SDF Network ; September 2015" \
-- get_admin
# 3. Pause the contract
stellar contract invoke \
--id "$MARKETPLACE_CONTRACT_ID" \
--source "$ADMIN_SECRET" \
--rpc-url "$STELLAR_RPC_URL" \
--network-passphrase "Test SDF Network ; September 2015" \
-- pause
# 4. Verify paused state — all buy/bid/settle calls should now fail
stellar contract invoke \
--id "$MARKETPLACE_CONTRACT_ID" \
--rpc-url "$STELLAR_RPC_URL" \
--network-passphrase "Test SDF Network ; September 2015" \
-- is_pausedExpected result: is_paused returns true. Any user-facing transaction that goes through
the contract will now revert with a Paused error.
Once the issue is resolved and a patched contract version is deployed (or after the investigation concludes with no action required):
stellar contract invoke \
--id "$MARKETPLACE_CONTRACT_ID" \
--source "$ADMIN_SECRET" \
--rpc-url "$STELLAR_RPC_URL" \
--network-passphrase "Test SDF Network ; September 2015" \
-- unpausePost-unpause: monitor the indexer /metrics endpoint for anomalous event rates, and confirm
the frontend shows active listings correctly.
Use when: admin private key is suspected compromised, admin account is lost, or during scheduled key rotation.
The marketplace uses a 2-step admin transfer (propose_admin → accept_admin) to prevent
a single compromised propose from transferring control.
# Step 1 — Current admin proposes the new admin address
stellar contract invoke \
--id "$MARKETPLACE_CONTRACT_ID" \
--source "$CURRENT_ADMIN_SECRET" \
--rpc-url "$STELLAR_RPC_URL" \
--network-passphrase "Test SDF Network ; September 2015" \
-- propose_admin \
--new_admin "$NEW_ADMIN_PUBLIC"
# Step 2 — New admin accepts (signs with the NEW key)
stellar contract invoke \
--id "$MARKETPLACE_CONTRACT_ID" \
--source "$NEW_ADMIN_SECRET" \
--rpc-url "$STELLAR_RPC_URL" \
--network-passphrase "Test SDF Network ; September 2015" \
-- accept_admin
# Step 3 — Confirm the transfer
stellar contract invoke \
--id "$MARKETPLACE_CONTRACT_ID" \
--rpc-url "$STELLAR_RPC_URL" \
--network-passphrase "Test SDF Network ; September 2015" \
-- get_admin
# Should return NEW_ADMIN_PUBLICIf the attacker has not yet called accept_admin, the pending proposal can be cancelled by
calling propose_admin again from the current admin with a safe replacement address before the
attacker can accept. If the current admin key cannot sign (key lost), there is no on-chain
recovery — this is a consequence of the non-custodial design. Coordinate with the Stellar RPC
provider and affected users; consider deploying a new contract.
Use when: the indexer reports a stalled or inconsistent state, /readyz returns stalled,
or the Prometheus gauge sync_last_ledger stops advancing.
# Check current sync state
curl http://localhost:4000/readyz
# Check Prometheus metrics for stall/reorg signals
curl http://localhost:4000/metrics | grep -E 'sync_|reorg_'
# Check indexer logs for re-org or hash-mismatch messages
docker compose logs --tail=200 indexer | grep -i 'reorg\|mismatch\|rollback'The indexer detects ledger hash mismatches and triggers an automatic rollback of the affected rows, then re-indexes from the fork point. This handles shallow re-orgs (< a few ledgers) without intervention.
# 1. Stop the indexer to avoid concurrent writes
docker compose stop indexer
# 2. Identify the last good ledger from Horizon
LAST_GOOD_LEDGER=<ledger number before the fork>
# 3. Roll back database to that point using Prisma migration + manual SQL if needed
# (Delete events and sync state with lastLedger > LAST_GOOD_LEDGER)
psql "$DATABASE_URL" -c "
DELETE FROM MarketplaceEvent WHERE ledger > $LAST_GOOD_LEDGER;
UPDATE SyncState SET lastLedger = $LAST_GOOD_LEDGER, lastHash = '<hash>'
WHERE id = 1;
"
# 4. Run the backfill CLI to replay missed ledgers
cd indexer
npx tsx src/backfill.ts --from $LAST_GOOD_LEDGER --to <current tip>
# 5. Restart the indexer
docker compose start indexer
# 6. Verify /readyz returns ready
curl http://localhost:4000/readyzAfter recovery, spot-check that key tables are consistent with on-chain state by querying contract events directly via the Stellar RPC and comparing against the indexer's REST responses.
- Log in to Pinata → API Keys → revoke the compromised key.
- Generate a new JWT.
- Update the
PINATA_JWTsecret in your deployment environment (Railway, Vercel, Kubernetes secret, etc.). - Redeploy the frontend.
- Verify new uploads succeed via the listing creation flow.
See Admin key rotation for the on-chain transfer procedure.
For the deployer key (used only at deploy time):
- Generate a new keypair:
./scripts/deploy/fund_account.sh - Transfer any remaining XLM from the old account using
stellar payment. - Revoke the old key from any stored secrets / CI variables.
- Log in to Sentry → Settings → Auth Tokens → revoke the compromised token.
- Issue a new DSN / token.
- Update secrets in your CI/CD environment and redeploy.
- Rotate the PostgreSQL password via your database provider.
- Update
DATABASE_URLin all deployment environments. - Restart the indexer:
docker compose restart indexer. - Confirm
/healthand/readyzreturn healthy responses.
- Rotate the Redis password or ACL entry.
- Update
REDIS_URLin all deployment environments. - Restart the indexer.
- Root cause identified and documented
- Affected users notified (if personal data or funds at risk)
- Fix deployed and verified in production
- Secrets rotated where applicable
- Public disclosure prepared (coordinated with reporter, if external)
- Runbook updated with any new learnings