Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions docs/contract-ops-runbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <FEE_ACCOUNT_G_ADDRESS> <network> <ASSET_CODE:ISSUER ...>
```
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.
14 changes: 14 additions & 0 deletions docs/multisig-preflight.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <FEE_ACCOUNT_G_ADDRESS> <testnet|mainnet> <ASSET_CODE:ISSUER>
```
- [ ] 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:
Expand Down
112 changes: 112 additions & 0 deletions scripts/admin/verify-fee-trustline.sh
Original file line number Diff line number Diff line change
@@ -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 <FEE_ACCOUNT_G_ADDRESS> [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 <FEE_ACCOUNT_G_ADDRESS> [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}"
3 changes: 2 additions & 1 deletion scripts/admin/verify-multisig.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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."