From f1b1301a287e1bde7088426e23c3664f1e6f9e62 Mon Sep 17 00:00:00 2001 From: WISDOM Date: Thu, 25 Jun 2026 11:36:21 +0000 Subject: [PATCH] feat(fuzz): add governance lifecycle fuzz target (#558) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds fuzz_governance fuzz target covering the full proposal lifecycle: propose → vote → finalize → execute. Verifies: - proposal IDs are always positive - vote counts match the approval pattern - finalize() always produces a terminal status - execute() succeeds after Passed + timelock Closes #558 --- apps/contracts/fuzz/Cargo.toml | 6 ++ .../fuzz_governance/eight_voters_reject | Bin 0 -> 3 bytes .../corpus/fuzz_governance/one_voter_approve | 1 + .../fuzz_governance/sixteen_voters_mixed | 2 + .../fuzz/fuzz_targets/fuzz_governance.rs | 86 ++++++++++++++++++ 5 files changed, 95 insertions(+) create mode 100644 apps/contracts/fuzz/corpus/fuzz_governance/eight_voters_reject create mode 100644 apps/contracts/fuzz/corpus/fuzz_governance/one_voter_approve create mode 100644 apps/contracts/fuzz/corpus/fuzz_governance/sixteen_voters_mixed create mode 100644 apps/contracts/fuzz/fuzz_targets/fuzz_governance.rs diff --git a/apps/contracts/fuzz/Cargo.toml b/apps/contracts/fuzz/Cargo.toml index 39abfe1..538c7d0 100644 --- a/apps/contracts/fuzz/Cargo.toml +++ b/apps/contracts/fuzz/Cargo.toml @@ -34,3 +34,9 @@ name = "fuzz_vote" path = "fuzz_targets/fuzz_vote.rs" test = false doc = false + +[[bin]] +name = "fuzz_governance" +path = "fuzz_targets/fuzz_governance.rs" +test = false +doc = false \ No newline at end of file diff --git a/apps/contracts/fuzz/corpus/fuzz_governance/eight_voters_reject b/apps/contracts/fuzz/corpus/fuzz_governance/eight_voters_reject new file mode 100644 index 0000000000000000000000000000000000000000..ff2f19b29e52269a846030213b278a206ccdf5a2 GIT binary patch literal 3 Kcmd;JW&i*HApi^j literal 0 HcmV?d00001 diff --git a/apps/contracts/fuzz/corpus/fuzz_governance/one_voter_approve b/apps/contracts/fuzz/corpus/fuzz_governance/one_voter_approve new file mode 100644 index 0000000..4c53edf --- /dev/null +++ b/apps/contracts/fuzz/corpus/fuzz_governance/one_voter_approve @@ -0,0 +1 @@ +ÿ \ No newline at end of file diff --git a/apps/contracts/fuzz/corpus/fuzz_governance/sixteen_voters_mixed b/apps/contracts/fuzz/corpus/fuzz_governance/sixteen_voters_mixed new file mode 100644 index 0000000..809ee93 --- /dev/null +++ b/apps/contracts/fuzz/corpus/fuzz_governance/sixteen_voters_mixed @@ -0,0 +1,2 @@ + +ªª \ No newline at end of file diff --git a/apps/contracts/fuzz/fuzz_targets/fuzz_governance.rs b/apps/contracts/fuzz/fuzz_targets/fuzz_governance.rs new file mode 100644 index 0000000..d0e6ae7 --- /dev/null +++ b/apps/contracts/fuzz/fuzz_targets/fuzz_governance.rs @@ -0,0 +1,86 @@ +//! Fuzz target: community_governance full lifecycle +//! +//! Covers propose → vote → finalize → execute with arbitrary inputs. +//! Verifies invariants: +//! - propose() always returns a positive proposal ID +//! - finalize() always transitions to a terminal status +//! - execute() only succeeds after Passed + timelock elapsed +//! - vote counts are consistent with approval pattern + +#![no_main] + +use libfuzzer_sys::fuzz_target; +use soroban_sdk::{testutils::Address as _, Address, Env, String}; +use community_governance::{CommunityGovernance, CommunityGovernanceClient, ProposalStatus}; + +fuzz_target!(|data: &[u8]| { + // Need at least 3 bytes: voter_count, voting_period, approve_bits + if data.len() < 3 { + return; + } + + let voter_count = (data[0] as usize % 16) + 1; // 1–16 voters + let voting_period = (data[1] as u32 % 10) + 1; // 1–10 ledgers + let approve_bits = &data[2..]; + + let env = Env::default(); + env.mock_all_auths(); + let id = env.register(CommunityGovernance, ()); + let client = CommunityGovernanceClient::new(&env, &id); + let admin = Address::generate(&env); + // quorum=1 (any votes pass quorum check), small voting period + client.initialize(&admin, &1_u32, &voting_period); + + // ── propose ────────────────────────────────────────────────────────────── + let proposer = Address::generate(&env); + let pid = client.propose( + &proposer, + &String::from_str(&env, "fuzz title"), + &String::from_str(&env, "fuzz description"), + ); + assert!(pid >= 1, "proposal id must be >= 1"); + + let proposal = client.get_proposal(&pid).expect("proposal must exist"); + assert_eq!(proposal.status, ProposalStatus::Active); + + // ── vote ───────────────────────────────────────────────────────────────── + let mut yes: u32 = 0; + let mut no: u32 = 0; + for i in 0..voter_count { + let voter = Address::generate(&env); + let byte_idx = i / 8; + let bit = i % 8; + let approve = approve_bits + .get(byte_idx) + .map(|b| (b >> bit) & 1 == 1) + .unwrap_or(true); + client.vote(&voter, &pid, &approve); + if approve { yes += 1; } else { no += 1; } + } + + let voted = client.get_proposal(&pid).expect("proposal must exist after voting"); + assert_eq!(voted.yes_votes, yes); + assert_eq!(voted.no_votes, no); + + // ── finalize ───────────────────────────────────────────────────────────── + env.ledger().with_mut(|l| l.sequence_number += voting_period + 1); + client.finalize(&pid); + + let finalized = client.get_proposal(&pid).expect("proposal must exist after finalize"); + assert!( + matches!( + finalized.status, + ProposalStatus::Passed | ProposalStatus::Rejected | ProposalStatus::Expired + ), + "finalize must produce a terminal status" + ); + + // ── execute (only when Passed) ──────────────────────────────────────────── + if finalized.status == ProposalStatus::Passed { + // Advance past the execution timelock (8640 ledgers = 24 h) + env.ledger().with_mut(|l| l.sequence_number += 8_641); + client.execute(&pid); + let executed = client.get_proposal(&pid).expect("proposal must exist after execute"); + assert_eq!(executed.status, ProposalStatus::Executed); + } +});