Skip to content
10 changes: 5 additions & 5 deletions content/stellar-contracts/access/access-control.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -53,22 +53,22 @@ impl MyContract {

// 1. Set MANAGER_ROLE as the admin role for GUARDIAN_ROLE:
// accounts with MANAGER_ROLE can manage accounts with GUARDIAN_ROLE
access_control::set_role_admin_no_auth(e, &admin, &GUARDIAN_ROLE, &MANAGER_ROLE);
access_control::set_role_admin_no_auth(e, &GUARDIAN_ROLE, &MANAGER_ROLE);

// 2. Admin grants MANAGER_ROLE to the manager account
access_control::grant_role_no_auth(e, &admin, &manager, &MANAGER_ROLE);
access_control::grant_role_no_auth(e, &manager, &MANAGER_ROLE, &admin);
}

pub fn manage_guardians(e: &Env, manager: Address, guardian1: Address, guardian2: Address) {
// Manager must be authorized
manager.require_auth();

// 3. Now the manager can grant GUARDIAN_ROLE to other accounts
access_control::grant_role_no_auth(e, &manager, &guardian1, &GUARDIAN_ROLE);
access_control::grant_role_no_auth(e, &manager, &guardian2, &GUARDIAN_ROLE);
access_control::grant_role_no_auth(e, &guardian1, &GUARDIAN_ROLE, &manager);
access_control::grant_role_no_auth(e, &guardian2, &GUARDIAN_ROLE, &manager);

// Manager can also revoke GUARDIAN_ROLE
access_control::revoke_role_no_auth(e, &manager, &guardian1, &GUARDIAN_ROLE);
access_control::revoke_role_no_auth(e, &guardian1, &GUARDIAN_ROLE, &manager);
}
}
```
Expand Down
136 changes: 62 additions & 74 deletions content/stellar-contracts/accounts/authorization-flow.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,27 @@
title: Authorization Flow
---

Authorization in smart accounts is determined by matching the current context against the account's context rules. Rules are gathered, ordered by recency, and evaluated until one satisfies the requirements. If a matching rule is found, its policies (if any) are enforced. Otherwise, authorization fails.
Authorization in smart accounts is determined by matching each auth context against explicitly selected context rules. The caller supplies `context_rule_ids` in the `AuthPayload`, specifying exactly one rule per auth context. If the selected rule passes all checks, its policies (if any) are enforced. Otherwise, authorization fails.

## AuthPayload

The `AuthPayload` structure is passed as the signature data in `__check_auth`:

```rust
#[contracttype]
pub struct AuthPayload {
/// Signature data mapped to each signer.
pub signers: Map<Signer, Bytes>,
/// Per-context rule IDs, aligned by index with `auth_contexts`.
pub context_rule_ids: Vec<u32>,
}
```

Each entry in `context_rule_ids` specifies the rule ID to validate against for the corresponding auth context (by index). Its length must equal `auth_contexts.len()`.

<Callout type="warning">
The `context_rule_ids` are bound into the signed digest: `sha256(signature_payload || context_rule_ids.to_xdr())`. This prevents rule-selection downgrade attacks where an attacker could redirect a signature to a less restrictive rule.
</Callout>

## Detailed Flow
```mermaid
Expand All @@ -14,11 +34,13 @@ sequenceDiagram
participant Verifier
participant Policy

User->>SmartAccount: Signatures
SmartAccount->>ContextRule: Match context<br/>(CallContract, Default, ...)
ContextRule->>ContextRule: Filter expired rules<br/>Sort newest first
User->>SmartAccount: AuthPayload (signers + context_rule_ids)
SmartAccount->>SmartAccount: Compute auth_digest<br/>sha256(payload || rule_ids.to_xdr())

loop Each auth context
SmartAccount->>ContextRule: Look up rule by ID<br/>from context_rule_ids
ContextRule->>ContextRule: Validate not expired<br/>and matches context type

loop Each rule until match
Note over ContextRule,DelegatedSigner: Built-in authorization <br/>for delegated signers
ContextRule->>DelegatedSigner: require_auth_for_args()
DelegatedSigner-->>ContextRule: Authorized
Expand All @@ -27,41 +49,29 @@ sequenceDiagram
ContextRule->>Verifier: verify()
Verifier-->>ContextRule: Valid

Note over ContextRule,Policy: Policy pre-checks
ContextRule->>Policy: can_enforce()
Policy-->>ContextRule: True/False

alt All checks pass
ContextRule->>Policy: enforce()
Policy->>Policy: Update state
ContextRule-->>SmartAccount: ✓ Authorized
else Any check fails
ContextRule->>ContextRule: Try next rule
end
Note over ContextRule,Policy: Policy enforcement (panics on failure)
ContextRule->>Policy: enforce()
Policy->>Policy: Validate + update state

ContextRule-->>SmartAccount: ✓ Authorized
end

SmartAccount-->>User: Success
```

### 1. Rule Collection
### 1. Rule Lookup

The smart account gathers all relevant context rules for evaluation:
The smart account reads the `context_rule_ids` from the `AuthPayload`. There must be exactly one rule ID per auth context — a mismatch is rejected with `ContextRuleIdsLengthMismatch`.

- Retrieve all non-expired rules for the specific context type
- Include default rules that apply to any context
- Sort specific and default rules by creation time (newest first)
For each auth context, the corresponding rule ID is used to look up the context rule directly. The rule must:

**Context Type Matching:**
- For a `CallContract(address)` context, both specific `CallContract(address)` rules and `Default` rules are collected
- For a `CreateContract(wasm_hash)` context, both specific `CreateContract(wasm_hash)` rules and `Default` rules are collected
- For any other context, only `Default` rules are collected

**Expiration Filtering:**
Rules with `valid_until` set to a ledger sequence that has passed are automatically filtered out during collection.
- Exist in the account's storage
- Not be expired (if `valid_until` is set, it must be ≥ current ledger sequence)
- Match the context type: a `CallContract(address)` rule matches a `CallContract(address)` context, and `Default` rules match any context

### 2. Rule Evaluation

For each rule in order (newest and most specific first):
For each (context, rule_id) pair:

#### Step 2.1: Signer Filtering

Expand All @@ -72,51 +82,33 @@ Extract authenticated signers from the rule's signer list. A signer is considere

Only authenticated signers proceed to the next step.

#### Step 2.2: Policy Validation
#### Step 2.2: Policy Enforcement

If the rule has attached policies, verify that all can be enforced:
If the rule has attached policies, the smart account calls `enforce()` on each policy. The `enforce()` method both validates conditions and applies state changes — it panics if the policy conditions are not satisfied:

```rust
for policy in rule.policies {
if !policy.can_enforce(e, account, rule_id, signers, auth_context) {
// This rule fails, try the next rule
}
policy.enforce(e, context, authenticated_signers, context_rule, smart_account);
// Panics if policy conditions aren't satisfied, causing the rule to fail
}
```

If any policy's `can_enforce()` returns false, the rule fails and evaluation moves to the next rule.
If any policy panics, authorization fails for that context.

Policy enforcement requires the smart account's authorization, ensuring that policies can only be enforced by the account itself.

#### Step 2.3: Authorization Check

The authorization check depends on whether policies are present:

**With Policies:**
- Success if all policies passed `can_enforce()`
- The presence of authenticated signers is verified during policy evaluation
- Success if all policies' `enforce()` calls completed without panicking

**Without Policies:**
- Success if all signers in the rule are authenticated
- At least one signer must be authenticated for the rule to match

#### Step 2.4: Rule Precedence

The first matching rule wins. Newer rules take precedence over older rules for the same context type. This allows overwriting old rules.

### 3. Policy Enforcement

If authorization succeeds, the smart account calls `enforce()` on all matched policies in order:

```rust
for policy in matched_rule.policies {
policy.enforce(e, account, rule_id, signers, auth_context);
}
```

This triggers any necessary state changes such as updating spending counters, recording timestamps, emitting audit events, or modifying allowances.

Policy enforcement requires the smart account's authorization, ensuring that policies can only be enforced by the account itself.

### 4. Result
### 3. Result

**Success:** Authorization is granted and the transaction proceeds. All policy state changes are committed.

Expand Down Expand Up @@ -151,15 +143,14 @@ ContextRule {
**Authorization Entries:** `[passkey_signature]`

**Flow:**
1. Collect: Rules 2 (specific) and 1 (default)
1. Lookup: Client specifies rule ID 2 in `context_rule_ids`
2. Evaluate Rule 2:
- Rule matches `CallContract(dex_address)` context and is not expired
- Signer filtering: Passkey authenticated
- Policy validation: Spending limit check passes
- Authorization check: All policies enforceable → Success
3. Enforce: Update spending counters, emit events
4. Result: Authorized
- Policy enforcement: Spending limit validates and updates counters
- Authorization check: All policies enforced successfully → Success
3. Result: Authorized

If the spending limit had been exceeded, Rule 2 would fail and evaluation would continue to Rule 1 (which would also fail since the passkey doesn't match Alice or Bob).

### Fallback to Default

Expand Down Expand Up @@ -188,12 +179,9 @@ ContextRule {
**Authorization Entries:** `[ed25519_alice_signature, ed25519_bob_signature]`

**Flow:**
1. Collect: Rule 2 filtered out (expired), only Rule 1 collected
2. Evaluate Rule 1: Both Alice and Bob authenticated → Success
3. Enforce: No policies to enforce
4. Result: Authorized

The expired session rule is automatically filtered out, and authorization falls back to the default admin rule.
1. Lookup: Client specifies rule ID 1 in `context_rule_ids` (rule 2 is known to be expired)
2. Evaluate Rule 1: Both Alice and Bob authenticated, no policies to enforce → Success
3. Result: Authorized

### Authorization Failure

Expand All @@ -213,24 +201,24 @@ ContextRule {
**Authorization Entries:** `[alice_signature]`

**Flow:**
1. Collect: Default rule retrieved
1. Lookup: Client specifies default rule ID in `context_rule_ids`
2. Evaluate:
- Signer filtering: Only Alice authenticated
- Policy validation: Threshold policy requires 2 signers, only 1 present → Fail
3. No more rules to evaluate
4. Result: Denied (transaction reverts)
- Policy enforcement: Threshold policy requires 2 signers, only 1 present → Panics
3. Result: Denied (transaction reverts)

## Performance Considerations

Protocol 23 optimizations make the authorization flow efficient:
- **Marginal storage read costs**: Reading multiple context rules has negligible cost
- **Cheaper cross-contract calls**: Calling verifiers and policies is substantially cheaper

The framework enforces limits to maintain predictability:
- Maximum context rules per smart account: 15
The framework enforces per-rule limits to maintain predictability:
- Maximum signers per context rule: 15
- Maximum policies per context rule: 5

There is no upper limit on the total number of context rules per smart account.

## See Also

- [Smart Account](/stellar-contracts/accounts/smart-account)
Expand Down
29 changes: 15 additions & 14 deletions content/stellar-contracts/accounts/context-rules.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -44,28 +44,29 @@ Each rule must contain at least one signer OR one policy. This enables pure poli
### Multiple Rules Per Context
Multiple rules can exist for the same context type with different signer sets and policies. This allows progressive authorization models where different combinations of credentials grant access to the same operations.

### Rule Precedence
Rules are evaluated in reverse chronological order (newest first). The first matching rule wins. This enables seamless permission updates: adding a new rule with different requirements immediately takes precedence over older rules for the same context.
### Explicit Rule Selection
Rules are not iterated or auto-discovered. Instead, off-chain clients specify exactly which context rule to use for each operation via the `context_rule_ids` field in `AuthPayload`. This explicit selection prevents downgrade attacks where an attacker could force the system to fall back to a weaker rule.

### Automatic Expiration
Expired rules are automatically filtered out during authorization evaluation.
Expired rules are rejected during authorization evaluation.

## Context Rule Limits

The framework enforces limits to keep costs predictable and encourage proactive context rule management (remove expired or non-valid rules):

- Maximum context rules per smart account: 15
- Maximum signers per context rule: 15
- Maximum policies per context rule: 5
- Maximum context rule name size: 20 bytes
- Maximum external signer key size: 256 bytes

There is no upper limit on the total number of context rules per smart account.

## Authorization Matching

During authorization, the framework:

1. Gathers all non-expired rules matching the context type plus default rules
2. Sorts rules by creation time (newest first)
3. Evaluates rules in order until one matches
4. Returns the first matching rule or fails if none match
1. Reads the `context_rule_ids` from the `AuthPayload` (one rule ID per auth context)
2. Looks up each rule directly by ID
3. Validates the rule is not expired and matches the context type
4. Authenticates signers and enforces policies, or fails if conditions aren't met

For detailed documentation on the authorization flow, see [Authorization Flow](/stellar-contracts/accounts/authorization-flow).

Expand Down Expand Up @@ -101,6 +102,7 @@ smart_account::add_context_rule(
e,
ContextRuleType::Default,
String::from_str(e, "Sudo"),
None, // No expiration
vec![
e,
Signer::External(bls_verifier, alice_key),
Expand All @@ -111,7 +113,6 @@ smart_account::add_context_rule(
e,
(threshold_policy, threshold_params) // 2-of-3 Threshold
],
None, // No expiration
);

// This rule applies only to calls to the USDC contract, expires in 1 year,
Expand All @@ -120,6 +121,7 @@ smart_account::add_context_rule(
e,
ContextRuleType::CallContract(usdc_addr),
String::from_str(e, "Dapp1 Subscription"),
Some(current_ledger + 1_year),
vec![
e,
Signer::External(ed25519_verifier, dapp1_key)
Expand All @@ -128,7 +130,6 @@ smart_account::add_context_rule(
e,
(spending_limit_policy, spending_params)
],
Some(current_ledger + 1_year)
);

// This rule applies only to calls to the dApp contract, expires in 7 days,
Expand All @@ -137,6 +138,7 @@ smart_account::add_context_rule(
e,
ContextRuleType::CallContract(dapp_addr),
String::from_str(e, "Dapp2 Session"),
Some(current_ledger + 7_days),
vec![
e,
Signer::External(ed25519_verifier, dapp2_key)
Expand All @@ -146,7 +148,6 @@ smart_account::add_context_rule(
(rate_limit_policy, rate_limit_params),
(time_window_policy, time_window_params)
],
Some(current_ledger + 7_days)
);

// This rule applies only to calls to a specific contract, expires in 12 hours,
Expand All @@ -155,6 +156,7 @@ smart_account::add_context_rule(
e,
ContextRuleType::CallContract(some_addr),
String::from_str(e, "AI Agent"),
Some(current_ledger + 12_hours),
vec![
e,
Signer::External(secp256r1_verifier, agent_key)
Expand All @@ -163,7 +165,6 @@ smart_account::add_context_rule(
e,
(volume_cap_policy, volume_cap_params)
],
Some(current_ledger + 12_hours)
);
```

Expand Down
Loading
Loading