Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

23 Commits
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

๐Ÿ›ก๏ธ SpendGuard Contract

Read-only Soroban policy-view-helper for OpenZeppelin smart-account spending limits

The on-chain read half of SpendGuard. Zero custody. Zero writes. Zero signing.

License: MIT CI Rust Soroban SDK Tests Stellar & Soroban

policy-view-helper is the smart-contract half of the SpendGuard ecosystem: a minimal, read-only Soroban contract that gives the SpendGuard indexer a single clean, versioned interface for querying an OpenZeppelin stellar-accounts smart account's declared spending_limit policy state โ€” no hand-decoding of nested ledger entries, no stale caches, no wall-clock guesswork.

It never holds funds, never writes state, and never sits in the payment path โ€” it only answers questions about public on-chain policy state.


TL;DR

policy-view-helper is policy-view-helper never
โœ… A read-only on-chain query wrapper โŒ Holds, moves, or signs for funds
โœ… A versioned public interface (WASM_VERSION = 1) โŒ Writes storage or emits events
โœ… Two cross-contract reads per query โŒ Blocks, freezes, or enforces anything
โœ… Raw ledger-accurate policy values โŒ Converts ledgers to wall-clock on-chain

If the helper is wrong or down, no funds are at risk โ€” the real backstop is the on-chain policy enforcement it merely reflects.


Why this matters

Smart accounts built on OpenZeppelin's framework declare spending-limit guardrails on-chain. But reading that state requires traversing complex nested structures (ContextRule โ†’ installed Policy contracts โ†’ SpendingLimitData) and the indexer would have to hand-decode multiple raw ledger entries per account per poll cycle.

policy-view-helper collapses that into one call:

  • ๐Ÿ“– Two functions, one purpose โ€” a version probe and a policy-state read, nothing else.
  • ๐Ÿ”— Cross-contract reads, zero state โ€” every query resolves addresses on-chain at call time; nothing is stored, cached, or remembered.
  • ๐Ÿงฎ Raw on-chain values โ€” returns precise stroop amounts and ledger sequences; wall-clock conversion is left to the indexer, which has access to Soroban RPC close-time data.
  • ๐Ÿ”’ Versioned for indexers โ€” get_version lets the SpendGuard indexer fail loudly instead of silently mis-reading a changed API.

How it works

sequenceDiagram
    autonumber
    participant IX as SpendGuard Indexer / SDK
    participant VH as policy-view-helper (this contract)
    participant SA as OZ Smart Account
    participant PL as spending_limit Policy Contract

    IX->>VH: simulateContract: get_spending_limit_state(account, rule_id)
    VH->>SA: get_context_rule(rule_id)
    SA-->>VH: ContextRule { context_type, policies: [...] }
    VH->>VH: Reject CreateContract (non-transfer) context rules
    loop For each policy address in rule
        VH->>PL: get_spending_limit_data(rule_id, account)
        alt Policy returns valid SpendingLimitData
            PL-->>VH: SpendingLimitData { limit, period, history, spent }
            VH-->>IX: SpendingLimitView (raw ledger-accurate values)
        end
    end
    Note over VH: No policy matched โ†’ PolicyViewError
Loading

Public interface

Function Signature Returns
get_version () -> u32 1 โ€” WASM version marker for indexer compatibility checks
get_spending_limit_state (account: Address, context_rule_id: u32) Result<SpendingLimitView, PolicyViewError>

SpendingLimitView

#[contracttype]
pub struct SpendingLimitView {
    /// Maximum amount (in stroops, 1 XLM = 10^7 stroops) in the rolling window.
    pub cap: i128,
    /// Rolling window length in ledgers (~5 s per ledger on Stellar).
    pub window_ledgers: u32,
    /// Total amount spent so far in the current rolling window.
    pub spent_in_window: i128,
    /// Ledger sequence of the earliest retained spending entry (0 = no history).
    pub window_started_ledger: u32,
}

Errors

Code Error Meaning
1 NoSpendingLimitPolicyInstalled No policy on the rule answered get_spending_limit_data
2 AccountNotFound The smart account rejected the query (rule missing, wrong address)
3 UnsupportedContextType The context rule targets a non-transfer context, where spending_limit never applies

Security posture

SpendGuard's whole design is built on a single principle: the least powerful tool that solves the problem. This contract is the extreme version of that.

  • ๐Ÿ”’ Read-only by construction โ€” zero storage writes, zero state mutations, zero events emitted (verifiable by reading the source)
  • ๐Ÿ”‘ Zero custody โ€” never holds, moves, or signs for funds
  • ๐Ÿ‘€ Public data only โ€” every input and output is public on-chain state; no secrets anywhere in the execution path
  • ๐Ÿงฎ No floats, no unwrap in production โ€” i128 integer math throughout, all errors propagated explicitly
  • ๐Ÿ“– See SECURITY.md for the full scope, audit status, and disclosure process

