Skip to content

Governance base contract#563

Open
ozgunozerk wants to merge 20 commits intomainfrom
governance-base-contract
Open

Governance base contract#563
ozgunozerk wants to merge 20 commits intomainfrom
governance-base-contract

Conversation

@ozgunozerk
Copy link
Collaborator

@ozgunozerk ozgunozerk commented Feb 6, 2026

Fixes #556

PR Checklist

  • Tests // TODO: after Votes and Counting are merged
  • Documentation

Summary by CodeRabbit

  • New Features

    • Added comprehensive on-chain governance system with proposal creation, voting, execution, and cancellation capabilities.
    • Added governance events for proposal lifecycle tracking (created, voted, queued, executed, cancelled).
    • Added proposal state management and voting period configuration.
  • Tests

    • Added test scaffolding for governance module.

@ozgunozerk ozgunozerk requested a review from brozorec February 6, 2026 09:18
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 6, 2026

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

The pull request introduces a comprehensive Governor base contract module for Soroban, establishing core traits (Votes, Counting, Governor), governance abstractions (ProposalState, ProposalCore), error handling, event infrastructure, and persistent storage management with key operations for proposal lifecycle (propose, execute, cancel, prepare_vote).

Changes

Cohort / File(s) Summary
Governor Core Module
packages/governance/src/governor/mod.rs
Defines public traits (Votes, Counting, Governor), enums (ProposalState, GovernorError), governance constants (DAY_IN_LEDGERS, TTL thresholds), and event structures (ProposalCreated, VoteCast, ProposalQueued, ProposalExecuted, ProposalCancelled) with corresponding emit helpers. Establishes foundational interfaces and type definitions.
Governor Storage Layer
packages/governance/src/governor/storage.rs
Implements storage abstractions via GovernorStorageKey enum and ProposalCore struct. Provides getter/setter functions for governance parameters (name, version, voting delay/period, proposal threshold). Implements core operations: propose (creates proposals with ID computation via hash_proposal), prepare_vote (authorizes voters), execute (runs proposal targets), and cancel (marks proposals as canceled).
Module Exposure & Tests
packages/governance/src/lib.rs, packages/governance/src/governor/test.rs
Exposes new governor module in library public API. Includes placeholder test scaffold for future test implementations.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 The Governor hops into place,
With traits and storage, a governance space,
Proposals bloom, votes are cast,
On-chain decisions encoded to last,
A voting haven for Soroban's grace!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Governance base contract' clearly summarizes the main change—introducing a comprehensive governance module with core traits, types, and storage management.
Description check ✅ Passed The PR description references the linked issue (#556), marks documentation as complete, but explicitly defers testing with a TODO comment pending dependency merges.
Linked Issues check ✅ Passed The PR implements the Governor base contract as specified in issue #556, providing core traits (Votes, Counting, Governor), state management, and governance operations. Extensions are designed as future integrations.
Out of Scope Changes check ✅ Passed All changes directly support the Governor base contract objective: mod.rs defines core interfaces/types, storage.rs implements persistence/lifecycle, test.rs provides scaffolding, and lib.rs exports the module.
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 unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch governance-base-contract

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 6, 2026

Codecov Report

❌ Patch coverage is 0% with 204 lines in your changes missing coverage. Please review.
✅ Project coverage is 92.80%. Comparing base (606ec84) to head (98d4372).

Files with missing lines Patch % Lines
packages/governance/src/governor/storage.rs 0.00% 204 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #563      +/-   ##
==========================================
- Coverage   96.24%   92.80%   -3.44%     
==========================================
  Files          57       58       +1     
  Lines        5507     5711     +204     
==========================================
  Hits         5300     5300              
- Misses        207      411     +204     

☔ 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: 2

🤖 Fix all issues with AI agents
In `@packages/governance/src/governor/mod.rs`:
- Around line 523-527: The doc comments for GOVERNOR_EXTEND_AMOUNT and
GOVERNOR_TTL_THRESHOLD are swapped; update the comment above
GOVERNOR_EXTEND_AMOUNT to read that it is the "TTL extension amount for storage
entries (in ledgers)" and update the comment above GOVERNOR_TTL_THRESHOLD to
read that it is the "TTL threshold for extending storage entries (in ledgers)".
Locate the two constants GOVERNOR_EXTEND_AMOUNT and GOVERNOR_TTL_THRESHOLD in
mod.rs and swap or correct their preceding doc comments so each accurately
describes the associated constant.
- Around line 519-527: Proposal TTL constants GOVERNOR_EXTEND_AMOUNT and
GOVERNOR_TTL_THRESHOLD are declared but never used, so add persistent TTL
extension calls whenever a proposal is created or its state is updated: after
the persistent set of a proposal in propose() (use
e.storage().persistent().set(&GovernorStorageKey::Proposal(proposal_id.clone()),
&proposal)), and likewise after updating the stored proposal in execute() and
cancel(), call
e.storage().persistent().extend_ttl(&GovernorStorageKey::Proposal(proposal_id.clone()),
GOVERNOR_TTL_THRESHOLD, GOVERNOR_EXTEND_AMOUNT); ensure you reference the exact
symbols GOVERNOR_EXTEND_AMOUNT, GOVERNOR_TTL_THRESHOLD,
GovernorStorageKey::Proposal, extend_ttl, and the existing
storage().persistent().set/update locations so the TTL is extended when
proposals are stored or modified.
🧹 Nitpick comments (3)
packages/governance/src/governor/mod.rs (2)

107-121: Placeholder traits acknowledged — ensure cleanup is tracked.

Both Votes and Counting are temporary stubs with TODO comments. Since these define the public API surface that implementers will code against, consider linking the specific tracking issues/PRs in the TODO comments so they don't become stale.


323-334: Snapshot used for proposer threshold check differs from the voting snapshot.

propose checks the proposer's voting power at current_ledger (Line 332), but the snapshot recorded for voting is vote_start = current_ledger + voting_delay. This is a deliberate design choice (matching OpenZeppelin's Solidity Governor), but worth being explicit about in the doc comment — a proposer could have sufficient votes at proposal time but not at the voting snapshot (or vice versa).

