You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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
8
8
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.
10
10
11
-
The standard governance lifecycle follows this sequence:
11
+
A typical governance system involves two contracts working together:
12
12
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.
17
15
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.
To enable queuing, override `proposals_need_queuing` to return `true`.
20
+
### The Proposal Lifecycle
23
21
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:
25
23
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
+
```
27
39
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.
29
41
30
-
```rust
31
-
fnname(e:&Env) ->String;
32
-
fnversion(e:&Env) ->String;
33
-
fnvoting_delay(e:&Env) ->u32;
34
-
fnvoting_period(e:&Env) ->u32;
35
-
fnproposal_threshold(e:&Env) ->u128;
36
-
fnget_token_contract(e:&Env) ->Address;
37
-
fncounting_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.
39
43
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?
44
47
45
-
### Query Methods
48
+
If both conditions are met, the proposal moves to **Succeeded**. Otherwise, it is **Defeated**.
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.
62
51
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.
64
53
65
-
### Lifecycle Methods
54
+
### Optional: Queuing with a Timelock
66
55
67
-
```rust
68
-
fnpropose(
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
-
fncast_vote(
78
-
e:&Env,
79
-
proposal_id:BytesN<32>,
80
-
vote_type:u32,
81
-
reason:String,
82
-
voter:Address,
83
-
) ->u128;
84
-
85
-
fnqueue(
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
-
fnexecute(
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
-
fncancel(
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.
112
57
113
-
fnproposals_need_queuing(e:&Env) ->bool; // defaults to false
114
-
```
58
+
This flow becomes: **Propose → Vote → Queue → Execute**
115
59
116
-
<Callouttype="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.
119
61
120
-
## Proposal States
62
+
## Voting and Counting
121
63
122
-
```rust
123
-
pubenumProposalState {
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
134
65
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**:
138
67
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 |
140
73
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.
142
75
143
-
-**Vote types**: Against (0), For (1), Abstain (2)
-**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
146
77
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.
148
79
149
-
## Dynamic Quorum
80
+
###Dynamic Quorum
150
81
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.
152
83
153
84
<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.
155
86
</Callout>
156
87
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
-
171
88
## Design Rationale
172
89
173
90
### Voting Power Lives on the Token
174
91
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.
180
93
181
94
### Queue Logic is Built-In but Disabled by Default
182
95
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.
184
97
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.
186
99
187
100
### Execution and Cancellation Require Implementation
188
101
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.
190
103
191
104
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.
192
105
@@ -208,6 +121,77 @@ The **proposal threshold** requires proposers to hold a minimum amount of voting
-**Voting delay** gives token holders time to position themselves before voting starts
210
123
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
+
fnname(e:&Env) ->String;
132
+
fnversion(e:&Env) ->String;
133
+
fnvoting_delay(e:&Env) ->u32; // ledgers before voting starts
134
+
fnvoting_period(e:&Env) ->u32; // ledgers during which voting is open
135
+
fnproposal_threshold(e:&Env) ->u128; // minimum voting power to propose
136
+
fnget_token_contract(e:&Env) ->Address; // the Votes-enabled token contract
137
+
fncounting_mode(e:&Env) ->Symbol; // identifies the counting strategy
138
+
fnproposals_need_queuing(e:&Env) ->bool; // defaults to false
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
+
pubenumProposalState {
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
+
211
195
## Events
212
196
213
197
| Event | Topics | Data |
@@ -243,7 +227,6 @@ The **proposal threshold** requires proposers to hold a minimum amount of voting
0 commit comments