Skip to content

Commit 90d36a3

Browse files
committed
rework governor docs
1 parent a751e8a commit 90d36a3

2 files changed

Lines changed: 182 additions & 179 deletions

File tree

content/stellar-contracts/governance/governor.mdx

Lines changed: 125 additions & 142 deletions
Original file line numberDiff line numberDiff line change
@@ -4,189 +4,102 @@ title: Governor
44

55
[Source Code](https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/governance/src/governor)
66

7-
The Governor module implements on-chain governance for Soroban contracts, providing the core primitives for decentralized decision-making: proposal creation, voting, counting, and execution.
7+
## Overview
88

9-
## Governance Flow
9+
The Governor module brings on-chain governance to Soroban contracts. It enables a community of token holders to collectively decide on protocol changes — proposing actions, debating them through votes, and executing the result on-chain — all without a centralized authority.
1010

11-
The standard governance lifecycle follows this sequence:
11+
A typical governance system involves two contracts working together:
1212

13-
1. **Propose**: A user with sufficient voting power creates a proposal
14-
2. **Vote**: Token holders vote during the voting period
15-
3. **Execute**: Successful proposals (meeting quorum and vote thresholds) can be executed
16-
4. **Cancel**: Proposals can be canceled by authorized accounts unless already Executed, Expired, or Cancelled
13+
- A **token contract** with the [Votes](/stellar-contracts/governance/votes) extension, which tracks who holds voting power and allows delegation.
14+
- A **governor contract** implementing the `Governor` trait, which manages the proposal lifecycle: creation, voting, counting, and execution.
1715

18-
When using the optional **Queue** mechanism (e.g., `TimelockControl`), an additional step is added:
16+
The Governor does not store or manage voting power directly. Instead, it references the token contract and queries it for historical voting power at specific ledgers. This separation keeps each contract focused on its own concern.
1917

20-
1. **Propose** → 2. **Vote** → 3. **Queue** → 4. **Execute**
18+
## How Governance Works
2119

22-
To enable queuing, override `proposals_need_queuing` to return `true`.
20+
### The Proposal Lifecycle
2321

24-
## The Governor Trait
22+
Every governance action starts as a **proposal** — a bundle of on-chain calls (targets, functions, arguments) paired with a human-readable description. Here is how a proposal moves through the system:
2523

26-
The `Governor` trait defines the core governance interface:
24+
```mermaid
25+
stateDiagram-v2
26+
[*] --> Pending: propose()
27+
Pending --> Active: voting delay passes
28+
Active --> Defeated: voting ends, quorum/majority not met
29+
Active --> Succeeded: voting ends, quorum + majority met
30+
Succeeded --> Executed: execute()
31+
Succeeded --> Queued: queue() (if enabled)
32+
Queued --> Executed: execute()
33+
Queued --> Expired: expiration passes
34+
Pending --> Canceled: cancel()
35+
Active --> Canceled: cancel()
36+
Succeeded --> Canceled: cancel()
37+
Queued --> Canceled: cancel()
38+
```
2739

28-
### Configuration
40+
1. **Propose**: Anyone with enough voting power (above the `proposal_threshold`) creates a proposal. A **voting delay** begins — a buffer period that gives token holders time to acquire tokens or delegate before the vote opens.
2941

30-
```rust
31-
fn name(e: &Env) -> String;
32-
fn version(e: &Env) -> String;
33-
fn voting_delay(e: &Env) -> u32;
34-
fn voting_period(e: &Env) -> u32;
35-
fn proposal_threshold(e: &Env) -> u128;
36-
fn get_token_contract(e: &Env) -> Address;
37-
fn counting_mode(e: &Env) -> Symbol;
38-
```
42+
2. **Vote**: Once the delay passes, the proposal becomes **Active** and token holders can vote: Against (0), For (1), or Abstain (2). Each voter's power is looked up at the snapshot ledger (when the voting period started), not at the moment they vote. This prevents flash loan attacks.
3943

