Soroban smart contracts for CosmosVote — decentralized on-chain governance and voting on the Stellar blockchain.
CosmosVote enables DAOs, protocols, and communities to create proposals, cast token-weighted votes, enforce quorum, and execute decisions — all transparently on-chain with an immutable audit trail.
- Project Overview
- Architecture.
- Features
- Quick Start
- Project Structure
- Governance Contract Reference
- Token Contract Reference
- Proposal Lifecycle
- Storage & Data Structures
- Configuration
- Development
- Testing
- Security
- Contributing
- Resources
Decentralized governance is critical for DAOs, protocols, and communities to make collective decisions transparently and fairly. CosmosVote provides a production-ready governance system on Stellar's Soroban platform with:
- Token-weighted voting — voting power proportional to economic stake
- Quorum enforcement — minimum participation thresholds
- Immutable audit trail — all votes and decisions recorded on-chain
- Flexible proposal lifecycle — from creation through execution or cancellation
- Cost-efficient storage — optimized for Soroban's tiered storage model
┌─────────────────────────────────────────────────────────────┐
│ CosmosVote System │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ Governance Contract │ │ Token Contract │ │
│ ├──────────────────────┤ ├──────────────────────┤ │
│ │ • Proposals │ │ • Balances │ │
│ │ • Voting │◄─────┤ • Transfers │ │
│ │ • Finalization │ │ • Mint/Burn │ │
│ │ • Execution │ │ • Allowances │ │
│ │ • Cancellation │ │ • Admin Control │ │
│ └──────────────────────┘ └──────────────────────┘ │
│ │ │ │
│ └──────────────┬───────────────┘ │
│ │ │
│ ┌─────▼──────┐ │
│ │ Soroban │ │
│ │ Blockchain │ │
│ │ (Stellar) │ │
│ └────────────┘ │
└─────────────────────────────────────────────────────────────┘
The user flow between the frontend, wallet, Soroban RPC, and smart contracts is documented below. This flow is also available as a dedicated Mermaid diagram in docs/frontend-contract-flow.md.
sequenceDiagram
participant User
participant Frontend
participant Wallet
participant SorobanRPC
participant Contract
User->>Frontend: open app
Frontend->>Wallet: request wallet connect
Wallet-->>Frontend: wallet connected / account authorized
Frontend->>SorobanRPC: fetch proposals
SorobanRPC-->>Frontend: proposals list
User->>Frontend: select proposal and cast vote
Frontend->>Wallet: request transaction signature
Wallet-->>Frontend: signed transaction
Frontend->>SorobanRPC: submit transaction
SorobanRPC->>Contract: invoke voting contract
Contract-->>SorobanRPC: transaction result
SorobanRPC-->>Frontend: confirmation
Frontend->>User: display confirmation
| Decision | Approach |
|---|---|
| Voting model | Token-weighted — vote weight = balance at vote time |
| Vote types | Yes / No / Abstain (abstain counts toward quorum, not outcome) |
| Double-vote prevention | Persistent HasVoted flag per (proposal, voter) |
| Storage tiers | Instance for config, Persistent for proposals/votes, Temporary for allowances |
| Events | Every state transition emits an on-chain event |
| Tie handling | Tie (yes == no) results in rejection |
| Admin vote restriction | When restrict_admin_vote=true, admin cannot vote on any proposal |
- Proposals — create governance proposals with title, description, quorum, and voting duration
- Token-weighted voting — vote weight equals the voter's governance token balance
- Yes / No / Abstain — three-way vote with quorum and majority enforcement
- Double-vote prevention — each address can vote exactly once per proposal
- Vote delegation — token holders can delegate voting power to a representative without transferring tokens
- Lifecycle management — Active → Passed/Rejected → Executed, or Cancelled by admin
- On-chain events — every action emits a verifiable event for off-chain indexers
- Admin controls — pause/unpause, update quorum, transfer admin privileges
- Proposal cooldown — optional rate limiting per proposer
- Minimum balance requirement — optional minimum tokens to create proposals
- Rust 1.75+ with
wasm32-unknown-unknowntarget. - Stellar CLI (optional, for deployment)
- Docker & Docker Compose (optional).
# Clone the repository
git clone https://github.com/PrincessnJoy/cosmosvote.git
cd cosmosvote
# Add WASM target
rustup target add wasm32-unknown-unknown
# Run tests
make test
# Build WASM binaries
make build
# View documentation
cargo doc --no-deps --opencosmosvote/
├── contracts/
│ ├── governance/ # Governance contract
│ │ ├── src/
│ │ │ ├── lib.rs # Main contract implementation
│ │ │ ├── storage.rs # Storage accessors & tier strategy
│ │ │ ├── events.rs # Event emission
│ │ │ ├── types.rs # Error types & data structures
│ │ │ ├── test.rs # Unit tests (40+ tests)
│ │ │ ├── test_helpers.rs # Test utilities
│ │ │ └── prop_tests.rs # Property-based tests
│ │ └── Cargo.toml
│ │
│ └── token/ # Token contract
│ ├── src/
│ │ ├── lib.rs # Token implementation
│ │ ├── storage.rs # Storage accessors
│ │ ├── events.rs # Event emission
│ │ ├── types.rs # Error types & data structures
│ │ └── test.rs # Unit tests (20+ tests)
│ └── Cargo.toml
│
├── docs/
│ ├── adr/ # Architecture Decision Records
│ ├── security/ # Security documentation
│ ├── examples/ # Integration examples
│ ├── GETTING_STARTED.md
│ ├── lifecycle.md
│ ├── storage.md
│ ├── errors.md
│ ├── faq.md
│ └── runbook.md
│
├── scripts/
│ ├── deploy.sh # Deploy to local/testnet
│ ├── deploy_mainnet.sh # Deploy to mainnet
│ └── test_wasm.sh # Test WASM builds
│
├── config/
│ ├── local.toml
│ ├── testnet.toml
│ └── mainnet.toml
│
├── notification-service/ # Off-chain notification service
│ ├── src/
│ │ ├── index.ts # CLI entry point
│ │ ├── watcher.ts # Horizon event poller
│ │ ├── notifier.ts # Email & webhook dispatch
│ │ ├── subscriptions.ts # Subscription management
│ │ └── types.ts # Shared types
│ ├── .env.example
│ ├── package.json
│ └── tsconfig.json
│
├── frontend/ # React + Vite proposal browser
├── Cargo.toml # Workspace manifest
├── Makefile
├── Dockerfile
├── docker-compose.yml
├── .env.example
├── CONTRIBUTING.md
├── SECURITY.md
├── AUDIT.md
├── CHANGELOG.md
└── README.md
pub fn initialize(
env: Env,
admin: Address,
voting_token: Address,
min_proposal_balance: i128,
proposal_cooldown: u64,
min_quorum_bps: u32,
restrict_admin_vote: bool,
) -> Result<(), ContractError>pub fn create_proposal(
env: Env,
proposer: Address,
title: String, // 1–128 chars
description: String, // 1–1024 chars
quorum: i128, // > 0, <= total supply
duration: u64, // 60–2,592,000 seconds
payload: Option<ExecutionPayload>, // Optional on-chain action
) -> Result<u64, ContractError>pub fn cast_vote(
env: Env,
voter: Address,
proposal_id: u64,
vote: Vote, // Yes | No | Abstain
) -> Result<(), ContractError>pub fn finalise(env: Env, proposal_id: u64) -> Result<(), ContractError>Pass conditions: total_votes >= quorum AND votes_yes > votes_no
The finalise() function is permissionless and is intended to be called by an off-chain keeper or bot after the voting period ends. This ensures proposals do not stay stuck in Active state even if no single voter submits the finalization transaction.
pub fn execute(env: Env, admin: Address, proposal_id: u64) -> Result<(), ContractError>
pub fn cancel(env: Env, admin: Address, proposal_id: u64) -> Result<(), ContractError>// Update the governance token address (only if no active proposals)
pub fn update_voting_token(env: Env, admin: Address, new_token: Address) -> Result<(), ContractError>
// Update quorum for an active proposal
pub fn update_quorum(env: Env, admin: Address, proposal_id: u64, new_quorum: i128) -> Result<(), ContractError>
// Two-step admin transfer
pub fn transfer_admin(env: Env, admin: Address, new_admin: Address) -> Result<(), ContractError>
pub fn accept_admin(env: Env, pending_admin: Address) -> Result<(), ContractError>The CosmosVote token contract implements the Stellar Enhancement Proposal 41 (SEP-41) standard for token contracts on Soroban. This ensures wallet and explorer compatibility for token discovery, display, and transfer operations.
The token contract stores all balances and the total supply as i128. The maximum representable value is i128::MAX = 170_141_183_460_469_231_731_687_303_715_884_105_727. Mint operations use checked_add to enforce this bound — any mint that would exceed i128::MAX returns ArithmeticOverflow. In practice, governance vote tallies are bounded by total supply, making arithmetic overflow in cast_vote impossible for any valid token supply.
pub fn initialize(
env: Env,
admin: Address,
initial_supply: i128,
name: String,
symbol: String,
decimals: u32
) -> Result<(), ContractError>Parameters:
admin— Receives initial supply and admin privilegesinitial_supply— Total tokens minted to adminname— Human-readable token name (e.g., "CosmosVote")symbol— Ticker symbol (e.g., "VOTE")decimals— Number of decimal places (typically 7 for Stellar)
pub fn transfer(env: Env, from: Address, to: Address, amount: i128) -> Result<(), ContractError>
pub fn mint(env: Env, admin: Address, to: Address, amount: i128) -> Result<(), ContractError>
pub fn burn(env: Env, admin: Address, from: Address, amount: i128) -> Result<(), ContractError>
pub fn burn_self(env: Env, owner: Address, amount: i128) -> Result<(), ContractError>
pub fn approve(env: Env, owner: Address, spender: Address, amount: i128) -> Result<(), ContractError>
pub fn transfer_from(env: Env, spender: Address, from: Address, to: Address, amount: i128) -> Result<(), ContractError>pub fn name(env: Env) -> String // Token name
pub fn symbol(env: Env) -> String // Ticker symbol
pub fn decimals(env: Env) -> u32 // Decimal places
pub fn total_supply(env: Env) -> i128 // Total supply
pub fn balance(env: Env, owner: Address) -> i128 // Account balancepub fn delegate(env: Env, owner: Address, delegate_to: Address) -> Result<(), ContractError>
pub fn undelegate(env: Env, owner: Address) -> Result<(), ContractError>
pub fn get_delegation(env: Env, owner: Address) -> Option<Address>
pub fn get_delegated_weight(env: Env, voter: Address, delegators: Vec<Address>) -> i128See docs/delegation.md for the full delegation model and behavior.
┌──────────────┐
│ Active │
└──────────────┘
│
┌──────────┼──────────┐
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌──────────┐
│ Passed │ │Rejected│ │Cancelled │
└────────┘ └────────┘ └──────────┘
│
▼
┌──────────┐
│ Executed │
└──────────┘
| Transition | Trigger | Caller | Condition |
|---|---|---|---|
| Active → Passed | finalise() |
Anyone | total_votes >= quorum AND yes > no |
| Active → Rejected | finalise() |
Anyone | quorum not met OR yes <= no |
| Active → Cancelled | cancel() |
Admin | — |
| Passed → Executed | execute() |
Admin | — |
| Key | Type | Purpose |
|---|---|---|
Admin |
Address |
Admin address |
VotingToken |
Address |
Governance token address |
ProposalCount |
u64 |
Monotonic proposal ID counter |
ActiveProposalCount |
u64 |
Current number of non-terminal proposals |
MinProposalBalance |
i128 |
Minimum balance to propose |
ProposalCooldown |
u64 |
Seconds between proposals |
RestrictAdminVote |
bool |
Admin vote restriction flag |
Paused |
bool |
Contract pause state |
| Key | Type | Purpose |
|---|---|---|
Proposal(id) |
Proposal |
Full proposal state |
HasVoted(id, voter) |
bool |
Double-vote guard |
VoteRecord(id, voter) |
VoteRecord |
Vote type + weight |
LastProposal(proposer) |
u64 |
Cooldown timestamp |
cp .env.example .env
# Edit .env with your valuesKey variables: NETWORK, STELLAR_RPC_URL, STELLAR_SECRET_KEY, GOVERNANCE_CONTRACT_ID, TOKEN_CONTRACT_ID.
The restrict_admin_vote parameter passed to initialize controls a narrow voting restriction on the admin:
false(default): The admin can vote on any proposal, including ones they created.true: The admin is blocked from voting only on proposals that the admin themselves created. The admin can still vote freely on proposals created by other addresses.
This is intentionally scoped — it prevents a conflict of interest when the admin is also the proposer, without removing the admin's ability to participate in governance generally.
Note: This behavior is tracked in issue #14, which documents the ambiguity in the original specification. The current implementation blocks admin voting only when
voter == admin && proposal.proposer == admin.
Example:
// Admin creates a proposal — admin CANNOT vote on it when restrict_admin_vote = true
gov.initialize(&admin, &token_id, &0, &0, &0, &true);
let id = gov.create_proposal(&admin, ...);
gov.cast_vote(&admin, &id, &Vote::Yes); // → Err(AdminVoteRestricted)
// Admin votes on a proposal created by someone else — this is ALLOWED
let id2 = gov.create_proposal(&other_user, ...);
gov.cast_vote(&admin, &id2, &Vote::Yes); // → Ok(())The Dockerfile uses a multi-stage build to keep the final image small and free of build tooling:
| Stage | Base image | Purpose |
|---|---|---|
builder |
rust:1.75-slim-bookworm (pinned to digest) |
Compiles WASM binaries |
runtime |
debian:bookworm-slim (pinned to digest) |
Ships only *.wasm artifacts + Stellar CLI |
Both base images are pinned to a specific digest for reproducible builds.
Build the runtime image (WASM artifacts only):
docker build --target runtime -t cosmosvote:latest .Build only the builder stage (useful for running tests in CI):
docker build --target builder -t cosmosvote:builder .
docker run --rm cosmosvote:builder make testRun the dev environment via Docker Compose:
# Start a dev shell (builder stage — full Rust toolchain)
docker compose up
docker compose run --rm dev make test
docker compose run --rm dev make build
# Build the minimal runtime image (WASM artifacts only)
docker compose --profile artifacts build artifacts
# Or build directly with Docker
docker build --target builder -t cosmosvote:builder . # dev / CI
docker build --target runtime -t cosmosvote:runtime . # production artifact imagerustup target add wasm32-unknown-unknown
make test
make build
make lint| Target | Description |
|---|---|
make test |
Run all tests |
make build |
Build WASM binaries |
make lint |
Run Clippy |
make fmt |
Format code |
make clean |
Remove build artifacts |
make ci |
Full CI check |
make test # All unit and integration tests
make test-verbose # Tests with output
cargo test -p cosmosvote-governance # Governance contract only
cargo test -p cosmosvote-token # Token contract only
cargo test --test integration_tests # Integration tests only
cargo test prop_ # Property-based testsEnd-to-end integration tests verify the full proposal lifecycle with real contract interactions between the governance and token contracts. Tests cover:
- Full pass lifecycle: Proposal creation → voting → finalization → execution
- Full reject lifecycle: Proposals that fail to meet quorum or vote threshold
- Cancel lifecycle: Admin-cancelled proposals
- Voting power: Verification that voting power correctly reflects token balances
- Quorum enforcement: Proposals are rejected if quorum is not met
Run integration tests with:
make test-integrationSee SECURITY.md for the vulnerability disclosure policy and docs/security/ for the full threat model.
The contract includes a pause mechanism for emergency response. When the contract is paused:
- Blocked:
create_proposal,cast_vote,finalise. - Allowed:
execute,cancel,unpause,transfer_admin,accept_admin,update_quorum.
Key security properties:
require_auth()on all state-changing operations- Double-vote prevention via persistent
HasVotedflag - Arithmetic overflow protection via
checked_add - Contract pause mechanism for emergency response
- One-time initialization guard
See CONTRIBUTING.md. Quick checklist:
- Fork → feature branch → changes →
make test→make lint→ PR
- Stellar Documentation
- Soroban Documentation
- Soroban SDK
- SEP-41 Token Standard
- Architecture Decision Records
- Security Documentation
- Notification Service
Apache 2.0 — see LICENSE.
Built with ❤️ on Stellar