Skip to content

feat: add property-based testing and fuzzing for escrow & governance …#134

Merged
anonfedora merged 8 commits intoanonfedora:masterfrom
GazzyLee:feature/property-based-testing-fuzzing
Apr 2, 2026
Merged

feat: add property-based testing and fuzzing for escrow & governance …#134
anonfedora merged 8 commits intoanonfedora:masterfrom
GazzyLee:feature/property-based-testing-fuzzing

Conversation

@GazzyLee
Copy link
Copy Markdown
Contributor

@GazzyLee GazzyLee commented Mar 25, 2026

Summary

Introduces property-based testing (proptest) and fuzzing (cargo-fuzz/libfuzzer) for the escrow-manager and governance smart contracts to systematically discover edge cases and verify critical invariants.

Problem

The existing test suite uses hand-written example-based tests, which only cover anticipated scenarios. Edge cases in escrow balance accounting, governance voting arithmetic, and parameter validation may go undetected.

Solution

Cargo-fuzz targets (contracts/fuzz/) — libfuzzer-powered harnesses that generate random sequences of operations and verify invariants after every step:

  • fuzz_escrow_invariants — Generates random Create/Refund/AdvanceTime operation sequences and asserts: "Total assets in contract must always equal the sum of active escrow amounts."
  • fuzz_governance_voting — Randomizes voter count, voting power, quorum/majority thresholds, and asserts: vote tallies match individual powers, double-voting is always rejected, and proposals cannot execute without quorum + majority.

