diff --git a/docs/contract-ops-runbook.md b/docs/contract-ops-runbook.md index 6bd52d4..e83fa7d 100644 --- a/docs/contract-ops-runbook.md +++ b/docs/contract-ops-runbook.md @@ -250,3 +250,28 @@ Everything in Section 4–5 is identical on mainnet except: - The cold recovery key actually lives in a safe; the daily signers are you + co-founder. - After rotation, **destroy the initial deploy key** (`shred -u`); it has no power post-rotation but leave nothing lying around. - The enumerable token index is complete **from genesis** — register USDC at deploy time and state enumeration is authoritative forever (no import-by-address needed, unlike the in-place-upgraded testnet contract). + + +--- + +## 5. Fee account trustline verification & event alerting (DoS.15) + +**Context:** +As identified in threat model finding **DoS.15**, `set_fee_account` performs no pre-flight verification, and `deposit_with_fee_at` / `release_with_fee_at` transfer protocol fees directly to `fee_account`. If the fee account lacks an active, authorized trustline for a supported token, user transactions (`create_event`, `add_funds`, and crowdfunding `claim_milestone`) will revert. + +### Pre-flight Verification Runbook +Before executing `register_supported_token` or `set_fee_account`: + +1. Run the trustline verification script against the fee account: + ```bash + ./scripts/admin/verify-fee-trustline.sh + ``` +2. Verify all checks pass: + - Account exists on target network. + - Trustline exists in `.balances[]` for each registered/target token. + - `is_authorized` is `true`. + +### Event Alerting & Monitoring +- Configure monitoring alerts on the indexer / event listener for: + - `FeeAccountUpdated`: Triggers automated check to verify the new fee account holds trustlines for all active supported tokens. + - `TokenRegistered`: Triggers verification that the current fee account holds a trustline for the newly registered token. diff --git a/docs/multisig-preflight.md b/docs/multisig-preflight.md index 3372b7b..aec0fbf 100644 --- a/docs/multisig-preflight.md +++ b/docs/multisig-preflight.md @@ -89,6 +89,20 @@ Before touching mainnet: The same procedure is required quarterly per policy §5.3; this just exercises it under realistic conditions before mainnet. + +--- + +## 4.B Fee account & token trustline pre-flight (DoS.15 mitigation) + +Before calling `register_supported_token` or rotating the fee account with `set_fee_account`: + +- [ ] Confirm the candidate `fee_account` G-address is funded and active on the target network. +- [ ] For every token already supported or proposed for registration, verify that `fee_account` holds an active, authorized trustline: + ```bash + ./scripts/admin/verify-fee-trustline.sh + ``` +- [ ] Verify that no un-trustlined or unauthorized token is registered, preventing contract fee collection reverts in `deposit_with_fee_at` / `release_with_fee_at`. + ## 5. Rotate admin authority Only after every box above is checked: diff --git a/scripts/admin/verify-fee-trustline.sh b/scripts/admin/verify-fee-trustline.sh new file mode 100755 index 0000000..8325281 --- /dev/null +++ b/scripts/admin/verify-fee-trustline.sh @@ -0,0 +1,112 @@ +#!/bin/bash +# +# verify-fee-trustline.sh: verify that the fee account holds active, authorized +# trustlines for all supported/candidate tokens before `register_supported_token` +# or `set_fee_account` is executed. +# +# Threat Model Reference: DoS.15 (Process control for unverified fee trustlines). +# Without a valid trustline, fee collection in `deposit_with_fee_at` / +# `release_with_fee_at` will revert, causing `create_event`, `add_funds`, and +# crowdfunding `claim_milestone` to fail. +# +# Usage: +# ./scripts/admin/verify-fee-trustline.sh [testnet|mainnet] [ASSET_CODE:ISSUER ...] +# +# Example: +# ./scripts/admin/verify-fee-trustline.sh GA... testnet USDC:GB... + +set -euo pipefail + +if ! command -v curl &>/dev/null; then + echo "Error: curl is required." >&2 + exit 1 +fi +if ! command -v jq &>/dev/null; then + echo "Error: jq is required (e.g., brew install jq or apt install jq)." >&2 + exit 1 +fi + +FEE_ACCOUNT=${1:-""} +NETWORK=${2:-"testnet"} +shift 2 || true +ASSETS=("$@") + +if [[ -z "$FEE_ACCOUNT" ]]; then + echo "Usage: $0 [testnet|mainnet] [ASSET_CODE:ISSUER ...]" >&2 + exit 1 +fi + +case "$NETWORK" in + testnet) HORIZON="https://horizon-testnet.stellar.org" ;; + mainnet) HORIZON="https://horizon.stellar.org" ;; + *) echo "Unknown network '$NETWORK'." >&2; exit 1 ;; +esac + +GREEN='\033[0;32m'; RED='\033[0;31m'; YELLOW='\033[1;33m'; NC='\033[0m' + +echo -e "${YELLOW}Verifying fee account ${FEE_ACCOUNT} on ${NETWORK}...${NC}" + +ACCOUNT_JSON=$(curl -sf "$HORIZON/accounts/$FEE_ACCOUNT" || true) +if [[ -z "$ACCOUNT_JSON" ]]; then + echo -e "${RED}Error: Fee account ${FEE_ACCOUNT} not found on ${NETWORK}.${NC}" >&2 + exit 1 +fi + +PASS=0 +FAIL=0 + +echo -e " ${GREEN}✓${NC} Account exists on ${NETWORK}" +PASS=$((PASS + 1)) + +# Inspect active balances on the fee account +BALANCES=$(echo "$ACCOUNT_JSON" | jq -r '.balances') + +if [[ ${#ASSETS[@]} -eq 0 ]]; then + echo -e "\n${YELLOW}Discovered trustlines/balances on fee account:${NC}" + echo "$BALANCES" | jq -r '.[] | " - " + (if .asset_type == "native" then "XLM (native)" else (.asset_code + ":" + .asset_issuer) end) + " (balance: " + .balance + ", authorized: " + (.is_authorized // true | tostring) + ")"' +else + echo -e "\n${YELLOW}Asserting required trustlines:${NC}" + for ASSET in "${ASSETS[@]}"; do + if [[ "$ASSET" == "XLM" || "$ASSET" == "native" ]]; then + HAS_NATIVE=$(echo "$BALANCES" | jq -r '[.[] | select(.asset_type == "native")] | length') + if [[ "$HAS_NATIVE" -gt 0 ]]; then + echo -e " ${GREEN}✓${NC} XLM (native balance available)" + PASS=$((PASS + 1)) + else + echo -e " ${RED}✗${NC} Missing native XLM balance on fee account" + FAIL=$((FAIL + 1)) + fi + continue + fi + + CODE="${ASSET%%:*}" + ISSUER="${ASSET##*:}" + + # Match either exact code+issuer or asset code + MATCH=$(echo "$BALANCES" | jq -r --arg code "$CODE" --arg issuer "$ISSUER" \ + '[.[] | select(.asset_code == $code and .asset_issuer == $issuer)] | .[0]') + + if [[ -n "$MATCH" && "$MATCH" != "null" ]]; then + IS_AUTH=$(echo "$MATCH" | jq -r '.is_authorized // false') + if [[ "$IS_AUTH" == "true" ]]; then + echo -e " ${GREEN}✓${NC} Trustline active & authorized: ${ASSET}" + PASS=$((PASS + 1)) + else + echo -e " ${RED}✗${NC} Trustline exists but is NOT authorized: ${ASSET}" + FAIL=$((FAIL + 1)) + fi + else + echo -e " ${RED}✗${NC} Missing trustline for: ${ASSET}" + FAIL=$((FAIL + 1)) + fi + done +fi + +echo +if [[ $FAIL -gt 0 ]]; then + echo -e "${RED}FAIL: $FAIL check(s) failed; $PASS passed.${NC}" >&2 + echo "Do NOT register this token or rotate fee account until trustlines are established and authorized." >&2 + exit 1 +fi + +echo -e "${GREEN}PASS: Fee account trustline verification successful.${NC}" diff --git a/scripts/admin/verify-multisig.sh b/scripts/admin/verify-multisig.sh index 4dbcab5..46fb8a3 100755 --- a/scripts/admin/verify-multisig.sh +++ b/scripts/admin/verify-multisig.sh @@ -105,4 +105,5 @@ echo echo "Next steps:" echo " 1. Compare the printed signer addresses against the founder's roster." echo " 2. Confirm each signer can sign a test transaction (drill)." -echo " 3. Only then run set_admin per docs/mainnet-deploy-runbook.md §2.7." +echo " 3. Verify fee account trustlines via ./scripts/admin/verify-fee-trustline.sh (DoS.15)." +echo " 4. Only then run set_admin per docs/mainnet-deploy-runbook.md §2.7."