Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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 or $code == $issuer))] | .[0]')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium Severity severity

Business Logic: false positive issuer match in verify-fee-trustline.sh

The trustline match predicate can succeed even when the provided issuer does not match, because it includes (.asset_issuer == $issuer or $code == $issuer). If an operator supplies malformed input (e.g., missing :ISSUER) or an attacker convinces ops to verify only by ASSET_CODE, the script may incorrectly PASS a fee account that lacks the correct trustline, leaving deposit_with_fee_at / release_with_fee_at vulnerable to the same revert/DoS condition this PR aims to mitigate.

Require exact asset_code+asset_issuer matching for credit assets (and reject inputs that are not CODE:ISSUER), so the script cannot pass on code-only matches or wrong issuers.

Suggested change
'[.[] | select(.asset_code == $code and (.asset_issuer == $issuer or $code == $issuer))] | .[0]')
'[.[] | select(.asset_code == $code and .asset_issuer == $issuer)] | .[0]')

Fix with MCP
Almanax found a vulnerability. Can you take a look and fix it?
Finding ID: 8b0424b8-3a25-4529-bfae-b9d14299c424
Actions
  • Reply /almanax ask <question> to ask a follow-up question.
  • Reply /almanax dismiss [<reason>] and it won't appear again in future scans.
  • Reply /almanax resolve [<reason>] to mark the finding as resolved.
  • Reply /almanax severity <level> [<reason>] to override the severity.


if [[ -n "$MATCH" && "$MATCH" != "null" ]]; then
IS_AUTH=$(echo "$MATCH" | jq -r '.is_authorized // true')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low Severity severity

Input and Parameter Validation: non-conservative is_authorized default in verify-fee-trustline.sh

For required trustlines, the script treats missing .is_authorized as true (.is_authorized // true). If Horizon omits this field in some responses/edge cases, the script can incorrectly mark an unauthorized trustline as authorized, again undermining the operational DoS mitigation.

Default missing .is_authorized to false when asserting required trustlines, so the script fails closed and forces manual verification.

Suggested change
IS_AUTH=$(echo "$MATCH" | jq -r '.is_authorized // true')
IS_AUTH=$(echo "$MATCH" | jq -r '.is_authorized // false')

Fix with MCP
Almanax found a vulnerability. Can you take a look and fix it?
Finding ID: deb2bf4e-96f1-4373-b765-d78b568850d8
Actions
  • Reply /almanax ask <question> to ask a follow-up question.
  • Reply /almanax dismiss [<reason>] and it won't appear again in future scans.
  • Reply /almanax resolve [<reason>] to mark the finding as resolved.
  • Reply /almanax severity <level> [<reason>] to override the severity.

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."