Proptest suites (contracts/*/tests/) — unit-level property checks with thousands of generated inputs:

  • Escrow (5 properties): non-positive amount rejection, active-sum invariant across N creates, balance preservation after refunds, monotonic escrow IDs, refund-before-expiry rejection.
  • Governance (6 properties): parameter range validation (liq_thr, liq_pen, grace_pd), vote tally consistency, double-vote rejection, monotonic proposal IDs, quorum/majority enforcement.

Changes

File Change
contracts/.cargo/config.toml New — cargo test-fuzz alias
contracts/Cargo.toml exclude = ["fuzz"] to isolate fuzz crate
contracts/escrow-manager/Cargo.toml Added rlib crate type + proptest dev-dep
contracts/governance/Cargo.toml Added rlib crate type + proptest dev-dep
contracts/governance/src/lib.rs set_voting_power gated on testutils feature (was #[cfg(test)] only)
contracts/fuzz/Cargo.toml New — standalone fuzz crate with libfuzzer-sys
contracts/fuzz/fuzz_targets/fuzz_escrow_invariants.rs New — escrow balance invariant fuzzer
contracts/fuzz/fuzz_targets/fuzz_governance_voting.rs New — governance voting invariant fuzzer
contracts/escrow-manager/tests/proptest_escrow.rs New — 5 proptest properties
contracts/governance/tests/proptest_governance.rs New — 6 proptest properties
.github/workflows/fuzz.yml New — dedicated CI: proptest (10k cases) + cargo-fuzz (1M iterations)
.github/workflows/ci.yml Added proptest step to existing contracts job

How to Run

All property tests (single command per acceptance criteria)
cd contracts && cargo test-fuzz

Individual proptest suites
cargo test --package escrow-manager --features testutils -- --include-ignored fuzz
cargo test --package governance --features testutils -- --include-ignored fuzz

Cargo-fuzz (requires nightly)
cd contracts/fuzz
cargo +nightly fuzz run fuzz_escrow_invariants -- -runs=1000000
cargo +nightly fuzz run fuzz_governance_voting -- -runs=1000000

Acceptance Criteria

  • Fuzzing suite runnable with cargo test-fuzz
  • No crashes or invariant violations during CI runs
  • 1,000,000+ iterations per fuzz target in CI
  • proptest for simpler unit-level property checks

Labels

infrastructure, testing, priority: high

Closes #129

Summary by CodeRabbit

  • Tests
    • Added extensive property-based and fuzz test suites for EscrowManager and Governance to improve correctness guarantees.
  • Chores
    • Introduced a new fuzzing workflow with configurable duration and crash artifact capture.
    • Updated CI to run an additional gated property-test phase and added a convenience test alias.
    • Adjusted build/workspace settings to enable test utilities and produce additional test artifacts.

…contracts

- Integrate cargo-fuzz with libfuzzer targets for EscrowManager and Governance
- Add proptest suites for unit-level property checks on both contracts
- Define EscrowManager invariant: total assets == sum of active escrows
- Define Governance invariants: vote tallies, double-vote rejection, quorum/majority
- Add cargo alias: 'cargo test-fuzz' runs all property tests
- Add dedicated CI workflow running 1M+ fuzzing iterations per target
- Update existing CI to include proptest in contract checks
@GazzyLee GazzyLee requested a review from anonfedora as a code owner March 25, 2026 16:36
@drips-wave
Copy link
Copy Markdown

drips-wave bot commented Mar 25, 2026

@GazzyLee Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@anonfedora
Copy link
Copy Markdown
Owner

Fix failing checks, please @GazzyLee

- Collapse short imports to single-line in proptest_escrow.rs and proptest_governance.rs (rustfmt)
- Collapse short function args to single-line in proptest_escrow.rs (rustfmt)
- Fix HostError(Auth, ExistingValue) in fuzz_governance_voting.rs by:
  - Writing config/total_power directly to storage instead of calling
    update_config/set_total_voting_power (which trigger admin.require_auth()
    conflicting with mock_all_auths + as_contract)
  - Splitting single large as_contract block into separate frames for
    init, proposal creation, voting, and invariant checks
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 26, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds property-based tests and libfuzzer targets for escrow and governance, CI workflows to run proptest and cargo-fuzz, a fuzz crate and cargo alias, workspace/config tweaks, expanded test-only visibility via testutils, and minor migration cleanup.

Changes

Cohort / File(s) Summary
CI Workflows & Cargo config
/.github/workflows/ci.yml, /.github/workflows/fuzz.yml, contracts/.cargo/config.toml, contracts/Cargo.toml
Added a proptest step to CI, a new "Fuzzing & Property Tests" workflow (proptest + cargo-fuzz), a test-fuzz cargo alias, and excluded the fuzz package from the contracts workspace.
Escrow manager: crate metadata & proptest
contracts/escrow-manager/Cargo.toml, contracts/escrow-manager/tests/proptest_escrow.rs
Enabled rlib crate-type, added proptest dev-dep, and added property-based tests covering amount validation, active-escrow-sum invariant, ID monotonicity, and refund expiry behavior.
Governance: crate metadata, API visibility & proptest
contracts/governance/Cargo.toml, contracts/governance/src/lib.rs, contracts/governance/tests/proptest_governance.rs
Enabled rlib, added proptest dev-dep, moved set_voting_power to #[cfg(any(test, feature = "testutils"))] under contractimpl, and added governance proptest suites (param validation, vote tallies, double-vote, monotonic IDs, execution gating).
Fuzz crate manifest & targets
contracts/fuzz/Cargo.toml, contracts/fuzz/fuzz_targets/fuzz_escrow_invariants.rs, contracts/fuzz/fuzz_targets/fuzz_governance_voting.rs
Added a stellovault-fuzz crate with two libfuzzer targets exercising escrow invariants and governance voting/invariants; includes dependencies, targets, and local mock contracts used by harnesses.
Fuzz job runtime & artifacts
/.github/workflows/fuzz.yml
CI job runs cargo-fuzz with time/size limits, uses caching, supports fuzz_duration input, and uploads crash artifacts on failure.
Misc migration cleanup
server/prisma/migrations/migration_lock.toml
Removed merge-conflict remnants and deduplicated the provider preamble.

Sequence Diagram(s)

sequenceDiagram
    participant CI as CI Workflow
    participant Runner as Proptest / Fuzzer Runner
    participant Env as Soroban Env
    participant Contracts as Contracts (mocks & targets)
    participant Storage as Contract Storage

    CI->>Runner: trigger proptest / cargo-fuzz job
    Runner->>Env: initialize test environment
    Env->>Contracts: deploy mocks and target contracts
    loop per operation / test case
        Runner->>Contracts: invoke operation (create/refund/vote/advance)
        Contracts->>Storage: read/update persistent state
        Contracts-->>Runner: return result
        Runner->>Runner: compute expected invariants
        Runner->>Storage: read stored values to verify
        alt invariant violated or crash
            Runner->>CI: report failure / upload artifacts
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • anonfedora

Poem

🐰
I hop through fuzz and proptest light,
tossing inputs in the night,
escrows counted, votes inspected,
invariants chased, crashes detected,
tiny paws, big tests — delight!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.74% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately summarizes the main change: introducing property-based testing and fuzzing infrastructure for escrow and governance contracts, which is the primary objective of the changeset.
Description check ✅ Passed The PR description is comprehensive and well-structured, covering Summary, Problem, Solution, detailed Changes table, How to Run instructions, Acceptance Criteria (all marked complete), and issue linkage. All template sections are addressed with substantive content.
Linked Issues check ✅ Passed The PR fully implements all coding requirements from issue #129: cargo-fuzz integration with two fuzz targets (escrow invariants and governance voting), EscrowManager invariant definition and verification, proptest suites with 5+6 properties, single-command execution via cargo test-fuzz, and CI automation with 1M+ iterations.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the PR objectives: fuzz infrastructure, proptest suites, CI workflows, and supporting configuration. A single incidental fix (migration_lock.toml merge-conflict cleanup) is minor and unrelated to core objectives.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
contracts/.cargo/config.toml (1)

1-2: Consider adding --test-threads=1 for consistency with CI.

The CI workflows (.github/workflows/ci.yml and .github/workflows/fuzz.yml) run these tests with --test-threads=1. Without this flag, the local alias might exhibit different behavior if tests have shared state or resource contention.

♻️ Suggested change
 [alias]
-test-fuzz = "test --features testutils -- --include-ignored fuzz"
+test-fuzz = "test --features testutils -- --include-ignored fuzz --test-threads=1"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/.cargo/config.toml` around lines 1 - 2, The test-fuzz cargo alias
currently runs "test --features testutils -- --include-ignored fuzz" without
forcing single-threaded execution; update the alias (test-fuzz) to append the
test runner flag --test-threads=1 so local runs match CI behavior and avoid
differing behavior from parallel test threads.
.github/workflows/fuzz.yml (1)

33-36: The wasm32-unknown-unknown target may be unnecessary for proptest.

The proptest job runs native Rust tests, not WASM compilation. Including this target adds installation overhead without benefit. Consider removing it from this job.

♻️ Remove unnecessary target
       - name: Install Rust
         uses: dtolnay/rust-toolchain@stable
-        with:
-          targets: wasm32-unknown-unknown
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/fuzz.yml around lines 33 - 36, The workflow currently adds
the unnecessary wasm target "wasm32-unknown-unknown" when using the
dtolnay/rust-toolchain action in the proptest job; remove the targets:
wasm32-unknown-unknown entry (or the entire with: block if no targets are
needed) from the "Install Rust" step that uses dtolnay/rust-toolchain@stable so
the job installs only the default native toolchain and avoids extra installation
overhead.
.github/workflows/ci.yml (1)

110-111: Consider setting PROPTEST_CASES environment variable for consistency.

The dedicated fuzz workflow (.github/workflows/fuzz.yml) sets PROPTEST_CASES: 10000 for the proptest runs. Without this variable here, the default proptest case count (typically 256) will be used, which provides less coverage than the dedicated fuzzing workflow.

If this is intentional to keep CI faster while the dedicated workflow provides thorough testing, consider adding a comment to clarify.

♻️ Optional: Add PROPTEST_CASES for more thorough CI coverage
       - name: Run Property-Based Tests (proptest)
-        run: cargo test --features testutils -- --include-ignored fuzz --test-threads=1
+        run: cargo test --features testutils -- --include-ignored fuzz --test-threads=1
+        env:
+          PROPTEST_CASES: 1000  # Lighter than fuzz.yml's 10000 for faster CI
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/ci.yml around lines 110 - 111, The proptest step "Run
Property-Based Tests (proptest)" currently invokes `cargo test --features
testutils -- --include-ignored fuzz --test-threads=1` without setting
PROPTEST_CASES, so it uses the default (lower) case count; set the environment
variable PROPTEST_CASES to the same value used in the fuzz workflow (e.g.,
`PROPTEST_CASES: 10000`) for consistency, or if the lower count is intentional
to speed CI, add a short comment next to the "Run Property-Based Tests
(proptest)" step explaining that the reduced case count is deliberate and that
the dedicated `fuzz.yml` workflow runs the full 10000 cases.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/fuzz.yml:
- Around line 78-82: Generate and commit the missing lockfile for the fuzz
workspace: run cargo generate-lockfile (or cargo build) inside the
contracts/fuzz workspace to produce contracts/fuzz/Cargo.lock, add and commit
that file so the GitHub Actions cache key referencing contracts/fuzz/Cargo.lock
becomes stable and cache invalidation works when fuzzer dependencies change;
ensure the committed file is included in the repo before relying on the cache
key in .github/workflows/fuzz.yml.

---

Nitpick comments:
In @.github/workflows/ci.yml:
- Around line 110-111: The proptest step "Run Property-Based Tests (proptest)"
currently invokes `cargo test --features testutils -- --include-ignored fuzz
--test-threads=1` without setting PROPTEST_CASES, so it uses the default (lower)
case count; set the environment variable PROPTEST_CASES to the same value used
in the fuzz workflow (e.g., `PROPTEST_CASES: 10000`) for consistency, or if the
lower count is intentional to speed CI, add a short comment next to the "Run
Property-Based Tests (proptest)" step explaining that the reduced case count is
deliberate and that the dedicated `fuzz.yml` workflow runs the full 10000 cases.

In @.github/workflows/fuzz.yml:
- Around line 33-36: The workflow currently adds the unnecessary wasm target
"wasm32-unknown-unknown" when using the dtolnay/rust-toolchain action in the
proptest job; remove the targets: wasm32-unknown-unknown entry (or the entire
with: block if no targets are needed) from the "Install Rust" step that uses
dtolnay/rust-toolchain@stable so the job installs only the default native
toolchain and avoids extra installation overhead.

In `@contracts/.cargo/config.toml`:
- Around line 1-2: The test-fuzz cargo alias currently runs "test --features
testutils -- --include-ignored fuzz" without forcing single-threaded execution;
update the alias (test-fuzz) to append the test runner flag --test-threads=1 so
local runs match CI behavior and avoid differing behavior from parallel test
threads.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e6370835-366b-4efe-b767-f3cb82634abe

📥 Commits

Reviewing files that changed from the base of the PR and between 9f71894 and 0bba557.

📒 Files selected for processing (12)
  • .github/workflows/ci.yml
  • .github/workflows/fuzz.yml
  • contracts/.cargo/config.toml
  • contracts/Cargo.toml
  • contracts/escrow-manager/Cargo.toml
  • contracts/escrow-manager/tests/proptest_escrow.rs
  • contracts/fuzz/Cargo.toml
  • contracts/fuzz/fuzz_targets/fuzz_escrow_invariants.rs
  • contracts/fuzz/fuzz_targets/fuzz_governance_voting.rs
  • contracts/governance/Cargo.toml
  • contracts/governance/src/lib.rs
  • contracts/governance/tests/proptest_governance.rs

@anonfedora
Copy link
Copy Markdown
Owner

Fix failing CI before EOD @Gazzy-Lee

The #[cfg(any(test, feature = testutils))] attribute on set_voting_power
inside the main #[contractimpl] block caused the Soroban proc macro to
generate dispatch code referencing __set_voting_power even when the method
was cfg-gated out, resulting in:
- E0433 (unresolved module) in non-test builds
- E0061/E0063 in fuzz and proptest builds

Moving it to its own #[cfg]-gated #[contractimpl] block ensures the entire
block (including macro-generated dispatch) is conditionally compiled.
…based-testing-fuzzing

# Conflicts:
#	contracts/governance/src/lib.rs
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@contracts/governance/src/lib.rs`:
- Around line 917-924: The set_voting_power method in impl Governance currently
allows unauthenticated callers to mutate storage (key built with
symbol_short!("vp")), which is dangerous when feature flags change; add an
explicit authorization check at the top of pub fn set_voting_power(env: Env,
voter: Address, power: i128) so only an authorized principal can call it (e.g.,
require the contract owner/admin or call the existing auth helper such as
env.require_auth(owner_address) or Governance::require_admin(&env) if present),
then proceed to set the storage; ensure the check uses the same types
(Address/Env) as in the function and returns/aborts on unauthorized access.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b20c6549-0578-4392-9fa6-3bee4a9af0e1

📥 Commits

Reviewing files that changed from the base of the PR and between c082dc0 and dbf7f00.

📒 Files selected for processing (1)
  • contracts/governance/src/lib.rs

Comment on lines +917 to +924
#[cfg(any(test, feature = "testutils"))]
#[contractimpl]
impl Governance {
/// Set voting power for testing
pub fn set_voting_power(env: Env, voter: Address, power: i128) {
let key = (symbol_short!("vp"), voter);
env.storage().persistent().set(&key, &power);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify that `testutils` is not enabled in deploy/release paths and is limited to test/fuzz usage.

set -euo pipefail

echo "== Feature definitions and references =="
rg -n --iglob '*.toml' '\btestutils\b|^\[features\]|^default\s*='

echo
echo "== CI/workflow references to feature flags =="
rg -n --iglob '*.yml' --iglob '*.yaml' -- '--features\s+testutils|--all-features|cargo\s+\+?nightly|soroban\s+contract\s+(build|deploy)|cargo\s+(build|run|install)'

echo
echo "== Direct calls to set_voting_power =="
rg -n '\bset_voting_power\s*\(' contracts/governance/src/lib.rs

Repository: anonfedora/stellovault

Length of output: 2516


🏁 Script executed:

cat -n contracts/governance/Cargo.toml | head -30

Repository: anonfedora/stellovault

Length of output: 783


🏁 Script executed:

rg -n '\bset_voting_power\b' --type rust

Repository: anonfedora/stellovault

Length of output: 7048


🏁 Script executed:

fd -name 'Makefile' -o -name '.github' -type d

Repository: anonfedora/stellovault

Length of output: 296


🏁 Script executed:

find . -type f \( -name '*.yml' -o -name '*.yaml' -o -name 'Makefile' \) | head -20

Repository: anonfedora/stellovault

Length of output: 204


🏁 Script executed:

cat -n .github/workflows/ci.yml | head -80

Repository: anonfedora/stellovault

Length of output: 2451


🏁 Script executed:

cat -n .github/workflows/fuzz.yml | head -50

Repository: anonfedora/stellovault

Length of output: 1757


🏁 Script executed:

sed -n '915,930p' contracts/governance/src/lib.rs

Repository: anonfedora/stellovault

Length of output: 613


🏁 Script executed:

sed -n '917,930p' contracts/governance/src/lib.rs

Repository: anonfedora/stellovault

Length of output: 532


🏁 Script executed:

sed -n '80,150p' .github/workflows/ci.yml

Repository: anonfedora/stellovault

Length of output: 1028


🏁 Script executed:

rg -n 'deploy|release|wasm|build' .github/workflows/ci.yml --context 2

Repository: anonfedora/stellovault

Length of output: 782


Add authentication to set_voting_power to harden against feature flag misconfiguration.

The function at lines 921–923 allows unauthenticated state mutation. While testutils is correctly absent from the release build configuration, expanding from #[cfg(test)] to any(test, feature = "testutils") creates a feature-flag drift risk—if a developer accidentally builds with --all-features or if feature flags are misconfigured in a deploy step, voting power can be rewritten by any caller.

🔒 Suggested hardening
 #[cfg(any(test, feature = "testutils"))]
 #[contractimpl]
 impl Governance {
     /// Set voting power for testing
-    pub fn set_voting_power(env: Env, voter: Address, power: i128) {
+    pub fn set_voting_power(env: Env, voter: Address, power: i128) -> Result<(), ContractError> {
+        let admin: Address = env
+            .storage()
+            .instance()
+            .get(&symbol_short!("admin"))
+            .ok_or(ContractError::Unauthorized)?;
+        admin.require_auth();
+        if power < 0 {
+            return Err(ContractError::InvalidValue);
+        }
         let key = (symbol_short!("vp"), voter);
         env.storage().persistent().set(&key, &power);
+        Ok(())
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#[cfg(any(test, feature = "testutils"))]
#[contractimpl]
impl Governance {
/// Set voting power for testing
pub fn set_voting_power(env: Env, voter: Address, power: i128) {
let key = (symbol_short!("vp"), voter);
env.storage().persistent().set(&key, &power);
}
#[cfg(any(test, feature = "testutils"))]
#[contractimpl]
impl Governance {
/// Set voting power for testing
pub fn set_voting_power(env: Env, voter: Address, power: i128) -> Result<(), ContractError> {
let admin: Address = env
.storage()
.instance()
.get(&symbol_short!("admin"))
.ok_or(ContractError::Unauthorized)?;
admin.require_auth();
if power < 0 {
return Err(ContractError::InvalidValue);
}
let key = (symbol_short!("vp"), voter);
env.storage().persistent().set(&key, &power);
Ok(())
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/governance/src/lib.rs` around lines 917 - 924, The set_voting_power
method in impl Governance currently allows unauthenticated callers to mutate
storage (key built with symbol_short!("vp")), which is dangerous when feature
flags change; add an explicit authorization check at the top of pub fn
set_voting_power(env: Env, voter: Address, power: i128) so only an authorized
principal can call it (e.g., require the contract owner/admin or call the
existing auth helper such as env.require_auth(owner_address) or
Governance::require_admin(&env) if present), then proceed to set the storage;
ensure the check uses the same types (Address/Env) as in the function and
returns/aborts on unauthorized access.

- Gate proptest_governance.rs with #![cfg(feature = testutils)] so
  it only compiles when testutils is enabled (fixes cargo test without
  --features testutils)
- Add missing 5th argument (num_votes) to all Governance::cast_vote
  calls in proptest and fuzz targets to match updated API signature
- Add missing tally_period field to GovernanceConfig initializations
- Set voter balances to power*power to account for quadratic voting
  sqrt in get_voting_power
- Remove unused imports (Symbol, ProposalStatus)
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
contracts/governance/tests/proptest_governance.rs (3)

221-222: Add a comment explaining the power * power voting power pattern.

The pattern of setting voting_power = power * power while casting a vote with power appears throughout the tests but isn't documented. This seems intentional to satisfy some contract invariant (perhaps voting_power >= vote_amount²), but readers will find it confusing without explanation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/governance/tests/proptest_governance.rs` around lines 221 - 222,
Add a short inline comment next to the Governance::set_voting_power(...) call
explaining why we set voting_power = power * power before calling
Governance::cast_vote(...): state that tests intentionally grant squared voting
power to satisfy the contract invariant (e.g., voting_power must be >=
vote_amount * vote_amount or similar square-based check) so casting a vote with
amount `power` succeeds; mention the invariant name if known and reference the
two functions (Governance::set_voting_power and Governance::cast_vote) so future
readers understand the relationship.

160-164: Inconsistent coverage: grace period test doesn't include negative values.

The liq_threshold and liq_penalty tests use i128::MIN.. ranges to include negative values, but grace_period only tests 0i128..300i128. For consistency and completeness, consider including negative values.

♻️ Suggested fix
         fn prop_grace_period_validation(value in prop_oneof![
             (300i128..=86400i128),
-            (0i128..300i128),
+            (i128::MIN..300i128),
             (86401i128..1_000_000i128),
         ]) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/governance/tests/proptest_governance.rs` around lines 160 - 164,
The property test function prop_grace_period_validation currently omits negative
numbers in its prop_oneof set; update its prop_oneof to include a negative range
(e.g., i128::MIN..0i128 or a bounded negative range similar to
liq_threshold/liq_penalty tests) so the grace period validation covers negative
values as well; modify the prop_oneof inside prop_grace_period_validation to add
that negative branch while keeping the existing valid and too-large ranges.

196-197: Consider generating equal-length vectors directly.

The powers and supports vectors are generated independently with potentially different lengths, requiring the min() at line 214. Using a single length generator would be cleaner.

♻️ Suggested fix
         fn prop_vote_tallies_consistent(
-            powers in proptest::collection::vec(1i128..100_000i128, 1..8),
-            supports in proptest::collection::vec(proptest::bool::ANY, 1..8),
+            num_voters in 1usize..8,
+            powers in proptest::collection::vec(1i128..100_000i128, 8),
+            supports in proptest::collection::vec(proptest::bool::ANY, 8),
         ) {

Then use num_voters as the loop bound instead of count.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/governance/tests/proptest_governance.rs` around lines 196 - 197,
The two vectors powers and supports are generated independently which can yield
different lengths and forces using min(); instead generate a single size
variable (e.g., num_voters) with proptest::num::usize::ANY or a range (1..8) and
use that size for both proptest::collection::vec calls so powers and supports
share the same length, then iterate up to num_voters (replace uses of count/min
with num_voters) when building votes; update references to functions/variables
powers, supports, num_voters, and the proptest::collection::vec calls
accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@contracts/governance/tests/proptest_governance.rs`:
- Around line 383-391: The test currently only asserts that execute_proposal
fails when quorum/majority aren't met but never asserts success when they are
met; update the test after the existing if-block to assert exec_result.is_ok()
(and optionally inspect the returned result) when total_votes >= quorum_needed
&& for_power >= majority_needed so the test fails if execute_proposal always
returns an error; reference the local variables total_votes, quorum_needed,
for_power, majority_needed and the exec_result/execute_proposal call when adding
the positive assertion and any additional checks (e.g., proposal state or
returned value) to verify successful execution.

---

Nitpick comments:
In `@contracts/governance/tests/proptest_governance.rs`:
- Around line 221-222: Add a short inline comment next to the
Governance::set_voting_power(...) call explaining why we set voting_power =
power * power before calling Governance::cast_vote(...): state that tests
intentionally grant squared voting power to satisfy the contract invariant
(e.g., voting_power must be >= vote_amount * vote_amount or similar square-based
check) so casting a vote with amount `power` succeeds; mention the invariant
name if known and reference the two functions (Governance::set_voting_power and
Governance::cast_vote) so future readers understand the relationship.
- Around line 160-164: The property test function prop_grace_period_validation
currently omits negative numbers in its prop_oneof set; update its prop_oneof to
include a negative range (e.g., i128::MIN..0i128 or a bounded negative range
similar to liq_threshold/liq_penalty tests) so the grace period validation
covers negative values as well; modify the prop_oneof inside
prop_grace_period_validation to add that negative branch while keeping the
existing valid and too-large ranges.
- Around line 196-197: The two vectors powers and supports are generated
independently which can yield different lengths and forces using min(); instead
generate a single size variable (e.g., num_voters) with
proptest::num::usize::ANY or a range (1..8) and use that size for both
proptest::collection::vec calls so powers and supports share the same length,
then iterate up to num_voters (replace uses of count/min with num_voters) when
building votes; update references to functions/variables powers, supports,
num_voters, and the proptest::collection::vec calls accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 38e45443-eddb-4530-99b7-40225e0b59eb

📥 Commits

Reviewing files that changed from the base of the PR and between dbf7f00 and 2a25270.

📒 Files selected for processing (2)
  • contracts/fuzz/fuzz_targets/fuzz_governance_voting.rs
  • contracts/governance/tests/proptest_governance.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • contracts/fuzz/fuzz_targets/fuzz_governance_voting.rs

Comment on lines +383 to +391
if total_votes < quorum_needed || for_power < majority_needed {
prop_assert!(
exec_result.is_err(),
"Should fail: votes={}, quorum_needed={}, for={}, majority_needed={}",
total_votes, quorum_needed, for_power, majority_needed
);
}
// If quorum and majority are met, success is valid
Ok(())
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Incomplete test coverage: missing assertion for successful execution.

The test verifies that execution fails when quorum/majority aren't met, but doesn't verify that execution succeeds when they ARE met. The comment on line 390 acknowledges this gap without addressing it. This means the test would pass even if execute_proposal always returned an error.

🐛 Suggested fix to verify both cases
                 if total_votes < quorum_needed || for_power < majority_needed {
                     prop_assert!(
                         exec_result.is_err(),
                         "Should fail: votes={}, quorum_needed={}, for={}, majority_needed={}",
                         total_votes, quorum_needed, for_power, majority_needed
                     );
+                } else {
+                    prop_assert!(
+                        exec_result.is_ok(),
+                        "Should succeed: votes={}, quorum_needed={}, for={}, majority_needed={}",
+                        total_votes, quorum_needed, for_power, majority_needed
+                    );
                 }
-                // If quorum and majority are met, success is valid
                 Ok(())
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if total_votes < quorum_needed || for_power < majority_needed {
prop_assert!(
exec_result.is_err(),
"Should fail: votes={}, quorum_needed={}, for={}, majority_needed={}",
total_votes, quorum_needed, for_power, majority_needed
);
}
// If quorum and majority are met, success is valid
Ok(())
if total_votes < quorum_needed || for_power < majority_needed {
prop_assert!(
exec_result.is_err(),
"Should fail: votes={}, quorum_needed={}, for={}, majority_needed={}",
total_votes, quorum_needed, for_power, majority_needed
);
} else {
prop_assert!(
exec_result.is_ok(),
"Should succeed: votes={}, quorum_needed={}, for={}, majority_needed={}",
total_votes, quorum_needed, for_power, majority_needed
);
}
Ok(())
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/governance/tests/proptest_governance.rs` around lines 383 - 391,
The test currently only asserts that execute_proposal fails when quorum/majority
aren't met but never asserts success when they are met; update the test after
the existing if-block to assert exec_result.is_ok() (and optionally inspect the
returned result) when total_votes >= quorum_needed && for_power >=
majority_needed so the test fails if execute_proposal always returns an error;
reference the local variables total_votes, quorum_needed, for_power,
majority_needed and the exec_result/execute_proposal call when adding the
positive assertion and any additional checks (e.g., proposal state or returned
value) to verify successful execution.

@anonfedora
Copy link
Copy Markdown
Owner

Fix buzzing CI, please

@anonfedora anonfedora merged commit 7b7b993 into anonfedora:master Apr 2, 2026
6 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

End-to-End Fuzz Testing for Smart Contracts

3 participants