Source: docs/threat-model.md v1.0, Spoof.2 (accepted; planned hardening).
Finding
claim_milestone for crowdfunding calls require_auth() on storage::get_admin(env) (grant.rs) — the same DataKey::Admin address that gates pause, fees, the token list, and (with the timelock at zero) upgrades. There is no separate co-signer role in the contract. The mainnet admin is a genuine 2-of-3 (three weight-1 signers, medium/high thresholds 2, master key weight 0 — Horizon, 2026-09-11), so a single backend-held key cannot satisfy the check: every crowdfunding milestone release requires the admin quorum.
Please confirm how crowdfunding milestone claims are actually signed in production. If a server holds two signer keys to make this routine, that is a full admin compromise waiting to happen and contradicts docs/admin-custody-policy.md; if the quorum signs each milestone by hand, the design imposes a ceremony on a routine flow.
Change
Source:
docs/threat-model.mdv1.0, Spoof.2 (accepted; planned hardening).Finding
claim_milestonefor crowdfunding callsrequire_auth()onstorage::get_admin(env)(grant.rs) — the sameDataKey::Adminaddress that gates pause, fees, the token list, and (with the timelock at zero) upgrades. There is no separate co-signer role in the contract. The mainnet admin is a genuine 2-of-3 (three weight-1 signers, medium/high thresholds 2, master key weight 0 — Horizon, 2026-09-11), so a single backend-held key cannot satisfy the check: every crowdfunding milestone release requires the admin quorum.Please confirm how crowdfunding milestone claims are actually signed in production. If a server holds two signer keys to make this routine, that is a full admin compromise waiting to happen and contradicts
docs/admin-custody-policy.md; if the quorum signs each milestone by hand, the design imposes a ceremony on a routine flow.Change
Validatorrole in instance storage (set by admin, two-step or timelocked rotation), used only byclaim_milestonefor crowdfundingrequire_auth(); the validator replaces the admin in that one check