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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,14 @@ jobs:
targets: wasm32-unknown-unknown
- uses: Swatinem/rust-cache@v2
- run: cargo clippy --workspace -- -D warnings

docs:
name: Check Documentation
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-unknown-unknown
- uses: Swatinem/rust-cache@v2
- run: cargo doc --no-deps --workspace
25 changes: 25 additions & 0 deletions contracts/dividend/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
#![no_std]

//! Dividend distribution contract.
//!
//! Distributes cooperative profits proportionally among members based on share
//! weight. Distributions are recorded on-chain and funds are transferred from
/// the linked treasury contract.

use soroban_sdk::{
contract, contractimpl, contracttype, token, Address, Env, Symbol, Vec,
};
Expand Down Expand Up @@ -31,6 +37,12 @@ pub struct DividendContract;

#[contractimpl]
impl DividendContract {
/// Initialize the dividend contract and link it to the asset and treasury.
///
/// # Authorization
/// Requires caller authentication as the admin.
///
/// Returns nothing; state is set directly.
pub fn initialize(env: Env, admin: Address, asset: Address, treasury: Address) {
admin.require_auth();
env.storage().instance().set(&DataKey::Admin, &admin);
Expand All @@ -45,6 +57,19 @@ impl DividendContract {
///
/// `recipients` and `shares` must be equal length.
/// Each member receives: `profit * (member_shares / total_shares)`
/// Distribute profit proportionally across recipients by share weight.
///
/// # Authorization
/// Requires authentication from the admin.
///
/// # Panics
/// Panics if `recipients` and `shares` differ in length, if `total_profit`
/// is not positive, or if the total share weight is zero.
///
/// # Events
/// Emits `dividend_distributed` with `(id, total_profit, recipient_count)`.
///
/// Returns the distribution ID.
pub fn distribute(
env: Env,
admin: Address,
Expand Down
12 changes: 12 additions & 0 deletions contracts/governance/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
#![no_std]

//! Cooperative governance contract.
//!
//! Stores default operating rules (contribution minimums, loan terms, voting
//! thresholds) and the addresses of the voting, loan, and treasury contracts.
//! Rules can be updated by the admin.

use soroban_sdk::{
contract, contractimpl, contracttype, Address, Env, Symbol, Vec,
};
Expand Down Expand Up @@ -31,6 +37,12 @@ pub struct GovernanceContract;

#[contractimpl]
impl GovernanceContract {
/// Initialize governance with linked contracts and default rules.
///
/// # Authorization
/// Requires caller authentication as the admin.
///
/// Returns nothing; state and default [`CoopRules`] are set directly.
pub fn initialize(
env: Env,
admin: Address,
Expand Down
60 changes: 60 additions & 0 deletions contracts/loan/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
#![no_std]

//! Member loan contract.
//!
//! Tracks loan requests, approvals, disbursements, and repayments for a
//! cooperative. Loans are funded from the linked treasury contract once a
//! pending request is approved by the admin or governance.

use soroban_sdk::{
contract, contractimpl, contracttype, token, Address, Env, Symbol, Vec, String,
};
Expand Down Expand Up @@ -44,6 +50,15 @@ pub struct LoanContract;

#[contractimpl]
impl LoanContract {
/// Initialize the loan contract and link it to the treasury and asset.
///
/// # Authorization
/// Requires caller authentication as the contract admin.
///
/// # Panics
/// Panics if the contract has already been initialized.
///
/// Returns nothing; state is set directly.
pub fn initialize(env: Env, admin: Address, treasury: Address, asset: Address) {
admin.require_auth();
if env.storage().instance().has(&DataKey::Admin) {
Expand All @@ -57,6 +72,18 @@ impl LoanContract {
}

/// Member submits a loan request.
/// Member submits a loan request.
///
/// # Authorization
/// Requires authentication from the borrowing member.
///
/// # Panics
/// Panics if `amount` is not positive.
///
/// # Events
/// Emits `loan_requested` with `(id, borrower, amount)`.
///
/// Returns the newly created loan ID.
pub fn request_loan(
env: Env,
borrower: Address,
Expand Down Expand Up @@ -101,6 +128,16 @@ impl LoanContract {
}

/// Admin (or governance contract) approves a loan and disburses funds.
/// Approve a pending loan and disburse funds from the treasury.
///
/// # Authorization
/// Requires authentication from the contract admin.
///
/// # Panics
/// Panics if the caller is not the admin or if the loan is not in `Pending` status.
///
/// # Events
/// Emits `loan_approved` with `(loan_id, borrower, amount)`.
pub fn approve_loan(env: Env, admin: Address, loan_id: u32) {
admin.require_auth();
Self::require_admin(&env, &admin);
Expand Down Expand Up @@ -136,6 +173,16 @@ impl LoanContract {
}

/// Borrower repays (partial or full).
/// Repay part or all of an approved loan.
///
/// # Authorization
/// Requires authentication from the borrower.
///
/// # Panics
/// Panics if the caller is not the loan borrower or if the loan is not active.
///
/// # Events
/// Emits `loan_repaid` with `(loan_id, borrower, amount, status)`.
pub fn repay(env: Env, borrower: Address, loan_id: u32, amount: i128) {
borrower.require_auth();

Expand Down Expand Up @@ -173,13 +220,26 @@ impl LoanContract {
}

/// Get all loans.
/// Get all loans.
///
/// Read-only — no auth required.
///
/// Returns a vector of [`Loan`] records.
pub fn get_loans(env: Env) -> Vec<Loan> {
env.storage().instance()
.get(&DataKey::Loans)
.unwrap_or(Vec::new(&env))
}

/// Get a single loan by ID.
/// Get a single loan by ID.
///
/// Read-only — no auth required.
///
/// # Panics
/// Panics if no loan with the given ID exists.
///
/// Returns the matching [`Loan`].
pub fn get_loan(env: Env, loan_id: u32) -> Loan {
let loans: Vec<Loan> = env.storage().instance()
.get(&DataKey::Loans).unwrap();
Expand Down
73 changes: 73 additions & 0 deletions contracts/treasury/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
#![no_std]

//! Cooperative treasury contract.
//!
//! Manages group membership, member contributions, and admin withdrawals for a
//! ROSCA/SACCO-style cooperative. Funds are held as a Soroban token and
//! contribution history is stored per member.

use soroban_sdk::{
contract, contractimpl, contracttype, token, Address, Env, Symbol, Vec, String,
};
Expand Down Expand Up @@ -62,6 +68,18 @@ pub struct TreasuryContract;
#[contractimpl]
impl TreasuryContract {
/// Initialize a new cooperative treasury group.
/// Initialize a new cooperative treasury group.
///
/// # Authorization
/// Requires caller authentication as the group admin.
///
/// # Panics
/// Panics if the contract has already been initialized.
///
/// # Events
/// Emits no event; state is set directly.
///
/// Returns the initial [`GroupInfo`] snapshot.
pub fn initialize(
env: Env,
admin: Address,
Expand Down Expand Up @@ -92,6 +110,17 @@ impl TreasuryContract {
}

/// Add a new member to the cooperative.
/// Add a new member to the cooperative.
///
/// # Authorization
/// Requires authentication from the group admin.
///
/// # Panics
/// Panics if `admin` is not the stored admin.
///
/// # Events
/// Emits `member_added` with the member address when a new address is added.
/// Duplicate additions are idempotent and emit no event.
pub fn add_member(env: Env, admin: Address, member: Address) {
admin.require_auth();
Self::require_admin(&env, &admin);
Expand All @@ -112,6 +141,19 @@ impl TreasuryContract {
}

/// Record a member contribution. Transfers USDC from member to this contract.
/// Record a member contribution and transfer `amount` of the group asset
/// from the member into this contract.
///
/// # Authorization
/// Requires authentication from the contributing member.
///
/// # Panics
/// Panics if `amount` is not positive or if `member` is not in the members list.
///
/// # Events
/// Emits `contribution` with `(member, amount, period)`.
///
/// Returns nothing; state is updated in place.
pub fn contribute(env: Env, member: Address, amount: i128, period: u32) {
member.require_auth();
Self::require_member(&env, &member);
Expand Down Expand Up @@ -155,6 +197,16 @@ impl TreasuryContract {
}

/// Withdraw funds — only callable by admin (e.g. for approved loans or expenses).
/// Withdraw group funds to a recipient address.
///
/// # Authorization
/// Requires authentication from the group admin.
///
/// # Panics
/// Panics if `admin` is not the stored admin or if the contract balance is insufficient.
///
/// # Events
/// Emits `withdrawal` with `(recipient, amount)`.
pub fn withdraw(env: Env, admin: Address, to: Address, amount: i128) {
admin.require_auth();
Self::require_admin(&env, &admin);
Expand All @@ -170,27 +222,48 @@ impl TreasuryContract {
}

/// Get current treasury balance.
/// Get the contract's current balance of the group asset.
///
/// Read-only — no auth required.
///
/// Returns the token balance in the asset's smallest unit.
pub fn balance(env: Env) -> i128 {
let asset: Address = env.storage().instance().get(&DataKey::AssetAddress).unwrap();
let token_client = token::Client::new(&env, &asset);
token_client.balance(&env.current_contract_address())
}

/// Get all members.
/// Get all member addresses.
///
/// Read-only — no auth required.
///
/// Returns a vector of member addresses; empty if none have been added.
pub fn get_members(env: Env) -> Vec<Address> {
env.storage().instance()
.get(&DataKey::Members)
.unwrap_or(Vec::new(&env))
}

/// Get contribution history for a member.
/// Get the contribution history for a specific member.
///
/// Read-only — no auth required.
///
/// Returns a vector of [`ContributionRecord`] entries; empty if the member
/// has never contributed.
pub fn get_contributions(env: Env, member: Address) -> Vec<ContributionRecord> {
env.storage().persistent()
.get(&DataKey::Contributions(member))
.unwrap_or(Vec::new(&env))
}

/// Get full group info.
/// Get a full snapshot of group state.
///
/// Read-only — no auth required.
///
/// Returns the current [`GroupInfo`].
pub fn get_info(env: Env) -> GroupInfo {
let members: Vec<Address> = env.storage().instance()
.get(&DataKey::Members)
Expand Down
Loading