Skip to content

DeveloperEmmy/votechain-contracts

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

178 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

VoteChain Contracts

CI Coverage License: Apache 2.0

Soroban smart contracts for VoteChain — decentralized on-chain governance and voting on the Stellar blockchain.

VoteChain 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.


Table of Contents


Project Overview

Motivation

Decentralized governance is critical for DAOs, protocols, and communities to make collective decisions transparently and fairly. VoteChain provides a production-ready, audited 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

What VoteChain Solves

  • Centralized governance risk — decisions made transparently on-chain, not by a central authority
  • Vote manipulation — double-vote prevention, balance snapshots, and immutable vote records
  • Governance token management — SEP-41-compatible token contract with mint/burn/transfer
  • Proposal tracking — full lifecycle from creation to execution with state transitions

Architecture

System Overview

VoteChain consists of two complementary Soroban smart contracts:

┌─────────────────────────────────────────────────────────────┐
│                    VoteChain System                          │
├─────────────────────────────────────────────────────────────┤
│                                                               │
│  ┌──────────────────────┐      ┌──────────────────────┐     │
│  │  Governance Contract │      │   Token Contract     │     │
│  ├──────────────────────┤      ├──────────────────────┤     │
│  │ • Proposals          │      │ • Balances           │     │
│  │ • Voting             │◄─────┤ • Transfers          │     │
│  │ • Finalization       │      │ • Mint/Burn          │     │
│  │ • Execution          │      │ • Allowances         │     │
│  │ • Cancellation       │      │ • Admin Control      │     │
│  └──────────────────────┘      └──────────────────────┘     │
│           │                              │                   │
│           └──────────────┬───────────────┘                   │
│                          │                                    │
│                    ┌─────▼──────┐                            │
│                    │ Soroban    │                            │
│                    │ Blockchain │                            │
│                    │ (Stellar)  │                            │
│                    └────────────┘                            │
│                                                               │
└─────────────────────────────────────────────────────────────┘

Key Design Decisions

1. Token-Weighted Voting (ADR-002)

  • Vote weight = voter's token balance at vote time
  • Aligns voting power with economic stake
  • Standard model for DAO governance

2. Live Balance Snapshots (ADR-003)

  • Each voter's balance captured at vote time and stored immutably
  • Prevents balance manipulation attacks (e.g., transfer-then-vote)
  • Storage cost proportional to participation, not total holders

3. Three-Way Voting (ADR-004)

  • Yes / No / Abstain votes
  • Abstain counts toward quorum but not outcome
  • Tie (yes == no) results in rejection

4. Storage Tier Optimization (ADR-006)

  • Instance storage — frequently-read config (Admin, VotingToken, ProposalCount, etc.)
    • Loaded once per call; cheapest reads
  • Persistent storage — per-proposal/per-voter data (Proposal, HasVoted, VoteRecord, etc.)
    • Survives ledger expiry independently
  • Temporary storage — short-lived allowances in token contract
    • Expires naturally with ledger entry TTL

5. On-Chain Events (ADR-005)

  • All state transitions emit events
  • Enables off-chain indexing and auditability
  • Immutable audit trail

Data Flow

User creates proposal
    ↓
Proposal stored in persistent storage
    ↓
Voting period opens (Active state)
    ↓
Voters cast votes (balance snapshot captured)
    ↓
Voting period ends
    ↓
Anyone calls finalise()
    ↓
Outcome determined (Passed/Rejected)
    ↓
Admin executes or cancels
    ↓
Proposal reaches terminal state

Features

  • 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 weight snapshots — balance captured at vote time, preventing manipulation
  • 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

Quick Start

Prerequisites

  • Rust 1.75+ with wasm32-unknown-unknown target
  • Stellar CLI (optional, for deployment)
  • Docker & Docker Compose (optional, for reproducible environment)

Installation & Testing

# Clone the repository
git clone https://github.com/Vera3289/votechain-contracts.git
cd votechain-contracts

# Add WASM target
rustup target add wasm32-unknown-unknown

# Run tests
make test

# Build WASM binaries
make build

# View generated documentation
cargo doc --no-deps --open

Verify Installation

