From efb7794c369068538c0a890c5abc6d86fe8350bf Mon Sep 17 00:00:00 2001
From: brozorec <9572072+brozorec@users.noreply.github.com>
Date: Thu, 26 Mar 2026 16:07:26 +0100
Subject: [PATCH 1/9] access control
---
content/stellar-contracts/access/access-control.mdx | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/content/stellar-contracts/access/access-control.mdx b/content/stellar-contracts/access/access-control.mdx
index aa5c7230..9cc40a4d 100644
--- a/content/stellar-contracts/access/access-control.mdx
+++ b/content/stellar-contracts/access/access-control.mdx
@@ -53,10 +53,10 @@ 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) {
@@ -64,11 +64,11 @@ impl MyContract {
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);
}
}
```
From 382bfbaaced9695e475572f0b0f10ecee2b9445a Mon Sep 17 00:00:00 2001
From: brozorec <9572072+brozorec@users.noreply.github.com>
Date: Thu, 2 Apr 2026 12:47:41 +0200
Subject: [PATCH 2/9] update sa docs
---
.../accounts/authorization-flow.mdx | 106 +++------
.../accounts/context-rules.mdx | 27 ++-
.../stellar-contracts/accounts/policies.mdx | 78 +++----
.../accounts/signers-and-verifiers.mdx | 220 +++++++++++++-----
.../accounts/smart-account.mdx | 7 +-
5 files changed, 240 insertions(+), 198 deletions(-)
diff --git a/content/stellar-contracts/accounts/authorization-flow.mdx b/content/stellar-contracts/accounts/authorization-flow.mdx
index 1ae843ea..2fd6fc60 100644
--- a/content/stellar-contracts/accounts/authorization-flow.mdx
+++ b/content/stellar-contracts/accounts/authorization-flow.mdx
@@ -2,7 +2,7 @@
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 off-chain client specifies which rule to use for each operation via `context_rule_ids` in the `AuthPayload`. The selected rule is validated, its signers authenticated, and its policies enforced. If any step fails, authorization is denied.
## Detailed Flow
```mermaid
@@ -14,11 +14,13 @@ sequenceDiagram
participant Verifier
participant Policy
- User->>SmartAccount: Signatures
- SmartAccount->>ContextRule: Match context
(CallContract, Default, ...)
- ContextRule->>ContextRule: Filter expired rules
Sort newest first
+ User->>SmartAccount: AuthPayload (signers + context_rule_ids)
+ SmartAccount->>SmartAccount: Compute auth_digest
sha256(payload || rule_ids.to_xdr())
+
+ loop Each auth context
+ SmartAccount->>ContextRule: Look up rule by ID
from context_rule_ids
+ ContextRule->>ContextRule: Validate not expired
and matches context type
- loop Each rule until match
Note over ContextRule,DelegatedSigner: Built-in authorization
for delegated signers
ContextRule->>DelegatedSigner: require_auth_for_args()
DelegatedSigner-->>ContextRule: Authorized
@@ -27,37 +29,25 @@ 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
@@ -72,51 +62,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.
@@ -151,13 +123,13 @@ 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).
@@ -188,10 +160,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
+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
The expired session rule is automatically filtered out, and authorization falls back to the default admin rule.
@@ -213,12 +184,11 @@ 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
diff --git a/content/stellar-contracts/accounts/context-rules.mdx b/content/stellar-contracts/accounts/context-rules.mdx
index 6c136584..d41146f2 100644
--- a/content/stellar-contracts/accounts/context-rules.mdx
+++ b/content/stellar-contracts/accounts/context-rules.mdx
@@ -44,28 +44,27 @@ 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
## 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).
@@ -101,6 +100,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),
@@ -111,7 +111,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,
@@ -120,6 +119,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)
@@ -128,7 +128,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,
@@ -137,6 +136,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)
@@ -146,7 +146,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,
@@ -155,6 +154,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)
@@ -163,7 +163,6 @@ smart_account::add_context_rule(
e,
(volume_cap_policy, volume_cap_params)
],
- Some(current_ledger + 12_hours)
);
```
diff --git a/content/stellar-contracts/accounts/policies.mdx b/content/stellar-contracts/accounts/policies.mdx
index 33572eec..aa01c174 100644
--- a/content/stellar-contracts/accounts/policies.mdx
+++ b/content/stellar-contracts/accounts/policies.mdx
@@ -6,7 +6,7 @@ title: 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.
+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 succeed** (i.e., `enforce` must not panic) for the rule to be considered matched and authorized.
## The Policy Trait
@@ -16,42 +16,32 @@ All policies must implement the `Policy` trait:
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
+ /// Validates authorization conditions and applies state changes.
+ /// Called when a context rule matches during the authorization flow.
+ /// Must panic if policy conditions aren't satisfied.
+ /// Requires smart account authorization.
fn enforce(
e: &Env,
context: Context,
authenticated_signers: Vec,
- rule: ContextRule,
+ context_rule: ContextRule,
smart_account: Address,
);
-
- /// Initialize policy-specific storage and configuration
- /// Called when a new context rule with attached policies is created
+
+ /// 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,
+ install_params: Self::AccountParams,
+ context_rule: ContextRule,
smart_account: Address,
);
-
- /// Clean up policy data when removed
- /// Called when a context rule is removed
+
+ /// Clean up policy data when removed.
+ /// Called when a context rule is removed.
fn uninstall(
e: &Env,
- rule: ContextRule,
+ context_rule: ContextRule,
smart_account: Address,
);
}
@@ -59,7 +49,7 @@ pub trait Policy {
## Policy Lifecycle
-The four trait methods form a complete lifecycle for policy management:
+The three trait methods form a complete lifecycle for policy management:
### Installation
@@ -71,28 +61,18 @@ This initialization step allows policies to configure their logic. For example:
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.
+Enforcement is triggered when a context rule matches during the authorization flow. The smart account calls `enforce()` on each policy attached to the matched rule. The `enforce()` method both validates conditions and applies state changes in a single call — it must panic if the policy conditions are not satisfied.
-This state-changing hook allows policies to:
-- Update counters
-- Emit events
-- Record timestamps
-- Track authorization activity
+This allows policies to:
+- Verify that a spending limit has not been exceeded
+- Check that enough signers are present
+- Validate that time-based restrictions are met
+- Update counters and record timestamps
+- Emit events for auditing
-For example, a spending limit policy might deduct from the available balance and emit an event documenting the transaction.
+For example, a spending limit policy checks that the transaction amount is within the allowed limit, deducts from the available balance, and emits an event documenting the transaction. If the limit is exceeded, the policy panics and the authorization flow moves to the next context rule.
### Uninstallation
@@ -138,11 +118,11 @@ fn add_policy(
e: &Env,
context_rule_id: u32,
policy: Address,
- account_params: Val,
-);
+ install_param: Val,
+) -> u32;
```
-Adds a policy to an existing context rule and calls its `install()` function. The rule must not exceed the maximum of 5 policies.
+Adds a policy to an existing context rule, calls its `install()` function, and returns the policy's numeric ID. The rule must not exceed the maximum of 5 policies.
### Removing Policies
@@ -150,11 +130,11 @@ Adds a policy to an existing context rule and calls its `install()` function. Th
fn remove_policy(
e: &Env,
context_rule_id: u32,
- policy: Address,
+ policy_id: u32,
);
```
-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.
+Removes a policy (identified by its numeric ID) from an existing context rule and calls its `uninstall()` function. The rule must maintain at least one signer OR one policy after removal.
### Caveats
diff --git a/content/stellar-contracts/accounts/signers-and-verifiers.mdx b/content/stellar-contracts/accounts/signers-and-verifiers.mdx
index 1196cd97..4efa76c7 100644
--- a/content/stellar-contracts/accounts/signers-and-verifiers.mdx
+++ b/content/stellar-contracts/accounts/signers-and-verifiers.mdx
@@ -60,7 +60,10 @@ When `require_auth_for_args` is called from within `__check_auth` (as with deleg
```rust
#[contracttype]
-pub struct Signatures(pub Map);
+pub struct AuthPayload {
+ pub signers: Map,
+ pub context_rule_ids: Vec,
+}
#[contract]
pub struct MySmartAccount;
@@ -70,10 +73,10 @@ impl CustomAccountInterface for MySmartAccount {
fn __check_auth(
e: Env,
payload: Hash<32>,
- signatures: Signatures,
+ signatures: AuthPayload,
auth_contexts: Vec,
) -> Result<(), SmartAccountError> {
- for (signer, _) in signatures.0.iter() {
+ for (signer, _) in signatures.signers.iter() {
match signer {
// ...
Signer::Delegated(addr) => {
@@ -89,7 +92,7 @@ impl CustomAccountInterface for MySmartAccount {
**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...")`.
+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 the `signers` map in `AuthPayload` has to contain one element that is `Signer::Delegated("GBDZXYMJ3SLYXCY...")`.
```mermaid
graph LR
@@ -139,7 +142,7 @@ When simulating this transaction, the following authorization entry is returned.
The client implementation requires constructing two authorization entries:
-1. Replace `"signature": "void"` with the proper `Signatures(Map)` structure
+1. Replace `"signature": "void"` with the proper `AuthPayload` structure (containing the `signers` map and `context_rule_ids`)
2. Create the missing authorization entry for the delegated signer's `__check_auth` call
The following typescript code demonstrates this process:
@@ -164,7 +167,7 @@ async function signAndSendTx(
const signedAuths: SorobanAuthorizationEntry[] = [];
- // 1) Construct the 1st auth entry: `Signatures(pub Map)` with `Signer::Delegated(Address)`
+ // 1) Construct the 1st auth entry: `AuthPayload { signers, context_rule_ids }`
const sigInnerMap = ScVal.scvMap([
new xdr.ScMapEntry({
key: ScVal.scvVec([
@@ -175,7 +178,19 @@ async function signAndSendTx(
}),
]);
- simAuth.credentials().address().signature(ScVal.scvVec([sigInnerMap]));
+ // AuthPayload is a named-field struct serialized as ScVal::Map
+ const authPayload = ScVal.scvMap([
+ new xdr.ScMapEntry({
+ key: ScVal.scvSymbol("context_rule_ids"),
+ val: ScVal.scvVec([ScVal.scvU32(ruleId)]), // one rule ID per auth context
+ }),
+ new xdr.ScMapEntry({
+ key: ScVal.scvSymbol("signers"),
+ val: sigInnerMap,
+ }),
+ ]);
+
+ simAuth.credentials().address().signature(authPayload);
simAuth.credentials().address().signatureExpirationLedger(validUntil);
signedAuths.push(simAuth);
@@ -227,25 +242,28 @@ After including both authorization entries in `signedAuths` and re-simulating th
"nonce": "7346653005027720525",
"signature_expiration_ledger": 1256083,
"signature": {
- "vec": [
+ "map": [
{
- "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`)
+ "key": { "symbol": "context_rule_ids" },
+ "val": { "vec": [{ "u32": 1 }] } // one rule ID per auth context
+ },
+ {
+ "key": { "symbol": "signers" },
+ "val": {
+ "map": [
+ {
+ "key": {
+ "vec": [
+ { "symbol": "Delegated" },
+ { "address": "GBDZXYMJ3SLYXCY..." } // the delegated signer (G-account)
+ ]
+ },
+ "val": {
+ "bytes": "" // `Bytes` value is empty for delegated signers (used only for `Signer::External`)
+ }
}
- }
- ]
+ ]
+ }
}
]
}
@@ -326,7 +344,10 @@ In the case of external signers verification is offloaded to specialized verifie
```rust
#[contracttype]
-pub struct Signatures(pub Map);
+pub struct AuthPayload {
+ pub signers: Map,
+ pub context_rule_ids: Vec,
+}
#[contract]
pub struct MySmartAccount;
@@ -336,10 +357,10 @@ impl CustomAccountInterface for MySmartAccount {
fn __check_auth(
e: Env,
payload: Hash<32>,
- signatures: Signatures,
+ signatures: AuthPayload,
auth_contexts: Vec,
) -> Result<(), SmartAccountError> {
- for (signer, sig_data) in signatures.0.iter() {
+ for (signer, sig_data) in signatures.signers.iter() {
match signer {
Signer::External(verifier, key_data) => {
let sig_payload = Bytes::from_array(e, &signature_payload.to_bytes().to_array());
@@ -365,7 +386,7 @@ This separation provides forward compatibility: when new cryptographic curves ar
**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.
+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 the `signers` map in `AuthPayload` has to contain one element that is `Signer::External("CDLDYJWEZSM6IAI4...", "2b6bad0cfdb3d4b6f2cd...")` and its signature.
```mermaid
graph LR
@@ -394,28 +415,29 @@ In contrast to `Delegated` signers, constructing the auth entry for an `External
"nonce": "7346653005027720525",
"signature_expiration_ledger": 1256083,
"signature": {
- "vec": [
+ "map": [
{
- "map": [
- {
- "key": {
- "vec": [
- {
- "symbol": "External"
- },
- {
- "address": "CDLDYJWEZSM6IAI4..." // the Ed25519 Verifier
- },
- {
- "bytes": "2b6bad0cfdb3d4b6f2cd..." // Signer's public key
- }
- ]
- },
- "val": {
- "bytes": "6ead27ab6e8cab36..." // Signer's signature
+ "key": { "symbol": "context_rule_ids" },
+ "val": { "vec": [{ "u32": 2 }] } // one rule ID per auth context
+ },
+ {
+ "key": { "symbol": "signers" },
+ "val": {
+ "map": [
+ {
+ "key": {
+ "vec": [
+ { "symbol": "External" },
+ { "address": "CDLDYJWEZSM6IAI4..." }, // the Ed25519 Verifier
+ { "bytes": "2b6bad0cfdb3d4b6f2cd..." } // Signer's public key
+ ]
+ },
+ "val": {
+ "bytes": "6ead27ab6e8cab36..." // Signer's signature
+ }
}
- }
- ]
+ ]
+ }
}
]
}
@@ -433,6 +455,75 @@ In contrast to `Delegated` signers, constructing the auth entry for an `External
See the [Verifiers](#verifiers) section below for architecture details and the `Verifier` trait that external signers rely on.
+## AuthPayload and Off-Chain Clients
+
+The `AuthPayload` struct is the signature type passed to `__check_auth`:
+
+```rust
+#[contracttype]
+pub struct AuthPayload {
+ pub signers: Map,
+ pub context_rule_ids: Vec,
+}
+```
+
+The `context_rule_ids` field contains one rule ID per auth context, explicitly selecting which stored context rule applies to each operation. No rule iteration or auto-discovery is performed — clients must know which rule to reference. A mismatch between the length of `context_rule_ids` and the number of auth contexts is rejected with `ContextRuleIdsLengthMismatch`.
+
+### Auth Digest Computation
+
+Signers must sign the **auth digest**, not the raw `signature_payload`. The context rule IDs are bound into the digest to prevent downgrade attacks:
+
+```
+auth_digest = sha256(signature_payload || context_rule_ids.to_xdr())
+```
+
+Steps:
+1. Obtain the 32-byte `signature_payload` from the host
+2. XDR-encode the `Vec` of rule IDs (one per auth context)
+3. Concatenate both byte sequences
+4. SHA-256 hash the result
+5. Sign the resulting digest
+
+In Rust (Soroban SDK):
+
+```rust
+use soroban_sdk::xdr::ToXdr;
+
+let mut preimage = signature_payload.to_bytes().to_bytes();
+preimage.append(&context_rule_ids.to_xdr(e));
+let auth_digest = e.crypto().sha256(&preimage);
+// Sign auth_digest, not signature_payload
+```
+
+### XDR Encoding
+
+The `AuthPayload` serializes as an `ScVal::Map` with two `Symbol`-keyed entries:
+
+```rust
+ScVal::Map([
+ (Symbol("context_rule_ids"), ScVal::Vec(/* rule IDs, one per auth context */)),
+ (Symbol("signers"), ScVal::Map(/* signer → signature entries */)),
+])
+```
+
+Using `stellar_xdr`:
+
+```rust
+creds.signature = ScVal::Map(Some(ScMap::sorted_from([
+ (
+ ScVal::Symbol("context_rule_ids".try_into()?),
+ ScVal::Vec(Some(ScVec(VecM::try_from([
+ ScVal::U32(rule_id_for_context_0),
+ ScVal::U32(rule_id_for_context_1),
+ ])?))),
+ ),
+ (
+ ScVal::Symbol("signers".try_into()?),
+ sig_map, // Map entries
+ ),
+])?));
+```
+
## 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:
@@ -444,13 +535,13 @@ fn add_signer(
e: &Env,
context_rule_id: u32,
signer: Signer,
-);
+) -> u32;
```
-Adds a signer to an existing context rule. The rule must not exceed the maximum of 15 signers.
+Adds a signer to an existing context rule and returns the signer's numeric ID. The rule must not exceed the maximum of 15 signers.
-**Important:**
+**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.
@@ -460,11 +551,11 @@ When adding signers to rules with threshold policies, administrators must manual
fn remove_signer(
e: &Env,
context_rule_id: u32,
- signer: Signer,
+ signer_id: u32,
);
```
-Removes a signer from an existing context rule. The rule must maintain at least one signer OR one policy after removal.
+Removes a signer (identified by its numeric ID) from an existing context rule. The rule must maintain at least one signer OR one policy after removal.
**Important:**
@@ -485,19 +576,22 @@ 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.
+ type KeyData: FromVal;
+ type SigData: FromVal;
+
+ /// Validates a signature against a hash and public key.
+ /// Must use constant-time operations and validate all inputs.
fn verify(e: &Env, hash: Bytes, key_data: Self::KeyData, sig_data: Self::SigData) -> bool;
+
+ /// Maps key data to a unique canonical byte representation.
+ /// Used to detect duplicate cryptographic identities across different byte encodings.
+ fn canonicalize_key(e: &Env, key_data: Self::KeyData) -> Bytes;
+
+ /// Batch variant of `canonicalize_key` for processing multiple keys in a single call.
+ fn batch_canonicalize_key(e: &Env, key_data: Vec) -> Vec;
}
```
-The trait uses associated types to allow different verifiers to define their own data structures for keys and signatures.
+The trait uses associated types to allow different verifiers to define their own data structures for keys and signatures. The `canonicalize_key` methods enable the framework to detect duplicate signers even when the same cryptographic identity is represented by different byte sequences.
### Advantages of the Verifier Pattern
diff --git a/content/stellar-contracts/accounts/smart-account.mdx b/content/stellar-contracts/accounts/smart-account.mdx
index e236be6f..5d39864e 100644
--- a/content/stellar-contracts/accounts/smart-account.mdx
+++ b/content/stellar-contracts/accounts/smart-account.mdx
@@ -81,9 +81,8 @@ This separation allows for clean composition of authorization requirements while
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
+1. **Rule Lookup**: Look up the context rule specified by the client via `context_rule_ids` in the `AuthPayload`
+2. **Rule Evaluation**: Validate the rule matches the context, authenticate signers, and enforce policies (policies panic on failure)
+3. **Result**: Grant or deny authorization
For detailed documentation, see [Authorization Flow](/stellar-contracts/accounts/authorization-flow).
From a3a8151ab33e20c034571364654d551057240c08 Mon Sep 17 00:00:00 2001
From: Boyan Barakov <9572072+brozorec@users.noreply.github.com>
Date: Thu, 2 Apr 2026 13:02:38 +0200
Subject: [PATCH 3/9] Update
content/stellar-contracts/accounts/smart-account.mdx
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
content/stellar-contracts/accounts/smart-account.mdx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/content/stellar-contracts/accounts/smart-account.mdx b/content/stellar-contracts/accounts/smart-account.mdx
index 5d39864e..2233cc4d 100644
--- a/content/stellar-contracts/accounts/smart-account.mdx
+++ b/content/stellar-contracts/accounts/smart-account.mdx
@@ -79,7 +79,7 @@ This separation allows for clean composition of authorization requirements while
## Authorization Flow
-Authorization is determined by matching the current call context against the account's context rules:
+Authorization is determined per authorization context by evaluating the specific context rule explicitly selected by the client:
1. **Rule Lookup**: Look up the context rule specified by the client via `context_rule_ids` in the `AuthPayload`
2. **Rule Evaluation**: Validate the rule matches the context, authenticate signers, and enforce policies (policies panic on failure)
From 6e8fba660f17dc4165363f121867650579257d9b Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 2 Apr 2026 11:03:58 +0000
Subject: [PATCH 4/9] fix: define ruleId as function parameter in TypeScript
example
Agent-Logs-Url: https://github.com/OpenZeppelin/docs/sessions/1cbe0b8d-1816-4e44-b637-714b416168f1
Co-authored-by: brozorec <9572072+brozorec@users.noreply.github.com>
---
content/stellar-contracts/accounts/signers-and-verifiers.mdx | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/content/stellar-contracts/accounts/signers-and-verifiers.mdx b/content/stellar-contracts/accounts/signers-and-verifiers.mdx
index 4efa76c7..3f08e5f3 100644
--- a/content/stellar-contracts/accounts/signers-and-verifiers.mdx
+++ b/content/stellar-contracts/accounts/signers-and-verifiers.mdx
@@ -152,7 +152,8 @@ async function signAndSendTx(
contract: string,
fnName: string,
fnArgs: ScVal[],
- signer: Keypair
+ signer: Keypair,
+ ruleId: number, // ID of the context rule to use for this authorization (e.g., obtained when the rule was created)
) {
const baseTx = new TransactionBuilder(...)
.addOperation(
From dacc11784c0fb65e190dc2849dad00ba0ecc2a67 Mon Sep 17 00:00:00 2001
From: Boyan Barakov <9572072+brozorec@users.noreply.github.com>
Date: Thu, 2 Apr 2026 15:08:51 +0200
Subject: [PATCH 5/9] Update content/stellar-contracts/accounts/policies.mdx
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
content/stellar-contracts/accounts/policies.mdx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/content/stellar-contracts/accounts/policies.mdx b/content/stellar-contracts/accounts/policies.mdx
index aa01c174..9ca34dca 100644
--- a/content/stellar-contracts/accounts/policies.mdx
+++ b/content/stellar-contracts/accounts/policies.mdx
@@ -72,7 +72,7 @@ This allows policies to:
- Update counters and record timestamps
- Emit events for auditing
-For example, a spending limit policy checks that the transaction amount is within the allowed limit, deducts from the available balance, and emits an event documenting the transaction. If the limit is exceeded, the policy panics and the authorization flow moves to the next context rule.
+For example, a spending limit policy checks that the transaction amount is within the allowed limit, deducts from the available balance, and emits an event documenting the transaction. If the limit is exceeded, the policy panics, authorization under that context rule fails, and the transaction reverts; to try a different authorization path, the caller must explicitly select a different context rule in a new attempt.
### Uninstallation
From e3403aa0ec5dd3bab25859b107ff983c05e50a00 Mon Sep 17 00:00:00 2001
From: Boyan Barakov <9572072+brozorec@users.noreply.github.com>
Date: Thu, 2 Apr 2026 15:09:19 +0200
Subject: [PATCH 6/9] Update
content/stellar-contracts/accounts/authorization-flow.mdx
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
content/stellar-contracts/accounts/authorization-flow.mdx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/content/stellar-contracts/accounts/authorization-flow.mdx b/content/stellar-contracts/accounts/authorization-flow.mdx
index 2fd6fc60..5bb3b590 100644
--- a/content/stellar-contracts/accounts/authorization-flow.mdx
+++ b/content/stellar-contracts/accounts/authorization-flow.mdx
@@ -164,7 +164,7 @@ ContextRule {
2. Evaluate Rule 1: Both Alice and Bob authenticated, no policies to enforce → Success
3. Result: Authorized
-The expired session rule is automatically filtered out, and authorization falls back to the default admin rule.
+The client ignores the expired session rule and explicitly selects the default admin rule (ID 1) in `context_rule_ids`.
### Authorization Failure
From 32d4aade8491ce72ca1a68017a0040fe2bba4595 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 2 Apr 2026 13:11:41 +0000
Subject: [PATCH 7/9] fix: rename signatures to auth_payload in Rust example;
remove per-account context rule limit
Agent-Logs-Url: https://github.com/OpenZeppelin/docs/sessions/91349b50-f0e5-4db1-9cf8-d9eb2cb1fca1
Co-authored-by: brozorec <9572072+brozorec@users.noreply.github.com>
---
content/stellar-contracts/accounts/authorization-flow.mdx | 1 -
content/stellar-contracts/accounts/signers-and-verifiers.mdx | 4 ++--
2 files changed, 2 insertions(+), 3 deletions(-)
diff --git a/content/stellar-contracts/accounts/authorization-flow.mdx b/content/stellar-contracts/accounts/authorization-flow.mdx
index 5bb3b590..55777347 100644
--- a/content/stellar-contracts/accounts/authorization-flow.mdx
+++ b/content/stellar-contracts/accounts/authorization-flow.mdx
@@ -197,7 +197,6 @@ Protocol 23 optimizations make the authorization flow efficient:
- **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
diff --git a/content/stellar-contracts/accounts/signers-and-verifiers.mdx b/content/stellar-contracts/accounts/signers-and-verifiers.mdx
index 3f08e5f3..45398555 100644
--- a/content/stellar-contracts/accounts/signers-and-verifiers.mdx
+++ b/content/stellar-contracts/accounts/signers-and-verifiers.mdx
@@ -73,10 +73,10 @@ impl CustomAccountInterface for MySmartAccount {
fn __check_auth(
e: Env,
payload: Hash<32>,
- signatures: AuthPayload,
+ auth_payload: AuthPayload,
auth_contexts: Vec,
) -> Result<(), SmartAccountError> {
- for (signer, _) in signatures.signers.iter() {
+ for (signer, _) in auth_payload.signers.iter() {
match signer {
// ...
Signer::Delegated(addr) => {
From 171094cc96057c7a6ec453c531d0e7d90d8bbe70 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 2 Apr 2026 13:17:17 +0000
Subject: [PATCH 8/9] fix: update grant_role_no_auth argument order in
sac-admin-wrapper.mdx and rwa.mdx
Agent-Logs-Url: https://github.com/OpenZeppelin/docs/sessions/8e710b20-eb02-4301-8f1e-caa29f43322e
Co-authored-by: brozorec <9572072+brozorec@users.noreply.github.com>
---
.../stellar-contracts/tokens/fungible/sac-admin-wrapper.mdx | 4 ++--
content/stellar-contracts/tokens/rwa/rwa.mdx | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/content/stellar-contracts/tokens/fungible/sac-admin-wrapper.mdx b/content/stellar-contracts/tokens/fungible/sac-admin-wrapper.mdx
index 9bec9a6f..9e21b2c2 100644
--- a/content/stellar-contracts/tokens/fungible/sac-admin-wrapper.mdx
+++ b/content/stellar-contracts/tokens/fungible/sac-admin-wrapper.mdx
@@ -57,10 +57,10 @@ impl ExampleContract {
access_control::set_admin(e, &default_admin);
// create a role "manager" and grant it to `manager1`
- access_control::grant_role_no_auth(e, &default_admin, &manager1, &symbol_short!("manager"));
+ access_control::grant_role_no_auth(e, &manager1, &symbol_short!("manager"), &default_admin);
// grant it to `manager2`
- access_control::grant_role_no_auth(e, &default_admin, &manager2, &symbol_short!("manager"));
+ access_control::grant_role_no_auth(e, &manager2, &symbol_short!("manager"), &default_admin);
fungible::sac_admin_wrapper::set_sac_address(e, &sac);
}
diff --git a/content/stellar-contracts/tokens/rwa/rwa.mdx b/content/stellar-contracts/tokens/rwa/rwa.mdx
index b208b777..4684020c 100644
--- a/content/stellar-contracts/tokens/rwa/rwa.mdx
+++ b/content/stellar-contracts/tokens/rwa/rwa.mdx
@@ -141,7 +141,7 @@ impl RealEstateToken {
access_control::set_admin(e, &admin);
// Create a "manager" role and grant it to the manager address
- access_control::grant_role_no_auth(e, &admin, &manager, &symbol_short!("manager"));
+ access_control::grant_role_no_auth(e, &manager, &symbol_short!("manager"), &admin);
// Mint initial supply to the admin (must be a verified identity)
RWA::mint(e, &admin, initial_supply);
From 555b3d469a7cf1b7ce4a6d023762379892b477d5 Mon Sep 17 00:00:00 2001
From: brozorec <9572072+brozorec@users.noreply.github.com>
Date: Fri, 3 Apr 2026 12:31:02 +0200
Subject: [PATCH 9/9] more improvements
---
.../accounts/authorization-flow.mdx | 31 +++++++++++++++----
.../accounts/context-rules.mdx | 2 ++
.../accounts/signers-and-verifiers.mdx | 14 ++++++++-
3 files changed, 40 insertions(+), 7 deletions(-)
diff --git a/content/stellar-contracts/accounts/authorization-flow.mdx b/content/stellar-contracts/accounts/authorization-flow.mdx
index 55777347..2cc57812 100644
--- a/content/stellar-contracts/accounts/authorization-flow.mdx
+++ b/content/stellar-contracts/accounts/authorization-flow.mdx
@@ -2,7 +2,27 @@
title: Authorization Flow
---
-Authorization in smart accounts is determined by matching each auth context against explicitly selected context rules. The off-chain client specifies which rule to use for each operation via `context_rule_ids` in the `AuthPayload`. The selected rule is validated, its signers authenticated, and its policies enforced. If any step fails, authorization is denied.
+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,
+ /// Per-context rule IDs, aligned by index with `auth_contexts`.
+ pub context_rule_ids: Vec,
+}
+```
+
+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()`.
+
+
+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.
+
## Detailed Flow
```mermaid
@@ -51,7 +71,7 @@ For each auth context, the corresponding rule ID is used to look up the 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
@@ -131,7 +151,6 @@ ContextRule {
- 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
@@ -164,8 +183,6 @@ ContextRule {
2. Evaluate Rule 1: Both Alice and Bob authenticated, no policies to enforce → Success
3. Result: Authorized
-The client ignores the expired session rule and explicitly selects the default admin rule (ID 1) in `context_rule_ids`.
-
### Authorization Failure
**Configuration:**
@@ -196,10 +213,12 @@ 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:
+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)
diff --git a/content/stellar-contracts/accounts/context-rules.mdx b/content/stellar-contracts/accounts/context-rules.mdx
index d41146f2..0540ef2e 100644
--- a/content/stellar-contracts/accounts/context-rules.mdx
+++ b/content/stellar-contracts/accounts/context-rules.mdx
@@ -57,6 +57,8 @@ Expired rules are rejected during authorization evaluation.
- 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:
diff --git a/content/stellar-contracts/accounts/signers-and-verifiers.mdx b/content/stellar-contracts/accounts/signers-and-verifiers.mdx
index 45398555..b9644b7e 100644
--- a/content/stellar-contracts/accounts/signers-and-verifiers.mdx
+++ b/content/stellar-contracts/accounts/signers-and-verifiers.mdx
@@ -539,7 +539,19 @@ fn add_signer(
) -> u32;
```
-Adds a signer to an existing context rule and returns the signer's numeric ID. The rule must not exceed the maximum of 15 signers.
+Adds a signer to an existing context rule and returns the assigned signer ID. The rule must not exceed the maximum of 15 signers.
+
+### Batch Adding Signers
+
+```rust
+fn batch_add_signer(
+ e: &Env,
+ context_rule_id: u32,
+ signers: Vec,
+);
+```
+
+Adds multiple signers to an existing context rule at once.
**Important:**