Trigger Pattern Protocol has privileged authorities (admin, operator, upgrade authority, governance) - Inject Into Breadth agents (optional), depth-state-trace
Trigger Pattern: Protocol has privileged authorities (admin, operator, upgrade authority, governance, multisig) Inject Into: Breadth agents (optional), depth-state-trace Finding prefix:
[CR-N]Rules referenced: R2, R6, R9, R10, R13 Required: NO (recommended when protocol has 3+ distinct privileged roles)
Covers: single points of failure, privilege escalation, contract upgrade risk, external governance dependencies, emergency powers. On Soroban, centralization risk has unique dimensions: update_current_contract_wasm() allows full contract replacement by the admin, Instance storage holds admin Address values (opaque — could be an account or another contract), no role-based modifiers like EVM's onlyOwner exist so access control is fully custom, and the Address type abstracts over both user accounts and contracts which can obscure who really controls a privilege.
Enumerate ALL privileged functions by scanning for require_auth calls against stored admin/operator addresses:
| # | Function | Contract | Authority Field (storage key) | What It Controls | Impact If Abused |
|---|---|---|---|---|---|
| 1 | {fn_name} | {contract} | {Instance key e.g. "Admin"} | {parameter/state} | {worst case} |
Soroban authority patterns to scan for:
admin.require_auth() — caller must be the stored admin addresslet admin: Address = e.storage().instance().get(&DataKey::Admin).unwrap() then admin.require_auth()operator.require_auth() — operator/keeper patterns stored in Instance or Persistent storagee.current_contract_address().require_auth() — contract authorizing itself (sub-contract call patterns)update_current_contract_wasm(new_wasm_hash) — upgrades the contract bytecode; whoever calls this controls all logicAddress that resolves to a Stellar multisig account (threshold signatures) vs a single keypairAddress resolving to a Soroban governance contractCategorize each by impact:
update_current_contract_wasm()Map the role hierarchy:
| Role | Stored In (storage type + key) | Granted By | Can Grant Others? | Revocable? | Timelock? |
|---|---|---|---|---|---|
| {role} | {Instance/Persistent, key} | {granting function} | YES/NO | YES/NO | YES/NO ({mechanism}) |
Address values?Address have both PARAMETER_CONTROL and FUND_CONTROL?Address opaque — could it be a contract whose own admin is unknown?| Contract | Upgrade Guard | Type | Immutable? | Risk Level |
|---|---|---|---|---|
| {contract_id} | update_current_contract_wasm caller check | EOA / Multisig / DAO-contract / None | YES/NO | {assessment} |
Risk levels:
update_current_contract_wasm is absent.For each privileged role:
| Role | Key Compromise Impact | Mitigation | Residual Risk |
|---|---|---|---|
| {role} | {what attacker can do} | {multisig? timelock? immutable?} | {what remains} |
| Risk | Description | Severity |
|---|---|---|
| Upgrade authority compromise | Attacker replaces contract wasm with malicious version. ALL contract state and funds at risk. | CRITICAL if single keypair, HIGH if multisig without timelock |
| Admin address is a contract | The admin Address resolves to another contract. That contract's own upgrade path is now the real privilege escalation vector. | Severity inherits from the outer contract's upgrade risk |
| Single admin with FUND_CONTROL | Admin can transfer all tokens out of the contract. | HIGH if single keypair, MEDIUM if multisig |
| No admin rotation function | Admin keypair loss = permanent loss of all admin capabilities. | HIGH — protocol becomes ungovernable |
| SAC admin authority active | Stellar Asset Contract admin can freeze/clawback user balances for the token. | HIGH if used in user-facing token flows |
Severity assessment:
Identify parameters or behaviors controlled by EXTERNAL governance:
| Dependency | External Entity | What They Control | Protocol Impact If Changed | Notification? |
|---|---|---|---|---|
| {dep} | {entity} | {parameter/behavior} | {impact on this protocol} | YES/NO |
Soroban-specific external governance:
invoke_contract — if that contract upgrades, behavior changesCheck:
Document emergency/pause capabilities:
| Emergency Function | Who Can Call | What It Affects | Recovery Path | Time to Recover |
|---|---|---|---|---|
| {function} | {authority} | {scope} | {how to resume} | {estimate} |
| Pattern | Description | Risk |
|---|---|---|
| Global pause flag | Instance storage has paused: bool. All user functions check it. | Standard — check: can users emergency-withdraw when paused? |
| SAC freeze | SAC admin freezes user balances for the protocol's token. | HIGH if no unfreeze path independent of admin |
| Admin-only withdrawal | Admin can drain contract token accounts. | CRITICAL if single keypair |
| TTL expiry as implicit pause | Contract Instance TTL expires; all instance().get() calls panic. Protocol effectively pauses if not extended. | MEDIUM — check: who can call extend_ttl() and under what conditions? |
Check:
For each authority type, assess revocation status and path:
| Authority | Current State | Revocation Path | Should Be Revoked? | Risk If Not Revoked |
|---|---|---|---|---|
| Upgrade authority | {active/none — function present?} | Remove update_current_contract_wasm call or gate to Address::zero() | {assessment} | {risk level} |
| Admin (FUND_CONTROL) | {active} | Two-step transfer to burn address or DAO | {assessment} | {risk level} |
| SAC admin/freeze | {active/none} | Stellar issuer account deauthorization | {assessment} | {risk level} |
| Operator/keeper | {active} | Setter function to zero/None | {assessment} | {risk level} |
Rule 13 check: Is the authority retention documented? If the protocol claims to be "decentralized" or "trustless" but retains upgrade or fund-control authority, apply the 5-question test:
## Finding [CR-N]: Title
**Verdict**: CONFIRMED / PARTIAL / REFUTED
**Step Execution**: check1,2,3,4,5,6 | skip(reason) | uncertain
**Severity**: Critical/High/Medium/Low/Info
**Location**: contract name or function name
**Centralization Type**: FUND_CONTROL / PARAMETER_CONTROL / OPERATIONAL_CONTROL / UPGRADE_CONTROL / MINT_CONTROL
**Affected Role**: {authority_name} (Address type: keypair / multisig / contract)
**Mitigation Present**: {Stellar multisig / DAO contract / timelock / Immutable / NONE}
**Description**: What is wrong
**Impact**: What can happen if authority is compromised or acts maliciously
**Recommendation**: How to mitigate (add timelock, remove upgrade function, use multisig, separate roles)
| Step | Required | Completed? | Notes |
|---|---|---|---|
| 1. Privilege Inventory (all functions with require_auth) | YES | ||
| 2. Role Hierarchy and Separation | YES | ||
| 3. Single Points of Failure (per role) | YES | ||
| 4. External Governance Dependencies | YES | ||
| 5. Emergency Powers and Recovery Paths | YES | ||
| 6. Authority Revocation Assessment | YES |
After Step 1: Cross-reference with auth validation — is require_auth() called on the stored admin, or on the transaction signer directly (bypassing stored admin check)?
After Step 2: If upgrade authority is a single keypair → immediate finding (minimum HIGH).
After Step 3: If admin Address is itself a contract, trace that contract's upgrade path — severity inherits.
After Step 5: If no emergency withdraw exists AND pause is possible → Rule 9 stranded asset finding.
After Step 6: If protocol claims trustlessness but retains mutable authorities → Rule 13 anti-normalization finding.