# Check Rust version
rustc --version

# Check WASM target
rustup target list | grep wasm32-unknown-unknown

# Run a quick test
make test 2>&1 | head -20

Project Structure

votechain-contracts/
├── 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
│   │   ├── test_snapshots/           # Regression test snapshots
│   │   └── 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)
│       ├── test_snapshots/           # Regression test snapshots
│       └── Cargo.toml
│
├── docs/
│   ├── adr/                          # Architecture Decision Records
│   │   ├── ADR-001-stellar-soroban-platform.md
│   │   ├── ADR-002-token-weighted-voting.md
│   │   ├── ADR-003-live-balance-over-snapshot.md
│   │   ├── ADR-004-three-way-vote.md
│   │   ├── ADR-005-on-chain-events.md
│   │   └── ADR-006-instance-vs-persistent-storage.md
│   ├── security/
│   │   ├── threat-model.md
│   │   ├── known-issues.md
│   │   ├── audit-scope.md
│   │   ├── SEC-008-token-balance-fetch-audit.md
│   │   ├── SEC-009-reinit-guard.md
│   │   └── SEC-010-reentrancy-cast-vote.md
│   ├── examples/
│   │   ├── rust.md                   # Rust integration examples
│   │   └── javascript.md             # JavaScript/TypeScript examples
│   ├── GETTING_STARTED.md
│   ├── lifecycle.md
│   ├── storage.md
│   ├── upgrading.md
│   ├── errors.md
│   ├── faq.md
│   └── roadmap.md
│
├── scripts/
│   ├── deploy.sh                     # Deploy to local/testnet
│   ├── deploy_mainnet.sh             # Deploy to mainnet
│   └── test_wasm.sh                  # Test WASM builds
│
├── config/
│   ├── local.toml                    # Local node config
│   ├── testnet.toml                  # Testnet config
│   └── mainnet.toml                  # Mainnet config
│
├── Cargo.toml                        # Workspace manifest
├── Cargo.lock
├── Makefile                          # Build & test targets
├── Dockerfile                        # Dev container
├── docker-compose.yml                # Local environment
├── .env.example                      # Environment template
├── CONTRIBUTING.md                   # Contribution guidelines
├── SECURITY.md                       # Security policy
├── AUDIT.md                          # Audit reports
├── CHANGELOG.md                      # Version history
└── README.md                         # This file

Governance Contract Reference

Overview

The governance contract manages the complete proposal lifecycle: creation, voting, finalization, execution, and cancellation. It enforces quorum thresholds, prevents double-voting, and maintains an immutable audit trail of all decisions.

Initialization

pub fn initialize(
    env: Env,
    admin: Address,
    voting_token: Address,
    min_proposal_balance: i128,
    proposal_cooldown: u64,
    restrict_admin_vote: bool,
) -> Result<(), ContractError>

Parameters:

  • admin — Address with privileged operations (execute, cancel, pause)
  • voting_token — Address of the governance token contract (must be SEP-41 compatible)
  • min_proposal_balance — Minimum token balance required to create proposals (0 = no minimum)
  • proposal_cooldown — Seconds between proposals per address (0 = no cooldown)
  • restrict_admin_vote — If true, admin cannot vote on proposals they created

Errors:

  • AlreadyInitialized — Contract already initialized

Example:

let admin = Address::from_string("GXXXXX...");
let token = Address::from_string("GYYYYY...");

GovernanceContract::initialize(
    env,
    admin,
    token,
    1_000_000,  // min 1M tokens to propose
    86_400,     // 1 day cooldown
    true,       // restrict admin voting
)?;

Proposal Management

Create Proposal

pub fn create_proposal(
    env: Env,
    proposer: Address,
    title: String,
    description: String,
    quorum: i128,
    duration: u64,
) -> Result<u64, ContractError>

Parameters:

  • proposer — Address creating the proposal (must have sufficient balance)
  • title — Proposal title (1-128 characters)
  • description — Proposal description (1-1024 characters)
  • quorum — Minimum votes required to pass (must be > 0 and <= total supply)
  • duration — Voting period in seconds (60-2,592,000 = 1 minute to 30 days)