packages/governance/src/governor/storage.rs (1)

547-577: Double storage read for proposal core data in cancel.

get_proposal_core is called on Line 557 to load the proposal, and then get_proposal_state on Line 563 internally calls get_proposal_core again. You could use the already-loaded proposal to derive the state inline, avoiding the extra persistent storage read.

♻️ Proposed refactor: reuse loaded proposal
     let proposal_id = hash_proposal(e, &targets, &functions, &args, description_hash);
 
     // Get proposal and verify it exists
     let mut proposal = get_proposal_core(e, &proposal_id);
 
     // Only proposer can cancel
     proposal.proposer.require_auth();
 
     // Can only cancel proposals that haven't reached a terminal state
-    let state = get_proposal_state(e, &proposal_id);
+    let state = derive_state_from_core(e, &proposal);
     match state {
         ProposalState::Pending | ProposalState::Active | ProposalState::Succeeded => {}
         _ => panic_with_error!(e, GovernorError::ProposalNotCancellable),
     }

This would require extracting the state derivation logic from get_proposal_state into a helper that accepts a &ProposalCore directly.

Comment on lines +503 to +508
if state == ProposalState::Executed {
panic_with_error!(e, GovernorError::ProposalAlreadyExecuted);
}
if state != ProposalState::Succeeded {
panic_with_error!(e, GovernorError::ProposalNotSuccessful);
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

missing check for ProposalState::Queued when proposal_needs_queuing returns true

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It is not missing, it is deliberate. Queue related logic should be implemented/overridden by the Queue extension/contract.

We define errors and types in the base module, that's why only the Queue state is defined, not the logic for it.

Copy link
Collaborator

@brozorec brozorec left a comment

Choose a reason for hiding this comment

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

Great progress 🙌 couple more points

  1. I noticed some discrepancies in measuring time/delays in the gov module, e.g. Governor uses ledgers, while Votes and Timelock use timestamps. I don't have a strong opinion, but we should def harmonize them.
  2. For coherency with the other modules, I suggest re-exporting all public functions and types and use them without absolute or relative path referencing.

Comment on lines +31 to +32
//! - *GovernorSettings* provides configurable parameters like voting delay,
//! voting period, and proposal threshold.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Actually, why do we need to move settings in an extension?

Copy link
Collaborator Author

@ozgunozerk ozgunozerk Feb 16, 2026

Choose a reason for hiding this comment

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

Because tweaking/changing the voting delay, voting period, and proposal threshold is probably won't happen after once they are set in the constructor for the majority of the use cases. So, exposing these methods in the base trait, I think is an overkill. The base trait is already convoluted, so I tried to minimize it as much as I could.

If one needs to tweak these, they can always use the extension.

ozgunozerk and others added 3 commits February 16, 2026 13:49
Co-authored-by: Boyan Barakov <9572072+brozorec@users.noreply.github.com>
Co-authored-by: Boyan Barakov <9572072+brozorec@users.noreply.github.com>
Co-authored-by: Boyan Barakov <9572072+brozorec@users.noreply.github.com>
@ozgunozerk
Copy link
Collaborator Author

ozgunozerk commented Feb 16, 2026

For coherency with the other modules, I suggest re-exporting all public functions and types and use them without absolute or relative path referencing.

That was a deliberate choice as well. I think, having the following is confusing:

pub Trait myTrait {
    fn my_method() {
        my_method();
   }
}

Seems like a recursive call. So, instead:

pub Trait myTrait {
    fn my_method() {
        storage::my_method();
   }
}

seems much better I think.

I wanted to showcase that in this PR. If you liked it, we can refactor the rest of the library w.r.t this approach

@ozgunozerk
Copy link
Collaborator Author

I noticed some discrepancies in measuring time/delays in the gov module, e.g. Governor uses ledgers, while Votes and Timelock use timestamps. I don't have a strong opinion, but we should def harmonize them.

Definitely agreed. I think ledger is will seem easier and more native. It is easier to interpret and know what's going on if we use ledger directly. What do you think?

@brozorec
Copy link
Collaborator

brozorec commented Feb 16, 2026

Definitely agreed. I think ledger is will seem easier and more native. It is easier to interpret and know what's going on if we use ledger directly. What do you think?

Yes, I'm also leaning more towards using ledgers. Created an issue #567

So, instead:
pub Trait myTrait {
fn my_method() {
storage::my_method();
}
}
seems much better I think.
I wanted to showcase that in this PR. If you liked it, we can refactor the rest of the library w.r.t this approach

Ok, agreed 👍 but that should be only for the default implementations and we should still re-export them, right? I think use governance::my_method is better than use governance::storage::my_method.

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.

Governor

2 participants

Comments