Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ members = [
"examples/fungible-blocklist",
"examples/fungible-capped",
"examples/fungible-merkle-airdrop",
"examples/fungible-votes",
"examples/fee-forwarder-permissioned",
"examples/fee-forwarder-permissionless",
"examples/fungible-pausable",
Expand Down
21 changes: 21 additions & 0 deletions examples/fungible-votes/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[package]
name = "fungible-votes-example"
edition.workspace = true
license.workspace = true
repository.workspace = true
publish = false
version.workspace = true

[lib]
crate-type = ["cdylib"]
doctest = false

[dependencies]
soroban-sdk = { workspace = true }
stellar-access = { workspace = true }
stellar-governance = { workspace = true }
stellar-macros = { workspace = true }
stellar-tokens = { workspace = true }

[dev-dependencies]
soroban-sdk = { workspace = true, features = ["testutils"] }
32 changes: 32 additions & 0 deletions examples/fungible-votes/src/contract.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
use soroban_sdk::{contract, contractimpl, Address, Env, MuxedAddress, String};
use stellar_access::ownable::{set_owner, Ownable};
use stellar_governance::votes::Votes;
use stellar_macros::only_owner;
use stellar_tokens::fungible::{votes::FungibleVotes, Base, FungibleToken};

#[contract]
pub struct ExampleContract;

#[contractimpl]
impl ExampleContract {
pub fn __constructor(e: &Env, owner: Address) {
Base::set_metadata(e, 7, String::from_str(e, "My Token"), String::from_str(e, "MTK"));
set_owner(e, &owner);
}

#[only_owner]
pub fn mint(e: &Env, to: &Address, amount: i128) {
FungibleVotes::mint(e, to, amount);
}
}

#[contractimpl(contracttrait)]
impl FungibleToken for ExampleContract {
type ContractType = FungibleVotes;
}

#[contractimpl(contracttrait)]
impl Votes for ExampleContract {}

#[contractimpl(contracttrait)]
impl Ownable for ExampleContract {}
3 changes: 3 additions & 0 deletions examples/fungible-votes/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#![no_std]

mod contract;
19 changes: 19 additions & 0 deletions packages/governance/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,29 @@ Stellar governance functionalities

This package provides governance modules for Soroban smart contracts:

- **Votes**: Vote tracking with delegation and historical checkpointing
- **Timelock**: Time-delayed execution of operations

## Modules

### Votes

The `votes` module provides vote tracking functionality with delegation and historical checkpointing for governance mechanisms.

#### Core Concepts

- **Voting Units**: The base unit of voting power, typically 1:1 with token balance
- **Delegation**: Accounts can delegate their voting power to another account (delegatee)
- **Checkpoints**: Historical snapshots of voting power at specific timestamps
- **Clock Mode**: Uses ledger timestamps (`e.ledger().timestamp()`) as the timepoint reference

#### Key Features

- Track voting power per account with historical checkpoints
- Support delegation (an account can delegate its voting power to another account)
- Provide historical vote queries at any past timestamp
- Explicit delegation required (accounts must self-delegate to use their own voting power)

### Timelock

The `timelock` module provides functionality for time-delayed execution of operations, enabling governance mechanisms where actions must wait for a minimum delay before execution.
Expand Down
1 change: 1 addition & 0 deletions packages/governance/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#![no_std]

pub mod timelock;
pub mod votes;
242 changes: 242 additions & 0 deletions packages/governance/src/votes/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
//! # Votes Module
//!
//! The module tracks voting power per account with historical checkpoints,
//! supports delegation (an account can delegate its voting power to another
//! account), and provides historical vote queries at any past timestamp.
//!
//! # Core Concepts
//!
//! - **Voting Units**: The base unit of voting power, typically 1:1 with token
//! balance
//! - **Delegation**: Accounts can delegate their voting power to another
//! account (delegatee)
//! - **Checkpoints**: Historical snapshots of voting power at specific
//! timestamps
//!
//! # Design
//!
//! This module follows the design of OpenZeppelin's Solidity `Votes.sol`:
//! - Voting units must be explicitly delegated to count as votes
//! - Self-delegation is required for an account to use its own voting power
//! - Historical vote queries use binary search over checkpoints
//!
//! # Usage
//!
//! This module provides storage functions that can be integrated into a token
//! contract. The contract is responsible for:
//! - Calling `transfer_voting_units` on every balance change
//! (mint/burn/transfer)
//! - Exposing delegation functionality to users
//!
//! # Example
//!
//! ```ignore
//! use stellar_governance::votes::{
//! delegate, get_votes, get_past_votes, transfer_voting_units,
//! };
//!
//! // In your token contract transfer:
//! pub fn transfer(e: &Env, from: Address, to: Address, amount: i128) {
//! // ... perform transfer logic ...
//! transfer_voting_units(e, Some(&from), Some(&to), amount as u128);
//! }
//!
//! // Expose delegation:
//! pub fn delegate(e: &Env, account: Address, delegatee: Address) {
//! votes::delegate(e, &account, &delegatee);
//! }
//! ```

mod storage;

#[cfg(test)]
mod test;

use soroban_sdk::{contracterror, contractevent, contracttrait, Address, Env};

pub use crate::votes::storage::{
delegate, get_delegate, get_past_total_supply, get_past_votes, get_total_supply, get_votes,
get_voting_units, num_checkpoints, transfer_voting_units, Checkpoint, VotesStorageKey,
};

/// Trait for contracts that support vote tracking with delegation.
///
/// This trait defines the interface for vote tracking functionality.
/// Contracts implementing this trait can be used in governance systems
/// that require historical vote queries and delegation.
///
/// # Implementation Notes
///
/// The implementing contract must:
/// - Call `transfer_voting_units` on every balance change
/// - Expose `delegate` functionality to users
#[contracttrait]
pub trait Votes {
/// Returns the current voting power of an account.
///
/// # Arguments
///
/// * `e` - Access to the Soroban environment.
/// * `account` - The address to query voting power for.
fn get_votes(e: &Env, account: Address) -> u128 {
get_votes(e, &account)
}

/// Returns the voting power of an account at a specific past timestamp.
///
/// # Arguments
///
/// * `e` - Access to the Soroban environment.
/// * `account` - The address to query voting power for.
/// * `timepoint` - The timestamp to query (must be in the past).
///
/// # Errors
///
/// * [`VotesError::FutureLookup`] - If `timepoint` >= current timestamp.
fn get_past_votes(e: &Env, account: Address, timepoint: u64) -> u128 {
get_past_votes(e, &account, timepoint)
}

/// Returns the total supply of voting units at a specific past timestamp.
///
/// # Arguments
///
/// * `e` - Access to the Soroban environment.
/// * `timepoint` - The timestamp to query (must be in the past).
///
/// # Errors
///
/// * [`VotesError::FutureLookup`] - If `timepoint` >= current timestamp.
fn get_past_total_supply(e: &Env, timepoint: u64) -> u128 {
get_past_total_supply(e, timepoint)
}

/// Returns the current delegate for an account.
///
/// # Arguments
///
/// * `e` - Access to the Soroban environment.
/// * `account` - The address to query the delegate for.
///
/// # Returns
///
/// * `Some(Address)` - The delegate address if delegation is set.
/// * `None` - If the account has not delegated.
fn get_delegate(e: &Env, account: Address) -> Option<Address> {
get_delegate(e, &account)
}

/// Delegates voting power from `account` to `delegatee`.
///
/// # Arguments
///
/// * `e` - Access to the Soroban environment.
/// * `account` - The account delegating its voting power.
/// * `delegatee` - The account receiving the delegated voting power.
///
/// # Events
///
/// * topics - `["DelegateChanged", delegator: Address]`
/// * data - `[from_delegate: Option<Address>, to_delegate: Address]`
///
/// * topics - `["DelegateVotesChanged", delegate: Address]`
/// * data - `[previous_votes: u128, new_votes: u128]`
///
/// # Notes
///
/// Authorization for `account` is required.
fn delegate(e: &Env, account: Address, delegatee: Address) {
delegate(e, &account, &delegatee);
}
}
// ################## ERRORS ##################

/// Errors that can occur in votes operations.
#[contracterror]
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
#[repr(u32)]
pub enum VotesError {
/// The timepoint is in the future
FutureLookup = 4100,
/// Arithmetic overflow occurred
MathOverflow = 4101,
/// Attempting to transfer more voting units than available
InsufficientVotingUnits = 4102,
}

// ################## CONSTANTS ##################

const DAY_IN_LEDGERS: u32 = 17280;

/// TTL extension amount for storage entries (in ledgers)
pub const VOTES_EXTEND_AMOUNT: u32 = 30 * DAY_IN_LEDGERS;

/// TTL threshold for extending storage entries (in ledgers)
pub const VOTES_TTL_THRESHOLD: u32 = VOTES_EXTEND_AMOUNT - DAY_IN_LEDGERS;

// ################## EVENTS ##################

/// Event emitted when an account changes its delegate.
#[contractevent]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DelegateChanged {
/// The account that changed its delegate
#[topic]
pub delegator: Address,
/// The previous delegate (if any)
pub from_delegate: Option<Address>,
/// The new delegate
pub to_delegate: Address,
}