Returns: Proposal ID (u64)

Errors:

  • InvalidTitle — Title empty or exceeds 128 characters
  • InvalidDescription — Description empty or exceeds 1024 characters
  • InvalidQuorum — Quorum is zero or negative
  • QuorumExceedsSupply — Quorum exceeds total token supply
  • InvalidDurationRange — Duration outside [60, 2,592,000] seconds
  • InsufficientBalance — Proposer balance below minimum
  • ProposalCooldown — Proposer within cooldown period
  • ContractPaused — Contract is paused

Example:

let proposal_id = GovernanceContract::create_proposal(
    env,
    proposer,
    String::from_slice(&env, "Increase Treasury"),
    String::from_slice(&env, "Allocate 10M tokens to treasury"),
    5_000_000,  // 5M token quorum
    604_800,    // 7 days
)?;

Get Proposal

pub fn get_proposal(env: Env, proposal_id: u64) -> Result<Proposal, ContractError>

Returns: Full proposal state including votes, status, and metadata

Errors:

  • ProposalNotFound — Proposal ID does not exist

Proposal Structure:

pub struct Proposal {
    pub id: u64,
    pub proposer: Address,
    pub title: String,
    pub description: String,
    pub votes_yes: i128,
    pub votes_no: i128,
    pub votes_abstain: i128,
    pub quorum: i128,
    pub start_time: u64,
    pub end_time: u64,
    pub state: ProposalState,  // Active, Passed, Rejected, Executed, Cancelled
}

Get Proposal Count

pub fn proposal_count(env: Env) -> u64

Returns: Total number of proposals created


Voting

Cast Vote

pub fn cast_vote(
    env: Env,
    voter: Address,
    proposal_id: u64,
    vote: Vote,
) -> Result<(), ContractError>

Parameters:

  • voter — Address casting the vote (must have token balance)
  • proposal_id — ID of the proposal to vote on
  • vote — Vote type: Yes, No, or Abstain

Vote Weight: Voter's token balance at vote time (captured and stored immutably)

Errors:

  • ProposalNotFound — Proposal does not exist
  • ProposalNotActive — Proposal is not in Active state
  • VotingNotStarted — Voting period has not started
  • VotingPeriodEnded — Voting period has ended
  • AlreadyVoted — Voter has already voted on this proposal
  • NoVotingPower — Voter has zero token balance
  • AdminVoteRestricted — Admin cannot vote on own proposals (if enabled)
  • ContractPaused — Contract is paused

Example:

GovernanceContract::cast_vote(
    env,
    voter,
    proposal_id,
    Vote::Yes,
)?;

Check if Voted

pub fn has_voted(env: Env, proposal_id: u64, voter: Address) -> bool

Returns: True if voter has cast a vote on this proposal

Get Vote Record

pub fn get_vote(
    env: Env,
    proposal_id: u64,
    voter: Address,
) -> Result<VoteRecord, ContractError>

Returns: Vote type and weight (balance snapshot)

VoteRecord Structure:

pub struct VoteRecord {
    pub vote: Vote,
    pub weight: i128,  // Balance snapshot at vote time
}

Finalization & Execution

Finalize Proposal

pub fn finalise(env: Env, proposal_id: u64) -> Result<(), ContractError>

Behavior:

  • Can only be called after voting period ends (ledger_timestamp > end_time)
  • Determines outcome based on pass conditions
  • Transitions proposal to Passed or Rejected state
  • Emits finalization event

Pass Conditions:

total_votes = votes_yes + votes_no + votes_abstain

Passed   if total_votes >= quorum  AND  votes_yes > votes_no
Rejected otherwise

Notes:

  • Abstain votes count toward quorum but not outcome
  • Tie (yes == no) results in rejection
  • Anyone can call this function

Errors:

  • ProposalNotFound — Proposal does not exist
  • ProposalNotActive — Proposal is not in Active state
  • VotingStillOpen — Voting period has not ended yet

Example:

GovernanceContract::finalise(env, proposal_id)?;

Execute Proposal

pub fn execute(env: Env, admin: Address, proposal_id: u64) -> Result<(), ContractError>

