diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 7d6eb1f..9976281 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -38,3 +38,31 @@ jobs:
targets: wasm32-unknown-unknown
- uses: Swatinem/rust-cache@v2
- run: cargo clippy --workspace -- -D warnings
+
+ docs:
+ name: Build rustdoc (warnings as errors)
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Install Rust
+ uses: dtolnay/rust-toolchain@stable
+ with:
+ targets: wasm32-unknown-unknown
+
+ - name: Cache Rust
+ uses: Swatinem/rust-cache@v2
+
+ - name: Build documentation
+ # -D warnings promotes any doc warning to a CI failure so that
+ # broken intra-doc links or missing docs are caught early.
+ # --no-deps skips generating docs for Soroban SDK + transitive deps.
+ run: cargo doc --workspace --no-deps -- -D warnings
+
+ - name: Upload docs artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: rustdoc-${{ github.sha }}
+ path: target/doc
+ if-no-files-found: warn
+ retention-days: 14
diff --git a/contracts/dividend/src/lib.rs b/contracts/dividend/src/lib.rs
index 10b3a9e..3cc6089 100644
--- a/contracts/dividend/src/lib.rs
+++ b/contracts/dividend/src/lib.rs
@@ -1,28 +1,53 @@
#![no_std]
+//! Profit distribution contract for a cooperative.
+//!
+//! [`DividendContract`] accepts an admin-defined profit pool and a list of
+//! member share weights, then transfers token payouts proportionally from
+//! the contract's own balance to each recipient. Every payout is recorded
+//! on-chain as a [`Distribution`] so that the treasury can later audit
+//! who received what and when.
+//!
+//! The contract assumes the asset address is a SAC22 token whose client
+//! implements the standard [`token::Client`] `transfer` interface.
+
use soroban_sdk::{
contract, contractimpl, contracttype, token, Address, Env, Symbol, Vec,
};
+/// Storage keys for [`DividendContract`].
#[contracttype]
#[derive(Clone)]
pub enum DataKey {
+ /// Admin address authorized to call [`DividendContract::distribute`].
Admin,
+ /// Address of the SAC22 token used for payouts.
AssetAddress,
+ /// Address of the sibling treasury contract (informational; not enforced).
TreasuryContract,
+ /// Append-only log of every [`Distribution`] executed by this contract.
Distributions,
+ /// Monotonically increasing counter used to assign distribution IDs.
DistributionCounter,
}
+/// Single on-chain record of one profit-distribution event.
#[contracttype]
#[derive(Clone, Debug)]
pub struct Distribution {
+ /// Auto-incremented ID assigned at execution time.
pub id: u32,
+ /// Total profit (in token base units) that was available to distribute.
pub total_profit: i128,
+ /// Sum of all `shares` in the call that produced this distribution.
pub total_shares: i128,
+ /// Recipients in the same order as the call's `recipients` argument.
pub recipients: Vec
,
+ /// Payout actually transferred to each recipient (rounded down).
pub amounts: Vec,
+ /// Ledger timestamp at which the distribution was executed.
pub executed_at: u64,
+ /// Human-readable period label (e.g. `"Q3-2026"`) supplied by the admin.
pub period: String,
}
@@ -31,6 +56,13 @@ pub struct DividendContract;
#[contractimpl]
impl DividendContract {
+ /// Initialize the dividend contract.
+ ///
+ /// Stores the admin, asset address, treasury contract address, and an
+ /// empty distribution counter + log. Must be called exactly once.
+ ///
+ /// # Authorization
+ /// Requires auth from `admin`.
pub fn initialize(env: Env, admin: Address, asset: Address, treasury: Address) {
admin.require_auth();
env.storage().instance().set(&DataKey::Admin, &admin);
@@ -109,12 +141,19 @@ impl DividendContract {
id
}
+ /// Return the full on-chain history of [`Distribution`] events, oldest first.
+ ///
+ /// Returns an empty vector if no distributions have been executed yet.
pub fn get_distributions(env: Env) -> Vec {
env.storage().instance()
.get(&DataKey::Distributions)
.unwrap_or(Vec::new(&env))
}
+ /// Internal: assert that `caller` is the admin registered at [`initialize`].
+ ///
+ /// # Panics
+ /// Panics with `"unauthorized"` when `caller` does not match.
fn require_admin(env: &Env, caller: &Address) {
let admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap();
if admin != *caller { panic!("unauthorized"); }
diff --git a/contracts/governance/src/lib.rs b/contracts/governance/src/lib.rs
index 7d9476b..8772923 100644
--- a/contracts/governance/src/lib.rs
+++ b/contracts/governance/src/lib.rs
@@ -1,36 +1,76 @@
#![no_std]
+//! Governance contract for cooperative rules and contract registry.
+//!
+//! Stores the [`CoopRules`] used by every other contract in the workspace
+//! (loan interest, voting quorum, contribution period) plus the addresses of
+//! the sibling contracts (`voting`, `loan`, `treasury`) that the coop needs
+//! to talk to. Only the admin address registered at [`initialize`] can call
+//! [`update_rules`].
+//!
+//! Defaults seeded in [`initialize`] are tuned for an African ROSCA / SACCO
+//! (10 USDC minimum monthly contribution, 5% loan interest, 3-vote quorum).
+
use soroban_sdk::{
contract, contractimpl, contracttype, Address, Env, Symbol, Vec,
};
+/// Storage keys used by [`GovernanceContract`].
#[contracttype]
#[derive(Clone)]
pub enum DataKey {
+ /// Admin address authorized to mutate rules.
Admin,
+ /// Address of the [`VotingContract`] (set at initialize).
VotingContract,
+ /// Address of the [`LoanContract`] (set at initialize).
LoanContract,
+ /// Address of the [`TreasuryContract`] (set at initialize).
TreasuryContract,
+ /// Current [`CoopRules`] stored as an instance value.
Rules,
}
+/// Cooperative governance parameters shared across the workspace.
+///
+/// All numeric fields are intentionally simple integer types so they can be
+/// persisted directly in Soroban instance storage and consumed by the loan,
+/// voting, and treasury contracts.
#[contracttype]
#[derive(Clone, Debug)]
pub struct CoopRules {
+ /// Minimum monthly contribution in token base units (7 decimals on USDC).
pub min_contribution: i128,
+ /// Length of a contribution cycle in days.
pub contribution_period_days: u32,
- pub max_loan_multiplier: u32, // e.g. 3 = max loan is 3x your total contributions
+ /// Maximum loan size as a multiple of total contributions
+ /// (e.g. `3` means a member can borrow up to 3x their contributed total).
+ pub max_loan_multiplier: u32,
+ /// Annualized loan interest rate in basis points (500 = 5.00%).
pub loan_interest_bps: u32,
+ /// Minimum number of yes votes required for a proposal to pass.
pub voting_quorum: u32,
+ /// Number of days a proposal stays open for voting.
pub voting_period_days: u32,
+ /// Penalty applied to late contributions, in basis points (200 = 2.00%).
pub late_penalty_bps: u32,
}
+/// Contract entry point. Single global instance per deployment.
#[contract]
pub struct GovernanceContract;
#[contractimpl]
impl GovernanceContract {
+ /// Initialize the governance contract with admin and sibling-contract
+ /// addresses, then seed sensible default [`CoopRules`].
+ ///
+ /// # Authorization
+ /// Requires auth from `admin`; the same address becomes the only account
+ /// that can mutate rules via [`update_rules`].
+ ///
+ /// # Events
+ /// * topic `"governance_initialized"` — payload `(admin, voting, loan, treasury)`
pub fn initialize(
env: Env,
admin: Address,
@@ -55,8 +95,20 @@ impl GovernanceContract {
late_penalty_bps: 200, // 2% penalty
};
env.storage().instance().set(&DataKey::Rules, &rules);
+
+ env.events().publish(
+ (Symbol::new(&env, "governance_initialized"),),
+ (admin, voting, loan, treasury),
+ );
}
+ /// Replace the current [`CoopRules`] with the provided `rules`.
+ ///
+ /// # Panics
+ /// Panics if `admin` is not the registered admin.
+ ///
+ /// # Events
+ /// * topic `"rules_updated"` — payload `()`
pub fn update_rules(env: Env, admin: Address, rules: CoopRules) {
admin.require_auth();
Self::require_admin(&env, &admin);
@@ -64,12 +116,20 @@ impl GovernanceContract {
env.events().publish((Symbol::new(&env, "rules_updated"),), ());
}
+ /// Read the current [`CoopRules`].
+ ///
+ /// # Panics
+ /// Panics if [`initialize`] has not yet been called.
pub fn get_rules(env: Env) -> CoopRules {
env.storage().instance().get(&DataKey::Rules).unwrap()
}
+ /// Internal: assert that `caller` is the admin registered at [`initialize`].
+ ///
+ /// # Panics
+ /// Panics with `"unauthorized"` when `caller` does not match.
fn require_admin(env: &Env, caller: &Address) {
let admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap();
if admin != *caller { panic!("unauthorized"); }
}
-}
+}
\ No newline at end of file
diff --git a/contracts/loan/src/lib.rs b/contracts/loan/src/lib.rs
index e08ceae..daefdc7 100644
--- a/contracts/loan/src/lib.rs
+++ b/contracts/loan/src/lib.rs
@@ -1,41 +1,75 @@
#![no_std]
+//! Member-loan contract for a cooperative.
+//!
+//! [`LoanContract`] is the lifecycle owner of every loan in the coop:
+//! a member calls [`request_loan`] to file an application, governance
+//! (or the admin) calls [`approve_loan`] to disburse funds from the
+//! treasury, and the borrower repays in one or more calls to [`repay`].
+//!
+//! All amounts are denominated in token base units (7 decimals on USDC),
+//! and interest is computed in basis points against the principal only.
+
use soroban_sdk::{
contract, contractimpl, contracttype, token, Address, Env, Symbol, Vec, String,
};
+/// Storage keys for [`LoanContract`].
#[contracttype]
#[derive(Clone)]
pub enum DataKey {
+ /// Admin address authorized to approve loans.
Admin,
+ /// Address of the sibling treasury contract.
TreasuryContract,
+ /// Address of the SAC22 token used for disbursement and repayment.
AssetAddress,
+ /// Append-only log of every [`Loan`] created on this contract.
Loans,
+ /// Monotonically increasing counter used to assign loan IDs.
LoanCounter,
}
+/// Status of a loan through its lifecycle.
#[contracttype]
#[derive(Clone, Debug, PartialEq)]
pub enum LoanStatus {
- Pending, // Awaiting approval vote
- Approved, // Disbursed
- Repaid, // Fully repaid
- Rejected, // Rejected by governance
- Defaulted, // Past due date, not repaid
+ /// Awaiting approval vote.
+ Pending,
+ /// Disbursed to the borrower.
+ Approved,
+ /// Fully repaid (principal + interest).
+ Repaid,
+ /// Rejected by governance.
+ Rejected,
+ /// Past due date, not repaid.
+ Defaulted,
}
+/// Single loan record. Fields are set incrementally as the loan moves
+/// through the [`LoanStatus`] lifecycle.
#[contracttype]
#[derive(Clone, Debug)]
pub struct Loan {
+ /// Auto-incremented ID assigned at request time.
pub id: u32,
+ /// Address of the borrower; principal is disbursed to this account.
pub borrower: Address,
+ /// Principal amount in token base units.
pub amount: i128,
- pub interest_bps: u32, // basis points, e.g. 500 = 5%
- pub repayment_due: u64, // ledger timestamp deadline
+ /// Interest rate in basis points (500 = 5.00%).
+ pub interest_bps: u32,
+ /// Ledger timestamp at which repayment is due.
+ pub repayment_due: u64,
+ /// Running total of all repayments applied to this loan.
pub amount_repaid: i128,
+ /// Current status; see [`LoanStatus`].
pub status: LoanStatus,
+ /// Human-readable purpose supplied by the borrower.
pub purpose: String,
+ /// Ledger timestamp when the loan was requested.
pub requested_at: u64,
+ /// Ledger timestamp when the loan was approved (0 until then).
pub approved_at: u64,
}
@@ -44,6 +78,17 @@ pub struct LoanContract;
#[contractimpl]
impl LoanContract {
+ /// Initialize the loan contract.
+ ///
+ /// Stores the admin, treasury contract address, asset address, and an
+ /// empty loan counter + log. Must be called exactly once.
+ ///
+ /// # Panics
+ /// Panics with `"already initialized"` if the contract already has an
+ /// admin on file.
+ ///
+ /// # Authorization
+ /// Requires auth from `admin`.
pub fn initialize(env: Env, admin: Address, treasury: Address, asset: Address) {
admin.require_auth();
if env.storage().instance().has(&DataKey::Admin) {
@@ -187,11 +232,19 @@ impl LoanContract {
loans.get(idx).unwrap()
}
+ /// Internal: assert that `caller` is the admin registered at [`initialize`].
+ ///
+ /// # Panics
+ /// Panics with `"unauthorized"` when `caller` does not match.
fn require_admin(env: &Env, caller: &Address) {
let admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap();
if admin != *caller { panic!("unauthorized"); }
}
+ /// Internal: linear search for a loan by ID in the on-chain log.
+ ///
+ /// # Panics
+ /// Panics with `"loan not found"` when no loan has the supplied ID.
fn find_loan_idx(loans: &Vec, id: u32) -> u32 {
for i in 0..loans.len() {
if loans.get(i).unwrap().id == id {
diff --git a/contracts/treasury/src/lib.rs b/contracts/treasury/src/lib.rs
index 786f8f7..66fb317 100644
--- a/contracts/treasury/src/lib.rs
+++ b/contracts/treasury/src/lib.rs
@@ -1,5 +1,30 @@
#![no_std]
+//! # TreasuryContract
+//!
+//! Soroban smart contract that implements the group wallet for a cooperative:
+//! member registration, periodic contribution collection, and admin-gated
+//! withdrawals.
+//!
+//! ## Storage layout
+//! All state lives in `env.storage().instance()` (group metadata, members
+//! list, totals) and `env.storage().persistent()` (per-member contribution
+//! history). Keys are described in [`DataKey`].
+//!
+//! ## Authorization model
+//! * `initialize` — admin only; re-initialization is rejected.
+//! * `add_member`, `withdraw` — admin only.
+//! * `contribute` — member only; signer must be in the members list.
+//! * Read-only views — open to anyone.
+//!
+//! ## Events
+//! * `member_added(member)`
+//! * `contribution(member, amount, period)`
+//! * `withdrawal(to, amount)`
+//!
+//! ## Panics
+//! All `panic!` conditions are documented on the individual functions.
+
use soroban_sdk::{
contract, contractimpl, contracttype, token, Address, Env, Symbol, Vec, String,
};
@@ -62,6 +87,25 @@ pub struct TreasuryContract;
#[contractimpl]
impl TreasuryContract {
/// Initialize a new cooperative treasury group.
+ ///
+ /// # Authorization
+ /// Requires `admin.require_auth()`.
+ ///
+ /// # Arguments
+ /// * `admin` - The address authorized to add members and withdraw funds.
+ /// * `group_name` - Human-readable group name (stored verbatim).
+ /// * `asset` - The SAC token contract address used for contributions and
+ /// withdrawals.
+ ///
+ /// # Returns
+ /// A fresh [`GroupInfo`] snapshot reflecting the just-initialized state
+ /// (zero members, zero contributions, `is_active = true`).
+ ///
+ /// # Events
+ /// None.
+ ///
+ /// # Panics
+ /// If the contract has already been initialized.
pub fn initialize(
env: Env,
admin: Address,
@@ -92,6 +136,21 @@ impl TreasuryContract {
}
/// Add a new member to the cooperative.
+ ///
+ /// # Authorization
+ /// Requires `admin.require_auth()` and `admin` must equal the stored
+ /// admin address.
+ ///
+ /// # Arguments
+ /// * `admin` - The admin performing the addition.
+ /// * `member` - Address to add. If already a member, the call is a
+ /// no-op (idempotent).
+ ///
+ /// # Events
+ /// Emits `member_added(member)` only if `member` was not already present.
+ ///
+ /// # Panics
+ /// If `admin` is not the stored admin.
pub fn add_member(env: Env, admin: Address, member: Address) {
admin.require_auth();
Self::require_admin(&env, &admin);
@@ -112,6 +171,24 @@ impl TreasuryContract {
}
/// Record a member contribution. Transfers USDC from member to this contract.
+ ///
+ /// # Authorization
+ /// Requires `member.require_auth()`, and `member` must be in the stored
+ /// members list.
+ ///
+ /// # Arguments
+ /// * `member` - The contributing member; must sign the call.
+ /// * `amount` - Token amount to contribute. Must be strictly positive.
+ /// * `period` - Cooperative-defined period number (e.g. month index)
+ /// stored alongside the contribution record.
+ ///
+ /// # Events
+ /// Emits `contribution(member, amount, period)` after the token transfer
+ /// succeeds and the record has been stored.
+ ///
+ /// # Panics
+ /// * If `amount <= 0`.
+ /// * If `member` is not in the members list.
pub fn contribute(env: Env, member: Address, amount: i128, period: u32) {
member.require_auth();
Self::require_member(&env, &member);
@@ -155,6 +232,24 @@ impl TreasuryContract {
}
/// Withdraw funds — only callable by admin (e.g. for approved loans or expenses).
+ ///
+ /// # Authorization
+ /// Requires `admin.require_auth()` and `admin` must equal the stored
+ /// admin address.
+ ///
+ /// # Arguments
+ /// * `admin` - The admin performing the withdrawal.
+ /// * `to` - Recipient address. May be a member, a loan contract, or any
+ /// arbitrary address authorized by the cooperative.
+ /// * `amount` - Token amount to transfer. The token client will reject
+ /// the transfer if the contract's balance is insufficient.
+ ///
+ /// # Events
+ /// Emits `withdrawal(to, amount)` after the token transfer succeeds.
+ ///
+ /// # Panics
+ /// * If `admin` is not the stored admin.
+ /// * If the contract does not hold at least `amount` of the asset.
pub fn withdraw(env: Env, admin: Address, to: Address, amount: i128) {
admin.require_auth();
Self::require_admin(&env, &admin);
@@ -170,6 +265,18 @@ impl TreasuryContract {
}
/// Get current treasury balance.
+ ///
+ /// # Authorization
+ /// None — read-only view.
+ ///
+ /// # Returns
+ /// The contract's current token balance in the asset's base units.
+ ///
+ /// # Events
+ /// None.
+ ///
+ /// # Panics
+ /// If the contract has not been initialized (no asset address stored).
pub fn balance(env: Env) -> i128 {
let asset: Address = env.storage().instance().get(&DataKey::AssetAddress).unwrap();
let token_client = token::Client::new(&env, &asset);
@@ -177,6 +284,19 @@ impl TreasuryContract {
}
/// Get all members.
+ ///
+ /// # Authorization
+ /// None — read-only view.
+ ///
+ /// # Returns
+ /// `Vec` of all members in insertion order. Returns an empty
+ /// vector if no member has been added yet.
+ ///
+ /// # Events
+ /// None.
+ ///
+ /// # Panics
+ /// Never panics.
pub fn get_members(env: Env) -> Vec {
env.storage().instance()
.get(&DataKey::Members)
@@ -184,6 +304,22 @@ impl TreasuryContract {
}
/// Get contribution history for a member.
+ ///
+ /// # Authorization
+ /// None — read-only view.
+ ///
+ /// # Arguments
+ /// * `member` - The member whose history to fetch.
+ ///
+ /// # Returns
+ /// `Vec` in insertion order. Returns an empty vector
+ /// for unknown members or members who have not yet contributed.
+ ///
+ /// # Events
+ /// None.
+ ///
+ /// # Panics
+ /// Never panics.
pub fn get_contributions(env: Env, member: Address) -> Vec {
env.storage().persistent()
.get(&DataKey::Contributions(member))
@@ -191,6 +327,18 @@ impl TreasuryContract {
}
/// Get full group info.
+ ///
+ /// # Authorization
+ /// None — read-only view.
+ ///
+ /// # Returns
+ /// A fresh [`GroupInfo`] snapshot assembled from current storage.
+ ///
+ /// # Events
+ /// None.
+ ///
+ /// # Panics
+ /// If the contract has not been initialized.
pub fn get_info(env: Env) -> GroupInfo {
let members: Vec = env.storage().instance()
.get(&DataKey::Members)
@@ -254,6 +402,12 @@ impl TreasuryContract {
// ── Internal helpers ─────────────────────────────────────────────────────
+ /// Verify that `caller` matches the stored admin address.
+ ///
+ /// Internal helper. Not part of the contract's public ABI.
+ ///
+ /// # Panics
+ /// If `caller` does not match the stored admin (or no admin is set).
fn require_admin(env: &Env, caller: &Address) {
let admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap();
if admin != *caller {
@@ -261,6 +415,12 @@ impl TreasuryContract {
}
}
+ /// Verify that `caller` is in the stored members list.
+ ///
+ /// Internal helper. Not part of the contract's public ABI.
+ ///
+ /// # Panics
+ /// If `caller` is not in the members list (or no members have been added).
fn require_member(env: &Env, caller: &Address) {
let members: Vec = env.storage().instance()
.get(&DataKey::Members)
diff --git a/contracts/voting/src/lib.rs b/contracts/voting/src/lib.rs
index f9a22f6..1217837 100644
--- a/contracts/voting/src/lib.rs
+++ b/contracts/voting/src/lib.rs
@@ -1,54 +1,97 @@
#![no_std]
+//! On-chain governance: proposal + simple yes/no voting for a cooperative.
+//!
+//! [`VotingContract`] owns the lifecycle of every [`Proposal`]: members
+//! call [`create_proposal`] to open one, then [`vote`] to cast their
+//! ballot, and finally [`finalize`] to close the proposal once the
+//! deadline has passed.
+//!
+//! Pass / fail criteria are intentionally simple: a proposal passes iff
+//! `votes_for > votes_against` AND total votes reach the configured quorum.
+//! There is no delegate voting and no vote replacement; one address, one
+//! vote per proposal.
+
use soroban_sdk::{
contract, contractimpl, contracttype, Address, Env, Map, Symbol, Vec, String,
};
+/// Storage keys for [`VotingContract`].
#[contracttype]
#[derive(Clone)]
pub enum DataKey {
+ /// Admin address (informational; not enforced in current code).
Admin,
+ /// Address of the sibling treasury contract.
TreasuryContract,
+ /// Append-only log of every [`Proposal`] created on this contract.
Proposals,
+ /// Monotonically increasing counter used to assign proposal IDs.
ProposalCounter,
- Votes(u32), // proposal_id -> Map
+ /// Persistent storage: per-proposal map of `voter -> approve`.
+ Votes(u32),
}
+/// Lifecycle status of a [`Proposal`].
#[contracttype]
#[derive(Clone, Debug, PartialEq)]
pub enum ProposalStatus {
+ /// Open for voting; [`vote`] is allowed.
Active,
+ /// Deadline passed and quorum + majority reached.
Passed,
+ /// Deadline passed and quorum / majority not reached.
Failed,
+ /// Passed proposal whose payload has been executed (terminal state).
Executed,
}
+/// Coarse categorization of a proposal; useful for off-chain UIs.
#[contracttype]
#[derive(Clone, Debug, PartialEq)]
pub enum ProposalType {
- LoanApproval, // Approve a member loan
- TreasurySpend, // Authorize a treasury withdrawal
- AddMember, // Add a new member to the coop
- RemoveMember, // Remove a member from the coop
- UpdateRule, // Change a group rule (interest rate, contrib amount, etc.)
- General, // General governance proposal
+ /// Approve a member loan.
+ LoanApproval,
+ /// Authorize a treasury withdrawal.
+ TreasurySpend,
+ /// Add a new member to the coop.
+ AddMember,
+ /// Remove a member from the coop.
+ RemoveMember,
+ /// Change a group rule (interest rate, contribution amount, etc.).
+ UpdateRule,
+ /// General governance proposal.
+ General,
}
+/// Single proposal record.
#[contracttype]
#[derive(Clone, Debug)]
pub struct Proposal {
+ /// Auto-incremented ID assigned at create time.
pub id: u32,
+ /// Address that opened the proposal.
pub proposer: Address,
+ /// Coarse proposal category.
pub proposal_type: ProposalType,
+ /// Short title (max 64 chars enforced off-chain).
pub title: String,
+ /// Long-form description (markdown encouraged off-chain).
pub description: String,
+ /// Running total of yes votes.
pub votes_for: u32,
+ /// Running total of no votes.
pub votes_against: u32,
- pub quorum: u32, // Minimum votes required
- pub deadline: u64, // Ledger timestamp
+ /// Minimum total votes required for the proposal to pass.
+ pub quorum: u32,
+ /// Ledger timestamp after which voting is closed.
+ pub deadline: u64,
+ /// Current status; see [`ProposalStatus`].
pub status: ProposalStatus,
+ /// Ledger timestamp at creation.
pub created_at: u64,
- pub payload: String, // JSON-encoded action payload
+ /// JSON-encoded action payload consumed by an executor contract.
+ pub payload: String,
}
#[contract]
@@ -56,6 +99,13 @@ pub struct VotingContract;
#[contractimpl]
impl VotingContract {
+ /// Initialize the voting contract.
+ ///
+ /// Stores the admin, treasury contract address, an empty proposal
+ /// counter, and an empty proposals log. Must be called exactly once.
+ ///
+ /// # Authorization
+ /// Requires auth from `admin`.
pub fn initialize(env: Env, admin: Address, treasury: Address) {
admin.require_auth();
env.storage().instance().set(&DataKey::Admin, &admin);
@@ -192,18 +242,29 @@ impl VotingContract {
status
}
+ /// Return every proposal ever created on this contract, oldest first.
+///
+/// Returns an empty vector if no proposals have been created yet.
pub fn get_proposals(env: Env) -> Vec {
env.storage().instance()
.get(&DataKey::Proposals)
.unwrap_or(Vec::new(&env))
}
+ /// Return the full `voter -> approve` map for `proposal_id`.
+ ///
+ /// Returns an empty map if no votes have been cast.
pub fn get_votes(env: Env, proposal_id: u32) -> Map {
env.storage().persistent()
.get(&DataKey::Votes(proposal_id))
.unwrap_or(Map::new(&env))
}
+ /// Internal: linear search for a proposal by ID in the on-chain log.
+ ///
+ /// # Panics
+ /// Panics with `"proposal not found"` when no proposal has the
+ /// supplied ID.
fn find_proposal_idx(proposals: &Vec, id: u32) -> u32 {
for i in 0..proposals.len() {
if proposals.get(i).unwrap().id == id { return i; }