Skip to content

quorum and counting#568

Open
ozgunozerk wants to merge 1 commit intomainfrom
counting
Open

quorum and counting#568
ozgunozerk wants to merge 1 commit intomainfrom
counting

Conversation

@ozgunozerk
Copy link
Collaborator

@ozgunozerk ozgunozerk commented Feb 17, 2026

Fixes #564

PR Checklist

  • Tests
  • Documentation

Summary by CodeRabbit

  • New Features
    • Added governance voting and counting functionality to the Soroban governance system with support for For, Against, and Abstain vote types.
    • Implemented quorum management and validation to ensure minimum participation thresholds are met.
    • Added vote tracking to prevent duplicate voting and support simple majority tally calculations.
    • Introduced event system for quorum changes to improve governance transparency.

@ozgunozerk ozgunozerk requested a review from brozorec February 17, 2026 12:13
@ozgunozerk ozgunozerk self-assigned this Feb 17, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 17, 2026

Walkthrough

Introduces a new counting module for the governance package that implements vote tallying functionality with quorum management, voter tracking, and support for Against/For/Abstain vote types. Includes trait definitions, storage layer, comprehensive test coverage, and constants for TTL semantics.

Changes

Cohort / File(s) Summary
Counting Trait & Errors
packages/governance/src/counting/mod.rs
Defines public Counting trait with methods for voting/tallying (counting_mode, has_voted, quorum, quorum_reached, tally_succeeded, count_vote); adds CountingError enum with variants (AlreadyVoted, InvalidVoteType, QuorumNotSet, MathOverflow); introduces QuorumChanged event and TTL constants (DAY_IN_LEDGERS, COUNTING_EXTEND_AMOUNT, COUNTING_TTL_THRESHOLD); re-exports storage types.
Counting Storage Implementation
packages/governance/src/counting/storage.rs
Implements storage layer with CountingStorageKey enum and ProposalVoteCounts struct; provides vote constants (VOTE_AGAINST, VOTE_FOR, VOTE_ABSTAIN); includes query functions (counting_mode, has_voted, get_quorum, quorum_reached, tally_succeeded, get_proposal_vote_counts) with TTL extension semantics; adds state mutators (set_quorum, count_vote) with validation and error handling for double-voting and invalid vote types.
Test Suite
packages/governance/src/counting/test.rs
Comprehensive test coverage for counting functionality including initialization, quorum management, vote counting across vote types, multi-voter scenarios, tallying validation, quorum reach checks, error conditions (double-voting, overflow, invalid types), and full governance flow demonstrations.
Module Export
packages/governance/src/lib.rs
Exports new counting module as public API alongside existing timelock and votes modules.

Sequence Diagram

sequenceDiagram
    participant Voter
    participant CountingTrait as Counting Trait
    participant Storage
    participant Event as Event System

    Voter->>CountingTrait: count_vote(proposal_id, account, vote_type, weight)
    CountingTrait->>Storage: has_voted(proposal_id, account)
    Storage-->>CountingTrait: bool (false = can vote)
    
    CountingTrait->>Storage: get_proposal_vote_counts(proposal_id)
    Storage-->>CountingTrait: ProposalVoteCounts
    
    CountingTrait->>Storage: Update vote tally by weight
    Storage->>Storage: Increment for/against/abstain
    
    CountingTrait->>Storage: Mark HasVoted(proposal_id, account)
    Storage-->>CountingTrait: vote recorded
    CountingTrait-->>Voter: success
    
    Note over Voter,Storage: Later: Check voting outcome
    Voter->>CountingTrait: tally_succeeded(proposal_id)
    CountingTrait->>Storage: get_proposal_vote_counts(proposal_id)
    Storage-->>CountingTrait: ProposalVoteCounts
    CountingTrait->>CountingTrait: Compare for_votes > against_votes
    CountingTrait-->>Voter: bool (success status)
    
    Voter->>CountingTrait: quorum_reached(proposal_id)
    CountingTrait->>Storage: get_proposal_vote_counts(proposal_id)
    Storage-->>CountingTrait: ProposalVoteCounts
    CountingTrait->>Storage: get_quorum()
    Storage-->>CountingTrait: u128
    CountingTrait->>CountingTrait: Check (for + abstain) >= quorum
    CountingTrait-->>Voter: bool (quorum status)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Poem

🐰 Hops of joy for votes so clear,
Against, For, Abstain brought near!
Quorum tallies, storage bright,
Counting logic shines so right!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'quorum and counting' accurately reflects the main changes, which introduce a new counting module with quorum functionality for the Governor contract.
Description check ✅ Passed The PR description follows the template, references issue #564, and confirms completion of Tests and Documentation checklist items.
Linked Issues check ✅ Passed The PR implements a default Counting extension for Governor as required by issue #564, including vote counting logic, quorum management, and tally calculations.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the counting extension feature requested in #564; the new module, storage logic, tests, and library exports are all in scope.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch counting

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


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

@codecov
Copy link

codecov bot commented Feb 17, 2026

Codecov Report

❌ Patch coverage is 95.94595% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 96.23%. Comparing base (606ec84) to head (7994a9a).

Files with missing lines Patch % Lines
packages/governance/src/counting/storage.rs 95.94% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #568      +/-   ##
==========================================
- Coverage   96.24%   96.23%   -0.01%     
==========================================
  Files          57       58       +1     
  Lines        5507     5581      +74     
==========================================
+ Hits         5300     5371      +71     
- Misses        207      210       +3     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Contributor