Behavior:

  • Marks a passed proposal as executed
  • Admin-only operation
  • Emits execution event

Errors:

  • ProposalNotFound — Proposal does not exist
  • ProposalNotPassed — Proposal is not in Passed state
  • NotAdmin — Caller is not the admin

Example:

GovernanceContract::execute(env, admin, proposal_id)?;

Cancel Proposal

pub fn cancel(env: Env, admin: Address, proposal_id: u64) -> Result<(), ContractError>

Behavior:

  • Cancels an active proposal
  • Admin-only operation
  • Emits cancellation event

Errors:

  • ProposalNotFound — Proposal does not exist
  • ProposalNotActive — Proposal is not in Active state
  • NotAdmin — Caller is not the admin

Example:

GovernanceContract::cancel(env, admin, proposal_id)?;

Admin Operations

Update Quorum

pub fn update_quorum(
    env: Env,
    admin: Address,
    proposal_id: u64,
    new_quorum: i128,
) -> Result<(), ContractError>

Behavior:

  • Adjusts quorum threshold on an active proposal
  • Admin-only operation
  • Can be called multiple times before finalization

Errors:

  • ProposalNotFound — Proposal does not exist
  • ProposalNotActive — Proposal is not in Active state
  • InvalidQuorum — New quorum is zero or negative
  • QuorumExceedsSupply — New quorum exceeds total supply
  • NotAdmin — Caller is not the admin

Transfer Admin

pub fn transfer_admin(
    env: Env,
    admin: Address,
    new_admin: Address,
) -> Result<(), ContractError>

Behavior:

  • Transfers admin privileges to a new address
  • Current admin-only operation
  • Emits admin transfer event

Errors:

  • NotAdmin — Caller is not the admin
  • InvalidNewAdmin — New admin address is invalid

Pause / Unpause

pub fn pause(env: Env, admin: Address) -> Result<(), ContractError>
pub fn unpause(env: Env, admin: Address) -> Result<(), ContractError>

Behavior:

  • Pause blocks all state-changing operations (create_proposal, cast_vote, etc.)
  • Read-only operations (get_proposal, has_voted) remain available
  • Admin-only operation

Errors:

  • NotAdmin — Caller is not the admin
  • ContractPaused — Already paused (for pause)
  • NotPaused — Not paused (for unpause)

Token Contract Reference

Overview

The token contract implements a SEP-41-compatible governance token with standard ERC-20-style operations: balances, transfers, mint, burn, and spending allowances.

Initialization

pub fn initialize(
    env: Env,
    admin: Address,
    initial_supply: i128,
) -> Result<(), ContractError>

Parameters:

  • admin — Address receiving initial supply and admin privileges
  • initial_supply — Total tokens minted to admin at initialization

Behavior:

  • Mints entire initial supply to admin
  • Sets admin address
  • Sets version to (1, 0, 0)

Errors:

  • AlreadyInitialized — Contract already initialized

Example:

TokenContract::initialize(
    env,
    admin,
    1_000_000_000,  // 1 billion tokens
)?;

Balance Queries

Total Supply

pub fn total_supply(env: Env) -> i128

Returns: Aggregate token supply across all holders

Balance

pub fn balance(env: Env, owner: Address) -> i128
pub fn balance_of(env: Env, owner: Address) -> i128

Parameters:

  • owner — Address to query

Returns: Token balance (0 if address has never held tokens)

Example:

let balance = TokenContract::balance(env, voter)?;

Transfers

Transfer

pub fn transfer(
    env: Env,
    from: Address,
    to: Address,
    amount: i128,
) -> Result<(), ContractError>

Parameters:

  • from — Sender address (must authorize the call)
  • to — Recipient address
  • amount — Number of tokens to transfer (must be positive)

Behavior:

  • Requires authorization from from
  • Transfers are atomic
  • Transfer to self is a no-op (auth still required)

Errors:

  • InvalidAmount — Amount is zero or negative
  • InsufficientBalance — Sender has fewer tokens than amount

Example:

TokenContract::transfer(env, from, to, 1_000_000)?;

Transfer From

pub fn transfer_from(
    env: Env,
    spender: Address,
    from: Address,
    to: Address,
    amount: i128,
) -> Result<(), ContractError>

