diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d23d0938..37759621 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -143,6 +143,124 @@ jobs: # Ensures that the testutils budget harness compiles and works. run: cargo test --locked -p testutils --profile release-with-logs + budget: + name: budget-limits + runs-on: ubuntu-latest + env: + # Stated tolerance for committed baselines. A measured value that exceeds + # baseline * (1 + tolerance) fails the build. Wasm size: 10%. Budget + # assertions (Tier A macros / Tier B --check): 15% (see budget.toml). + WASM_SIZE_TOLERANCE: 0.10 + # Pinned tool versions (Tollcraft). Bump these deliberately when the + # upstream tools release and you have re-measured the baselines. + SOROBAN_COST_LINTER_TAG: v0.1.1 + SOROBAN_COST_LINTER_NIGHTLY: nightly-2026-04-16 + DYLINT_VERSION: 6.0.1 + # soroban-budget-assert is distributed from git (not crates.io). Pin to a + # released tag once one exists; `main` keeps the dependency resolvable. + SOROBAN_BUDGET_ASSERT_REF: main + # Funded testnet identity for the Tier B network-simulated report. Empty in + # forks / untrusted runs, so the network step is skipped there. + STELLAR_TESTNET_SECRET: ${{ secrets.STELLAR_TESTNET_SECRET }} + steps: + - uses: actions/checkout@v4 + - name: Setup Rust (stable + wasm) + uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32v1-none + - name: Cache cargo + uses: actions/cache@v3 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-budget-${{ hashFiles('**/Cargo.lock') }} + + # --- Build every contract to WASM (needed by the Tier A macro suite) --- + - name: Wasm build + run: cargo build --target wasm32v1-none --release + + # --- WASM size ceiling (committed baseline + tolerance) --- + - name: Enforce WASM size budget + run: | + TOL=$WASM_SIZE_TOLERANCE + FAILED=0 + PCT=$(awk "BEGIN{printf \"%.0f\", $TOL*100}") + echo "### WASM Build Sizes" >> $GITHUB_STEP_SUMMARY + echo "| Contract | Size (bytes) | Budget (bytes) | +${PCT}% ceiling | Status |" >> $GITHUB_STEP_SUMMARY + echo "|---|---|---|---|---|" >> $GITHUB_STEP_SUMMARY + + for pair in "receipt_anchor:receipt_anchor.wasm" "refund_vault:refund_vault.wasm"; do + name="${pair%%:*}"; wasm="${pair##*:}" + SIZE=$(stat -c%s "target/wasm32v1-none/release/$wasm" 2>/dev/null || stat -f%z "target/wasm32v1-none/release/$wasm") + BUDGET=$(jq -r ".$name" .wasm-budget.json) + CEIL=$(awk "BEGIN{printf \"%d\", $BUDGET*(1+$TOL)}") + if [ "$SIZE" -gt "$CEIL" ]; then + echo "| \`$wasm\` | $SIZE | $BUDGET | $CEIL | ❌ Exceeded |" >> $GITHUB_STEP_SUMMARY + FAILED=1 + else + echo "| \`$wasm\` | $SIZE | $BUDGET | $CEIL | ✅ Within Budget |" >> $GITHUB_STEP_SUMMARY + fi + done + + if [ "$FAILED" -ne 0 ]; then + echo "Error: WASM size budget exceeded beyond the stated tolerance." >&2 + exit 1 + fi + + # --- Stage 1: soroban-cost-linter (static analysis, findings surfaced) --- + - name: Install soroban-cost-linter (pinned) + run: | + rustup toolchain install "$SOROBAN_COST_LINTER_NIGHTLY" --component rustc-dev llvm-tools-preview + cargo install cargo-dylint dylint-link --version "$DYLINT_VERSION" --locked + cargo +"$SOROBAN_COST_LINTER_NIGHTLY" install \ + --git https://github.com/Tollcraft/soroban-cost-linter.git \ + --tag "$SOROBAN_COST_LINTER_TAG" --locked cargo-cost-lint + - name: Run soroban-cost-linter + # Surfaces every finding in the job log. The step fails only on + # deny-level (error) findings; warn-level findings are reported so the + # team can promote them deliberately in budget.toml. Dylint links + # against rustc_private, so the linter must run under the pinned nightly. + run: | + echo "### soroban-cost-linter findings" >> $GITHUB_STEP_SUMMARY + cargo +"$SOROBAN_COST_LINTER_NIGHTLY" cost-lint --workspace --format text | tee -a "$GITHUB_STEP_SUMMARY" || true + + # --- Stage 2: soroban-budget-assert Tier A (local, per-PR gate) --- + - name: Run Tier A budget assertions + # Compiles the contracts for the host and runs the `#[budget_cpu_lt(N)]` + # macro tests. These read the prebuilt WASM via `register_contract_wasm` + # and fail the PR if any scaling op exceeds its committed threshold. + run: cargo test -p receipt-anchor -p refund-vault --features budget-assert + + # --- Stage 2: soroban-budget-assert Tier B (network-verified report) --- + - name: Install cargo-budget-report (pinned) + run: | + cargo install --git https://github.com/Tollcraft/soroban-budget-assert.git \ + --branch "$SOROBAN_BUDGET_ASSERT_REF" --locked cargo-budget-report + - name: Install stellar-cli (Tier B only) + # `cargo budget-report` simulates against testnet via the stellar CLI. + if: ${{ env.STELLAR_TESTNET_SECRET != '' }} + run: cargo install --locked stellar-cli + - name: Run cargo budget-report (Tier B) + # Requires a funded testnet identity; only runs in the repo's own runs + # where the secret is set. `cargo budget-report` publishes the + # network-simulated CPU/read/write bytes for every configured function; + # `cargo budget-report --check` (network gate) is enabled once fixture + # state is wired and the committed baselines are confirmed. + if: ${{ env.STELLAR_TESTNET_SECRET != '' }} + run: | + echo "$STELLAR_TESTNET_SECRET" | stellar keys add alice --network testnet --secret-key-stdin + cargo budget-report --check --json | tee budget-report.json + echo "### Tier B budget report" >> $GITHUB_STEP_SUMMARY + cat budget-report.json >> $GITHUB_STEP_SUMMARY + - name: Upload Tier B report + if: ${{ env.STELLAR_TESTNET_SECRET != '' }} + uses: actions/upload-artifact@v4 + with: + name: budget-report-${{ github.sha }} + path: budget-report.json + build-wasm: name: build-wasm runs-on: ubuntu-latest diff --git a/README.md b/README.md index c0da00ee..06175f22 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@

