The Pausable trait signature returns (), not Result, so a
caller/owner mismatch can only be signaled by aborting โ done via
panic_with_error! with this contract's own typed error (matching
every other failure path here) rather than a bare string panic!,
which surfaced as an untyped trap a client couldn't match against
the way it can Error(Contract, #8011).
fn pause(env: soroban_sdk::Env, caller: soroban_sdk::Address)
Emergency freeze. One-way by design: there is no unfreeze()
entrypoint. Normal operation is restored only via apply_recovery,
so a compromised owner cannot both freeze the account and later
unfreeze it unilaterally without going through guardian quorum.
fn freeze(env: soroban_sdk::Env) -> Result<(), SmartAccountTreasuryError>
Returns true if the contract is paused, and false otherwise.
e - Access to Soroban environment.fn paused(env: soroban_sdk::Env) -> bool
fn status(env: soroban_sdk::Env) -> Result
fn unpause(env: soroban_sdk::Env, caller: soroban_sdk::Address)
Returns Some(Address) if ownership is set, or None if ownership has
been renounced.
e - Access to the Soroban environment.fn get_owner(env: soroban_sdk::Env) -> Option
Adds a new policy to an existing context rule, installs it, and returns
the assigned policy ID. The policy's install method will be called
during this operation.
e - Access to the Soroban environment.context_rule_id - The ID of the context rule to modify.policy - The address of the policy contract to add.install_param - The installation parameter for the policy.SmartAccountError::ContextRuleNotFound] - When no context rule
exists with the given ID.SmartAccountError::DuplicatePolicy] - When the policy already
exists in the rule.SmartAccountError::TooManyPolicies] - When adding would exceed
MAX_POLICIES (5).["policy_added", context_rule_id: u32][policy_id: u32]Defaults to requiring authorization from the smart account itself
(e.current_contract_address().require_auth()) and then delegating to
[storage::add_policy].
fn add_policy(
env: soroban_sdk::Env,
context_rule_id: u32,
policy: soroban_sdk::Address,
install_param: soroban_sdk::Val,
) -> u32
Adds a new signer to an existing context rule, returning the assigned signer ID.
e - Access to the Soroban environment.context_rule_id - The ID of the context rule to modify.signer - The signer to add to the context rule.SmartAccountError::ContextRuleNotFound] - When no context rule
exists with the given ID.SmartAccountError::DuplicateSigner] - When the signer already
exists in the rule.SmartAccountError::TooManySigners] - When adding would exceed
MAX_SIGNERS (15).["signer_added", context_rule_id: u32][signer_id: u32]Defaults to requiring authorization from the smart account itself
(e.current_contract_address().require_auth()) and then delegating to
[storage::add_signer].
fn add_signer(env: soroban_sdk::Env, context_rule_id: u32, signer: Signer) -> u32
Bootstraps the treasury: sets the owner, seeds a Default context
rule with the founding signers/policies (e.g. wallet signers plus a
weighted_threshold policy, or passkey External signers), pins the
subordinate module addresses, and bootstraps intent_registry in the
same call (see below).
This calls the OZ storage primitives directly (ownable::set_owner,
add_context_rule) instead of going through their Ownable/
SmartAccount trait entrypoints, both of which require an
already-authorized owner/signer to exist โ impossible to satisfy
before the very first signer is registered.
intent_registry must be a freshly deployed, not-yet-initialized
IntentRegistry instance. This function initializes it directly,
passing env.current_contract_address() (this treasury) as its
admin โ which is the whole reason it needs to happen here rather
than as a separate call from outside. IntentRegistry::initialize
requires admin.require_auth(); with admin set to this treasury's
own address and th
fn initialize(
env: soroban_sdk::Env,
owner: soroban_sdk::Address,
initial_signers: soroban_sdk::Vec,
initial_policies: soroban_sdk::Map,
config: InitConfig,
) -> Result<(), SmartAccountTreasuryError>
fn __check_auth(
env: soroban_sdk::Env,
signature_payload: soroban_sdk::BytesN<32>,
signatures: AuthPayload,
auth_contexts: soroban_sdk::Vec,
) -> Result<(), OzSmartAccountError>
fn contract_name(env: soroban_sdk::Env) -> soroban_sdk::Symbol
Retrieves the global registry ID for a policy.
e - Access to the Soroban environment.policy - The policy address to look up.SmartAccountError::PolicyNotFound] - When the policy is not
registered in the global registry.fn get_policy_id(env: soroban_sdk::Env, policy: soroban_sdk::Address) -> u32
Retrieves the global registry ID for a signer.
e - Access to the Soroban environment.signer - The signer to look up.SmartAccountError::SignerNotFound] - When the signer is not
registered in the global registry.fn get_signer_id(env: soroban_sdk::Env, signer: Signer) -> u32
fn is_nonce_used(env: soroban_sdk::Env, nonce: u64) -> bool
Removes a policy from an existing context rule and uninstalls it. The
policy's uninstall method will be called during this operation.
Removing the last policy is allowed only if the rule has at least
one signer.
e - Access to the Soroban environment.context_rule_id - The ID of the context rule to modify.policy_id - The ID of the policy to remove from the context rule.SmartAccountError::ContextRuleNotFound] - When no context rule
exists with the given ID.SmartAccountError::PolicyNotFound] - When the policy doesn't exist
in the rule.["policy_removed", context_rule_id: u32][policy_id: u32]Defaults to requiring authorization from the smart account itself
(e.current_contract_address().require_auth()) and then delegating to
[storage::remove_policy].
fn remove_policy(env: soroban_sdk::Env, context_rule_id: u32, policy_id: u32)
Removes a signer from an existing context rule. Removing the last signer is allowed only if the rule has at least one policy.
e - Access to the Soroban environment.context_rule_id - The ID of the context rule to modify.signer_id - The ID of the signer to remove from the context rule.SmartAccountError::ContextRuleNotFound] - When no context rule
exists with the given ID.SmartAccountError::SignerNotFound] - When the signer doesn't exist
in the rule.["signer_removed", context_rule_id: u32][signer_id: u32]Defaults to requiring authorization from the smart account itself
(e.current_contract_address().require_auth()) and then delegating to
[storage::remove_signer].
fn remove_signer(env: soroban_sdk::Env, context_rule_id: u32, signer_id: u32)
Pulls a finalized recovery outcome from the configured
recovery_manager and applies it: force-overwrites ownership,
replaces the entire signer/context-rule topology with the
guardian-approved replacement, syncs the guardian-freeze epoch, and
lifts the freeze. Never pushed by recovery_manager โ this
contract decides, on its own terms, whether to consume a finalized
request, exactly once (AppliedRecovery replay guard).
Permissionless: anyone may call this once recovery_manager
reports the request finalized, mirroring finalize_recovery's own
permissionless design.
Security review finding, two parts. First: this used to overwrite
only Ownable's owner. Spend authority โ execute_transfer_payment
and everything else gated by e.current_contract_address() .require_auth() โ is the OZ context-rule/signer registry, a
completely separate system owner never gates (see
docs/GOVERNANCE_MULTISIG_DESIGN.md ยง9). A compromised payment
signer โ the more likely real-world compromise, not the own
fn apply_recovery(
env: soroban_sdk::Env,
request_id: soroban_sdk::BytesN<32>,
) -> Result
Accepts a pending ownership transfer.
e - Access to the Soroban environment.crate::role_transfer::RoleTransferError::NoPendingTransfer] - If
there is no pending transfer to accept.["ownership_transfer_completed"][new_owner: Address]fn accept_ownership(env: soroban_sdk::Env)
Creates a new context rule with the specified configuration, returning
the newly created ContextRule with a unique ID assigned. Installs
all specified policies during creation.
e - Access to the Soroban environment.context_type - The type of context this rule applies to.name - Human-readable name for the context rule.valid_until - Optional expiration ledger sequence.signers - List of signers authorized by this rule.policies - Map of policy addresses to their installation parameters.SmartAccountError::NoSignersAndPolicies] - When both signers and
policies are empty.SmartAccountError::TooManySigners] - When signers exceed
MAX_SIGNERS (15).SmartAccountError::TooManyPolicies] - When policies exceed
MAX_POLICIES (5).SmartAccountError::DuplicateSigner] - When the same signer appears
multiple times.SmartAccountError::PastValidUntil] - When valid_until is in the
past.SmartAccountError::MathOverflow] - When the context rule, sifn add_context_rule(
env: soroban_sdk::Env,
context_type: ContextRuleType,
name: soroban_sdk::String,
valid_until: Option,
signers: soroban_sdk::Vec,
policies: soroban_sdk::Map,
) -> ContextRule
Retrieves a context rule by its unique ID, returning the
ContextRule containing all metadata, signers, and policies.
e - Access to the Soroban environment.context_rule_id - The unique identifier of the context rule to
retrieve.SmartAccountError::ContextRuleNotFound] - When no context rule
exists with the given ID.fn get_context_rule(env: soroban_sdk::Env, context_rule_id: u32) -> ContextRule
Renounces ownership of the contract.
Permanently removes the owner, disabling all functions gated by
#[only_owner].
e - Access to the Soroban environment.OwnableError::TransferInProgress] - If there is a pending ownership
transfer.OwnableError::OwnerNotSet] - If the owner is not set.fn renounce_ownership(env: soroban_sdk::Env)
Initiates a 2-step ownership transfer to a new address.
Requires authorization from the current owner. The new owner must later
call accept_ownership() to complete the transfer.
e - Access to the Soroban environment.new_owner - The proposed new owner.live_until_ledger - Ledger number until which the new owner can
accept. A value of 0 cancels any pending transfer.OwnableError::OwnerNotSet] - If the owner is not set.crate::role_transfer::RoleTransferError::NoPendingTransfer] - If
trying to cancel a transfer that doesn't exist.crate::role_transfer::RoleTransferError::InvalidLiveUntilLedger] -
If the specified ledger is in the past.crate::role_transfer::RoleTransferError::InvalidPendingAccount] -
If the specified pending account is not the same as the provided new
address.fn transfer_ownership(
env: soroban_sdk::Env,
new_owner: soroban_sdk::Address,
live_until_ledger: u32,
)
Permissionless TTL maintenance for this contract's instance storage
(which PendingAdapter lives in, among other instance-scoped
config) โ extending TTL creates no authority, matching
recovery_manager::extend_ttl's reasoning. Independent security
review finding: propose_adapter_change only bumps the instance TTL
once, at proposal time; a proposal left unapplied long enough after
its delay elapses, on an otherwise fully dormant treasury, could
archive before apply_adapter_change is ever called. This lets
anyone keep it alive without needing the owner to act.
fn extend_instance_ttl(env: soroban_sdk::Env)
Removes a context rule and cleans up all associated data. This function uninstalls all policies associated with the rule and removes all stored data including signers, policies, and metadata.
e - Access to the Soroban environment.context_rule_id - The ID of the context rule to remove.SmartAccountError::ContextRuleNotFound] - When no context rule
exists with the given ID.["context_rule_removed", context_rule_id: u32][]Defaults to requiring authorization from the smart account itself
(e.current_contract_address().require_auth()) and then delegating to
[storage::remove_context_rule].
fn remove_context_rule(env: soroban_sdk::Env, context_rule_id: u32)
Applies a pending adapter change once its delay has elapsed.
Permissionless, like apply_recovery/apply_guardian_freeze: the
authorization decision (the owner proposing this specific change)
already happened at propose_adapter_change time, so when an
already-authorized change actually lands needs no further gate to
be safe โ it only needs the delay to have passed.
fn apply_adapter_change(
env: soroban_sdk::Env,
operation: soroban_sdk::Symbol,
) -> Result<(), SmartAccountTreasuryError>
Real gap this session found: docs/TECHNICAL_ARCHITECTURE.md ยง12.7
documents the canonical recovery workflow as starting with "Guardian
... triggers freeze," but freeze() above is owner-gated only, with
no guardian-facing path at all โ if the owner is the one compromised,
guardians had no way to stop ongoing spend authority while a full
recovery (quorum + timelock) is still pending. This pulls a
guardian-raised freeze flag from recovery_manager the same way
apply_recovery pulls a finalized request: permissionlessly, on
this contract's own terms, with recovery_manager carrying no
knowledge of or dependency on this treasury.
Security review finding, twice over. First revision: this called
recovery_manager's read-only guardian_freeze_requested, a flag
set once and never cleared โ since this entrypoint is deliberately
permissionless (matching apply_recovery's "pull an
already-authorized fact" pattern), anyone could re-freeze the
treasury at any future point, even long after a full recovery ha
fn apply_guardian_freeze(
env: soroban_sdk::Env,
) -> Result<(), SmartAccountTreasuryError>
Cancels a pending adapter change before it takes effect. Owner-gated,
mirroring recovery_manager::cancel_recovery's pattern of letting
the legitimate owner walk back an erroneous or no-longer-wanted
proposal before its delay elapses.
fn cancel_adapter_change(
env: soroban_sdk::Env,
operation: soroban_sdk::Symbol,
) -> Result<(), SmartAccountTreasuryError>
Executes a one-to-many SAC split from the treasury. Each recipient is independently policy-checked (own recipient-allowlist entry, own per-recipient amount cap) before the adapter call, so a split cannot be used to move funds to a disallowed recipient by hiding it inside an aggregate-approved total.
fn execute_split_payment(
env: soroban_sdk::Env,
asset: soroban_sdk::Address,
recipients: soroban_sdk::Vec,
amounts: soroban_sdk::Vec,
nonce: u64,
expected_policy_version: u32,
) -> Result<(), SmartAccountTreasuryError>
Proposes rewiring which adapter contract handles a given operation.
Owner-gated. Unlike policy_engine / intent_registry /
recovery_manager (pinned at initialize, see Architecture
Decision in docs/TECHNICAL_ARCHITECTURE.md ยง6.9), adapters are
mutable post-init because the doc explicitly scopes
set_adapter_config as an owner-configurable entrypoint.
This does not take effect immediately โ see apply_adapter_change.
An independent security review (docs/SMART_CONTRACT_AUDIT_REPORT.md
finding 3) noted that immediate, undelayed reconfiguration of
execution routing is a meaningful risk for a treasury contract
specifically (it decides where funds go, unlike e.g. freeze,
which only stops movement and is deliberately kept immediate).
Overwrites any existing pending proposal for the same operation โ
the most recent proposal wins, matching how re-proposing normally
works elsewhere in this workspace (e.g. policy_engine::set_asset_rule).
Explicitly extends the instance TTL on write. Review findi
fn propose_adapter_change(
env: soroban_sdk::Env,
operation: soroban_sdk::Symbol,
adapter: soroban_sdk::Address,
) -> Result<(), SmartAccountTreasuryError>
Retrieves the number of all context rules, including expired rules. Defaults to 0.
e - Access to the Soroban environment.fn get_context_rules_count(env: soroban_sdk::Env) -> u32
Cancels a previously created scheduled payment. Requires this contract's own authorization, same as creation.
This closes a real gap: intent_registry::cancel_intent exists and
is ensure_admin-gated, but intent_registry's configured admin is
this contract's own address โ before this entrypoint existed, there
was no way for anyone, including the treasury's own signers, to ever
call it, since only smart_account itself can satisfy that admin
check via a nested sub-invocation.
fn cancel_scheduled_payment(
env: soroban_sdk::Env,
intent_id: soroban_sdk::BytesN<32>,
) -> Result<(), SmartAccountTreasuryError>
Creates a scheduled/recurring payment intent. Requires this
contract's own authorization (same signer/context-rule/policy model
as interactive payments), then delegates canonical lifecycle and
replay state to intent_registry โ see Architecture Decision 2.
intent_registry must have been configured with this contract's
own address as its admin, so this nested call succeeds via
sub-invocation tree membership rather than a second signature.
The caller-supplied policy_version and adapter on intent are
both ignored and overwritten: policy_version with policy_engine's
current version, and adapter with the transfer_adapter currently
configured (via propose_adapter_change/apply_adapter_change).
Both must reflect what was actually in
effect when the signer approved this schedule, not a value the
caller chose โ otherwise a later adapter reconfiguration could
silently redirect an already-approved scheduled payment through a
different adapter at execution time (see execute_scheduled_payment
fo
fn create_scheduled_payment(
env: soroban_sdk::Env,
intent: ScheduledIntentArgs,
) -> Result<(), SmartAccountTreasuryError>
Executes a single-recipient SAC payment. Requires this contract's
own authorization (delegated entirely to the OZ context-rule/signer/
policy model via __check_auth), then treasury policy, then a
narrowly preauthorized adapter call.
fn execute_transfer_payment(
env: soroban_sdk::Env,
asset: soroban_sdk::Address,
destination: soroban_sdk::Address,
amount: i128,
nonce: u64,
expected_policy_version: u32,
) -> Result<(), SmartAccountTreasuryError>
Updates the name of an existing context rule, returning the updated
ContextRule with the new name.
e - Access to the Soroban environment.context_rule_id - The ID of the context rule to update.name - The new human-readable name for the context rule.SmartAccountError::ContextRuleNotFound] - When no context rule
exists with the given ID.["context_rule_meta_updated", context_rule_id: u32][name: String, context_type: ContextRuleType, valid_until: Option<u32>]Defaults to requiring authorization from the smart account itself
(e.current_contract_address().require_auth()) and then delegating to
[storage::update_context_rule_name].
fn update_context_rule_name(
env: soroban_sdk::Env,
context_rule_id: u32,
name: soroban_sdk::String,
) -> ContextRule
Executes an already-approved scheduled payment. Permissionless by
design (matches recovery_manager::finalize_recovery): the signer
approval already happened at create_scheduled_payment time. Safety
here comes entirely from intent_registry's own execution-window,
cancellation, and cumulative-usage checks โ not from caller
identity โ except that intent_registry.mark_child_executed itself
still requires its configured Executor address to authorize the
call, so an operational relayer key (not a treasury signer) gates
when within the valid window execution happens.
Deliberately takes only intent_id and child_sequence from the
caller. An earlier revision also accepted asset, destination,
amount, and expected_policy_version as caller-supplied
parameters and used them directly for the policy check and adapter
dispatch โ since this entrypoint has no require_auth() gate of its
own (by design, see above) and the relayer role is explicitly
untrusted (docs/TECHNICAL_ARCHITECTURE.md ยง13.
fn execute_scheduled_payment(
env: soroban_sdk::Env,
intent_id: soroban_sdk::BytesN<32>,
child_sequence: u32,
) -> Result<(), SmartAccountTreasuryError>
Updates the expiration time of an existing context rule, returning the
updated ContextRule with the new expiration time.
e - Access to the Soroban environment.context_rule_id - The ID of the context rule to update.valid_until - New optional expiration ledger sequence. Use None
for no expiration.SmartAccountError::ContextRuleNotFound] - When no context rule
exists with the given ID.SmartAccountError::PastValidUntil] - When valid_until is in the
past.["context_rule_meta_updated", context_rule_id: u32][name: String, context_type: ContextRuleType, valid_until: Option<u32>]Defaults to requiring authorization from the smart account itself
(e.current_contract_address().require_auth()) and then delegating to
[storage::update_context_rule_valid_until].
fn update_context_rule_valid_until(
env: soroban_sdk::Env,
context_rule_id: u32,
valid_until: Option,
) -> ContextRule