Parameters:

  • spender — Address authorized to spend (must authorize the call)
  • from — Token owner
  • to — Recipient
  • amount — Number of tokens to transfer

Behavior:

  • Requires authorization from spender
  • Deducts from spender's allowance
  • Transfers from from's balance

Errors:

  • InvalidAmount — Amount is zero or negative
  • InsufficientBalance — Owner has fewer tokens than amount
  • AllowanceExceeded — Spender allowance is insufficient

Allowances

Approve

pub fn approve(
    env: Env,
    owner: Address,
    spender: Address,
    amount: i128,
) -> Result<(), ContractError>

Parameters:

  • owner — Token owner (must authorize the call)
  • spender — Address to grant spending rights
  • amount — Maximum tokens spender can transfer

Behavior:

  • Requires authorization from owner
  • Overwrites previous allowance
  • Allowances stored in temporary storage (expire with ledger entry TTL)

Errors:

  • InvalidAmount — Amount is negative

Example:

TokenContract::approve(env, owner, spender, 1_000_000)?;

Mint & Burn

Mint

pub fn mint(
    env: Env,
    admin: Address,
    to: Address,
    amount: i128,
) -> Result<(), ContractError>

Parameters:

  • admin — Admin address (must authorize the call)
  • to — Recipient of new tokens
  • amount — Number of tokens to mint (must be positive)

Behavior:

  • Admin-only operation
  • Increases total supply
  • Emits mint event

Errors:

  • NotAdmin — Caller is not the admin
  • InvalidAmount — Amount is zero or negative

Burn

pub fn burn(
    env: Env,
    admin: Address,
    from: Address,
    amount: i128,
) -> Result<(), ContractError>

Parameters:

  • admin — Admin address (must authorize the call)
  • from — Address to burn tokens from
  • amount — Number of tokens to burn (must be positive)

Behavior:

  • Admin-only operation
  • Decreases total supply
  • Emits burn event

Errors:

  • NotAdmin — Caller is not the admin
  • InvalidAmount — Amount is zero or negative
  • InsufficientBalance — Address has fewer tokens than amount

Transfer Admin

pub fn transfer_admin(
    env: Env,
    admin: Address,
    new_admin: Address,
) -> Result<(), ContractError>

Behavior:

  • Transfers admin privileges to a new address
  • Current admin-only operation
  • Emits admin transfer event

Errors:

  • NotAdmin — Caller is not the admin
  • InvalidNewAdmin — New admin address is invalid

Usage Examples

Rust Integration

Complete Rust examples for integrating VoteChain into your application:

use soroban_sdk::{Address, Env, String};

// Initialize governance contract
let governance = GovernanceContract::initialize(
    env,
    admin,
    token_address,
    1_000_000,  // min balance to propose
    86_400,     // 1 day cooldown
    true,       // restrict admin voting
)?;

// Create a proposal
let proposal_id = GovernanceContract::create_proposal(
    env,
    proposer,
    String::from_slice(&env, "Increase Treasury"),
    String::from_slice(&env, "Allocate 10M tokens"),
    5_000_000,  // 5M quorum
    604_800,    // 7 days
)?;

// Cast votes
GovernanceContract::cast_vote(env, voter1, proposal_id, Vote::Yes)?;
GovernanceContract::cast_vote(env, voter2, proposal_id, Vote::No)?;
GovernanceContract::cast_vote(env, voter3, proposal_id, Vote::Abstain)?;

// Check vote status
let has_voted = GovernanceContract::has_voted(env, proposal_id, voter1);
let vote_record = GovernanceContract::get_vote(env, proposal_id, voter1)?;

// Finalize after voting period
GovernanceContract::finalise(env, proposal_id)?;

// Execute if passed
let proposal = GovernanceContract::get_proposal(env, proposal_id)?;
if proposal.state == ProposalState::Passed {
    GovernanceContract::execute(env, admin, proposal_id)?;
}

See docs/examples/rust.md for more detailed examples.

JavaScript/TypeScript Integration

Complete JavaScript examples for integrating VoteChain into web applications:

import { Address, Contract } from '@stellar/js-stellar-sdk';

// Connect to governance contract
const governance = new Contract(governanceAddress, governanceSpec);

// Create proposal
const result = await governance.methods
  .create_proposal(
    proposer,
    "Increase Treasury",
    "Allocate 10M tokens",
    5_000_000n,  // quorum
    604_800n     // 7 days
  )
  .simulate(server);

const proposalId = result.result.retval;

// Cast vote
await governance.methods
  .cast_vote(voter, proposalId, { tag: 'Yes' })
  .simulate(server);

// Check vote status
const hasVoted = await governance.methods
  .has_voted(proposalId, voter)
  .simulate(server);

// Finalize
await governance.methods
  .finalise(proposalId)
  .simulate(server);

See docs/examples/javascript.md for more detailed examples.


Proposal Lifecycle

State Diagram

                    ┌─────────────────────────────────┐
                    │                                 │
                    ▼                                 │
            ┌──────────────┐                         │
            │   Active     │◄────────────────────────┘
            └──────────────┘
                    │
        ┌───────────┼───────────┐
        │           │           │
        ▼           ▼           ▼
    ┌────────┐ ┌────────┐ ┌──────────┐
    │ Passed │ │Rejected│ │Cancelled │
    └────────┘ └────────┘ └──────────┘
        │
        ▼
    ┌──────────┐
    │ Executed │
    └──────────┘

State Transitions

From To Trigger Caller Condition
Active Passed finalise() Anyone total_votes >= quorum AND votes_yes > votes_no
Active Rejected finalise() Anyone total_votes < quorum OR votes_yes <= votes_no
Active Cancelled cancel() Admin None
Passed Executed execute() Admin None

Pass Conditions

A proposal passes when both conditions hold after the voting period ends:

total_votes = votes_yes + votes_no + votes_abstain

Passed   if total_votes >= quorum  AND  votes_yes > votes_no
Rejected otherwise

Key Points:

  • Abstain votes count toward quorum but not outcome
  • Tie (yes == no) results in rejection even if quorum is met
  • Voting period is immutable after proposal creation
  • Anyone can call finalise() after voting period ends

Example Lifecycle

1. Proposer creates proposal (ID: 1)
   - Title: "Increase Treasury"
   - Duration: 7 days
   - Quorum: 5M tokens
   - State: Active

2. Voting period (7 days)
   - Voter A votes Yes (weight: 2M)
   - Voter B votes No (weight: 1M)
   - Voter C votes Abstain (weight: 3M)
   - Total: 6M votes (meets 5M quorum)

3. After 7 days, anyone calls finalise()
   - total_votes = 6M >= quorum (5M) ✓
   - votes_yes (2M) > votes_no (1M) ✓
   - State: Passed

4. Admin calls execute()
   - State: Executed
   - Proposal is now complete

Storage & Data Structures

Governance Contract Storage

Instance Storage (contract-wide config, cheap reads):

Key Type Purpose
Admin Address Admin address (set once at init)
VotingToken Address Governance token address
ProposalCount u64 Monotonic proposal ID counter
MinProposalBalance i128 Minimum balance to create proposals
ProposalCooldown u64 Seconds between proposals per address
RestrictAdminVote bool Whether admin can vote on own proposals
Version (u32, u32, u32) Semantic version
ContractState ContractState Uninitialized or Ready
Paused bool Whether contract is paused

Persistent Storage (per-proposal/per-voter data, survives ledger expiry):

Key Type Purpose
Proposal(id) Proposal Full proposal state
HasVoted(id, voter) bool Deduplication flag
VoteRecord(id, voter) VoteRecord Vote type + weight
VoterSnapshot(id, voter) i128 Balance snapshot at vote time
LastProposal(proposer) u64 Proposer's last proposal timestamp

Token Contract Storage

Instance Storage (contract-wide singletons):

Key Type Purpose
Admin Address Admin address
TotalSupply i128 Aggregate token supply
Version (u32, u32, u32) Semantic version

Persistent Storage (per-address data):

Key Type Purpose
Balance(owner) i128 Token balance per address

Temporary Storage (short-lived, expires with ledger entry TTL):

