diff --git a/content/stellar-contracts/changelog.mdx b/content/stellar-contracts/changelog.mdx
deleted file mode 100644
index 62a2751b..00000000
--- a/content/stellar-contracts/changelog.mdx
+++ /dev/null
@@ -1,167 +0,0 @@
----
-title: Changelog
----
-
-
-# [v0.6.0](https://github.com/OpenZeppelin/stellar-contracts/releases/tag/v0.6.0) - 2026-01-26
-
-## What's New
-
-* Timelock (governor sub module)
-* WAD (fixed point arithmetic)
-* Power Function (fixed point arithmetic)
-* Fee Forwarder
-* Added `get_existing_roles` for Access Control
-
-## What's Changed
-
-* Inconsistencies across mint and caller in https://github.com/OpenZeppelin/stellar-contracts/pull/495
-* Use muxed addresses in fungible transfers in https://github.com/OpenZeppelin/stellar-contracts/pull/493
-* Renounce admin rejects when transfer in progress in https://github.com/OpenZeppelin/stellar-contracts/pull/502
-* Uncaught panics on policy uninstall in https://github.com/OpenZeppelin/stellar-contracts/pull/504
-* NFT name and symbol bound for metadata in https://github.com/OpenZeppelin/stellar-contracts/pull/508
-* Bound metadata entries in rwa in https://github.com/OpenZeppelin/stellar-contracts/pull/511
-* Allowance bug fix in https://github.com/OpenZeppelin/stellar-contracts/pull/513
-* Vault consistency fix in https://github.com/OpenZeppelin/stellar-contracts/pull/523
-* Contract trait, deprecate `default_impl` macro in https://github.com/OpenZeppelin/stellar-contracts/pull/525
-
-[Changes][v0.6.0]
-
-
-
-# [v0.5.1](https://github.com/OpenZeppelin/stellar-contracts/releases/tag/v0.5.1) - 2025-10-31
-
-## What's Changed
-
-* fix the javascript compilation issue for `examples` package https://github.com/OpenZeppelin/stellar-contracts/issues/484 by renaming the generic datakey enum
-
-[Changes][v0.5.1]
-
-
-
-# [v0.5.0](https://github.com/OpenZeppelin/stellar-contracts/releases/tag/v0.5.0) - 2025-10-28
-
-## What's Changed
-
-### Major
-* Added Real-World Asset token implementation based on ERC-3643 (T-Rex)
-* Added Smart Accounts: context-centric framework to compose authorization intents with signers and policies
-* Added Token Vault implementation compatible with ERC-4626
-
-### Minor
-* Prevent test_snapshots from being checked in; make PRs less noisy in https://github.com/OpenZeppelin/stellar-contracts/pull/335
-* Remove cdylib crate type from default-impl-macro-test Cargo.toml in https://github.com/OpenZeppelin/stellar-contracts/pull/468
-* Migrate to target wasmv1-none in https://github.com/OpenZeppelin/stellar-contracts/pull/441
-* Remove antora docs in https://github.com/OpenZeppelin/stellar-contracts/pull/483
-
-[Changes][v0.5.0]
-
-
-
-# [v0.4.1](https://github.com/OpenZeppelin/stellar-contracts/releases/tag/v0.4.1) - 2025-07-22
-
-Added `readme.md` for each new package category:
-- access
-- contract-utils
-- macros
-- tokens
-
-[Changes][v0.4.1]
-
-
-
-# [v0.4.0](https://github.com/OpenZeppelin/stellar-contracts/releases/tag/v0.4.0) - 2025-07-22
-
-Restructures the crates to group them under:
-- `access`
-- `tokens`
-- `macros`
-- `contract-utils`
-
-None of the audited code has changed. This release consists only of moving things around and restructuring
-
-[Changes][v0.4.0]
-
-
-
-# [v0.3.0](https://github.com/OpenZeppelin/stellar-contracts/releases/tag/v0.3.0) - 2025-07-03
-
-This release is audited, you can find the audit report [here](https://github.com/OpenZeppelin/stellar-contracts/blob/main/audits/Stellar%20Contracts%20Library%20v0.3.0-rc.2%20Audit.pdf).
-
-## Breaking Changes
-- Fungible module got reworked, see: [#234](https://github.com/OpenZeppelin/stellar-contracts/pull/234)
-
-## What's Changed
-* Access control by [@ozgunozerk](https://github.com/ozgunozerk) in [#214](https://github.com/OpenZeppelin/stellar-contracts/pull/214)
-* Ownable by [@ozgunozerk](https://github.com/ozgunozerk) in [#216](https://github.com/OpenZeppelin/stellar-contracts/pull/216)
-* Error numbering by [@ozgunozerk](https://github.com/ozgunozerk) in [#226](https://github.com/OpenZeppelin/stellar-contracts/pull/226)
-* Merkle Proofs by [@brozorec](https://github.com/brozorec) in [#222](https://github.com/OpenZeppelin/stellar-contracts/pull/222)
-* SacAdmin by [@brozorec](https://github.com/brozorec) in [#215](https://github.com/OpenZeppelin/stellar-contracts/pull/215)
-* Merkle Distributor by [@brozorec](https://github.com/brozorec) in [#229](https://github.com/OpenZeppelin/stellar-contracts/pull/229)
-* Royalty by [@ozgunozerk](https://github.com/ozgunozerk) in [#221](https://github.com/OpenZeppelin/stellar-contracts/pull/221)
-* SAC Admin Generic by [@brozorec](https://github.com/brozorec) in [#232](https://github.com/OpenZeppelin/stellar-contracts/pull/232)
-* Fungible refactor by [@ozgunozerk](https://github.com/ozgunozerk) in [#234](https://github.com/OpenZeppelin/stellar-contracts/pull/234)
-* Fix binver by [@brozorec](https://github.com/brozorec) in [#224](https://github.com/OpenZeppelin/stellar-contracts/pull/224)
-* Allowlist and blocklist by [@ozgunozerk](https://github.com/ozgunozerk) in [#237](https://github.com/OpenZeppelin/stellar-contracts/pull/237)
-
-## New Contributors
-* [@orangelash](https://github.com/orangelash) made their first contribution in [#331](https://github.com/OpenZeppelin/stellar-contracts/pull/331)
-
-**Full Changelog**: https://github.com/OpenZeppelin/stellar-contracts/compare/v0.2.0...v0.3.0
-
-[Changes][v0.3.0]
-
-
-
-# [v0.2.0](https://github.com/OpenZeppelin/stellar-contracts/releases/tag/v0.2.0) - 2025-05-12
-
-This release is audited, you can find the audit report [here](https://github.com/OpenZeppelin/stellar-contracts/blob/main/audits/2025-05-v0.2.0.pdf).
-
-In this release, you can find:
-
-- Non-Fungible Token base implementation with the following extensions:
- - enumerable
- - consecutive
- - burnable
-- Upgradeable and Migrations utilities
-- Capped extension for Fungible Token
-- Showcase examples:
- - nft-consecutive
- - nft-enumerable
- - nft-sequential-minting
- - fungible-capped
- - upgradeable
-
-[Changes][v0.2.0]
-
-
-
-# [v0.1.0](https://github.com/OpenZeppelin/stellar-contracts/releases/tag/v0.1.0) - 2025-02-21
-
-The first release of OpenZeppelin Contracts for Stellar Soroban.
-
-This release is audited, you can find the audit report [here](https://github.com/OpenZeppelin/stellar-contracts/blob/main/audits/2025-02-v0.1.0-rc.pdf)
-
-In this release, you can find:
-- Fungible Token standard (similar to ERC20) implemented for Stellar Soroban, compliant with SEP-41
-- The following extensions for the Fungible Token standard:
- - Mintable
- - Burnable
- - Metadata
-- `Pausable` utility for your contracts.
-- Examples folder to showcase what's possible:
- - fungible-pausable
- - fungible-token-interface
- - pausable
-
-[Changes][v0.1.0]
-
-
-[v0.6.0]: https://github.com/OpenZeppelin/stellar-contracts/compare/v0.5.1...v0.6.0
-[v0.5.1]: https://github.com/OpenZeppelin/stellar-contracts/compare/v0.5.0...v0.5.1
-[v0.5.0]: https://github.com/OpenZeppelin/stellar-contracts/compare/v0.4.1...v0.5.0
-[v0.4.1]: https://github.com/OpenZeppelin/stellar-contracts/compare/v0.4.0...v0.4.1
-[v0.4.0]: https://github.com/OpenZeppelin/stellar-contracts/compare/v0.3.0...v0.4.0
-[v0.3.0]: https://github.com/OpenZeppelin/stellar-contracts/compare/v0.2.0...v0.3.0
-[v0.2.0]: https://github.com/OpenZeppelin/stellar-contracts/compare/v0.1.0...v0.2.0
-[v0.1.0]: https://github.com/OpenZeppelin/stellar-contracts/tree/v0.1.0
diff --git a/content/stellar-contracts/governance/governor.mdx b/content/stellar-contracts/governance/governor.mdx
new file mode 100644
index 00000000..d5f9b7b9
--- /dev/null
+++ b/content/stellar-contracts/governance/governor.mdx
@@ -0,0 +1,247 @@
+---
+title: Governor
+---
+
+[Source Code](https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/governance/src/governor)
+
+## Overview
+
+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.
+
+A typical governance system involves two contracts working together:
+
+- A **token contract** with the [Votes](/stellar-contracts/governance/votes) extension, which tracks who holds voting power and allows delegation.
+- A **governor contract** implementing the `Governor` trait, which manages the proposal lifecycle: creation, voting, counting, and execution.
+
+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.
+
+## How Governance Works
+
+### The Proposal Lifecycle
+
+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:
+
+```mermaid
+stateDiagram-v2
+ [*] --> Pending: propose()
+ Pending --> Active: voting delay passes
+ Active --> Defeated: voting ends, quorum/majority not met
+ Active --> Succeeded: voting ends, quorum + majority met
+ Succeeded --> Executed: execute()
+ Succeeded --> Queued: queue() (if enabled)
+ Queued --> Executed: execute()
+ Queued --> Expired: expiration passes
+ Pending --> Canceled: cancel()
+ Active --> Canceled: cancel()
+ Succeeded --> Canceled: cancel()
+ Queued --> Canceled: cancel()
+```
+
+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.
+
+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.
+
+3. **Succeed or Defeat**: When the **voting period** ends, the system checks two conditions:
+ - **Majority**: Do `for` votes strictly exceed `against` votes?
+ - **Quorum**: Does the sum of `for` and `abstain` votes meet or exceed the required quorum?
+
+ 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.
+
+5. **Cancel**: The proposer (or another authorized role) can cancel a proposal at any point before it is executed, expired, or already cancelled.
+
+### Optional: Queuing with a Timelock
+
+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.
+
+This flow becomes: **Propose → Vote → Queue → Execute**
+
+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.
+
+## Voting and Counting
+
+### How Votes Are Counted
+
+The default counting mode is **simple counting**:
+
+| Vote Type | Value | Meaning |
+|-----------|-------|---------|
+| Against | 0 | Opposes the proposal |
+| For | 1 | Supports the proposal |
+| Abstain | 2 | Counted toward quorum but not toward majority |
+
+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.
+
+### Custom Counting Strategies
+
+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 or weighted quorum relative to total supply. The `counting_mode()` method returns a symbol identifying the active strategy, which UIs can use for display purposes.
+
+### Dynamic Quorum
+
+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.
+
+
+When overriding `quorum()`, ensure that configurable parameters are themselves queried at the historical ledger to avoid retroactively changing the outcome of existing proposals.
+
+
+## Design Rationale
+
+### Voting Power Lives on the Token
+
+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.
+
+### Queue Logic is Built-In but Disabled by Default
+
+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.
+
+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.
+
+### Execution and Cancellation Require Implementation
+
+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.
+
+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.
+
+## Security Considerations
+
+### Flash Loan Voting Attack
+
+An attacker could borrow voting tokens, vote, and return them within the same transaction. This implementation mitigates this with **snapshot-based voting power**:
+
+1. **Proposer snapshot** (`current_ledger - 1`): Prevents flash-loaning tokens and creating a proposal in the same transaction.
+2. **Voter snapshot** (`vote_start`): Voters' power is looked up at the `vote_start` ledger. Since checkpoints record state after all transactions in a ledger are finalized, a flash loan within the same ledger shows a net-zero balance.
+
+### Proposal Spam
+
+The **proposal threshold** requires proposers to hold a minimum amount of voting power, making spam attacks economically costly.
+
+### Governance Capture
+
+- **Quorum requirements** ensure minimum participation
+- **Voting delay** gives token holders time to position themselves before voting starts
+
+## The Governor Trait
+
+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.
+
+### Configuration
+
+```rust
+fn name(e: &Env) -> String;
+fn version(e: &Env) -> String;
+fn voting_delay(e: &Env) -> u32; // ledgers before voting starts
+fn voting_period(e: &Env) -> u32; // ledgers during which voting is open
+fn proposal_threshold(e: &Env) -> u128; // minimum voting power to propose
+fn get_token_contract(e: &Env) -> Address; // the Votes-enabled token contract
+fn counting_mode(e: &Env) -> Symbol; // identifies the counting strategy
+fn proposals_need_queuing(e: &Env) -> bool; // defaults to false
+```
+
+### Proposal Lifecycle
+
+```rust
+fn propose(e: &Env, targets: Vec
, functions: Vec,
+ args: Vec>, description: String, proposer: Address) -> BytesN<32>;
+
+fn cast_vote(e: &Env, proposal_id: BytesN<32>, vote_type: u32,
+ reason: String, voter: Address) -> u128;
+
+fn queue(e: &Env, targets: Vec, functions: Vec,
+ args: Vec>, description_hash: BytesN<32>,
+ eta: u32, operator: Address) -> BytesN<32>;
+
+fn execute(e: &Env, targets: Vec, functions: Vec,
+ args: Vec>, description_hash: BytesN<32>,
+ executor: Address) -> BytesN<32>; // no default — must implement
+
+fn cancel(e: &Env, targets: Vec, functions: Vec,
+ args: Vec>, description_hash: BytesN<32>,
+ operator: Address) -> BytesN<32>; // no default — must implement
+```
+
+### Query Methods
+
+```rust
+fn has_voted(e: &Env, proposal_id: BytesN<32>, account: Address) -> bool;
+fn quorum(e: &Env, ledger: u32) -> u128;
+fn proposal_state(e: &Env, proposal_id: BytesN<32>) -> ProposalState;
+fn proposal_snapshot(e: &Env, proposal_id: BytesN<32>) -> u32;
+fn proposal_deadline(e: &Env, proposal_id: BytesN<32>) -> u32;
+fn proposal_proposer(e: &Env, proposal_id: BytesN<32>) -> Address;
+fn get_proposal_id(e: &Env, targets: Vec, functions: Vec,
+ args: Vec>, description_hash: BytesN<32>) -> BytesN<32>;
+```
+
+The proposal ID is a deterministic keccak256 hash of the proposal parameters, allowing anyone to compute it without storing the full proposal data.
+
+### Proposal States
+
+```rust
+pub enum ProposalState {
+ Pending = 0, // Voting has not started
+ Active = 1, // Voting is ongoing
+ Defeated = 2, // Voting ended without success
+ Canceled = 3, // Cancelled by authorized account
+ Succeeded = 4, // Met quorum and vote thresholds
+ Queued = 5, // Queued for execution (via extension)
+ Expired = 6, // Expired after queuing (via extension)
+ Executed = 7, // Successfully executed
+}
+```
+
+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.
+
+## Example
+
+```rust
+use soroban_sdk::{contract, contractimpl, Address, BytesN, Env, String, Symbol, Val, Vec};
+use stellar_governance::governor::{self, storage, Governor, hash_proposal, get_proposal_snapshot};
+
+#[contract]
+pub struct MyGovernor;
+
+#[contractimpl]
+impl Governor for MyGovernor {
+ // Open execution — anyone can trigger a succeeded proposal
+ fn execute(
+ e: &Env,
+ targets: Vec,
+ functions: Vec,
+ args: Vec>,
+ description_hash: BytesN<32>,
+ _executor: Address,
+ ) -> BytesN<32> {
+ let proposal_id = hash_proposal(e, &targets, &functions, &args, &description_hash);
+ let quorum = Self::quorum(e, get_proposal_snapshot(e, &proposal_id));
+ storage::execute(
+ e, targets, functions, args, &description_hash,
+ Self::proposals_need_queuing(e), quorum,
+ )
+ }
+
+ // Only the original proposer can cancel
+ fn cancel(
+ e: &Env,
+ targets: Vec,
+ functions: Vec,
+ args: Vec>,
+ description_hash: BytesN<32>,
+ operator: Address,
+ ) -> BytesN<32> {
+ let proposal_id = storage::hash_proposal(
+ e, &targets, &functions, &args, &description_hash,
+ );
+ let proposer = storage::get_proposal_proposer(e, &proposal_id);
+ assert!(operator == proposer);
+ operator.require_auth();
+ storage::cancel(e, targets, functions, args, &description_hash)
+ }
+}
+```
+
+## See Also
+
+- [Votes](/stellar-contracts/governance/votes)
+- [Timelock Controller](/stellar-contracts/governance/timelock-controller)
+- [Fungible Token](/stellar-contracts/tokens/fungible/fungible)
diff --git a/content/stellar-contracts/governance/timelock-controller.mdx b/content/stellar-contracts/governance/timelock-controller.mdx
index a07fd022..4c3230b9 100644
--- a/content/stellar-contracts/governance/timelock-controller.mdx
+++ b/content/stellar-contracts/governance/timelock-controller.mdx
@@ -37,7 +37,7 @@ Timelocked operations follow a specific lifecycle:
### Schedule
-When a proposer calls `schedule_operation`, the `OperationState` moves from `Unset` to `Waiting`. This starts a timer that must be greater than or equal to the minimum delay. The timer expires at a timestamp accessible through `get_timestamp`. Once the timer expires, the `OperationState` automatically moves to the `Ready` state. At this point, it can be executed.
+When a proposer calls `schedule_operation`, the `OperationState` moves from `Unset` to `Waiting`. This starts a timer that must be greater than or equal to the minimum delay (specified in ledger sequence numbers). The timer expires at a ledger sequence accessible through `get_operation_ledger`. Once the current ledger passes that point, the `OperationState` automatically moves to the `Ready` state. At this point, it can be executed.
### Execute
diff --git a/content/stellar-contracts/governance/votes.mdx b/content/stellar-contracts/governance/votes.mdx
new file mode 100644
index 00000000..765b692d
--- /dev/null
+++ b/content/stellar-contracts/governance/votes.mdx
@@ -0,0 +1,123 @@
+---
+title: Votes
+---
+
+[Source Code](https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/governance/src/votes)
+
+## Overview
+
+The Votes module adds voting power tracking to a token contract. It is the foundation that makes governance possible — without it, the [Governor](/stellar-contracts/governance/governor) has no way to know how much influence each account holds.
+
+In a governance system, you typically have two contracts: a **token** that tracks voting power (this module) and a **governor** that manages proposals. The Votes module is implemented on the token side, where it hooks into balance changes and maintains a history of who held how much power at each point in time.
+
+## How Voting Power Works
+
+### Voting Units
+
+Every token can carry one **voting unit** per unit of balance. When tokens are minted, transferred, or burned, the corresponding voting units move with them. The implementing contract must call `transfer_voting_units` on every balance change to keep voting power in sync with token balances.
+
+### Delegation: Activating Your Votes
+
+Holding tokens alone does not give you voting power. You must **delegate** your voting units to an address — either to yourself or to someone else — before they count as active votes. This is a deliberate design choice:
+
+- **Self-delegation**: An account delegates to itself to activate its own votes.
+- **Delegating to another**: An account transfers its voting power to a trusted representative, who votes on its behalf.
+- **Undelegating**: There is no separate undelegate operation. To reclaim delegated power, simply delegate back to yourself.
+
+Until an account calls `delegate` at least once, its voting units exist but are not counted in any vote.
+
+### Checkpoints: Snapshots of the Past
+
+Every time voting power changes — through delegation, transfers, mints, or burns — the module records a **checkpoint**: a snapshot of that account's voting power at the current ledger sequence number. These checkpoints allow the Governor to query an account's voting power at any past ledger, which is critical for:
+
+- **Fair voting**: A proposal records a snapshot ledger when it is created. All votes are counted based on power held at that snapshot, not at the time of voting. This prevents vote manipulation through last-minute token purchases.
+- **Flash loan protection**: Since checkpoints record state after all transactions in a ledger are finalized, borrowing and returning tokens within the same ledger leaves a net-zero checkpoint.
+
+## Integrating Votes into a Token Contract
+
+The Votes module is designed to be layered onto an existing fungible or non-fungible token. The integration requires two things:
+
+1. **Hook into balance changes** — call `transfer_voting_units` after every mint, burn, and transfer.
+2. **Expose delegation** — let users delegate their voting power.
+
+```rust
+use stellar_governance::votes::{self, transfer_voting_units};
+
+// After every transfer, update voting units:
+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);
+}
+
+// For minting (no sender):
+pub fn mint(e: &Env, to: Address, amount: i128) {
+ // ... perform mint logic ...
+ transfer_voting_units(e, None, Some(&to), amount as u128);
+}
+
+// For burning (no receiver):
+pub fn burn(e: &Env, from: Address, amount: i128) {
+ // ... perform burn logic ...
+ transfer_voting_units(e, Some(&from), None, amount as u128);
+}
+
+// Expose delegation to users:
+pub fn delegate(e: &Env, account: Address, delegatee: Address) {
+ votes::delegate(e, &account, &delegatee);
+}
+```
+
+
+If you are using the Fungible or Non-Fungible token from this library, the **Votes extension** handles all of this automatically. You only need to enable the extension — no manual integration required. See [Fungible Token](/stellar-contracts/tokens/fungible/fungible) or [Non-Fungible Token](/stellar-contracts/tokens/non-fungible/non-fungible).
+
+```rust
+use stellar_governance::votes::Votes;
+use stellar_tokens::fungible::{votes::FungibleVotes, FungibleToken};
+
+// 1. Set ContractType to FungibleVotes — this hooks into all balance
+// changes (transfers, mints, burns) and updates voting units automatically.
+#[contractimpl(contracttrait)]
+impl FungibleToken for MyToken {
+ type ContractType = FungibleVotes;
+}
+
+// 2. Implement the Votes trait to expose voting queries to the Governor.
+#[contractimpl(contracttrait)]
+impl Votes for MyToken {}
+```
+
+
+## The Votes Trait
+
+The `Votes` trait defines the interface that the Governor queries. All methods have default implementations.
+
+```rust
+#[contracttrait]
+pub trait Votes {
+ /// Current voting power (delegated votes) of an account.
+ fn get_votes(e: &Env, account: Address) -> u128;
+
+ /// Voting power at a specific past ledger.
+ fn get_votes_at_checkpoint(e: &Env, account: Address, ledger: u32) -> u128;
+
+ /// Total voting units in circulation (regardless of delegation).
+ fn get_total_supply(e: &Env) -> u128;
+
+ /// Total supply at a specific past ledger.
+ fn get_total_supply_at_checkpoint(e: &Env, ledger: u32) -> u128;
+
+ /// The current delegate for an account (None if never delegated).
+ fn get_delegate(e: &Env, account: Address) -> Option;
+
+ /// Delegate voting power from `account` to `delegatee`.
+ fn delegate(e: &Env, account: Address, delegatee: Address);
+}
+```
+
+All query methods return `0` if the account has no voting power or does not exist.
+
+## See Also
+
+- [Governor](/stellar-contracts/governance/governor)
+- [Fungible Token](/stellar-contracts/tokens/fungible/fungible) (Votes extension)
+- [Non-Fungible Token](/stellar-contracts/tokens/non-fungible/non-fungible) (Votes extension)
diff --git a/content/stellar-contracts/index.mdx b/content/stellar-contracts/index.mdx
index 25e9c1a2..0bb78d77 100644
--- a/content/stellar-contracts/index.mdx
+++ b/content/stellar-contracts/index.mdx
@@ -24,6 +24,8 @@ for access control and contract management.
## Governance
+* **[Governor](/stellar-contracts/governance/governor)**: On-chain governance with proposals, voting, and execution.
+* **[Votes](/stellar-contracts/governance/votes)**: Delegation-based voting power tracking with historical checkpoints.
* **[Timelock Controller](/stellar-contracts/governance/timelock-controller)**: Enforce time delays on transaction execution, allowing users to exit the system if they disagree with a decision before it is executed.
## Utilities
diff --git a/content/stellar-contracts/tokens/fungible/fungible.mdx b/content/stellar-contracts/tokens/fungible/fungible.mdx
index da4666db..b1c12891 100644
--- a/content/stellar-contracts/tokens/fungible/fungible.mdx
+++ b/content/stellar-contracts/tokens/fungible/fungible.mdx
@@ -101,6 +101,11 @@ The `FungibleBlockList` trait extends the `FungibleToken` trait to provide a blo
can be managed by an authorized account. This extension ensures that blocked accounts cannot transfer/receive
tokens, or approve token transfers.
+### - Votes
+[Source Code](https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/tokens/src/fungible/extensions/votes)
+
+The `FungibleVotes` extension integrates with the governance [Votes](/stellar-contracts/governance/votes) module to track voting power based on token balances. It overrides `transfer`, `transfer_from`, `burn`, and `burn_from` to call `transfer_voting_units` after each balance change, enabling delegation-based governance. Token holders must call `delegate` (even to themselves) before their voting power is counted.
+
## Stellar Asset Contract (SAC)
The Stellar Asset Contract (SAC) is a special built-in implementation of [CAP-46-6 Smart Contract Standardized Asset](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0046-06.md) and [SEP-41 Token Interface](https://developers.stellar.org/docs/tokens/token-interface). The SAC acts as a bridge between traditional Stellar assets and Soroban smart contracts.
diff --git a/content/stellar-contracts/tokens/non-fungible/non-fungible.mdx b/content/stellar-contracts/tokens/non-fungible/non-fungible.mdx
index c40c3f0f..ff6fdceb 100644
--- a/content/stellar-contracts/tokens/non-fungible/non-fungible.mdx
+++ b/content/stellar-contracts/tokens/non-fungible/non-fungible.mdx
@@ -103,3 +103,8 @@ This extension is build around the contract variant `Enumerable`. Here is an exa
The `NonFungibleRoyalties` trait extends the `NonFungibleToken` trait to provide royalty information for tokens, similar to ERC-2981 standard. This allows marketplaces to query royalty information and pay appropriate fees to creators.
Note: The royalties extension allows both collection-wide default royalties and per-token royalty settings.
+
+### - Votes
+[Source Code](https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/tokens/src/non_fungible/extensions/votes)
+
+The `NonFungibleVotes` extension integrates with the governance [Votes](/stellar-contracts/governance/votes) module. Each NFT represents one voting unit. It overrides `transfer`, `transfer_from`, `burn`, and `burn_from` to call `transfer_voting_units` after each balance change. Token holders must call `delegate` (even to themselves) before their voting power is counted.
diff --git a/content/stellar-contracts/tokens/rwa/rwa.mdx b/content/stellar-contracts/tokens/rwa/rwa.mdx
index b208b777..3f8d7919 100644
--- a/content/stellar-contracts/tokens/rwa/rwa.mdx
+++ b/content/stellar-contracts/tokens/rwa/rwa.mdx
@@ -294,6 +294,30 @@ graph TD
The compliance contract is designed to be shared across multiple RWA tokens, with each hook function accepting a
`token` parameter to identify the calling token.
+### - Compliance Modules
+[Source Code](https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/tokens/src/rwa/compliance/modules)
+
+Individual compliance modules implement the `ComplianceModule` trait, which defines hooks that the compliance contract forwards to:
+
+```rust
+pub trait ComplianceModule {
+ fn on_transfer(e: &Env, token: Address, from: Address, to: Address, amount: i128);
+ fn on_created(e: &Env, token: Address, to: Address, amount: i128);
+ fn on_destroyed(e: &Env, token: Address, from: Address, amount: i128);
+ fn can_transfer(e: &Env, token: Address, from: Address, to: Address, amount: i128) -> bool;
+ fn can_create(e: &Env, token: Address, to: Address, amount: i128) -> bool;
+ fn name(e: &Env) -> String;
+ fn get_compliance_address(e: &Env) -> Address;
+ fn set_compliance_address(e: &Env, compliance: Address);
+}
+```
+
+Modules can be registered for specific hooks via the compliance contract. The `ComplianceHook` enum defines which hooks a module handles: `Transferred`, `Created`, `Destroyed`, `CanTransfer`, `CanCreate`.
+
+
+State-modifying hooks (`on_transfer`, `on_created`, `on_destroyed`) should restrict their caller to the compliance contract address. Read-only hooks (`can_transfer`, `can_create`) can be exposed more broadly.
+
+
### - Identity Verifier
[Source Code](https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/tokens/src/rwa/identity_verifier)
diff --git a/content/stellar-contracts/utils/upgradeable.mdx b/content/stellar-contracts/utils/upgradeable.mdx
index 5a8407af..3992e0c4 100644
--- a/content/stellar-contracts/utils/upgradeable.mdx
+++ b/content/stellar-contracts/utils/upgradeable.mdx
@@ -13,29 +13,18 @@ contract developers who can choose to make the contract immutable by simply not
the other hand, providing upgradability on a protocol level significantly reduces the risk surface, compared to other
smart contract platforms, which lack native support for upgradability.
-While Soroban’s built-in upgradability eliminates many of the challenges, related to managing smart contract upgrades
+While Soroban's built-in upgradability eliminates many of the challenges, related to managing smart contract upgrades
and migrations, certain caveats must still be considered.
## Overview
The [upgradeable](https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/contract-utils/src/upgradeable) module
-provides a lightweight upgradeability framework with additional support for structured and safe migrations.
-
-It consists of two main components:
-
-1. ***[`Upgradeable`](#upgrade-only)*** for cases where only the WASM binary needs to be updated.
-2. ***[`UpgradeableMigratable`](#upgrade-and-migrate)*** for more advanced scenarios where, in addition to the WASM binary, specific storage entries
-must be modified (migrated) during the upgrade process.
-
-The recommended way to use this module is through the `#[derive(Upgradeable)]` and `#[derive(UpgradeableMigratable)]`
-macros.
-
-They handle the implementation of the necessary functions, allowing developers to focus solely on managing authorizations
-and access control. These derive macros also leverage the crate version from the contract’s `Cargo.toml` and set it as
-the binary version in the WASM metadata, aligning with the guidelines outlined in
-[SEP-49](https://github.com/stellar/stellar-protocol/blob/master/ecosystem%2Fsep-0049.md).
+provides a lightweight upgradeability framework with additional guidance for structured and safe migrations.
+It consists of two components:
+1. **The `Upgradeable` trait** — a standardized entry point for contract upgrades, generating a client (`UpgradeableClient`) for calling upgrades from other contracts.
+2. **Migration pattern guidelines** — three documented patterns for handling storage changes during upgrades.
While the framework structures the upgrade flow, it does NOT perform deeper checks and verifications such as:
@@ -45,112 +34,193 @@ While the framework structures the upgrade flow, it does NOT perform deeper chec
* Checking for storage consistency, ensuring that the new contract does not inadvertently introduce storage mismatches.
-## Usage
-
-### Upgrade Only
-#### `Upgradeable`
+## The Upgradeable Trait
-When only the WASM binary needs to be upgraded and no additional migration logic is required, developers should implement
-the `UpgradeableInternal` trait. This trait is where authorization and custom access control logic are defined,
-specifying who can perform the upgrade. This minimal implementation keeps the focus solely on controlling upgrade
-permissions.
+The `Upgradeable` trait defines a standardized upgrade entry point:
```rust
-use soroban_sdk::{
- contract, contracterror, contractimpl, panic_with_error, symbol_short, Address, Env,
-};
-use stellar_contract_utils::upgradeable::UpgradeableInternal;
-use stellar_macros::Upgradeable;
-
-#[contracterror]
-#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
-#[repr(u32)]
-pub enum ExampleContractError {
- Unauthorized = 1,
+#[contractclient(name = "UpgradeableClient")]
+pub trait Upgradeable {
+ fn upgrade(e: &Env, new_wasm_hash: BytesN<32>, operator: Address);
}
+```
+
+All access control and authorization checks are the implementor's responsibility. Implement this trait directly using `#[contractimpl]` and call the `upgradeable::upgrade()` free function inside:
+```rust
+use soroban_sdk::{contract, contractimpl, Address, BytesN, Env};
+use stellar_contract_utils::upgradeable::{self as upgradeable, Upgradeable};
+use stellar_macros::only_role;
-#[derive(Upgradeable)]
#[contract]
pub struct ExampleContract;
#[contractimpl]
-impl ExampleContract {
- pub fn __constructor(e: &Env, admin: Address) {
- e.storage().instance().set(&symbol_short!("OWNER"), &admin);
+impl Upgradeable for ExampleContract {
+ #[only_role(operator, "admin")]
+ fn upgrade(e: &Env, new_wasm_hash: BytesN<32>, operator: Address) {
+ upgradeable::upgrade(e, &new_wasm_hash);
}
}
+```
-impl UpgradeableInternal for ExampleContract {
- fn _require_auth(e: &Env, operator: &Address) {
- operator.require_auth();
- // `operator` is the invoker of the upgrade function and can be used
- // to perform a role-based access control if implemented
- let owner: Address = e.storage().instance().get(&symbol_short!("OWNER")).unwrap();
- if *operator != owner {
- panic_with_error!(e, ExampleContractError::Unauthorized)
- }
- }
-}
+## Storage Migration
+
+When upgrading contracts, data structures may change (e.g., adding new fields, removing old ones, or restructuring data). This section explains how to handle those changes safely.
+
+### Why There Is No Migratable Trait
+
+Migration is deliberately not standardized into a trait:
+
+- Migration rarely has a single entrypoint: a contract may need to migrate several independent storage structures at different times.
+- A fixed trait signature would force all migration arguments into a single `#[contracttype]` struct, removing the flexibility to choose argument types, authorization roles, or split migration across multiple functions.
+- Lazy migration (Pattern 2) has no discrete migration call at all.
+
+The patterns below are therefore guidelines rather than enforced interfaces.
+
+### The Problem: Host-Level Type Validation
+
+Soroban validates types at the host level when reading from storage. If a data structure's shape changes between versions, the host traps before the SDK can handle the mismatch:
+
+```rust
+// V1 stored this type:
+#[contracttype]
+pub struct Config { pub rate: u32 }
+
+// V2 adds a field. Reading old storage with the new type traps, because
+// the host validates field count before the SDK sees the value.
+#[contracttype]
+pub struct Config { pub rate: u32, pub active: bool }
+
+// Traps with Error(Object, UnexpectedSize)
+let config: Config = e.storage().instance().get(&key).unwrap();
```
-### Upgrade and Migrate
-#### `UpgradeableMigratable`
+### Pattern 1: Eager Migration (Bounded Data)
-When both the WASM binary and specific storage entries need to be modified as part of the upgrade process, the
-`UpgradeableMigratableInternal` trait should be implemented. In addition to defining access control and migration
-logic, the developer must specify an associated type that represents the data required for the migration.
+For bounded data in instance storage (config, metadata, settings), add a `migrate` function to the upgraded contract that reads old-format data and converts it. Use `set_schema_version` / `get_schema_version` to guard against double invocation.
-The `#[derive(UpgradeableMigratable)]` macro manages the sequencing of operations, ensuring that the migration can
-only be invoked after a successful upgrade, preventing potential state inconsistencies and storage corruption.
+The old type must be defined in the new contract code so the host can deserialize it correctly.
```rust
-use soroban_sdk::{
- contract, contracterror, contracttype, panic_with_error, symbol_short, Address, Env,
-};
-use stellar_contract_utils::upgradeable::UpgradeableMigratableInternal;
-use stellar_macros::UpgradeableMigratable;
-
-#[contracterror]
-#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
-#[repr(u32)]
-pub enum ExampleContractError {
- Unauthorized = 1,
+// Old type (matches what v1 stored, field names and types must match)
+#[contracttype]
+pub struct ConfigV1 {
+ pub rate: u32,
}
+// New type
#[contracttype]
-pub struct Data {
- pub num1: u32,
- pub num2: u32,
+pub struct Config {
+ pub rate: u32,
+ pub active: bool,
}
-#[derive(UpgradeableMigratable)]
-#[contract]
-pub struct ExampleContract;
+const CONFIG_KEY: Symbol = symbol_short!("CONFIG");
-impl UpgradeableMigratableInternal for ExampleContract {
- type MigrationData = Data;
+pub fn migrate(e: &Env, operator: Address) {
+ assert!(upgradeable::get_schema_version(e) < 2, "already migrated");
- fn _require_auth(e: &Env, operator: &Address) {
- operator.require_auth();
- let owner: Address = e.storage().instance().get(&symbol_short!("OWNER")).unwrap();
- if *operator != owner {
- panic_with_error!(e, ExampleContractError::Unauthorized)
+ let old: ConfigV1 = e.storage().instance().get(&CONFIG_KEY).unwrap();
+ let new = Config { rate: old.rate, active: true };
+ e.storage().instance().set(&CONFIG_KEY, &new);
+
+ upgradeable::set_schema_version(e, 2);
+}
+```
+
+Migration must happen in a separate transaction after the upgrade completes, or atomically via a third-party upgrader contract (see [Atomic Upgrade and Migration](#atomic-upgrade-and-migration) below).
+
+### Pattern 2: Lazy Migration (Unbounded Data)
+
+For unbounded persistent storage (user balances, approvals, etc.), eager migration is impractical as it's impossible to iterate all entries in one transaction without hitting resource limits.
+
+Instead, use version markers alongside each entry and convert lazily on read:
+
+```rust
+// Old type must match what v1 stored exactly.
+#[contracttype]
+pub struct BalanceV1 { pub amount: i128 }
+
+// New type with an added field.
+#[contracttype]
+pub struct Balance { pub amount: i128, pub frozen: bool }
+
+#[contracttype]
+pub enum StorageKey {
+ Balance(Address),
+ BalanceVersion(Address),
+}
+
+fn get_balance(e: &Env, account: &Address) -> Balance {
+ let version: u32 = e.storage().persistent()
+ .get(&StorageKey::BalanceVersion(account.clone()))
+ .unwrap_or(1);
+
+ match version {
+ 1 => {
+ let v1: BalanceV1 = e.storage().persistent()
+ .get(&StorageKey::Balance(account.clone())).unwrap();
+ let migrated = Balance { amount: v1.amount, frozen: false };
+ set_balance(e, account, &migrated);
+ migrated
}
+ _ => e.storage().persistent()
+ .get(&StorageKey::Balance(account.clone())).unwrap(),
}
+}
+
+fn set_balance(e: &Env, account: &Address, balance: &Balance) {
+ e.storage().persistent()
+ .set(&StorageKey::BalanceVersion(account.clone()), &2u32);
+ e.storage().persistent()
+ .set(&StorageKey::Balance(account.clone()), balance);
+}
+```
+
+### Pattern 3: Enum Wrapper (Plan-Ahead)
- fn _migrate(e: &Env, data: &Self::MigrationData) {
- e.storage().instance().set(&symbol_short!("DATA_KEY"), data);
+For contracts that anticipate future migrations from the start, wrap stored data in a versioned enum. Soroban serializes enum variants as `(tag, data)`, so the host can distinguish between versions without trapping.
+
+```rust
+#[contracttype]
+pub enum ConfigEntry {
+ V1(ConfigV1),
+}
+
+// Store wrapped from day one:
+e.storage().instance().set(&key, &ConfigEntry::V1(config));
+```
+
+When v2 comes, add a variant and a converter:
+
+```rust
+#[contracttype]
+pub enum ConfigEntry {
+ V1(ConfigV1),
+ V2(ConfigV2),
+}
+
+impl ConfigEntry {
+ pub fn into_latest(self) -> ConfigV2 {
+ match self {
+ ConfigEntry::V1(v1) => ConfigV2 { rate: v1.rate, active: true },
+ ConfigEntry::V2(v2) => v2,
+ }
}
}
```
+
+This pattern cannot work retroactively — reading old bare-struct data as an enum would trap.
+
+
If a rollback is required, the contract can be upgraded to a newer version where the rollback-specific logic
is defined and performed as a migration.
-#### Atomic upgrade and migration
+## Atomic Upgrade and Migration
When performing an upgrade, the new implementation only becomes effective after the current invocation completes.
This means that if migration logic is included in the new implementation, it cannot be executed within the same
diff --git a/src/navigation/stellar.json b/src/navigation/stellar.json
index 0e03182c..be1994a8 100644
--- a/src/navigation/stellar.json
+++ b/src/navigation/stellar.json
@@ -90,6 +90,16 @@
"type": "folder",
"name": "Governance",
"children": [
+ {
+ "type": "page",
+ "name": "Governor",
+ "url": "/stellar-contracts/governance/governor"
+ },
+ {
+ "type": "page",
+ "name": "Votes",
+ "url": "/stellar-contracts/governance/votes"
+ },
{
"type": "page",
"name": "Timelock Controller",
@@ -145,11 +155,6 @@
"name": "Fee Abstraction",
"url": "/stellar-contracts/fee-abstraction"
},
- {
- "type": "page",
- "name": "Changelog",
- "url": "/stellar-contracts/changelog"
- },
{
"type": "separator",
"name": "Open Source Tools"