40-
- **`voting_delay`**: Number of ledgers between proposal creation and the start of voting.
41-
- **`voting_period`**: Number of ledgers during which voting is open.
42-
- **`proposal_threshold`**: Minimum voting power required to create a proposal.
43-
- **`get_token_contract`**: Address of the token contract that implements the [Votes](/stellar-contracts/governance/votes) trait.
44+
3. **Succeed or Defeat**: When the **voting period** ends, the system checks two conditions:
45+
- **Majority**: Do `for` votes strictly exceed `against` votes?
46+
- **Quorum**: Does the sum of `for` and `abstain` votes meet or exceed the required quorum?
4447

45-
### Query Methods
48+
If both conditions are met, the proposal moves to **Succeeded**. Otherwise, it is **Defeated**.
4649

47-
```rust
48-
fn has_voted(e: &Env, proposal_id: BytesN<32>, account: Address) -> bool;
49-
fn quorum(e: &Env, ledger: u32) -> u128;
50-
fn proposal_state(e: &Env, proposal_id: BytesN<32>) -> ProposalState;
51-
fn proposal_snapshot(e: &Env, proposal_id: BytesN<32>) -> u32;
52-
fn proposal_deadline(e: &Env, proposal_id: BytesN<32>) -> u32;
53-
fn proposal_proposer(e: &Env, proposal_id: BytesN<32>) -> Address;
54-
fn get_proposal_id(
55-
e: &Env,
56-
targets: Vec<Address>,
57-
functions: Vec<Symbol>,
58-
args: Vec<Vec<Val>>,
59-
description_hash: BytesN<32>,
60-
) -> BytesN<32>;
61-
```
50+
4. **Execute**: A succeeded proposal can be executed, which triggers the on-chain calls it contains. Who can call `execute` is up to the implementer — it can be open to anyone or restricted to a specific role.
6251

63-
The proposal ID is a deterministic keccak256 hash of the XDR-serialized targets, functions, args, and description hash, allowing anyone to compute the ID without storing the full proposal data.
52+
5. **Cancel**: The proposer (or another authorized role) can cancel a proposal at any point before it is executed, expired, or already cancelled.
6453

65-
### Lifecycle Methods
54+
### Optional: Queuing with a Timelock
6655

67-
```rust
68-
fn propose(
69-
e: &Env,
70-
targets: Vec<Address>,
71-
functions: Vec<Symbol>,
72-
args: Vec<Vec<Val>>,
73-
description: String,
74-
proposer: Address,
75-
) -> BytesN<32>;
76-
77-
fn cast_vote(
78-
e: &Env,
79-
proposal_id: BytesN<32>,
80-
vote_type: u32,
81-
reason: String,
82-
voter: Address,
83-
) -> u128;
84-
85-
fn queue(
86-
e: &Env,
87-
targets: Vec<Address>,
88-
functions: Vec<Symbol>,
89-
args: Vec<Vec<Val>>,
90-
description_hash: BytesN<32>,
91-
eta: u32,
92-
operator: Address,
93-
) -> BytesN<32>;
94-
95-
fn execute(
96-
e: &Env,
97-
targets: Vec<Address>,
98-
functions: Vec<Symbol>,
99-
args: Vec<Vec<Val>>,
100-
description_hash: BytesN<32>,
101-
executor: Address,
102-
) -> BytesN<32>;
103-
104-
fn cancel(
105-
e: &Env,
106-
targets: Vec<Address>,
107-
functions: Vec<Symbol>,
108-
args: Vec<Vec<Val>>,
109-
description_hash: BytesN<32>,
110-
operator: Address,
111-
) -> BytesN<32>;
56+
For systems that need a safety delay between a successful vote and execution, the Governor supports an optional **queue** step. When enabled, succeeded proposals must first be queued, which starts a timelock delay. During this delay, community members can review the upcoming change and exit the protocol if they disagree.
11257

113-
fn proposals_need_queuing(e: &Env) -> bool; // defaults to false
114-
```
58+
This flow becomes: **Propose → Vote → Queue → Execute**
11559