Key Type Purpose
Allowance(owner, spender) i128 Spending allowance

Data Structures

Proposal:

pub struct Proposal {
    pub id: u64,
    pub proposer: Address,
    pub title: String,
    pub description: String,
    pub votes_yes: i128,
    pub votes_no: i128,
    pub votes_abstain: i128,
    pub quorum: i128,
    pub start_time: u64,
    pub end_time: u64,
    pub state: ProposalState,
}

ProposalState:

pub enum ProposalState {
    Active,
    Passed,
    Rejected,
    Executed,
    Cancelled,
}

Vote:

pub enum Vote {
    Yes,
    No,
    Abstain,
}

VoteRecord:

pub struct VoteRecord {
    pub vote: Vote,
    pub weight: i128,  // Balance snapshot at vote time
}

Configuration

Environment Setup

Copy .env.example to .env and configure for your environment:

cp .env.example .env

Environment Variables:

# Network selection
NETWORK=local              # local, testnet, or mainnet

# Stellar RPC endpoint
STELLAR_RPC_URL=http://localhost:8000

# Stellar network passphrase
STELLAR_NETWORK_PASSPHRASE="Standalone Network ; February 2021"

# Admin secret key (for deployment)
STELLAR_SECRET_KEY=SXXXXX...

# Deployed contract addresses (after deployment)
GOVERNANCE_CONTRACT_ID=CXXXXX...
TOKEN_CONTRACT_ID=CYYYYY...

Network Configuration

Configuration files in config/ directory:

Local Development (config/local.toml):

rpc_url = "http://localhost:8000"
network_passphrase = "Standalone Network ; February 2021"

Testnet (config/testnet.toml):

rpc_url = "https://soroban-testnet.stellar.org"
network_passphrase = "Test SDF Network ; September 2015"

Mainnet (config/mainnet.toml):

rpc_url = "https://soroban-mainnet.stellar.org"
network_passphrase = "Public Global Stellar Network ; September 2015"

Switching Environments

# Local (default)
./scripts/deploy.sh

# Testnet
NETWORK=testnet ./scripts/deploy.sh

# Mainnet
NETWORK=mainnet ./scripts/deploy.sh

Development

Docker Usage

A reproducible development environment is provided via Docker.

Prerequisites: Docker and Docker Compose installed.

Start the Full Environment

docker compose up

This starts two services:

  • dev — Rust + wasm32 + Stellar CLI, with the repo mounted at /app
  • stellar-node — local Stellar node with Soroban RPC on http://localhost:8000

Run Commands in Container

# Run tests
docker compose run --rm dev make test

# Build WASM
docker compose run --rm dev make build

# Build contract with Stellar CLI
docker compose run --rm dev stellar contract build

# Deploy to local node
docker compose run --rm dev bash -c "NETWORK=local ./scripts/deploy.sh"

# Open interactive shell
docker compose run --rm dev bash

From Inside Container:

  • Local Stellar node RPC: http://stellar-node:8000
  • Repo mounted at: /app

Local Development (Without Docker)

# Install Rust and WASM target
rustup target add wasm32-unknown-unknown

# Run tests
make test

# Build WASM
make build

# Format code
make fmt

# Lint with Clippy
make lint

# View documentation
cargo doc --no-deps --open

Makefile Targets

make test              # Run all unit tests
make build             # Build WASM binaries
make fmt               # Format code with rustfmt
make fmt-check         # Check formatting without changes
make lint              # Run Clippy linter
make clean             # Remove build artifacts
make doc               # Generate documentation

Testing

Unit Tests

Comprehensive test coverage for both contracts:

Governance Contract (40+ tests):

  • Proposal creation and validation
  • Voting mechanics and double-vote prevention
  • Finalization and pass conditions
  • Admin operations (execute, cancel, update quorum)
  • Access control and authorization
  • Edge cases and error conditions

Token Contract (20+ tests):

  • Initialization and supply management
  • Transfers and balance updates
  • Mint and burn operations
  • Allowances and transfer_from
  • Admin operations
  • Event emission

Running Tests

# Run all tests
make test

# Run tests with output
cargo test -- --nocapture

