From 8de72d666f87af4265f9a88cc0f3f76e0da7cb6e Mon Sep 17 00:00:00 2001 From: brozorec <9572072+brozorec@users.noreply.github.com> Date: Mon, 27 Oct 2025 17:55:10 +0100 Subject: [PATCH 1/4] init --- .../accounts/authorization-flow.mdx | 202 ++++++ .../accounts/context-rules.mdx | 173 +++++ .../stellar-contracts/accounts/policies.mdx | 240 +++++++ .../accounts/signers-and-verifiers.mdx | 602 ++++++++++++++++++ .../accounts/smart-account.mdx | 51 ++ content/stellar-contracts/index.mdx | 5 + src/navigation/ecosystems.json | 31 + src/navigation/stellar.json | 31 + 8 files changed, 1335 insertions(+) create mode 100644 content/stellar-contracts/accounts/authorization-flow.mdx create mode 100644 content/stellar-contracts/accounts/context-rules.mdx create mode 100644 content/stellar-contracts/accounts/policies.mdx create mode 100644 content/stellar-contracts/accounts/signers-and-verifiers.mdx create mode 100644 content/stellar-contracts/accounts/smart-account.mdx diff --git a/content/stellar-contracts/accounts/authorization-flow.mdx b/content/stellar-contracts/accounts/authorization-flow.mdx new file mode 100644 index 00000000..cc320a3c --- /dev/null +++ b/content/stellar-contracts/accounts/authorization-flow.mdx @@ -0,0 +1,202 @@ +--- +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. + +## Detailed Flow + +### 1. Rule Collection + +The smart account gathers all relevant context rules for evaluation: + +- 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) + +**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. + +### 2. Rule Evaluation + +For each rule in order (newest and most specific first): + +#### Step 2.1: Signer Filtering + +Extract authenticated signers from the rule's signer list. A signer is considered authenticated if: + +- **Delegated Signer**: The address has authorized the operation via `require_auth_for_args(payload)` +- **External Signer**: The verifier contract confirms the signature is valid for the public key + +Only authenticated signers proceed to the next step. + +#### Step 2.2: Policy Validation + +If the rule has attached policies, verify that all can be enforced: + +```rust +for policy in rule.policies { + if !policy.can_enforce(e, account, rule_id, signers, auth_context) { + // This rule fails, try the next rule + } +} +``` + +If any policy's `can_enforce()` returns false, the rule fails and evaluation moves to the next rule. + +#### 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 + +**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 + +**Success:** Authorization is granted and the transaction proceeds. All policy state changes are committed. + +**Failure:** Authorization is denied and the transaction reverts. No state changes are committed. + +## Examples + +### Specific Context with Policy + +**Configuration:** +```rust +// DEX-specific rule with session key and spending limit +ContextRule { + id: 2, + context_type: CallContract(dex_address), + valid_until: Some(current_ledger + 24_hours), + signers: [passkey], + policies: [spending_limit_policy] +} + +// Default admin rule +ContextRule { + id: 1, + context_type: Default, + signers: [ed25519_alice, ed25519_bob], + policies: [] +} +``` + +**Call Context:** `CallContract(dex_address)` + +**Authorization Entries:** `[passkey_signature]` + +**Flow:** +1. Collect: Rules 2 (specific) and 1 (default) +2. Evaluate Rule 2: + - 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 + +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 + +**Configuration:** +```rust +// Session rule (expired) +ContextRule { + id: 2, + context_type: CallContract(dex_address), + valid_until: Some(current_ledger - 100), // Expired + signers: [session_key], + policies: [spending_limit_policy] +} + +// Default admin rule +ContextRule { + id: 1, + context_type: Default, + signers: [ed25519_alice, ed25519_bob], + policies: [] +} +``` + +**Call Context:** `CallContract(dex_address)` + +**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. + +### Authorization Failure + +**Configuration:** +```rust +// Default rule requiring 2-of-3 threshold +ContextRule { + id: 1, + context_type: Default, + signers: [alice, bob, carol], + policies: [threshold_policy(2)] +} +``` + +**Call Context:** `CallContract(any_address)` + +**Authorization Entries:** `[alice_signature]` + +**Flow:** +1. Collect: Default rule retrieved +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) + +## 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 +- Maximum signers per context rule: 15 +- Maximum policies per context rule: 5 + +## See Also + +- [Smart Account](/stellar-contracts/accounts/smart-account) +- [Context Rules](/stellar-contracts/accounts/context-rules) +- [Signers and Verifiers](/stellar-contracts/accounts/signers-and-verifiers) +- [Policies](/stellar-contracts/accounts/policies) diff --git a/content/stellar-contracts/accounts/context-rules.mdx b/content/stellar-contracts/accounts/context-rules.mdx new file mode 100644 index 00000000..5878c1aa --- /dev/null +++ b/content/stellar-contracts/accounts/context-rules.mdx @@ -0,0 +1,173 @@ +--- +title: Context Rules +--- + +[Source Code](https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/accounts/src/smart_account) + +Context rules function like routing tables for authorization. For each context, they specify scope, lifetime, and the conditions (signers and policies) that must be satisfied before execution proceeds. + +## Structure + +A context rule contains the following components: + +#### ID +Unique identifier for the rule within the smart account. + +#### Name +Human-readable description of the rule's purpose (e.g., "Admin Access", "DeFi Session"). + +#### Context Type +Defines the scope where the rule applies: + +- `Default`: Applies to any context. Used for admin-like authorization that spans all operations. +- `CallContract(Address)`: Applies to specific contract calls. Useful for scoped permissions like session logins to a particular dApp. +- `CreateContract(BytesN<32>)`: Applies to contract deployments with a specific WASM hash. Enables control over which contracts can be deployed. + +#### Valid Until +Optional expiration defined by a ledger sequence. Rules with expiration automatically become invalid after the specified ledger, enabling time-limited permissions like 24-hour sessions. + +#### Signers +List of authorized signers (maximum 15 per rule). Signers can be either delegated (any Soroban address) or external (using verifier contracts). + +For detailed documentation on signers, see [Signers](/stellar-contracts/accounts/signers-and-verifiers). + +#### Policies +List of policy contracts (maximum 5 per rule). Policies act as enforcement modules that perform read-only prechecks and state-changing enforcement logic. + +For detailed documentation on policies, see [Policies](/stellar-contracts/accounts/policies). + +## Key Properties + +### Requirement Flexibility +Each rule must contain at least one signer OR one policy. This enables pure policy-based authorization (like a spending limit without signature checks) or pure signature-based authorization (like an n-of-n multisig). + +### 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. + +### Automatic Expiration +Expired rules are automatically filtered out 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 + +## 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 + +For detailed documentation on the authorization flow, see [Authorization Flow](/stellar-contracts/accounts/authorization-flow). + +## Example Configuration + +```mermaid +graph TD + SA["Smart Account"] + + CR1["Context Rule


ID: 1
Name: Sudo
Context: Default
Valid Until: None

Signers
External(bls_verifier, alice_key)
External(bls_verifier, bob_key)
Delegated(dave_addr)

Policies
2-of-3 Threshold
"] + + CR2["Context Rule


ID: 2
Name: Dapp1 Subscription
Context: CallContract(usdc)
Valid Until: 1 year

Signers
External(ed25519_verifier, dapp1_key)