@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 `@packages/governance/src/counting/storage.rs`:
- Around line 263-268: The vote-count and has-voted entries are written with
e.storage().persistent().set(...) but do not extend TTL; after calling
e.storage().persistent().set(&CountingStorageKey::ProposalVote(proposal_id.clone()),
&counts) and after e.storage().persistent().set(&voted_key, &true) call the
storage's extend_ttl() for the same keys (i.e., invoke
e.storage().persistent().extend_ttl(&vote_key) and
e.storage().persistent().extend_ttl(&voted_key) or the equivalent API) so both
newly-created entries get their TTL extended to match the codebase TTL policy.

Comment on lines +263 to +268
// Store updated vote counts
let vote_key = CountingStorageKey::ProposalVote(proposal_id.clone());
e.storage().persistent().set(&vote_key, &counts);

// Mark account as having voted
e.storage().persistent().set(&voted_key, &true);
Copy link
Contributor

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:

# Check the actual file and lines in question
fd -t f "storage.rs" packages/governance/src/counting/ | head -1 | xargs wc -l

Repository: OpenZeppelin/stellar-contracts

Length of output: 121


🏁 Script executed:

# Read the relevant section
cat -n packages/governance/src/counting/storage.rs | sed -n '260,270p'

Repository: OpenZeppelin/stellar-contracts

Length of output: 470


🏁 Script executed:

# Find TTL constant definitions
rg "COUNTING_EXTEND_AMOUNT|COUNTING_TTL_THRESHOLD" packages/governance/src/counting/

Repository: OpenZeppelin/stellar-contracts

Length of output: 898


🏁 Script executed:

# Search for extend_ttl usage patterns in the codebase
rg "extend_ttl" packages/governance/src/counting/ -A 2 -B 2

Repository: OpenZeppelin/stellar-contracts

Length of output: 1411


🌐 Web query:

soroban-sdk persistent storage set method TTL behavior

💡 Result:

In soroban-sdk, calling env.storage().persistent().set(&key, &val) writes the value but does not give you control over (or explicitly “refresh”) the entry’s TTL—TTL management is a separate concern you must handle yourself. The network assigns/maintains TTL, and you must periodically extend it to keep entries live. [1]

To manage TTL for a persistent entry, use:

  • env.storage().persistent().extend_ttl(&key, threshold, extend_to) which extends only if the current TTL is below threshold, and then sets it to extend_to (in ledgers). [2]
  • TTL policies are monotonic: they extend TTL, and cannot reduce/reset an entry to a shorter lifetime. [3]

Also note: when a persistent entry’s TTL reaches 0, it becomes archived (not deleted forever) and must be restored before it can be accessed again. [1]

Sources
[1] Stellar Docs — State archival (TTL rules; persistent entries archive at TTL=0): https://developers.stellar.org/docs/learn/fundamentals/contract-development/storage/state-archival
[2] soroban-sdk Rust docs — Persistent::extend_ttl semantics: https://docs.rs/soroban-sdk/latest/soroban_sdk/storage/struct.Persistent.html
[3] OpenZeppelin audit note — TTL can only be extended, not shortened/reset: https://www.openzeppelin.com/news/stellar-contracts-library-v0.3.0-rc.2-audit


Extend TTL when writing vote and HasVoted entries.

The persistent().set() calls on lines 265 and 268 do not extend TTL. Without an explicit extend_ttl() call, newly created entries rely only on the network's minimum TTL and could expire before the proposal ends, causing vote tallies to become inaccessible and enabling double-voting. This is inconsistent with the codebase's TTL management pattern (which extends TTL on reads). Call extend_ttl() immediately after both set() operations:

🛠️ Suggested fix
     // Store updated vote counts
     let vote_key = CountingStorageKey::ProposalVote(proposal_id.clone());
     e.storage().persistent().set(&vote_key, &counts);
+    e.storage().persistent().extend_ttl(
+        &vote_key,
+        COUNTING_TTL_THRESHOLD,
+        COUNTING_EXTEND_AMOUNT,
+    );

     // Mark account as having voted
     e.storage().persistent().set(&voted_key, &true);
+    e.storage().persistent().extend_ttl(
+        &voted_key,
+        COUNTING_TTL_THRESHOLD,
+        COUNTING_EXTEND_AMOUNT,
+    );
📝 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
// Store updated vote counts
let vote_key = CountingStorageKey::ProposalVote(proposal_id.clone());
e.storage().persistent().set(&vote_key, &counts);
// Mark account as having voted
e.storage().persistent().set(&voted_key, &true);
// Store updated vote counts
let vote_key = CountingStorageKey::ProposalVote(proposal_id.clone());
e.storage().persistent().set(&vote_key, &counts);
e.storage().persistent().extend_ttl(
&vote_key,
COUNTING_TTL_THRESHOLD,
COUNTING_EXTEND_AMOUNT,
);
// Mark account as having voted
e.storage().persistent().set(&voted_key, &true);
e.storage().persistent().extend_ttl(
&voted_key,
COUNTING_TTL_THRESHOLD,
COUNTING_EXTEND_AMOUNT,
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/governance/src/counting/storage.rs` around lines 263 - 268, The
vote-count and has-voted entries are written with
e.storage().persistent().set(...) but do not extend TTL; after calling
e.storage().persistent().set(&CountingStorageKey::ProposalVote(proposal_id.clone()),
&counts) and after e.storage().persistent().set(&voted_key, &true) call the
storage's extend_ttl() for the same keys (i.e., invoke
e.storage().persistent().extend_ttl(&vote_key) and
e.storage().persistent().extend_ttl(&voted_key) or the equivalent API) so both
newly-created entries get their TTL extended to match the codebase TTL policy.

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.

🏗️ [Core Feature]: Counting extension for Governor

1 participant

Comments