Skip to content

Contract architecture ​

The Smart Treasury Account is a set of Soroban contracts. The treasury itself is a Soroban custom account: it holds Stellar Asset Contract balances and authorizes its own outgoing calls through __check_auth, rather than being a wallet that an admin key drains.

Modules ​

Seven contracts are implemented and deployed on Stellar testnet:

ModuleResponsibilityStatus
SmartAccountRoot treasury authority. Custom account (__check_auth), context rules/signers/thresholds, policy-version binding, nonce replay protection, pause/freeze/recovery, adapter timelock, scheduled-payment lifecycle, execution coordination.Implemented
PolicyEngineOperation/asset/recipient allowlists, amount caps, policy version checks, fail-closed validation.Implemented
TransferAdapter / SplitAdapterNarrow, preauthorized Stellar Asset Contract execution — single-recipient and one-to-many. Only reachable through SmartAccount, never called directly by a client.Implemented
IntentRegistryCanonical scheduled-payment state: execution windows, cumulative usage, executor-gated exactly-once child execution.Implemented
RecoveryManagerGuardian quorum, timelocked guardian administration, guardian-pulled emergency freeze, finalized-recovery record that SmartAccount pulls from.Implemented
WebAuthnVerifierPasskey/secp256r1 signature verification backing Signer::External, wired for real cryptography — no dApp write path exercises it yet.Implemented
ConditionVerifierOptional proof-gated execution using signed external attestations.Not yet included — explicitly deferred

See Status and each module's own page for the live testnet addresses, WASM hashes, and full entrypoint/error/event reference.

Built on OpenZeppelin ​

STA does not write its own access control, pause primitive, signature verification, or smart-account plumbing. It builds on OpenZeppelin's Stellar libraries (0.7.2), used by SmartAccount:

LibraryUsed for
stellar-accessOwnable — owner storage, two-step transfer_ownership/accept_ownership
stellar-contract-utilsThe audited Pausable primitive
stellar-accountsThe SmartAccount trait — context rules, signers, AuthPayload, do_check_auth — used by SmartAccount only

PolicyEngine is deliberately not composed into this authorization layer — it does not implement OZ's Policy trait and is not attached to a context rule. It is a separate contract that SmartAccount calls cross-contract, on purpose: by the time a call reaches validate_policy, SmartAccount has already decided who is authorized; PolicyEngine decides a different question — whether the treasury's own risk rules allow the action at all, independent of signer identity. See "Separation of concerns" below.

Signature verification, including WebAuthn and passkeys, is delegated to stellar_accounts::verifiers. STA writes treasury logic, not cryptography.

Audit status

OpenZeppelin's libraries are audited. The Smart Treasury Account contracts themselves have not been independently audited by a third party — an internal/independent review pass did find and fix eleven real defects across three rounds (see docs/SMART_CONTRACT_AUDIT_REPORT.md in the smart-contracts repo), but that is not the same as a professional external audit. Do not represent them as audited.

How an approved payment flows ​

This is the live flow on the deployed testnet contracts, not a target — both halves below are implemented and reproducible; see docs/TESTNET_DEPLOYMENT.md in the smart-contracts repo for a real executed transaction.

Preflight — off-chain, nothing committed ​

#Caller → targetCallPurpose
1Client → PolicyEngineversion()Read the live version and pin it into the approval
2Client → PolicyEnginevalidate_policy(check)Simulated. Same answer the chain will give, because the entrypoint is read-only
3Client → SmartAccountis_nonce_used(nonce)Reject a reused nonce before asking the wallet to sign

Preflight exists so a user is never asked to approve something that cannot succeed. It is an ergonomic layer, not a security boundary — the contract checks below are the authority.

Execution — on-chain ​

#ActorStepOn failure
4Client → SmartAccountexecute_transfer_payment(asset, destination, amount, nonce, expected_policy_version)—
5SmartAccount__check_auth: context rule, signers, thresholdsRejected, nothing moves
6SmartAccountReject if paused or frozenRejected, nothing moves
7SmartAccountReject if the nonce was already consumed, then consume itRejected, nothing moves
8SmartAccountReject if amount <= 0Rejected, nothing moves
9SmartAccount → PolicyEnginevalidate_policy(check) cross-contract, pinned to expected_policy_versionRejected, nothing moves
10SmartAccount → TransferAdapter → Asset Contracttransfer(smart_account, destination, amount)—
11SmartAccountEmit TransferPaid—

Two orderings in that sequence are load-bearing rather than incidental:

  • The nonce is consumed before the transfer (step 7, ahead of the policy check and the transfer itself). A token with a callback cannot re-enter and spend the same nonce twice.
  • The policy version is passed through to the engine, not compared inside the account. There is exactly one authority on what a version means.

See SmartAccount for the split-payment variant (independent per-recipient policy checks, duplicate-recipient rejection) and the scheduled-payment path, which pins expected_policy_version and the adapter at creation time rather than at execution time.

Separation of concerns ​

The account decides who may act. The engine decides what may be done. They are separate contracts so a policy change is an ordinary transaction against the engine, and so the engine can be simulated on its own — which is what lets a client preflight a payment and get the same answer the chain will give.

The engine is not attached to a context rule and does not implement OpenZeppelin's Policy trait — it is called cross-contract from inside SmartAccount's own entrypoint logic instead, after __check_auth has already run. This is a deliberate choice, not an oversight: it keeps the question "is this signer set authorized" (SmartAccount + OZ's composed context-rule model) fully independent from "does this action pass the treasury's risk rules" (PolicyEngine) — an authorized signer set alone is never sufficient to move funds.

The Smart Treasury Account contracts have not been independently audited. OpenZeppelin's Stellar libraries, which they build on, have been.