Policies
Spending Limit
"] + + CR3["Context Rule


ID: 5
Name: Dapp2 Session
Context: CallContract(dapp_addr)
Valid Until: 7 days

Signers
External(ed25519_verifier, dapp2_key)

Policies
Rate Limit
Time Window
"] + + CR4["Context Rule


ID: 8
Name: AI Agent
Context: CallContract(some_addr)
Valid Until: 12 hours

Signers
External(secp256r1_verifier, agent_key)

Policies
Volume Cap
"] + + SA --- CR1 + SA --- CR2 + SA --- CR3 + SA --- CR4 +``` + + +```rust +use soroban_sdk::{map, vec, Env, String} +use stellar_accounts::smart_account::{self as smart_account, ContextRuleType}; + +// This rule applies to all contexts and requires 2-of-3 signatures from Alice, Bob, or Dave. +smart_account::add_context_rule( + e, + ContextRuleType::Default, + String::from_str(e, "Sudo"), + vec![ + e, + Signer::External(bls_verifier, alice_key), + Signer::External(bls_verifier, bob_key), + Signer::Delegated(dave_addr) + ], + map![ + 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, +// requires a dapp1 key signature, and enforces spending limits. +smart_account::add_context_rule( + e, + ContextRuleType::CallContract(usdc_addr), + String::from_str(e, "Dapp1 Subscription"), + vec![ + e, + Signer::External(ed25519_verifier, dapp1_key) + ], + map![ + 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, +// requires a dapp2 key signature, and enforces rate limiting and time window policies. +smart_account::add_context_rule( + e, + ContextRuleType::CallContract(dapp_addr), + String::from_str(e, "Dapp2 Session"), + vec![ + e, + Signer::External(ed25519_verifier, dapp2_key) + ], + map![ + e, + (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, +// requires an AI agent key signature, and enforces volume caps. +smart_account::add_context_rule( + e, + ContextRuleType::CallContract(some_addr), + String::from_str(e, "AI Agent"), + vec![ + e, + Signer::External(secp256r1_verifier, agent_key) + ], + map![ + e, + (volume_cap_policy, volume_cap_params) + ], + Some(current_ledger + 12_hours) +); +``` + +## See Also + +- [Smart Account](/stellar-contracts/accounts/smart-account) +- [Signers and Verifiers](/stellar-contracts/accounts/signers-and-verifiers) +- [Policies](/stellar-contracts/accounts/policies) +- [Authorization Flow](/stellar-contracts/accounts/authorization-flow) diff --git a/content/stellar-contracts/accounts/policies.mdx b/content/stellar-contracts/accounts/policies.mdx new file mode 100644 index 00000000..33572eec --- /dev/null +++ b/content/stellar-contracts/accounts/policies.mdx @@ -0,0 +1,240 @@ +--- +title: Policies +--- + +[Source Code](https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/accounts/src/policies) + +Policies are enforcement modules that add constraints to context rules in smart accounts. While signers determine who can authorize actions, policies determine how those authorizations are enforced, enabling sophisticated patterns like multi-signature thresholds, spending limits, and time-based restrictions. + +Policies attach to context rules and execute during the authorization flow. A context rule can have up to **5 policies** attached, and policies are executed in the order they were added. If policies are present in a context rule, **all of them must be enforceable** (i.e., `can_enforce` must return `true`) for the rule to be considered matched and authorized. + +## The Policy Trait + +All policies must implement the `Policy` trait: + +```rust +pub trait Policy { + type AccountParams: FromVal; + + /// Read-only pre-check to validate conditions + /// Must be idempotent and side-effect free + /// Returns true if the policy would allow the action + fn can_enforce( + e: &Env, + context: Context, + authenticated_signers: Vec, + rule: ContextRule, + smart_account: Address, + ) -> bool; + + /// State-changing enforcement hook + /// Called when a context rule successfully matches and all can_enforce checks pass + /// Requires smart account authorization + fn enforce( + e: &Env, + context: Context, + authenticated_signers: Vec, + rule: ContextRule, + smart_account: Address, + ); + + /// Initialize policy-specific storage and configuration + /// Called when a new context rule with attached policies is created + fn install( + e: &Env, + param: Self::AccountParams, + rule: ContextRule, + smart_account: Address, + ); + + /// Clean up policy data when removed + /// Called when a context rule is removed + fn uninstall( + e: &Env, + rule: ContextRule, + smart_account: Address, + ); +} +``` + +## Policy Lifecycle + +The four trait methods form a complete lifecycle for policy management: + +### Installation + +Installation occurs when a new context rule is created with attached policies or a policy is added to an existing context rule. The smart account calls `install()` on each policy contract, passing account-specific and context-specific parameters. + +This initialization step allows policies to configure their logic. For example: +- A threshold policy might define the required number of signatures for that particular account and context rule +- A spending limit policy might set daily or per-transaction caps + +Installation ensures that each policy has the necessary state and configuration ready before authorization checks begin. + +### Pre-check Validation + +Pre-check validation happens during authorization. When the matching algorithm iterates over context rules and their associated policies, it calls `can_enforce()` on each policy as a read-only pre-check. + +This function examines the current state without modifying it, for instance: +- Verifying that a spending limit has not been exceeded +- Checking that enough signers are present +- Validating that time-based restrictions are met + +Policies that fail this check cause the algorithm to move to the next context rule. + +### Enforcement + +Enforcement is triggered when a context rule successfully matches. Once all policies in the matched rule pass their `can_enforce()` checks, the smart account calls `enforce()` on each policy. + +This state-changing hook allows policies to: +- Update counters +- Emit events +- Record timestamps +- Track authorization activity + +For example, a spending limit policy might deduct from the available balance and emit an event documenting the transaction. + +### Uninstallation + +Uninstallation occurs when a context rule is removed from the smart account. The account calls `uninstall()` on each attached policy, allowing them to clean up any stored data associated with that specific account and context rule pairing. + +This ensures that policies do not leave orphaned state in storage. + +## Stateful vs Stateless Policies + +Policies can be implemented as either stateful or stateless: + +### Stateless Policies + +Stateless policies perform validation based solely on the provided parameters without maintaining any storage: + +- No storage operations +- No `require_auth` calls needed +- Lower resource consumption +- Example: Hard-coded threshold + +### Stateful Policies + +Stateful policies maintain storage to track state across multiple authorizations: + +- **Storage Segregation**: Must segregate storage entries by **both** smart account address AND context rule ID +- **Multiple Rules Support**: The same policy contract can be installed on multiple context rules from the same smart account, with separate storage for each +- **Authorization Required**: Must call `require_auth` from the smart account in `install`, `enforce`, and `uninstall` +- **Event Emission**: Should emit events for state changes to enable tracking and auditing +- Example: Spending limit (tracks cumulative spending over time) + +## Policy Sharing Models + +Policies can be deployed and used in different ways. A single policy contract instance can be shared across multiple smart accounts, multiple context rules within the same smart account, or different combinations of both. This shared model provides lower deployment costs (deploy once, use many times) and ensures consistent behavior across accounts, but requires proper storage segregation by smart account and rule ID. Alternatively, each smart account or context rule can have their own dedicated policy contract attached. + +## Policy Management + +The `SmartAccount` trait provides functions for managing policies within context rules: + +### Adding Policies + +```rust +fn add_policy( + e: &Env, + context_rule_id: u32, + policy: Address, + account_params: Val, +); +``` + +Adds a policy to an existing context rule and calls its `install()` function. The rule must not exceed the maximum of 5 policies. + +### Removing Policies + +```rust +fn remove_policy( + e: &Env, + context_rule_id: u32, + policy: Address, +); +``` + +Removes a policy from an existing context rule and calls its `uninstall()` function. The rule must maintain at least one signer OR one policy after removal. + +### Caveats + +**Signer Set Divergence in Threshold Policies** + +Threshold policies (both simple and weighted) store authorization requirements that are validated at installation time. However, policies are not automatically notified when signers are added to or removed from their parent context rule. This creates a state divergence that can lead to operational issues. + +**Removing Signers:** If signers are removed after policy installation, the total available signatures or weight may fall below the stored threshold, making it impossible to meet the authorization requirement and permanently blocking actions governed by that policy. + +**Example:** A 5-of-5 multisig where two signers are removed leaves only three signers, making the threshold of five unreachable. + +**Adding Signers:** Conversely, if signers are added without updating the threshold, the security guarantee silently weakens. A strict 3-of-3 multisig becomes a 3-of-5 multisig after adding two signers, reducing the required approval from 100% to 60% without any explicit warning. + +**Resolution:** Administrators must manually update thresholds and weights when modifying signer sets: +1. Before removing signers, verify that the threshold remains achievable +2. After adding signers, adjust thresholds or assign weights to maintain the desired security level +3. Ideally, bundle these updates in the same transaction as the signer modifications + +## Example Policies + +The OpenZeppelin Stellar Contracts library provides the necessary utilities for three policy implementations: + +### Simple Threshold + +[Source Code](https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/accounts/src/policies/simple_threshold.rs) + +The `simple_threshold` policy implements N-of-M multisig authorization, requiring a minimum number of valid signatures before allowing an action. This is the most common multisig pattern, treating all signers equally. For example, a 2-of-3 multisig requires any 2 signatures from 3 allowed signers. + +The policy requires a single configuration parameter: + +- **Threshold**: The minimum number of signatures required (N) + +The total number of signers (M) is determined by the context rule's signer list. + + +When using threshold policies, be aware of signer set divergence issues. See the [Caveats](#caveats) section above for details on how adding or removing signers affects threshold policies and how to properly manage these changes. + + +### Weighted Threshold + +[Source Code](https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/accounts/src/policies/weighted_threshold.rs) + +The `weighted_threshold` policy implements flexible multisig authorization where each signer has an assigned weight. Authorization requires that the sum of signature weights meets or exceeds a specified threshold, enabling hierarchical and role-based authorization patterns. + +Unlike `simple_threshold` where all signers are equal, `weighted_threshold` assigns different weights to signers based on their authority level. This enables sophisticated patterns like "1 admin OR 2 managers OR 3 users" within a single rule. + +The policy requires two configuration parameters: + +- **Signer Weights**: Map of signers to their weights +- **Threshold**: The minimum total weight required + + +When using threshold policies, be aware of signer set divergence issues. See the [Caveats](#caveats) section above for details on how adding or removing signers affects threshold policies and how to properly manage these changes. + + +### Spending Limit + +[Source Code](https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/accounts/src/policies/spending_limit.rs) + +The `spending_limit` policy enforces spending caps over time periods, enabling budget controls, allowances, and rate limiting for smart accounts. This is particularly useful for session keys, sub-accounts, and automated operations that need spending constraints. + +The policy tracks cumulative spending within a rolling time window and rejects transactions that would exceed the configured limit. The policy maintains state to track spending and automatically resets when the time window expires. + +The policy requires two configuration parameters: + +- **Limit Amount**: Maximum spending allowed in the time window +- **Time Window**: Duration in seconds for the spending period + +## Best Practices + +1. **Order Matters**: Policies execute in order; place cheaper checks first +2. **Keep Policies Focused**: Each policy should enforce one concern +3. **Test Policy Combinations**: Ensure multiple policies work together correctly +4. **Handle Errors Gracefully**: Return clear error messages from `enforce` +5. **Clean Up Storage**: Always implement `uninstall` to free storage +6. **Document Configuration**: Clearly document policy configuration parameters + +## See Also + +- [Smart Account](/stellar-contracts/accounts/smart-account) +- [Context Rules](/stellar-contracts/accounts/context-rules) +- [Signers and Verifiers](/stellar-contracts/accounts/signers-and-verifiers) +- [Authorization Flow](/stellar-contracts/accounts/authorization-flow) diff --git a/content/stellar-contracts/accounts/signers-and-verifiers.mdx b/content/stellar-contracts/accounts/signers-and-verifiers.mdx new file mode 100644 index 00000000..1952baf0 --- /dev/null +++ b/content/stellar-contracts/accounts/signers-and-verifiers.mdx @@ -0,0 +1,602 @@ +--- +title: Signers and Verifiers +--- + +## Signers + +[Source Code](https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/accounts/src/smart_account) + +Signers define who can authorize operations in a smart account. The framework supports two variants that accommodate different authorization patterns and cryptographic schemes. + +```rust +#[contracttype] +pub enum Signer { + Delegated(Address), + External(Address, Bytes), +} +``` + +### Delegated + +```rust +Signer::Delegated(Address) +``` + +Delegated signers represent any Soroban address (C-account, G-account) and delegate authorization checks to that address using the built-in `require_auth_for_args()`. This enables using traditional Stellar account address (G-accounts) as signers, but also makes possible powerful composition patterns such as nested smart accounts (one smart account authorizing on behalf of another) and contract-based signers with custom authorization logic. This variant is particularly useful for building multi-level authorization hierarchies. + +```mermaid +graph TD + G1[G-account] + G2[G-account] + C1[C-account] + SA1[Smart Account] + SA2[Smart Account] + SA3[Smart Account] + SA4[Smart Account] + + G1 --> SA1 + SA3 --> SA4 + C1 --> SA2 + G2 --> SA2 + + style G1 fill:#6B9BD1,stroke:#333,stroke-width:2px + style G2 fill:#6B9BD1,stroke:#333,stroke-width:2px + style C1 fill:#F4D03F,stroke:#333,stroke-width:2px + style SA1 fill:#E8E8E8,stroke:#333,stroke-width:2px + style SA2 fill:#E8E8E8,stroke:#333,stroke-width:2px + style SA3 fill:#E8E8E8,stroke:#333,stroke-width:2px + style SA4 fill:#E8E8E8,stroke:#333,stroke-width:2px +``` + +#### Transaction Simulation Behavior + +However, there is a caveat when using delegated signers: authorization entries are not automatically included in transaction simulation results and clients must manually construct these entries, which adds additional implementation complexity. This limitation may be overcome with future protocol improvements such as [CAP-71](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0071.md). + +When building transactions in Soroban, clients typically simulate them first to obtain the authorization trees and nonces needed for signing. The simulation mechanism precomputes the `SorobanAuthorizedInvocation` trees that must be authorized by the `Address`es for all `require_auth`/`require_auth_for_args` checks to pass. + +The usual flow is: simulate, collect the returned auth trees and nonces, have each `Address` sign those payloads, and then submit the final transaction that combines the simulation output with the corresponding signatures. See the [official docs](https://developers.stellar.org/docs/learn/fundamentals/contract-development/contract-interactions/transaction-simulation#authorization) for details. + +When `require_auth_for_args` is called from within `__check_auth` (as with delegated signers), the authorization entry for that signer is **not included** in the simulation output. + +```rust +#[contracttype] +pub struct Signatures(pub Map); + +#[contract] +pub struct MySmartAccount; + +#[contractimpl] +impl CustomAccountInterface for MySmartAccount { + fn __check_auth( + e: Env, + payload: Hash<32>, + signatures: Signatures, + auth_contexts: Vec, + ) -> Result<(), SmartAccountError> { + for (signer, _) in signatures.0.iter() { + match signer { + // ... + Signer::Delegated(addr) => { + let payload = (payload.clone(),).into_val(e); + addr.require_auth_for_args(payload); + } + } + } + // ... + } +} +``` + +**Example Scenario** + +Consider a scenario where a target contract `CAGCDFLG4WKPYG...` requires authorization from a smart account `CBH6XACZFDCJUHX...`. The smart account will grant authorization only if the G-account `GBDZXYMJ3SLYXCY...` has signed, meaning `Map` has to contain one element that is `Signer::Delegated("GBDZXYMJ3SLYXCY...")`. + +```mermaid +graph LR + G1[G-account:
GBDZXYMJ3SLYXCY...] + SA1[Smart Account:
CBH6XACZFDCJUHX...] + TC[Target Contract:
CAGCDFLG4WKPYG...] + + TC --> |"require_auth()"| SA1 + SA1 --> |"require_auth_for_args()"| G1 + + style G1 fill:#6B9BD1,stroke:#333,stroke-width:2px + style SA1 fill:#E8E8E8,stroke:#333,stroke-width:2px + style TC fill:#C8D9A3,stroke:#333,stroke-width:2px +``` + +When simulating this transaction, the following authorization entry is returned. Note that `"auth"` contains a single element and the delegated signer address (G-account) is not present at all: +```json +{ + "tx": { + // ... + "auth": [ + { + "credentials": { + "address": { + "address": "CBH6XACZFDCJUHX...", // the Smart Account + "nonce": "7346653005027720525", + "signature_expiration_ledger": 0, + "signature": "void" + } + }, + "root_invocation": { + "function": { + "contract_fn": { + "contract_address": "CAGCDFLG4WKPYG...", // the Target Contract that's initially invoked + "function_name": "some_fn", + "args": [/* fn args if any */] + } + }, + "sub_invocations": [] + } + } + ] + // ... + } +} +``` + +The client implementation requires constructing two authorization entries: + +1. Replace `"signature": "void"` with the proper `Signatures(Map)` structure +2. Create the missing authorization entry for the delegated signer's `__check_auth` call + +The following typescript code demonstrates this process: + +```typescript +async function signAndSendTx( + contract: string, + fnName: string, + fnArgs: ScVal[], + signer: Keypair +) { + const baseTx = new TransactionBuilder(...) + .addOperation( + Operation.invokeContractFunction({ contract, function: fnName, args: fnArgs }), + ) + .setTimeout(600) + .build(); + + const simRes = await server.simulateTransaction(baseTx); + // we assume only one authorization is returned + const simAuth = simRes.result.auth[0]; + + const signedAuths: SorobanAuthorizationEntry[] = []; + + // 1) Construct the 1st auth entry: `Signatures(pub Map)` with `Signer::Delegated(Address)` + const sigInnerMap = ScVal.scvMap([ + new xdr.ScMapEntry({ + key: ScVal.scvVec([ + ScVal.scvSymbol("Delegated"), + Address.fromString(signer.publicKey()).toScVal(), // "GBDZXYMJ3SLYXCY..." + ]), + val: ScVal.scvBytes(""), + }), + ]); + + simAuth.credentials().address().signature(ScVal.scvVec([sigInnerMap])); + simAuth.credentials().address().signatureExpirationLedger(validUntil); + signedAuths.push(simAuth); + + // 2) Construct the 2nd auth entry for `__check_auth` and sign the invocation + const payload = HashIdPreimage.envelopeTypeSorobanAuthorization( + new HashIdPreimageSorobanAuthorization({ + networkId, + nonce: simAuth.credentials().address().nonce(), + signatureExpirationLedger: validUntil, + invocation: simAuth.rootInvocation(), + }), + ).toXDR(); + const hashed_payload = hash(payload); + + const args = new InvokeContractArgs({ + contractAddress: Address.fromString(contract).toScAddress(), + functionName: "__check_auth", + args: [ScVal.scvBytes(hashed_payload)] + }); + const invocation = new SorobanAuthorizedInvocation({ + function: SorobanAuthorizedFunction.sorobanAuthorizedFunctionTypeContractFn(args), + subInvocations: [], + }); + const signedEntry = await authorizeInvocation( + signer, + validUntil, + invocation, + signer.publicKey(), + Networks.TESTNET, + ); + signedAuths.push(signedEntry); + + // rebuild transaction with both auth entries in signedAuths + // re-simulate +} +``` + +After including both authorization entries in `signedAuths` and re-simulating the transaction, the `"auth"` array contains now two elements: + +```json +{ + "tx": { + // ... + "auth": [ + { + "credentials": { + "address": { + "address": "CBH6XACZFDCJUHX...", // the Smart Account + "nonce": "7346653005027720525", + "signature_expiration_ledger": 1256083, + "signature": { + "vec": [ + { + "map": [ + { + "key": { + "vec": [ + { + "symbol": "Delegated" + }, + { + "address": "GBDZXYMJ3SLYXCY..." // the delegated signer (G-account) + } + ] + }, + "val": { + "bytes": "" // `Bytes` value from `Map` is empty here (it's used only for the `Signer::External`) + } + } + ] + } + ] + } + } + }, + "root_invocation": { + // ... + } + }, + { + "credentials": { + "address": { + "address": "GBDZXYMJ3SLYXCY...", // the delegated signer + "nonce": "172051086", + "signature_expiration_ledger": 1256083, + "signature": { + "vec": [ + { + "map": [ + { + "key": { + "symbol": "public_key" + }, + "val": { + "bytes": "479be189dc978b8b3b463a0c6f..." + } + }, + { + "key": { + "symbol": "signature" + }, + "val": { + "bytes": "c54aae899cc29374ef81745bf46612...." + } + } + ] + } + ] + } + } + }, + "root_invocation": { + "function": { + "contract_fn": { + "contract_address": "CBH6XACZFDCJUHX...", // the Smart Account + "function_name": "__check_auth", + "args": [ + { + "bytes": "ed0cfe2903d64e5383..." // the signature payload + } + ] + } + }, + } + } + ] + // ... + } +} +``` + +#### Future Improvements + +While delegated signers currently require manual authorization entry construction, future protocol changes like [CAP-71](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0071.md) aim to streamline this process. These improvements would eliminate the need for the additional authorization entry, making delegated signers more intuitive and reducing transaction overhead. + +Despite the current complexity, delegated signers are valuable both for simple scenarios using traditional Stellar accounts (G-accounts) as signers and for advanced use cases requiring multi-level authorization or custom contract-based signing logic. + + +### External + +Each `External` signer pairs a verifier address with a public key. + +```rust +Signer::External(Address, Bytes) +``` + +In the case of external signers verification is offloaded to specialized verifier contracts. During authorization, the smart account makes a cross-contract call to the verifier contract to validate the signature, as shown in the example below where `VerifierClient::new(e, &verifier).verify()` invokes the external verifier's verification logic. + +```rust +#[contracttype] +pub struct Signatures(pub Map); + +#[contract] +pub struct MySmartAccount; + +#[contractimpl] +impl CustomAccountInterface for MySmartAccount { + fn __check_auth( + e: Env, + payload: Hash<32>, + signatures: Signatures, + auth_contexts: Vec, + ) -> Result<(), SmartAccountError> { + for (signer, sig_data) in signatures.0.iter() { + match signer { + Signer::External(verifier, key_data) => { + let sig_payload = Bytes::from_array(e, &signature_payload.to_bytes().to_array()); + if !VerifierClient::new(e, &verifier).verify( + &sig_payload, + &key_data.into_val(e), + &sig_data.into_val(e), + ) { + panic_with_error!(e, SmartAccountError::ExternalVerificationFailed) + } + } + // ... + } + } + // ... + } +} +``` + +This design separates cryptographic logic from the smart account to facilitate support for diverse schemes (ed25519, secp256k1, secp256r1, BLS, RSA, zero-knowledge proofs). It also minimizes setup costs by allowing many accounts to reuse the same verifier contracts. + +This separation provides forward compatibility: when new cryptographic curves are added on Soroban, smart accounts can adopt them immediately by referencing the appropriate verifier contract. If signature verification were embedded directly in the account, adopting a new scheme would require either upgrading the account (if upgradeable) or migrating to an entirely new account. + +**Example Scenario** + +Consider again a target contract `CAGCDFLG4WKPYG...` that requires authorization from a smart account `CBH6XACZFDCJUHX...`. This time the smart account will grant authorization only if the Ed25519 public key `2b6bad0cfdb3d4b6f2cd...` has signed, meaning `Map` has to contain one element that is `Signer::External("CDLDYJWEZSM6IAI4...", "2b6bad0cfdb3d4b6f2cd...")` and its signature. + +```mermaid +graph LR + V[Ed25519 Verifier:
CDLDYJWEZSM6IAI4...] + SA1[Smart Account:
CBH6XACZFDCJUHX...] + TC[Target Contract:
CAGCDFLG4WKPYG...] + + TC --> |"require_auth()"| SA1 + SA1 --> |"verify()"| V + + style V fill:#16D9AA1,stroke:#333,stroke-width:2px + style SA1 fill:#E8E8E8,stroke:#333,stroke-width:2px + style TC fill:#C8D9A3,stroke:#333,stroke-width:2px +``` + +In contrast to `Delegated` signers, constructing the auth entry for an `External` signer is straightforward: +```json +{ + "tx": { + // ... + "auth": [ + { + "credentials": { + "address": { + "address": "CBH6XACZFDCJUHX...", // the Smart Account + "nonce": "7346653005027720525", + "signature_expiration_ledger": 1256083, + "signature": { + "vec": [ + { + "map": [ + { + "key": { + "vec": [ + { + "symbol": "External" + }, + { + "address": "CDLDYJWEZSM6IAI4..." // the Ed25519 Verifier + }, + { + "bytes": "2b6bad0cfdb3d4b6f2cd..." // Signer's public key + } + ] + }, + "val": { + "bytes": "6ead27ab6e8cab36..." // Signer's signature + } + } + ] + } + ] + } + } + }, + "root_invocation": { + // ... + } + } + ] + // ... + } +} +``` + +See the [Verifiers](#verifiers) section below for architecture details and the `Verifier` trait that external signers rely on. + +## Signer Management + +The [`SmartAccount`](https://github.com/OpenZeppelin/stellar-contracts/blob/main/packages/accounts/src/smart_account/mod.rs) trait provides functions for managing signers within context rules: + +### Adding Signers + +```rust +fn add_signer( + e: &Env, + context_rule_id: u32, + signer: Signer, +); +``` + +Adds a signer to an existing context rule. The rule must not exceed the maximum of 15 signers. + + +**Important:** +When adding signers to rules with threshold policies, administrators must manually update policy thresholds to maintain security guarantees. See the Policies documentation for details. + + +### Removing Signers + +```rust +fn remove_signer( + e: &Env, + context_rule_id: u32, + signer: Signer, +); +``` + +Removes a signer from an existing context rule. The rule must maintain at least one signer OR one policy after removal. + + +**Important:** +When removing signers from rules with threshold policies, verify that the threshold remains achievable with the remaining signers. + + +## Verifiers + +[Source Code](https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/accounts/src/verifiers) + +Verifiers serve as cryptographic oracles for signature validation: specialized, trusted contracts that validate signatures on behalf of smart accounts. They are the foundation for `External` signers, providing the actual signature verification logic. + +Each signer is represented as a `(verifier_address, public_key)` pair, where the verifier address points to shared verification logic and the public key identifies the specific signer. A single verifier contract can validate signatures for any number of keys. + +This architecture separates verification logic from smart accounts, enabling a clean division of concerns and promoting code reuse across the ecosystem. + +The `Verifier` trait defines the interface for verifier contracts: + +```rust +pub trait Verifier { + type KeyData: FromVal; + type SigData: FromVal; + + /// # Arguments + /// + /// * `e` - Access to the Soroban environment. + /// * `hash` - The hash of the data that was signed (typically 32 bytes). + /// * `key_data` - The public key data in the format expected by this verifier. + /// * `sig_data` - The signature data in the format expected by this verifier. + fn verify(e: &Env, hash: Bytes, key_data: Self::KeyData, sig_data: Self::SigData) -> bool; +} +``` +The trait uses associated types to allow different verifiers to define their own data structures for keys and signatures. + +### Advantages of the Verifier Pattern + +**No Setup Costs**: Once a verifier is deployed, new keys can be used immediately without any on-chain setup or deployment expenses. Users simply reference the verifier address and provide their public key when creating signers. + +**Cryptographic Flexibility**: The verifier pattern supports diverse cryptographic schemes from standard curves to emerging authentication methods like zero-knowledge proofs and email-based signing. Developers can implement custom verifiers for specialized cryptographic schemes. + +**Address-less Keys**: Keys remain separate from account addresses, maintaining clear boundaries between accounts (which hold assets) and the keys that control them. This separation improves security and flexibility. + +**Shared Security**: Verification logic is centralized in well-audited, immutable contracts. The entire network benefits from shared security guarantees, reduced deployment overhead, community-reviewed implementations, and consistent verification behavior. + +**Ecosystem Trust**: Well-known verifier addresses build trust as they are used across many accounts. A small set of thoroughly audited verifiers can serve the entire ecosystem. + +### Implementation Recommendations + +Verifiers logic should be implemented as pure verification functions with the following characteristics: + +- **Stateless**: No internal state that could be manipulated +- **Immutable**: Not upgradeable once deployed, minimizing the trust +- **Deterministic**: Same inputs always produce the same output +- **Efficient**: Optimized for gas costs and performance + +## Example Verifiers + +The "accounts" package provides utility functions for implementing **Ed25519** and **WebAuthn** verifiers: + +### Ed25519 + +[Source Code](https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/accounts/src/verifiers/ed25519.rs) + +The `ed25519` verifier utilities package provides standard Ed25519 signature verification for external signers in smart accounts. This enables using Ed25519 keys that are not native Soroban addresses, such as keys generated by external systems or hardware wallets. + +Ed25519 public keys must be exactly **32 bytes**: + +```rust +let public_key = Bytes::from_array(e, &[ + 0x12, 0x34, 0x56, 0x78, // ... 32 bytes total +]); +``` + +Ed25519 signatures must be exactly **64 bytes**: + +```rust +let signature = Bytes::from_array(e, &[ + 0xab, 0xcd, 0xef, 0x01, // ... 64 bytes total +]); +``` + +### WebAuthn (Passkeys) + +[Source Code](https://github.com/OpenZeppelin/stellar-contracts/blob/main/packages/accounts/src/verifiers/webauthn.rs) + +[WebAuthn (Web Authentication)](https://www.w3.org/TR/webauthn-2/) is a web standard for passwordless authentication that allows users to authenticate using: + +- **Passkeys**: Biometric authentication (Face ID, Touch ID, Windows Hello) +- **Hardware Keys**: Physical security keys (YubiKey, Titan Key) +- **Platform Authenticators**: Built-in device authenticators + +WebAuthn uses secp256r1 (P-256) public keys, which must be exactly **65 bytes** in uncompressed format: + +```rust +let public_key = Bytes::from_array(e, &[ + 0x04, // Uncompressed point indicator + // ... 32 bytes for X coordinate + // ... 32 bytes for Y coordinate +]); +``` + +Unlike simpler signature schemes, WebAuthn signature data is a complex structure that must be XDR-encoded. The signature data contains three components: + +```rust +#[contracttype] +pub struct WebAuthnSigData { + /// The cryptographic signature (64 bytes for secp256r1) + pub signature: BytesN<64>, + /// Raw authenticator data from the WebAuthn response + pub authenticator_data: Bytes, + /// Raw client data JSON from the WebAuthn response + pub client_data: Bytes, +} +``` + +Components: + +- **`signature`**: The secp256r1 signature (64 bytes) +- **`authenticator_data`**: Binary data from the authenticator (minimum 37 bytes), containing: + - RP ID hash (32 bytes) + - Flags byte (1 byte) - indicates user presence, user verification, and backup state + - Signature counter (4 bytes) +- **`client_data`**: JSON string from the browser/platform (max 1024 bytes), containing: + - `type`: Must be `"webauthn.get"` for authentication + - `challenge`: Base64url-encoded signature payload + - `origin`: The origin where authentication occurred + +The `sig_data` parameter passed to the verifier must be the XDR-encoded representation of this structure to ensure proper serialization and deserialization. + +## See Also + +- [Smart Account](/stellar-contracts/accounts/smart-account) +- [Context Rules](/stellar-contracts/accounts/context-rules) +- [Policies](/stellar-contracts/accounts/policies) +- [Authorization Flow](/stellar-contracts/accounts/authorization-flow) diff --git a/content/stellar-contracts/accounts/smart-account.mdx b/content/stellar-contracts/accounts/smart-account.mdx new file mode 100644 index 00000000..adca2c6e --- /dev/null +++ b/content/stellar-contracts/accounts/smart-account.mdx @@ -0,0 +1,51 @@ +--- +title: Smart Accounts +--- + +[Source Code](https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/accounts) + +Smart accounts in Soroban are contracts that manage the composition of authorization intents from multiple sources, such as policies and signing keys from different cryptographic curves. They enable flexible combinations and allow multiple authorization mechanisms to work together seamlessly. + +## Overview + +The [accounts](https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/accounts) package provides a comprehensive smart account framework for Soroban, enabling flexible, programmable authorization. Instead of hard-coding signature checks, smart accounts organize authorization as a composition of context rules, signers, and policies. + +Smart accounts in Soroban implement `CustomAccountInterface` and define authorization as data and behavior that can evolve over time. The framework is context-centric: it distinguishes who is allowed to act (signers), what they are allowed to do (scope), and how those permissions are enforced (policies). + +The framework externalizes some parts of the logic and state to separate contracts. Policies are external contracts that manage enforcement rules and can maintain their own state, while verifiers are external contracts that handle signature validation logic. This separation of concerns enables modularity and flexibility, and allows multiple smart accounts to share well-audited verification and policy logic. Protocol 23 improvements make this modular design practical, with substantially cheaper cross-contract calls enabling efficient composition of multiple external components. + +## Core Components + +The framework separates three distinct concerns: + +- **What** (Context Rules): Defines the scope and conditions +- **Who** (Signers): Identifies the authorized entities +- **How** (Policies): Enforces business logic and constraints + +### Context Rules +Context rules function like routing tables for authorization. For each context, they specify scope, lifetime, and the conditions (signers and policies) that must be satisfied before execution proceeds. + +For detailed documentation, see [Context Rules](/stellar-contracts/accounts/context-rules). + +### Signers and Verifiers +Signers define who can authorize operations. The framework supports both delegated signers (any Soroban address) and external signers that use specialized verifier contracts for signature validation. Verifiers are cryptographic oracles that validate signatures for external signers. + +For detailed documentation, see [Signers and Verifiers](/stellar-contracts/accounts/signers-and-verifiers). + +### Policies +Policies act as enforcement modules attached to context rules. They perform read-only prechecks and can update state to enforce limits or workflows. + +For detailed documentation, see [Policies](/stellar-contracts/accounts/policies). + +This separation allows for clean composition of authorization requirements while maintaining auditability and flexibility. + +## Authorization Flow + +Authorization is determined by matching the current call context against the account's context rules: + +1. **Rule Collection**: Retrieve all non-expired rules for the specific context type and default rules +2. **Rule Evaluation**: For each rule (newest first), authenticate signers and validate policies +3. **Policy Enforcement**: If enough signers are authenticated and policy prechecks succeed, trigger policy state changes +4. **Result**: Grant or deny authorization + +For detailed documentation, see [Authorization Flow](/stellar-contracts/accounts/authorization-flow). diff --git a/content/stellar-contracts/index.mdx b/content/stellar-contracts/index.mdx index bb0c5382..5df31316 100644 --- a/content/stellar-contracts/index.mdx +++ b/content/stellar-contracts/index.mdx @@ -4,6 +4,10 @@ title: Stellar Smart Contracts Suite Explore our comprehensive suite of secure and scalable smart contract utilities for Stellar Soroban. Our libraries provide robust implementations for fungible and non-fungible tokens, along with powerful tools for access control and contract management. +## Accounts + +* **[Smart Account](/stellar-contracts/accounts/smart-account)**: Context-centric framework for Soroban smart accounts composing authorization intents through signers and policies. + ## Tokens * **[Fungible Tokens](/stellar-contracts/tokens/fungible/fungible)**: Digital assets representing a fixed or dynamic supply of identical units. @@ -30,6 +34,7 @@ we use the following convention: * Fungible: `1XX` * Non-Fungible: `2XX` +* Accounts: `3XXX` Any future tokens will continue from `4XX`, `5XX`, and so on. diff --git a/src/navigation/ecosystems.json b/src/navigation/ecosystems.json index 96b753ce..0752df2a 100644 --- a/src/navigation/ecosystems.json +++ b/src/navigation/ecosystems.json @@ -153,6 +153,37 @@ "name": "Get Started", "url": "/stellar-contracts/get-started" }, + { + "type": "folder", + "name": "Accounts", + "index": { + "type": "page", + "name": "Smart Account", + "url": "/stellar-contracts/accounts/smart-account" + }, + "children": [ + { + "type": "page", + "name": "Context Rules", + "url": "/stellar-contracts/accounts/context-rules" + }, + { + "type": "page", + "name": "Signers and Verifiers", + "url": "/stellar-contracts/accounts/signers-and-verifiers" + }, + { + "type": "page", + "name": "Policies", + "url": "/stellar-contracts/accounts/policies" + }, + { + "type": "page", + "name": "Authorization Flow", + "url": "/stellar-contracts/accounts/authorization-flow" + } + ] + }, { "type": "page", "name": "Changelog", diff --git a/src/navigation/stellar.json b/src/navigation/stellar.json index 9b3bea6d..8221daf0 100644 --- a/src/navigation/stellar.json +++ b/src/navigation/stellar.json @@ -13,6 +13,37 @@ "name": "Get Started", "url": "/stellar-contracts/get-started" }, + { + "type": "folder", + "name": "Accounts", + "index": { + "type": "page", + "name": "Smart Account", + "url": "/stellar-contracts/accounts/smart-account" + }, + "children": [ + { + "type": "page", + "name": "Context Rules", + "url": "/stellar-contracts/accounts/context-rules" + }, + { + "type": "page", + "name": "Signers and Verifiers", + "url": "/stellar-contracts/accounts/signers-and-verifiers" + }, + { + "type": "page", + "name": "Policies", + "url": "/stellar-contracts/accounts/policies" + }, + { + "type": "page", + "name": "Authorization Flow", + "url": "/stellar-contracts/accounts/authorization-flow" + } + ] + }, { "type": "folder", "name": "Tokens", From c248e6cbdf64d12cc1a4ad823cb548f19bf938bf Mon Sep 17 00:00:00 2001 From: brozorec <9572072+brozorec@users.noreply.github.com> Date: Mon, 3 Nov 2025 16:46:57 +0100 Subject: [PATCH 2/4] add mermaid charts and apply suggestions --- .../accounts/authorization-flow.mdx | 35 ++++++++++++++ .../accounts/context-rules.mdx | 2 + .../accounts/signers-and-verifiers.mdx | 16 +++---- .../accounts/smart-account.mdx | 48 +++++++++++++++++-- 4 files changed, 88 insertions(+), 13 deletions(-) diff --git a/content/stellar-contracts/accounts/authorization-flow.mdx b/content/stellar-contracts/accounts/authorization-flow.mdx index cc320a3c..8b5ecf63 100644 --- a/content/stellar-contracts/accounts/authorization-flow.mdx +++ b/content/stellar-contracts/accounts/authorization-flow.mdx @@ -5,6 +5,41 @@ 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. ## Detailed Flow +```mermaid +sequenceDiagram + participant User + participant SmartAccount + participant ContextRule + participant DelegateSigner + participant Verifier + participant Policy + + User->>SmartAccount: Signatures + SmartAccount->>ContextRule: Match context
(CallContract, Default, ...) + ContextRule->>ContextRule: Filter expired rules
Sort newest first + + loop Each rule until match + Note over ContextRule,DelegateSigner: Built-in authorization
for delegate signers + ContextRule->>DelegateSigner: require_auth_for_args() + DelegateSigner-->>ContextRule: Authorized + Note over ContextRule,Verifier: Signature verification for external signers + 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 + end + + SmartAccount-->>User: Success +``` ### 1. Rule Collection diff --git a/content/stellar-contracts/accounts/context-rules.mdx b/content/stellar-contracts/accounts/context-rules.mdx index 5878c1aa..6c136584 100644 --- a/content/stellar-contracts/accounts/context-rules.mdx +++ b/content/stellar-contracts/accounts/context-rules.mdx @@ -87,6 +87,8 @@ graph TD SA --- CR2 SA --- CR3 SA --- CR4 + + style SA fill:#E8E8E8,stroke:#333,stroke-width:2px ``` diff --git a/content/stellar-contracts/accounts/signers-and-verifiers.mdx b/content/stellar-contracts/accounts/signers-and-verifiers.mdx index 1952baf0..1196cd97 100644 --- a/content/stellar-contracts/accounts/signers-and-verifiers.mdx +++ b/content/stellar-contracts/accounts/signers-and-verifiers.mdx @@ -39,9 +39,9 @@ graph TD C1 --> SA2 G2 --> SA2 - style G1 fill:#6B9BD1,stroke:#333,stroke-width:2px - style G2 fill:#6B9BD1,stroke:#333,stroke-width:2px - style C1 fill:#F4D03F,stroke:#333,stroke-width:2px + style G1 fill:#6B9BD1 + style G2 fill:#6B9BD1 + style C1 fill:#F4D03F style SA1 fill:#E8E8E8,stroke:#333,stroke-width:2px style SA2 fill:#E8E8E8,stroke:#333,stroke-width:2px style SA3 fill:#E8E8E8,stroke:#333,stroke-width:2px @@ -100,9 +100,9 @@ graph LR TC --> |"require_auth()"| SA1 SA1 --> |"require_auth_for_args()"| G1 - style G1 fill:#6B9BD1,stroke:#333,stroke-width:2px + style G1 fill:#6B9BD1 style SA1 fill:#E8E8E8,stroke:#333,stroke-width:2px - style TC fill:#C8D9A3,stroke:#333,stroke-width:2px + style TC fill:#C8D9A3 ``` When simulating this transaction, the following authorization entry is returned. Note that `"auth"` contains a single element and the delegated signer address (G-account) is not present at all: @@ -376,9 +376,9 @@ graph LR TC --> |"require_auth()"| SA1 SA1 --> |"verify()"| V - style V fill:#16D9AA1,stroke:#333,stroke-width:2px + style V fill:#16D9AA1 style SA1 fill:#E8E8E8,stroke:#333,stroke-width:2px - style TC fill:#C8D9A3,stroke:#333,stroke-width:2px + style TC fill:#C8D9A3 ``` In contrast to `Delegated` signers, constructing the auth entry for an `External` signer is straightforward: @@ -501,7 +501,7 @@ The trait uses associated types to allow different verifiers to define their own ### Advantages of the Verifier Pattern -**No Setup Costs**: Once a verifier is deployed, new keys can be used immediately without any on-chain setup or deployment expenses. Users simply reference the verifier address and provide their public key when creating signers. +**No Setup Costs**: Once a verifier is deployed, new keys can be used immediately without any on-chain setup. Users simply reference the verifier address and provide their public key when creating signers. **Cryptographic Flexibility**: The verifier pattern supports diverse cryptographic schemes from standard curves to emerging authentication methods like zero-knowledge proofs and email-based signing. Developers can implement custom verifiers for specialized cryptographic schemes. diff --git a/content/stellar-contracts/accounts/smart-account.mdx b/content/stellar-contracts/accounts/smart-account.mdx index adca2c6e..cd048280 100644 --- a/content/stellar-contracts/accounts/smart-account.mdx +++ b/content/stellar-contracts/accounts/smart-account.mdx @@ -4,15 +4,15 @@ title: Smart Accounts [Source Code](https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/accounts) -Smart accounts in Soroban are contracts that manage the composition of authorization intents from multiple sources, such as policies and signing keys from different cryptographic curves. They enable flexible combinations and allow multiple authorization mechanisms to work together seamlessly. +Smart accounts are contracts that manage the composition of authorization intents coming from multiple sources, such as policies, signing keys from different cryptographic curves or other Soroban accounts. This lays the ground for flexible combinations where multiple authorization mechanisms work together seamlessly. ## Overview -The [accounts](https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/accounts) package provides a comprehensive smart account framework for Soroban, enabling flexible, programmable authorization. Instead of hard-coding signature checks, smart accounts organize authorization as a composition of context rules, signers, and policies. +The [accounts](https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/accounts) package provides a comprehensive smart account framework, enabling programmable authorization. Rather than hard-coding signature checks directly into the account contract, this framework organizes authorization as a composition of three core elements: context rules, signers, and policies. -Smart accounts in Soroban implement `CustomAccountInterface` and define authorization as data and behavior that can evolve over time. The framework is context-centric: it distinguishes who is allowed to act (signers), what they are allowed to do (scope), and how those permissions are enforced (policies). +To achieve this composability, smart accounts implement the `CustomAccountInterface` and define authorization as data and behavior that can evolve over time. The framework takes a context-centric approach, separating three distinct concerns: who is allowed to act (signers), what they are allowed to do (scope or context rules), and how those permissions are enforced (policies). -The framework externalizes some parts of the logic and state to separate contracts. Policies are external contracts that manage enforcement rules and can maintain their own state, while verifiers are external contracts that handle signature validation logic. This separation of concerns enables modularity and flexibility, and allows multiple smart accounts to share well-audited verification and policy logic. Protocol 23 improvements make this modular design practical, with substantially cheaper cross-contract calls enabling efficient composition of multiple external components. +This separation is made practical by externalizing parts of the logic and state to dedicated contracts. Specifically, policies are external contracts that enforce constraints and can maintain their own state, while verifiers are external contracts that handle signature validation logic. This modular architecture enables flexibility and allows multiple smart accounts to share well-audited verification and policy logic. The Protocol 23 improvements make this design efficient, with substantially cheaper cross-contract calls enabling practical composition of multiple external contracts. ## Core Components @@ -22,13 +22,51 @@ The framework separates three distinct concerns: - **Who** (Signers): Identifies the authorized entities - **How** (Policies): Enforces business logic and constraints +```mermaid +graph LR + P1((Policy)) --- CR1 + P2((Policy)) --- CR2 + P2((Policy)) --- CR3 + P3((Policy)) --- CR3 + + subgraph SA[Smart Account] + CR1[Context Rule] + CR2[Context Rule] + CR3[Context Rule] + end + + S1["Delegated Signer
G-account"] + S2["Delegated Signer
G-account"] + S3["Delegated Signer
C-account"] + S4["External Signer
Verifier Contract || Pubkey"] + S5["External Signer
Verifier Contract || Pubkey"] + + CR1 --- S1 + CR1 --- S2 + CR2 --- S3 + CR3 --- S4 + CR3 --- S5 + + style SA fill:#E8E8E8,stroke:#333,stroke-width:2px + style S1 fill:#6B9BD1 + style S2 fill:#6B9BD1 + style S3 fill:#F4D03F + style S4 fill:#A9DA9B + style S5 fill:#A9DA9B +``` + ### Context Rules Context rules function like routing tables for authorization. For each context, they specify scope, lifetime, and the conditions (signers and policies) that must be satisfied before execution proceeds. +#### Examples +1. Subscription: A dapp public key (signer) can withdraw 100 USDC every month (policy) for one year (lifetime) +2. Deployment: 2-of-3 (policy) Ed25519 keys (signers) can deploy a new contract +3. Vote: 3-of-3 P256 keys (signers) can cast a vote at a specific voting contract + For detailed documentation, see [Context Rules](/stellar-contracts/accounts/context-rules). ### Signers and Verifiers -Signers define who can authorize operations. The framework supports both delegated signers (any Soroban address) and external signers that use specialized verifier contracts for signature validation. Verifiers are cryptographic oracles that validate signatures for external signers. +Signers define who can authorize operations. The framework supports both **delegated** signers (any Soroban address) and **external** signers that use specialized verifier contracts for signature validation. Verifiers can be seen as some sort of cryptographic oracles that validate signatures for external signers. For detailed documentation, see [Signers and Verifiers](/stellar-contracts/accounts/signers-and-verifiers). From a5f637decf35340281cddcb3d2b469dd5ec1f1f6 Mon Sep 17 00:00:00 2001 From: brozorec <9572072+brozorec@users.noreply.github.com> Date: Mon, 3 Nov 2025 16:52:45 +0100 Subject: [PATCH 3/4] typo --- .../stellar-contracts/accounts/authorization-flow.mdx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/content/stellar-contracts/accounts/authorization-flow.mdx b/content/stellar-contracts/accounts/authorization-flow.mdx index 8b5ecf63..1ae843ea 100644 --- a/content/stellar-contracts/accounts/authorization-flow.mdx +++ b/content/stellar-contracts/accounts/authorization-flow.mdx @@ -10,7 +10,7 @@ sequenceDiagram participant User participant SmartAccount participant ContextRule - participant DelegateSigner + participant DelegatedSigner participant Verifier participant Policy @@ -19,12 +19,14 @@ sequenceDiagram ContextRule->>ContextRule: Filter expired rules
Sort newest first loop Each rule until match - Note over ContextRule,DelegateSigner: Built-in authorization
for delegate signers - ContextRule->>DelegateSigner: require_auth_for_args() - DelegateSigner-->>ContextRule: Authorized + Note over ContextRule,DelegatedSigner: Built-in authorization
for delegated signers + ContextRule->>DelegatedSigner: require_auth_for_args() + DelegatedSigner-->>ContextRule: Authorized + Note over ContextRule,Verifier: Signature verification for external signers ContextRule->>Verifier: verify() Verifier-->>ContextRule: Valid + Note over ContextRule,Policy: Policy pre-checks ContextRule->>Policy: can_enforce() Policy-->>ContextRule: True/False From 4d25460f64f9b7434cc2617a232c2db257becfd1 Mon Sep 17 00:00:00 2001 From: brozorec <9572072+brozorec@users.noreply.github.com> Date: Mon, 3 Nov 2025 17:52:24 +0100 Subject: [PATCH 4/4] add some eg in initial overview --- content/stellar-contracts/accounts/smart-account.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/content/stellar-contracts/accounts/smart-account.mdx b/content/stellar-contracts/accounts/smart-account.mdx index cd048280..e236be6f 100644 --- a/content/stellar-contracts/accounts/smart-account.mdx +++ b/content/stellar-contracts/accounts/smart-account.mdx @@ -18,9 +18,9 @@ This separation is made practical by externalizing parts of the logic and state The framework separates three distinct concerns: -- **What** (Context Rules): Defines the scope and conditions -- **Who** (Signers): Identifies the authorized entities -- **How** (Policies): Enforces business logic and constraints +- **What** (Context Rules): Defines the scope (e.g. call `transfer()` on some token, call any function of a specific contract, deploy contracts, or just any call to any contract) +- **Who** (Signers): Identifies the authorized entities (e.g. cryptographic keys, G-accounts, or other C-accounts) +- **How** (Policies): Enforces business logic and constraints (e.g. amount < 500, every month, or 2-of-3 threshold) ```mermaid graph LR