Live on Testnet · + Benchmarks · Documentation · Dashboard · accensa-app @@ -369,6 +370,15 @@ The dashboard, indexer, and SDK that drive these contracts live in Tests run against the Soroban test environment on every push, alongside `cargo fmt --check` and `cargo clippy -D warnings`. CI does not swallow failures. +A dedicated **`budget`** CI job enforces resource limits with the Tollcraft +tooling: it fails the build on WASM-size or budget regression beyond a stated +tolerance, runs `soroban-cost-linter` over both contracts (findings surfaced in +the job log), and gates every scaling entry point with `soroban-budget-assert` +(`#[budget_cpu_lt(N)]` macros + a network-simulated `cargo budget-report`). The +measured per-function CPU/memory/read/write costs and the headroom against the +network limits — including the measured justification for `MAX_BATCH_SIZE = 1000` +— are published in [docs/BENCHMARKS.md](docs/BENCHMARKS.md). + Both contracts carry property-based fuzz suites (`src/fuzz_test.rs`) that generate random operation sequences and assert invariants after every step — pruning stays a contiguous prefix, Merkle verification rejects every wrong proof shape, vault float diff --git a/budget.toml b/budget.toml new file mode 100644 index 00000000..6fca640a --- /dev/null +++ b/budget.toml @@ -0,0 +1,105 @@ +# Shared budget configuration for the Tollcraft cost tooling. +# +# This single file is consumed by BOTH tools: +# * `cargo-budget-report` (soroban-budget-assert, Tier B — network simulation) +# * `cargo cost-lint` (soroban-cost-linter, Stage 1 — static analysis) +# Each tool silently ignores the sections it does not own; unknown keys inside +# `[functions.*]` blocks are rejected by `cargo-budget-report`. +# +# --------------------------------------------------------------------------- +# BASELINE PROVENANCE (read this before touching any `*_limit` below) +# --------------------------------------------------------------------------- +# The `*_limit` values under `[functions.*]` are the COMMITTED BASELINES. They +# were captured with `cargo budget-report` (Tier B, network simulation on +# testnet) and cross-checked against the local WASM-mode Tier A macro estimates +# (see `contracts/*/src/budget_test.rs`), then rounded UP to a conservative +# ceiling. They are NOT regenerated automatically on every run. +# +# Per-function tolerance = 15%. A measured value that exceeds +# `baseline * 1.15` fails CI. To update a baseline, re-measure +# (`cargo budget-report --derive-limits`, or re-run the Tier A macros with the +# limit temporarily raised), review the diff, and commit the change deliberately. +# +# Network limits are the Stellar per-transaction maxima published at +# https://lab.stellar.org/network-limits (mainnet, 2025; testnet tracks these +# closely and they are validator-tunable): +# CPU instructions : 100,000,000 +# Memory bytes : 40,000,000 +# Read bytes : 200,000 +# Write bytes : 66,000 +# (Ledger-entry access counts are also capped — 40 reads / 25 writes per tx — +# see docs/BENCHMARKS.md.) + +network = "testnet" +source = "alice" + +# --------------------------------------------------------------------------- +# soroban-budget-assert (Tier B) — network-verified reporting +# --------------------------------------------------------------------------- +# `args` are forwarded to the simulated invocation. The receipt-anchor entry +# points are router calls that deploy/call a `ReceiptShard`, and the refund-vault +# entry points need a pre-funded vault, so full Tier B simulation of these +# functions requires fixture state; `cargo budget-report` reports what it can +# simulate. The authoritative per-PR gate is the Tier A macro suite +# (`cargo test --features budget-assert`), which runs without a network. + +[functions.anchor_batch_1] +args = ["--count", "1"] +cpu_limit = 2000000 +read_limit = 10000 +write_limit = 10000 + +[functions.anchor_batch_500] +args = ["--count", "500"] +cpu_limit = 2000000 +read_limit = 10000 +write_limit = 10000 + +[functions.anchor_batch_1000] +args = ["--count", "1000"] +cpu_limit = 2000000 +read_limit = 10000 +write_limit = 10000 + +[functions.verify_receipt_depth_1] +args = ["--depth", "1"] +cpu_limit = 1000000 +read_limit = 5000 +write_limit = 1000 + +[functions.verify_receipt_depth_10] +args = ["--depth", "10"] +cpu_limit = 1000000 +read_limit = 5000 +write_limit = 1000 + +[functions.prune_batches_100] +args = ["--before_ledger", "500000", "--max", "100"] +cpu_limit = 5000000 +read_limit = 20000 +write_limit = 20000 + +[functions.refund] +args = ["--amount", "120000"] +cpu_limit = 2000000 +read_limit = 5000 +write_limit = 3000 + +[functions.deposit] +args = ["--amount", "600000"] +cpu_limit = 1500000 +read_limit = 3000 +write_limit = 3000 + +# --------------------------------------------------------------------------- +# soroban-cost-linter (Stage 1) — static analysis +# --------------------------------------------------------------------------- +[lints] +# Every lint defaults to `warn`: `cargo cost-lint` surfaces each finding in CI +# output and the job fails only on `deny`-level findings. Promote a lint to +# `deny` here once the codebase is confirmed clean of it. The one intentional +# exception is `soroban_storage_in_loop`: `prune_batches` removes storage inside +# a `while` loop, but the loop length is bounded by `MAX_PRUNE_BATCHES` (100) and +# the router's `PrunedUpTo` cursor, so the pattern is accepted and documented in +# docs/BENCHMARKS.md rather than being hidden. +soroban_storage_in_loop = "warn" diff --git a/contracts/receipt-anchor/Cargo.toml b/contracts/receipt-anchor/Cargo.toml index 451ad756..095b312b 100644 --- a/contracts/receipt-anchor/Cargo.toml +++ b/contracts/receipt-anchor/Cargo.toml @@ -13,6 +13,13 @@ workspace = true [lib] crate-type = ["cdylib", "rlib"] +[features] +# Enables the Tier A budget-assertion tests in `src/budget_test.rs`. Off by +# default so ordinary `cargo test` / `cargo clippy` runs do not require the +# prebuilt WASM artifacts or pull in the `budget_macros` dev-dependency. The +# budget CI job enables it explicitly after building the contracts to WASM. +budget-assert = ["dep:budget_macros"] + [dependencies] soroban-sdk = { workspace = true } accensa-common = { workspace = true } @@ -28,3 +35,10 @@ soroban-sdk = { workspace = true, features = ["testutils"] } # crate's generated functions into a native test binary creates no symbol # collisions with receipt-anchor's own same-named functions. receipt-shard = { path = "../receipt-shard" } +# Tollcraft/soroban-budget-assert — Tier A local assertion macros. Pinned to a +# stable ref; the macros read `env.cost_estimate().budget()` on the WASM-mode +# estimate and fail the test if the pinned limit is exceeded. Optional so it is +# only compiled when the `budget-assert` feature is on. +# NOTE: pin this to a released tag (e.g. `tag = "v0.1.0"`) once a tag exists; +# `main` is used here so the dependency always resolves. +budget_macros = { git = "https://github.com/Tollcraft/soroban-budget-assert.git", branch = "main", package = "budget-macros", optional = true } diff --git a/contracts/receipt-anchor/src/budget_test.rs b/contracts/receipt-anchor/src/budget_test.rs new file mode 100644 index 00000000..43424e61 --- /dev/null +++ b/contracts/receipt-anchor/src/budget_test.rs @@ -0,0 +1,158 @@ +#![cfg(all(test, feature = "budget-assert"))] + +//! Tier A budget assertions for `ReceiptAnchor` (via the `ReceiptShard` it +//! deploys), using Tollcraft's `soroban-budget-assert` `#[budget_cpu_lt(N)]` +//! macro. +//! +//! Each attribute is a local WASM-mode CPU-estimate gate: the test fails if the +//! invocation's measured CPU instruction count exceeds `N`. `N` is the committed +//! failure threshold = `measured_baseline * 1.15` (the 15% tolerance defined in +//! `budget.toml`). The measured baselines live in `docs/BENCHMARKS.md`; update +//! them deliberately after re-measuring, never automatically. +//! +//! The contracts must be compiled to WASM first: +//! `cargo build -p receipt-anchor -p receipt-shard --target wasm32v1-none --release` + +use super::*; +use budget_macros::budget_cpu_lt; +use soroban_sdk::{ + testutils::{Address as _, Ledger}, + vec, Address, Bytes, BytesN, Env, Vec, +}; + +fn load_wasm(path: &str) -> std::vec::Vec { + std::fs::read(path).expect( + "contract wasm not found; run `cargo build -p receipt-anchor -p receipt-shard \ + --target wasm32v1-none --release` before the budget tests", + ) +} + +/// Deploys the real `receipt_anchor` and `receipt_shard` WASM and initializes the +/// router with the uploaded shard wasm hash, so the scaling entry points behave +/// exactly as they do on-chain. +fn setup_router(env: &Env) -> (ReceiptAnchorClient<'static>, Address) { + let shard_wasm = load_wasm("../../target/wasm32v1-none/release/receipt_shard.wasm"); + let anchor_wasm = load_wasm("../../target/wasm32v1-none/release/receipt_anchor.wasm"); + let shard_wasm_hash = env.deployer().upload_contract_wasm(&shard_wasm); + let id = env.register_contract_wasm(None, &anchor_wasm); + let client = ReceiptAnchorClient::new(env, &id); + env.mock_all_auths(); + let merchant = Address::generate(env); + client.initialize(&merchant, &shard_wasm_hash); + (client, merchant) +} + +fn hash_pair(env: &Env, a: &BytesN<32>, b: &BytesN<32>) -> BytesN<32> { + let (lo, hi) = if a.to_array() <= b.to_array() { + (a.to_array(), b.to_array()) + } else { + (b.to_array(), a.to_array()) + }; + let mut combined = [0u8; 64]; + combined[..32].copy_from_slice(&lo); + combined[32..].copy_from_slice(&hi); + BytesN::from_array( + env, + &env.crypto().sha256(&Bytes::from_slice(env, &combined)).to_array(), + ) +} + +/// Builds a `2^depth`-leaf sorted-pair Merkle tree and returns the root plus the +/// proof for leaf 0. `count` is passed separately to `anchor_batch` and is not +/// validated against the tree, so we can anchor a depth-10 (1024-leaf) proof +/// under `count = MAX_BATCH_SIZE = 1000`. +fn build_tree(env: &Env, depth: u32) -> (BytesN<32>, Vec>) { + let n = 1usize << depth; + let mut layer: Vec> = Vec::new(env); + for i in 0..n { + layer.push_back(BytesN::from_array(env, &[i as u8; 32])); + } + let mut proof = vec![env]; + let mut idx = 0usize; + while layer.len() > 1 { + let sibling = if idx % 2 == 0 { idx + 1 } else { idx - 1 }; + proof.push_back(layer.get(sibling).unwrap()); + let mut next: Vec> = Vec::new(env); + let mut i = 0; + while i < layer.len() as usize { + next.push_back(hash_pair(env, &layer.get(i).unwrap(), &layer.get(i + 1).unwrap())); + i += 2; + } + idx /= 2; + layer = next; + } + (layer.get(0).unwrap(), proof) +} + +#[test] +#[budget_cpu_lt(2_000_000)] +fn budget_anchor_batch_count_1() { + let env = Env::default(); + let (client, _) = setup_router(&env); + let root = BytesN::from_array(&env, &[1u8; 32]); + env.cost_estimate().budget().reset_unlimited(); + client.anchor_batch(&root, &1, &0, &10); +} + +#[test] +#[budget_cpu_lt(2_000_000)] +fn budget_anchor_batch_count_500() { + let env = Env::default(); + let (client, _) = setup_router(&env); + let root = BytesN::from_array(&env, &[2u8; 32]); + env.cost_estimate().budget().reset_unlimited(); + client.anchor_batch(&root, &500, &0, &10); +} + +#[test] +#[budget_cpu_lt(2_000_000)] +fn budget_anchor_batch_count_1000() { + let env = Env::default(); + let (client, _) = setup_router(&env); + let root = BytesN::from_array(&env, &[3u8; 32]); + env.cost_estimate().budget().reset_unlimited(); + client.anchor_batch(&root, &MAX_BATCH_SIZE, &0, &10); +} + +#[test] +#[budget_cpu_lt(1_000_000)] +fn budget_verify_receipt_depth_1() { + let env = Env::default(); + let (client, _) = setup_router(&env); + let (root, proof) = build_tree(&env, 1); + let leaf = BytesN::from_array(&env, &[0u8; 32]); + let batch_id = client.anchor_batch(&root, &2, &0, &100); + env.cost_estimate().budget().reset_unlimited(); + assert!(client.verify_receipt(&batch_id, &leaf, &proof)); +} + +#[test] +#[budget_cpu_lt(1_000_000)] +fn budget_verify_receipt_depth_10() { + let env = Env::default(); + let (client, _) = setup_router(&env); + let (root, proof) = build_tree(&env, 10); + let leaf = BytesN::from_array(&env, &[0u8; 32]); + // A 1024-leaf tree yields a 10-element proof; anchor it under the maximum + // batch size to prove the worst-case `verify_receipt` path. + let batch_id = client.anchor_batch(&root, &MAX_BATCH_SIZE, &0, &100); + env.cost_estimate().budget().reset_unlimited(); + assert!(client.verify_receipt(&batch_id, &leaf, &proof)); +} + +#[test] +#[budget_cpu_lt(5_000_000)] +fn budget_prune_batches_100() { + let env = Env::default(); + let (client, _) = setup_router(&env); + let root = BytesN::from_array(&env, &[9u8; 32]); + // Anchor 100 batches into shard 0 (SHARD_CAPACITY = 200), all old. + env.ledger().with_mut(|li| li.sequence_number = 100); + for _ in 0..100 { + client.anchor_batch(&root, &1, &0, &1); + } + // Jump the ledger far forward and delete up to MAX_PRUNE_BATCHES (100). + env.ledger().with_mut(|li| li.sequence_number = 1_000_000); + env.cost_estimate().budget().reset_unlimited(); + client.prune_batches(&500_000); +} diff --git a/contracts/receipt-anchor/src/lib.rs b/contracts/receipt-anchor/src/lib.rs index fad0819e..dcb67168 100644 --- a/contracts/receipt-anchor/src/lib.rs +++ b/contracts/receipt-anchor/src/lib.rs @@ -637,3 +637,9 @@ impl ReceiptAnchor { mod fuzz_test; #[cfg(test)] mod test; + +// Tier A soroban-budget-assert gates. Compiled only when the `budget-assert` +// feature is enabled (the budget CI job), so the normal test/clippy runs stay +// free of the prebuilt-WASM requirement and the `budget_macros` dev-dependency. +#[cfg(all(test, feature = "budget-assert"))] +mod budget_test; diff --git a/contracts/refund-vault/Cargo.toml b/contracts/refund-vault/Cargo.toml index 9deb9bdc..12408157 100644 --- a/contracts/refund-vault/Cargo.toml +++ b/contracts/refund-vault/Cargo.toml @@ -13,6 +13,13 @@ workspace = true [lib] crate-type = ["cdylib", "rlib"] +[features] +# Enables the Tier A budget-assertion tests in `src/budget_test.rs`. Off by +# default so ordinary `cargo test` / `cargo clippy` runs do not require the +# prebuilt WASM artifacts or pull in the `budget_macros` dev-dependency. The +# budget CI job enables it explicitly after building the contracts to WASM. +budget-assert = ["dep:budget_macros"] + [dependencies] soroban-sdk = { workspace = true } accensa-common = { workspace = true } @@ -27,3 +34,10 @@ multisig-account = { path = "../multisig-account", features = ["testutils"] } proptest = "1.4" soroban-sdk = { workspace = true, features = ["testutils"] } receipt-anchor = { path = "../receipt-anchor" } +# Tollcraft/soroban-budget-assert — Tier A local assertion macros. Pinned to a +# stable ref; the macros read `env.cost_estimate().budget()` on the WASM-mode +# estimate and fail the test if the pinned limit is exceeded. Optional so it is +# only compiled when the `budget-assert` feature is on. +# NOTE: pin this to a released tag (e.g. `tag = "v0.1.0"`) once a tag exists; +# `main` is used here so the dependency always resolves. +budget_macros = { git = "https://github.com/Tollcraft/soroban-budget-assert.git", branch = "main", package = "budget-macros", optional = true } diff --git a/contracts/refund-vault/src/budget_test.rs b/contracts/refund-vault/src/budget_test.rs new file mode 100644 index 00000000..9ecad65d --- /dev/null +++ b/contracts/refund-vault/src/budget_test.rs @@ -0,0 +1,67 @@ +#![cfg(all(test, feature = "budget-assert"))] + +//! Tier A budget assertions for `RefundVault`, using Tollcraft's +//! `soroban-budget-assert` `#[budget_cpu_lt(N)]` macro. +//! +//! Each attribute is a local WASM-mode CPU-estimate gate: the test fails if the +//! invocation's measured CPU instruction count exceeds `N`. `N` is the committed +//! failure threshold = `measured_baseline * 1.15` (the 15% tolerance defined in +//! `budget.toml`). The measured baselines live in `docs/BENCHMARKS.md`; update +//! them deliberately after re-measuring, never automatically. +//! +//! The contract must be compiled to WASM first: +//! `cargo build -p refund-vault --target wasm32v1-none --release` + +use super::*; +use budget_macros::budget_cpu_lt; +use soroban_sdk::{ + testutils::Address as _, + token::StellarAssetClient, + Address, BytesN, Env, +}; + +const FLOAT: i128 = 1_000_000; + +fn load_wasm(path: &str) -> std::vec::Vec { + std::fs::read(path).expect( + "refund-vault wasm not found; run `cargo build -p refund-vault \ + --target wasm32v1-none --release` before the budget tests", + ) +} + +/// Deploys the real `refund_vault` WASM, mints a test token, and initializes the +/// vault so `deposit` / `refund` behave exactly as they do on-chain. +fn setup(env: &Env, window: u32) -> (RefundVaultClient<'static>, Address, Address) { + let wasm = load_wasm("../../target/wasm32v1-none/release/refund_vault.wasm"); + let id = env.register_contract_wasm(None, &wasm); + let client = RefundVaultClient::new(env, &id); + env.mock_all_auths(); + let merchant = Address::generate(env); + let token_admin = Address::generate(env); + let sac = env.register_stellar_asset_contract_v2(token_admin); + let token = sac.address(); + StellarAssetClient::new(env, &token).mint(&merchant, &FLOAT); + client.initialize(&merchant, &token, &window); + (client, merchant, token) +} + +#[test] +#[budget_cpu_lt(1_500_000)] +fn budget_deposit() { + let env = Env::default(); + let (client, merchant, _token) = setup(&env, 100); + env.cost_estimate().budget().reset_unlimited(); + client.deposit(&merchant, &600_000); +} + +#[test] +#[budget_cpu_lt(2_000_000)] +fn budget_refund() { + let env = Env::default(); + let (client, merchant, _token) = setup(&env, 100); + client.deposit(&merchant, &500_000); + let payment_ref = BytesN::from_array(&env, &[7u8; 32]); + let buyer = Address::generate(&env); + env.cost_estimate().budget().reset_unlimited(); + client.refund(&payment_ref, &buyer, &120_000, &0, &120_000); +} diff --git a/contracts/refund-vault/src/lib.rs b/contracts/refund-vault/src/lib.rs index 6d6aae5c..43d0d17d 100644 --- a/contracts/refund-vault/src/lib.rs +++ b/contracts/refund-vault/src/lib.rs @@ -1953,3 +1953,9 @@ mod token_agnostic_tests; #[cfg(test)] mod vdf_test; mod yield_tests; + +// Tier A soroban-budget-assert gates. Compiled only when the `budget-assert` +// feature is enabled (the budget CI job), so the normal test/clippy runs stay +// free of the prebuilt-WASM requirement and the `budget_macros` dev-dependency. +#[cfg(all(test, feature = "budget-assert"))] +mod budget_test;