# Run specific test
cargo test test_create_proposal -- --nocapture

# Run tests in specific contract
cargo test -p votechain_governance

# Run with coverage (requires tarpaulin)
cargo tarpaulin --out Html

Test Helpers

Reusable test utilities in test_helpers.rs:

use test_helpers::*;

#[test]
fn test_voting_flow() {
    let env = Env::default();
    let (governance, token, admin, voter) = setup_env(&env);
    
    // Create proposal
    let proposal_id = create_test_proposal(&env, &governance, &admin);
    
    // Mint tokens and vote
    mint_and_vote(&env, &token, &governance, &voter, proposal_id, Vote::Yes);
}

Property-Based Tests

Property-based tests using proptest for invariant checking:

cargo test prop_

Test Snapshots

Regression tests with JSON snapshots in test_snapshots/:

# Update snapshots after intentional changes
cargo test -- --nocapture --test-threads=1

Security

Threat Model

VoteChain has been designed with security as a first-class concern. The threat model identifies five threat actors and their mitigations:

T1 — Malicious Voter

  • Goal: Cast more votes than token balance entitles
  • Mitigation: has_voted guard, zero-balance check, vote weight snapshots
  • Residual Risk: None

T2 — Malicious Proposer

  • Goal: Create proposals that pass without genuine support
  • Mitigation: Quorum enforcement, admin oversight
  • Residual Risk: Low

T3 — Malicious Admin

  • Goal: Abuse privileged functions
  • Mitigation: Accepted risk; admin is a trusted role
  • Residual Risk: Medium (by design)

T4 — External Attacker (No Tokens)

  • Goal: Manipulate outcomes or disrupt governance
  • Mitigation: require_auth(), initialization guard, state checks
  • Residual Risk: None

T5 — Compromised Token Contract

  • Goal: Return inflated balances to favored voters
  • Mitigation: Admin must deploy trustworthy token
  • Residual Risk: High (external dependency)

See docs/security/threat-model.md for full analysis.

Security Properties

  1. Vote Integrity — Each address votes once per proposal with weight = live balance
  2. Admin Confinement — Admin can only cancel/execute, not alter votes
  3. Initialization Safety — One-time init, immutable admin/token after
  4. Arithmetic Safetychecked_add prevents overflow
  5. Finalization Correctness — Pass condition evaluated atomically

Security Measures

  • require_auth() on all state-changing operations
  • Double-vote prevention via persistent HasVoted flag
  • Vote weight snapshots prevent balance manipulation
  • Quorum enforcement at finalization
  • Admin vote restriction option
  • Contract pause/unpause mechanism
  • All amounts use i128 (no floating-point)

Known Issues

See docs/security/known-issues.md for:

  • KI-001: Flash-loan attacks (mitigated by vote weight snapshots)
  • Dependency audit results
  • Vulnerability disclosure process

Audit & Compliance


Contributing

We welcome contributions from the community. Please see CONTRIBUTING.md for:

  • Code of conduct
  • Development workflow
  • Pull request process
  • Coding standards
  • Testing requirements
  • Commit message guidelines

Quick Contribution Checklist

  • Fork the repository
  • Create a feature branch (git checkout -b feature/my-feature)
  • Make your changes
  • Run tests (make test)
  • Run linter (make lint)
  • Format code (make fmt)
  • Commit with clear message
  • Push to your fork
  • Open a pull request

Resources

Documentation

Architecture Decision Records

Examples

Security

External Resources

Community


Technology Stack

Layer Technology Version
Blockchain Stellar (Soroban) Protocol 22+
Language Rust 1.75+
SDK Soroban SDK v22.0.0
Build Target WebAssembly wasm32-unknown-unknown
Testing soroban-sdk testutils v22.0.0
Property Testing proptest v1.6.0
CI/CD GitHub Actions Latest
Code Quality Clippy Latest
Formatting rustfmt Latest

License

Apache 2.0


Changelog

See CHANGELOG.md for version history and release notes.


Built with ❤️ on Stellar

About

No description, website, or topics provided.

Resources

License

Contributing

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages

  • Rust 92.4%
  • Shell 6.8%
  • Other 0.8%