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 succeed (i.e., enforce must not panic) for the rule to be considered matched and authorized.
The Policy Trait
All policies must implement the Policy trait:
pub trait Policy {
type AccountParams: FromVal<Env, Val>;
/// 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<Signer>,
context_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,
install_params: Self::AccountParams,
context_rule: ContextRule,
smart_account: Address,
);
/// Clean up policy data when removed.
/// Called when a context rule is removed.
fn uninstall(
e: &Env,
context_rule: ContextRule,
smart_account: Address,
);
}Policy Lifecycle
The three 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.
Enforcement
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 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 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
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_authcalls 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_authfrom the smart account ininstall,enforce, anduninstall - 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
fn add_policy(
e: &Env,
context_rule_id: u32,
policy: Address,
install_param: Val,
) -> u32;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
fn remove_policy(
e: &Env,
context_rule_id: u32,
policy_id: u32,
);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. The ID is returned by add_policy, and the get_policy_id(policy) view looks it up for an already registered policy.
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:
- Before removing signers, verify that the threshold remains achievable, lowering it first if needed
- After adding signers, adjust thresholds or assign weights to maintain the desired security level
- Ideally, bundle these updates in the same transaction as the signer modifications
For weighted thresholds, these updates must also happen in a specific order; see Required Operation Ordering.
Example Policies
The OpenZeppelin Stellar Contracts library provides the necessary utilities for three policy implementations:
Simple Threshold
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 section above for details on how adding or removing signers affects threshold policies and how to properly manage these changes.
Weighted Threshold
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 section above for details on how adding or removing signers affects threshold policies and how to properly manage these changes.
Required Operation Ordering
Both set_threshold() and set_signer_weight() enforce threshold <= total signer weight on every individual call. Because each call is validated in isolation, reconfiguration is order-sensitive:
- Adding weight and raising the threshold: call
set_signer_weight()first to increase the total weight, thenset_threshold(). Raising the threshold first reverts if it exceeds the current total weight. - Removing a signer or reducing weight: call
set_threshold()first to lower the threshold, thenset_signer_weight(). Reducing the weight first reverts if the total weight drops below the current threshold.
A call that would leave threshold > total signer weight reverts with InvalidThreshold (error 3211), so a wrong-order sequence fails at the first step whose intermediate state violates the invariant.
Spending Limit
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 of transfer calls within a rolling window and rejects transactions that would exceed the configured limit. Entries older than the window are evicted before each new transfer is evaluated, so the available budget frees up as the window rolls forward. Any other call under a rule with this policy is rejected.
The policy requires two configuration parameters:
- Spending Limit (
spending_limit): Maximum amount that can be spent within the window - Period (
period_ledgers): Length of the rolling window, in ledgers (e.g.17280for roughly one day)
Zero-amount transfers move no funds and are always permitted, even when the amount already spent exceeds a limit that was lowered in the meantime.
The spending history is capped at 815 entries (MAX_HISTORY_ENTRIES), the largest history that still fits in a single ledger entry. Once the window holds that many transfers, further transfers fail with HistoryCapacityExceeded until older entries fall out of the window.
Best Practices
- Order Matters: Policies execute in order; place cheaper checks first
- Keep Policies Focused: Each policy should enforce one concern
- Test Policy Combinations: Ensure multiple policies work together correctly
- Handle Errors Gracefully: Return clear error messages from
enforce - Clean Up Storage: Always implement
uninstallto free storage - Document Configuration: Clearly document policy configuration parameters