/// Emits an event when an account changes its delegate.
///
/// # Arguments
///
/// * `e` - Access to Soroban environment.
/// * `delegator` - The account that changed its delegate.
/// * `from_delegate` - The previous delegate (if any).
/// * `to_delegate` - The new delegate.
pub fn emit_delegate_changed(
e: &Env,
delegator: &Address,
from_delegate: Option<Address>,
to_delegate: &Address,
) {
DelegateChanged {
delegator: delegator.clone(),
from_delegate,
to_delegate: to_delegate.clone(),
}
.publish(e);
}

/// Event emitted when a delegate's voting power changes.
#[contractevent]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DelegateVotesChanged {
/// The delegate whose voting power changed
#[topic]
pub delegate: Address,
/// The previous voting power
pub previous_votes: u128,
/// The new voting power
pub new_votes: u128,
}

/// Emits an event when a delegate's voting power changes.
///
/// # Arguments
///
/// * `e` - Access to Soroban environment.
/// * `delegate` - The delegate whose voting power changed.
/// * `previous_votes` - The previous voting power.
/// * `new_votes` - The new voting power.
pub fn emit_delegate_votes_changed(
e: &Env,
delegate: &Address,
previous_votes: u128,
new_votes: u128,
) {
DelegateVotesChanged { delegate: delegate.clone(), previous_votes, new_votes }.publish(e);
}
Loading
Loading