116-
<Callout type="warning">
117-
**`execute` and `cancel` have no default implementation.** The implementer must define who is authorized to call these functions. For example, open execution (anyone can trigger a succeeded proposal) or restricted execution (only a timelock contract or specific role).
118-
</Callout>
60+
To enable queuing, override `proposals_need_queuing` to return `true`. See [Design Rationale](#queue-logic-is-built-in-but-disabled-by-default) for why this is built into the base trait.
11961

120-
## Proposal States
62+
## Voting and Counting
12163

122-
```rust
123-
pub enum ProposalState {
124-
Pending = 0, // Voting has not started
125-
Active = 1, // Voting is ongoing
126-
Defeated = 2, // Voting ended without success
127-
Canceled = 3, // Cancelled by authorized account
128-
Succeeded = 4, // Met quorum and vote thresholds
129-
Queued = 5, // Queued for execution (via extension)
130-
Expired = 6, // Expired after queuing (via extension)
131-
Executed = 7, // Successfully executed
132-
}
133-
```
64+
### How Votes Are Counted
13465

135-
States are divided into:
136-
- **Time-based** (Pending, Active, Defeated): Derived from the current ledger relative to the voting schedule. Never stored explicitly.
137-
- **Explicit** (Canceled, Succeeded, Queued, Expired, Executed): Persisted in storage. Once set, they take precedence over time-based derivation.
66+
The default counting mode is **simple counting**:
13867

139-
## Default Counting
68+
| Vote Type | Value | Meaning |
69+
|-----------|-------|---------|
70+
| Against | 0 | Opposes the proposal |
71+
| For | 1 | Supports the proposal |
72+
| Abstain | 2 | Counted toward quorum but not toward majority |
14073

141-
The default counting implementation provides **simple counting**:
74+
A proposal succeeds when `for > against` **and** the quorum is reached. Quorum values are stored as checkpoints, so updating the quorum does not retroactively change the outcome of existing proposals.
14275

143-
- **Vote types**: Against (0), For (1), Abstain (2)
144-
- **Vote success**: `for` votes strictly exceed `against` votes
145-
- **Quorum**: Sum of `for` and `abstain` votes meets or exceeds the quorum value in effect at the proposal's `vote_snapshot` ledger
76+
### Custom Counting Strategies
14677

147-
Quorum values are stored as checkpoints, so updates do not retroactively affect existing proposals.
78+
The counting logic is fully pluggable. The default three-type system works for most cases, but you can override the counting methods (`count_vote`, `tally_succeeded`, `quorum_reached`) to implement alternative strategies such as fractional voting, weighted quorum relative to total supply, or NFT-based voting schemes. The `counting_mode()` method returns a symbol identifying the active strategy, which UIs can use for display purposes.
14879

149-
## Dynamic Quorum
80+
### Dynamic Quorum
15081

151-
The default `quorum()` implementation uses checkpoint-based fixed quorum. For supply-relative quorum (e.g., "10% of total supply"), override `quorum()` to compute the value dynamically from on-chain state at the requested ledger.
82+
The default `quorum()` uses a fixed checkpoint-based value. For supply-relative quorum (e.g., "10% of total supply"), override `quorum()` to compute the value dynamically.
15283

15384
<Callout>
154-
When overriding `quorum()`, do not use `set_quorum`/`get_quorum` as those are designed for fixed checkpoints. Also ensure that configurable parameters are themselves queried at the historical ledger to avoid retroactively changing the outcome of existing proposals.
85+
When overriding `quorum()`, ensure that configurable parameters are themselves queried at the historical ledger to avoid retroactively changing the outcome of existing proposals.
15586
</Callout>
15687

157-
## Extensions
158-
159-
### GovernorSettings
160-
161-
Provides configurable parameters for `voting_delay`, `voting_period`, and `proposal_threshold`. Override the corresponding trait methods to read from storage initialized during construction.
162-
163-
### TimelockControl
164-
165-
Integrates the Governor with a [Timelock Controller](/stellar-contracts/governance/timelock-controller) for delayed execution. When enabled:
166-
- Override `proposals_need_queuing` to return `true`
167-
- `queue()` transitions proposals from `Succeeded` to `Queued`
168-
- `execute()` requires proposals to be in `Queued` state
169-
- The timelock contract enforces the delay before execution
170-
17188
## Design Rationale
17289

17390
### Voting Power Lives on the Token
17491

175-
The `Governor` trait does not include vote-querying methods like `get_votes`. Instead, voting power is managed entirely by a separate token contract implementing the [Votes](/stellar-contracts/governance/votes) trait. The Governor references this token via `get_token_contract()` and queries it for voting power at specific ledgers. This keeps the Governor focused on proposal lifecycle and counting, while the token handles delegation and checkpointing.
176-
177-
### Counting is Pluggable
178-
179-
The default counting mode uses three vote types — Against (0), For (1), Abstain (2) — with simple majority. However, the counting-related methods (`count_vote`, `tally_succeeded`, `quorum_reached`) can all be overridden to implement alternative strategies such as fractional voting, weighted quorum based on total supply, or NFT-based voting schemes. The `counting_mode()` method returns a symbol identifying the active strategy for UI consumption.
92+
The `Governor` trait does not include vote-querying methods. Instead, voting power is managed entirely by a separate token contract implementing the [Votes](/stellar-contracts/governance/votes) trait. The Governor references this token via `get_token_contract()` and queries it for voting power at specific ledgers. This keeps the Governor focused on proposal lifecycle and counting, while the token handles delegation and checkpointing.
18093

18194
### Queue Logic is Built-In but Disabled by Default
18295

183-
Queuing is integrated into the base `Governor` trait rather than being a loosely coupled external module. This is a deliberate choice: queue state transitions are tightly coupled with the proposal lifecycle — `execute` must know whether to expect a `Succeeded` or `Queued` state, and `proposal_state` must be able to return `Queued` and `Expired` variants. Extracting this into a separate module would force implementers to manually wire these interactions.
96+
Queuing is integrated into the base `Governor` trait rather than being an external module. This is a deliberate choice: queue state transitions are tightly coupled with the proposal lifecycle — `execute` must know whether to expect a `Succeeded` or `Queued` state, and `proposal_state` must be able to return `Queued` and `Expired` variants. Extracting this into a separate module would force implementers to manually wire these interactions.
18497

185-
By default, `proposals_need_queuing()` returns `false`, making queue logic inert. Extensions like `TimelockControl` simply override this to return `true`, which activates the full queuing flow — `queue()` transitions proposals from `Succeeded` to `Queued`, and `execute()` then requires the `Queued` state. This approach means enabling queuing is a single-line override, with no need to modify proposal creation, voting, or execution logic.
98+
By default, `proposals_need_queuing()` returns `false`, making queue logic inert. To enable queuing (e.g., for integration with a [Timelock Controller](/stellar-contracts/governance/timelock-controller)), simply override this single method to return `true`. This activates the full queuing flow — `queue()` transitions proposals from `Succeeded` to `Queued`, and `execute()` then requires the `Queued` state — without touching proposal creation, voting, or execution logic.
18699

187100
### Execution and Cancellation Require Implementation
188101

189-
The `execute` and `cancel` functions have no default implementation. This is intentional: access control for these operations varies significantly between deployments. An open governance system may allow anyone to trigger execution of a succeeded proposal, while a guarded system may restrict it to a timelock contract or admin role. Forcing an explicit implementation ensures that the developer consciously decides their authorization model rather than inheriting a default that may not fit.
102+
The `execute` and `cancel` functions have no default implementation. Access control for these operations varies significantly between deployments — an open governance system may allow anyone to trigger execution, while a guarded system may restrict it to a timelock contract or admin role. Forcing an explicit implementation ensures the developer consciously decides their authorization model rather than inheriting a default that may not fit.
190103

191104
Note that the `executor` parameter in `execute` represents the account *triggering* execution, not the entity performing the underlying calls — the Governor contract itself is the caller of the target contracts.
192105

@@ -208,6 +121,77 @@ The **proposal threshold** requires proposers to hold a minimum amount of voting
208121
- **Quorum requirements** ensure minimum participation
209122
- **Voting delay** gives token holders time to position themselves before voting starts
210123

124+
## The Governor Trait
125+
126+
The `Governor` trait defines the full governance interface. Most methods have default implementations — you only need to implement `execute` and `cancel` (for access control) and optionally override configuration methods.
127+
128+
### Configuration
129+
130+
```rust
131+
fn name(e: &Env) -> String;
132+
fn version(e: &Env) -> String;
133+
fn voting_delay(e: &Env) -> u32; // ledgers before voting starts
134+
fn voting_period(e: &Env) -> u32; // ledgers during which voting is open
135+
fn proposal_threshold(e: &Env) -> u128; // minimum voting power to propose
136+
fn get_token_contract(e: &Env) -> Address; // the Votes-enabled token contract
137+
fn counting_mode(e: &Env) -> Symbol; // identifies the counting strategy
138+
fn proposals_need_queuing(e: &Env) -> bool; // defaults to false
139+
```
140+
141+
### Proposal Lifecycle
142+
143+
```rust
144+
fn propose(e: &Env, targets: Vec<Address>, functions: Vec<Symbol>,
145+
args: Vec<Vec<Val>>, description: String, proposer: Address) -> BytesN<32>;
146+
147+
fn cast_vote(e: &Env, proposal_id: BytesN<32>, vote_type: u32,
148+
reason: String, voter: Address) -> u128;
149+
150+
fn queue(e: &Env, targets: Vec<Address>, functions: Vec<Symbol>,
151+
args: Vec<Vec<Val>>, description_hash: BytesN<32>,
152+
eta: u32, operator: Address) -> BytesN<32>;
153+
154+
fn execute(e: &Env, targets: Vec<Address>, functions: Vec<Symbol>,
155+
args: Vec<Vec<Val>>, description_hash: BytesN<32>,
156+
executor: Address) -> BytesN<32>; // no default — must implement
157+
158+
fn cancel(e: &Env, targets: Vec<Address>, functions: Vec<Symbol>,
159+
args: Vec<Vec<Val>>, description_hash: BytesN<32>,
160+
operator: Address) -> BytesN<32>; // no default — must implement
161+
```
162+
163+
### Query Methods
164+
165+
```rust
166+
fn has_voted(e: &Env, proposal_id: BytesN<32>, account: Address) -> bool;
167+
fn quorum(e: &Env, ledger: u32) -> u128;
168+
fn proposal_state(e: &Env, proposal_id: BytesN<32>) -> ProposalState;
169+
fn proposal_snapshot(e: &Env, proposal_id: BytesN<32>) -> u32;
170+
fn proposal_deadline(e: &Env, proposal_id: BytesN<32>) -> u32;
171+
fn proposal_proposer(e: &Env, proposal_id: BytesN<32>) -> Address;
172+
fn get_proposal_id(e: &Env, targets: Vec<Address>, functions: Vec<Symbol>,
173+
args: Vec<Vec<Val>>, description_hash: BytesN<32>) -> BytesN<32>;
174+
```
175+
176+
The proposal ID is a deterministic keccak256 hash of the proposal parameters, allowing anyone to compute it without storing the full proposal data.
177+
178+
### Proposal States
179+
180+
```rust
181+
pub enum ProposalState {
182+
Pending = 0, // Voting has not started
183+
Active = 1, // Voting is ongoing
184+
Defeated = 2, // Voting ended without success
185+
Canceled = 3, // Cancelled by authorized account
186+
Succeeded = 4, // Met quorum and vote thresholds
187+
Queued = 5, // Queued for execution (via extension)
188+
Expired = 6, // Expired after queuing (via extension)
189+
Executed = 7, // Successfully executed
190+
}
191+
```
192+
193+
States are divided into **time-based** (Pending, Active, Defeated) — derived from the current ledger, never stored — and **explicit** (all others) — persisted in storage, taking precedence once set.
194+
211195
## Events
212196

213197
| Event | Topics | Data |
@@ -243,7 +227,6 @@ The **proposal threshold** requires proposers to hold a minimum amount of voting
243227
```rust
244228
use soroban_sdk::{contract, contractimpl, Address, BytesN, Env, String, Symbol, Val, Vec};
245229
use stellar_governance::governor::{self, storage, Governor, hash_proposal, get_proposal_snapshot};
246-
use stellar_macros::only_role;
247230

248231
#[contract]
249232
pub struct MyGovernor;

0 commit comments

Comments
 (0)