Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 36 additions & 7 deletions .github/workflows/contracts.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
name: Contracts

name: Contracts

on:
push:
branches: [main]
Expand All @@ -11,12 +9,14 @@ on:
branches: [main]
paths:
- "contracts/**"
schedule:
- cron: "0 2 * * *"

permissions:
contents: read

concurrency:
group: contracts-$ {{ github.workflow }}-$ {{ github.ref }}
group: contracts-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
Expand Down Expand Up @@ -64,7 +64,10 @@ jobs:
- name: Run contract fuzz tests
run: cargo test -p subscription_renewal -p escrow -p payment-channel -p virtual-card fuzz_
env:
PROPTYST_CASES: "8"
PROPTEST_CASES: "8"
# Fixed seed so the property/state-machine tests are
# reproducible on every PR run.
PROPTEST_SEED: "0x1234567890abcdef1234567890abcdef"

- name: Verify backend contract interface alignment
working-directory: ..
Expand All @@ -80,13 +83,13 @@ jobs:

wasm_files=(target/wasm32-unknown-unknown/release/*.wasm)

if [ ${#wasm_files[@]_ -} eq 0 ]; then
if [ "${#wasm_files[@]}" -eq 0 ]; then
echo "No WASM artifacts found in target/wasm32-unknown-unknown/release"
exit 1
fi

for wasm in "${wasm_files[@]_ }"; do
size=$wc -c < "$wasm"
for wasm in "${wasm_files[@]}"; do
size=$(wc -c < "$wasm")
echo "$wasm: ${size} bytes"

if [ "$size" -gt 65536 ]; then
Expand All @@ -97,3 +100,29 @@ jobs:
- name: Verify mainnet promotion gates
working-directory: ..
run: npx -y tsx deploy/verify-gates.ts

fuzz-nightly:
runs-on: ubuntu-latest
if: github.event_name == 'schedule'
timeout-minutes: 60
defaults:
run:
working-directory: contracts

steps:
- uses: actions/checkout@v7

- name: Install Rust
uses: dtolnay/rust-toolchain@stable

- name: Cache Rust dependencies
uses: Swatinem/rust-cache@v2
with:
workspaces: contracts

- name: Run extended property/fuzz runs
run: cargo test -p subscription_renewal -p escrow -p payment-channel -p virtual-card fuzz_
env:
# Extended case count for the nightly soak (PR runs use a
# small, fixed count via PROPTEST_CASES=8 + PROPTEST_SEED).
PROPTEST_CASES: "512"
15 changes: 15 additions & 0 deletions contracts/FUZZING_EDGE_CASES.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,20 @@ cargo test fuzz_
| Approval reuse after successful renewal | Panics — approvals are single-use | `fuzz_approval_single_use` |
| Admin operations on uninitialized contract | Panics — no admin in storage | `fuzz_uninitialized_contract_rejects_admin_ops` |
| Random amounts/intervals on init | Stored values match inputs; state stays `Active` | `fuzz_init_sub_amounts_and_intervals` |
| Random entrypoint sequences | All renewal invariants hold (caps, cycle guard, state graph, lock release) | `fuzz_renewal_state_machine` |

The `fuzz_renewal_state_machine` property test drives a proptest state machine
through random sequences of `init_sub`, `approve_renewal`, `renew`, `cancel_sub`,
`set_window` and `set_user_cap`, asserting after every step:

1. Every accepted renewal respects the per-subscription spending cap.
2. Cumulative `UserSpent` never exceeds the global `UserCap` as a result of a renewal.
3. At most one successful renewal per billing window (cycle guard).
4. `SubscriptionState` transitions follow the declared graph.
5. The renewal lock is never held after a completed call.

The invariant list is documented on the crate root (`subscription_renewal/src/lib.rs`)
and mirrored in `fuzz.rs`.

## escrow

Expand All @@ -43,6 +57,7 @@ cargo test fuzz_
## Notes

- Fuzz tests use 8 cases per property (`ProptestConfig::with_cases(8)`) for fast CI runs.
- CI runs the fuzz/state-machine suite on every PR with a fixed `PROPTEST_SEED` and a bounded case count (`PROPTEST_CASES=8`) for reproducibility, and a separate nightly job (`fuzz-nightly` in `.github/workflows/contracts.yml`) with an extended case count (`PROPTEST_CASES=512`).
- Fuzz tests disable Soroban snapshot capture (`EnvTestConfig::capture_snapshot_at_drop = false`); no snapshot JSON files are committed.
- Integer overflow is guarded by Rust `overflow-checks = true` in release profile and explicit `saturating_add` checks in fuzz assertions where applicable.
- Panic-based contracts (`subscription_renewal`, `escrow`) use `catch_unwind` to verify rejection paths; `Result`-based `payment-channel` checks `Err` variants directly.
Loading