Quick start

git clone https://github.com/Spendguard/spendguard-contract.git
cd spendguard-contract
stellar contract build     # โ†’ target/wasm32v1-none/release/policy_view_helper.wasm
cargo test --workspace     # 9 tests: 6 behavior + 3 OZ serialization-compat

Requirements: Rust stable (pinned in rust-toolchain.toml with the wasm32v1-none target and rustfmt/clippy/rust-src components) and the Soroban CLI.

Deployed instances

Network Contract ID Status
Stellar Testnet CCAM4NRAUB6SO3XLL2SRZQEHSOUYQGDKGNPCUIQ5KKI2S6QKWC2VN6NX Active

This is the ID the SpendGuard app reads via POLICY_VIEW_HELPER_CONTRACT_ID.


Architecture

spendguard-contract/
โ”œโ”€โ”€ contracts/policy-view-helper/   # The single Soroban contract
โ”‚   โ”œโ”€โ”€ src/
โ”‚   โ”‚   โ”œโ”€โ”€ lib.rs                  # #[contract] entry point + public interface
โ”‚   โ”‚   โ”œโ”€โ”€ read.rs                 # Cross-contract query implementation
โ”‚   โ”‚   โ”œโ”€โ”€ types.rs                # SpendingLimitView, errors, OZ mirror types
โ”‚   โ”‚   โ””โ”€โ”€ test.rs                 # 9 tests incl. real-OZ serialization round-trips
โ”‚   โ””โ”€โ”€ test_snapshots/             # Soroban snapshot fixtures
โ”œโ”€โ”€ .github/workflows/ci.yml        # Test & Clippy on every push/PR to master
โ”œโ”€โ”€ rust-toolchain.toml             # Pinned Rust stable + wasm32v1-none target
โ”œโ”€โ”€ CONTRIBUTING.md ยท SECURITY.md ยท drips.json ยท LICENSE
โ””โ”€โ”€ Cargo.toml                      # Workspace root manifest

Technical stack

Component Version / Target
Rust Edition 2021
Soroban SDK 26.1.0 (experimental_spec_shaking_v2)
OpenZeppelin stellar-accounts =0.7.2 (pinned dev-dependency)
Soroban Test Helpers 0.2.4
Compilation Target wasm32v1-none
Release profile opt-level = "z", lto = true, overflow-checks = true, panic = "abort"

The stellar-accounts dependency is pinned and only used in tests โ€” the production contract carries its own mirror types and verifies they still decode real OZ data via oz_serialization_compat round-trip tests.


Scripts & CI

Command Description
stellar contract build Builds the optimized WASM to target/wasm32v1-none/release/policy_view_helper.wasm
cargo test --workspace Runs the 9-test suite (positive, negative, and OZ compatibility cases)
cargo clippy --workspace --all-targets -- -D warnings Lints with warnings denied (CI gate)

CI (GitHub Actions) runs Test & Clippy on every push and pull request to master.


Known limitations (honest)

  1. spending_limit policy type only โ€” the helper reads the spending_limit policy installed on a transfer-context rule. simple_threshold and weighted_threshold policies are not queried (yet), and OZ scopes spending_limit to transfer contexts by design โ€” an upstream constraint, not a SpendGuard one.
  2. Ledger windows, not wall-clock โ€” all window fields are ledger sequences; the indexer is responsible for converting them to timestamps using Soroban RPC close-time data.
  3. Mirror types must track OpenZeppelin โ€” the production mirror types are verified against stellar-accounts 0.7.2; an OZ upgrade requires re-running the compatibility tests before the pin moves.
  4. No events emitted โ€” indexing still relies on the OZ contracts' own transfer / spending_limit_enforced events; this contract is query-only.

Roadmap

  • ๐Ÿงฉ Support additional OZ policy types (simple_threshold, weighted_threshold)
  • ๐ŸŒ Mainnet deployment alongside the app's live-testnet flow
  • โšก Batch / multi-rule convenience queries for the indexer
  • ๐Ÿ” Independent audit of the narrow cross-contract read path

Contributing

Contributors are welcome โ€” and rewarded. This contract is a great Drips-wave contribution target: ~300 lines of Rust, a pinned dependency, and a test suite that encodes the entire contract surface.

  • ๐ŸŽฏ Start with the issues labelled good first issue
  • ๐Ÿ“– Read CONTRIBUTING.md โ€” Conventional Commits, no unwrap outside tests, positive + negative test required per function
  • ๐Ÿ› Security-sensitive bugs go through SECURITY.md
  • ๐Ÿงช cargo test --workspace before opening a PR

License

MIT ยฉ SpendGuard Contributors

About

No description or website provided.

Topics

Resources

Contributing

Security policy

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages