Skip to content
Merged
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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@
- [**Changelog**](CHANGELOG.md) - Release notes and version history
- [**Security Policy**](SECURITY.md) - Security guidelines and vulnerability reporting

## 📖 Technical Documentation

- [**State Machine & Authorization Matrix**](docs/state-machine.md) - Complete reference for entrypoint security posture, authorization gates, freeze/reentrancy locks, and state transitions
- [**Event Schema**](docs/events.md) - All contract events, their topics, and data structures
- [**Deployment Guide**](docs/deployment.md) - Deployment workflow and network configuration

## Contract Canonicalization

Decision: **Option B (conservative)** — keep `campaign/` (`milestonex-campaign`) as the canonical crowdfunding contract for all new development, audits, deployments, and integrations. The `campaign/` implementation remains the authoritative contract for milestone flows, refunds, freeze/upgrade controls, reentrancy protection, typed errors, and dashboard analytics.
Expand Down
38 changes: 25 additions & 13 deletions campaign/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,20 @@ use crate::types::{CampaignStatus, Error};
use crate::{validate_campaign_transition, MAX_DEADLINE_GAP_SECONDS};
use soroban_sdk::{panic_with_error, Env};

/// Issue #212 – End the campaign early (before deadline).
/// Issue #212, #243 – End the campaign early (before deadline).
///
/// **Authorization:** Creator must call `require_auth()`.
/// **Freeze Gate:** Panics with `Error::ContractFrozen` if contract is frozen.
///
/// Transitions the campaign from `Active` or `GoalReached` to `Ended`.
/// Requires creator authorization.
///
/// **See also:** `docs/state-machine.md` for campaign status transitions, authorization rules,
/// and complete entrypoint matrix.
///
/// # Panics
/// - `Error::NotInitialized` if campaign not initialized
/// - `Error::Unauthorized` if caller is not the creator
/// - `Error::ContractFrozen` if contract is frozen (freeze invariant: all writes rejected)
/// - `Error::ContractFrozen` if contract is frozen
/// - `Error::InvalidCampaignTransition` if campaign is already Ended or Cancelled
pub fn end_campaign(env: &Env) {
let mut campaign =
Expand All @@ -40,15 +45,20 @@ pub fn end_campaign(env: &Env) {
event::campaign_ended(env);
}

/// Issue #214 – Cancel the campaign.
/// Issue #214, #243 – Cancel the campaign.
///
/// **Authorization:** Creator must call `require_auth()`.
/// **Freeze Gate:** Panics with `Error::ContractFrozen` if contract is frozen.
///
/// Transitions the campaign from `Active`, `GoalReached`, or `Ended` to
/// `Cancelled`. Requires creator authorization.
/// Transitions the campaign from `Active`, `GoalReached`, or `Ended` to `Cancelled`.
/// All donors immediately become refund-eligible.
///
/// **See also:** `docs/state-machine.md` for campaign status transitions and authorization matrix.
///
/// # Panics
/// - `Error::NotInitialized` if campaign not initialized
/// - `Error::Unauthorized` if caller is not the creator
/// - `Error::ContractFrozen` if contract is frozen (freeze invariant: all writes rejected)
/// - `Error::ContractFrozen` if contract is frozen
/// - `Error::InvalidCampaignTransition` if campaign is already Cancelled
pub fn cancel_campaign(env: &Env) {
let mut campaign =
Expand All @@ -71,18 +81,20 @@ pub fn cancel_campaign(env: &Env) {
event::campaign_cancelled(env, &campaign.creator);
}

/// Issue #215 – Extend the campaign deadline.
/// Issue #215, #243 – Extend the campaign deadline.
///
/// **Authorization:** Creator must call `require_auth()`.
/// **Freeze Gate:** Panics with `Error::ContractFrozen` if contract is frozen.
///
/// Extends the campaign's `end_time` to a new future timestamp.
/// The new deadline cannot be more than ten years from the current ledger time;
/// this preserves the contract's time arithmetic invariants for status views,
/// refund windows, milestone release metadata, and campaign reports.
/// Requires creator authorization.
/// The new deadline cannot be more than ten years from current ledger time.
///
/// **See also:** `docs/state-machine.md` for deadline validation rules and authorization matrix.
///
/// # Panics
/// - `Error::NotInitialized` if campaign not initialized
/// - `Error::Unauthorized` if caller is not the creator
/// - `Error::ContractFrozen` if contract is frozen (freeze invariant: all writes rejected)
/// - `Error::ContractFrozen` if contract is frozen
/// - `Error::InvalidEndTime` if `new_end_time <= current ledger timestamp`
/// - `Error::InvalidEndTime` if `new_end_time` is more than ten years out
/// - `Error::InvalidCampaignTransition` if campaign is not Active or GoalReached
Expand Down
45 changes: 36 additions & 9 deletions campaign/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,12 @@ impl CampaignContract {
/// - `Error::InvalidMilestoneCount` if milestone count is not 1-5
/// - `Error::InvalidMilestones` if milestones are not sorted ascending
/// - `Error::MilestoneMismatch` if last milestone.target_amount != goal_amount
/// Issue #175, #194 – Initialize the campaign contract.
///
/// **Authorization:** Creator must call `require_auth()`.
///
/// **See also:** `docs/state-machine.md` for full authorization matrix, freeze/reentrancy gates,
/// and state transition rules for all entrypoints.
pub fn initialize(
env: Env,
creator: soroban_sdk::Address,
Expand Down Expand Up @@ -153,19 +159,18 @@ impl CampaignContract {
Ok(())
}

/// Issue #194 – Donate to the campaign, enforcing campaign status.
/// Issue #194, #242, #243 – Donate to the campaign, enforcing campaign status.
///
/// Issue #242 – Reentrancy protection: acquires lock at entry, releases at exit.
/// Issue #243 – Authorization: `donor...()`.
/// **Authorization:** Donor must call `require_auth()`.
/// **Freeze Gate:** Panics with `Error::ContractFrozen` if contract is frozen.
/// **Reentrancy:** Acquires lock at entry, releases at exit. See `docs/state-machine.md`.
///
/// Panics with `Error::CampaignNotActive` unless status is `Active` or `GoalReached`.
/// Panics with `Error::CampaignEnded` if the current ledger timestamp is >= `end_time`,
/// regardless of whether status is still `Active` or `GoalReached`. This deadline
/// gate fires before any state mutation or storage TTL bump.
/// regardless of whether status is still `Active` or `GoalReached`.
///
/// Issue #195 – After updating raised_amount, loops over milestones and unlocks
/// any whose target_amount <= raised_amount and status == Locked.
/// Issue #198 – After donation, transitions to GoalReached if raised_amount >= goal_amount.
/// **See also:** `docs/state-machine.md` for full entrypoint authorization matrix,
/// freeze/reentrancy gates, and state transition diagrams.
pub fn donate(env: Env, donor: Address, amount: i128, asset: AssetInfo) {
// Issue #242 – Reentrancy protection: acquire lock
acquire_lock(&env);
Expand Down Expand Up @@ -392,7 +397,14 @@ impl CampaignContract {
///
/// Issue #242 – Reentrancy protection: acquires lock at entry, releases at exit.
/// Issue #243 – Authorization: `donor.require_auth()`.
/// Issue #244 – Balance verification: checks contract balance before each transfer.
/// Issue #211, #242, #243 – Claim a refund for donations.
///
/// **Authorization:** Donor must call `require_auth()`.
/// **Freeze Gate:** Panics with `Error::ContractFrozen` if contract is frozen.
/// **Reentrancy:** Acquires lock at entry, releases at exit.
///
/// **See also:** `docs/state-machine.md` for refund eligibility rules, freeze/reentrancy gates,
/// and complete authorization matrix.
///
/// # Panics
/// - `Error::NotInitialized` if campaign not initialized
Expand Down Expand Up @@ -571,9 +583,15 @@ impl CampaignContract {

/// Issue #246 – Upgrade the contract's WASM hash.
///
/// **Authorization:** Creator must call `require_auth()`.
/// **Freeze Gate:** Panics with `Error::ContractFrozen` if contract is frozen.
///
/// Only the admin (creator address stored at initialization) can call this.
/// Emits `contract_upgraded` event on success.
///
/// **See also:** `docs/state-machine.md` for pre-upgrade migration checklist and
/// freeze/upgrade interaction rules.
///
/// # Panics
/// - `Error::Unauthorized` if not called by the creator
/// - `Error::NotInitialized` if campaign not yet initialized
Expand All @@ -599,9 +617,14 @@ impl CampaignContract {

/// Issue #246 – Freeze the contract, blocking all mutating operations.
///
/// **Authorization:** Creator must call `require_auth()`.
///
/// Only the admin (creator) can call this.
/// While frozen, all write operations are rejected with `Error::ContractFrozen`.
///
/// **See also:** `docs/state-machine.md` for freeze module specification, freeze check pattern,
/// and complete freeze matrix showing which operations are blocked.
///
/// # Panics
/// - `Error::Unauthorized` if not called by the creator
/// - `Error::NotInitialized` if campaign not yet initialized
Expand All @@ -619,8 +642,12 @@ impl CampaignContract {

/// Issue #246 – Unfreeze the contract, re-enabling mutating operations.
///
/// **Authorization:** Creator must call `require_auth()`.
///
/// Only the admin (creator) can call this.
///
/// **See also:** `docs/state-machine.md` for freeze/unfreeze lifecycle and emergency procedures.
///
/// # Panics
/// - `Error::Unauthorized` if not called by the creator
/// - `Error::NotInitialized` if campaign not yet initialized
Expand Down
21 changes: 12 additions & 9 deletions campaign/src/multi_asset_release.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,26 +46,29 @@ fn compute_asset_release(

// ─── Main entrypoint ─────────────────────────────────────────────────────────

/// Issue #208 — Multi-asset milestone release
/// Issue #208, #242, #244 – Release milestone funds proportionally across all assets.
///
/// Releases milestone funds proportionally across every accepted asset.
/// **Authorization:** Creator must call `require_auth()` (via wrapper in `lib.rs`).
/// **Freeze Gate:** Checked before any mutation.
/// **Reentrancy:** Acquires lock at entry, releases at exit.
///
/// Releases milestone funds proportionally across every accepted asset using the formula:
/// `per_asset_release = floor((asset_raised * milestone_release) / total_raised)`
///
/// **See also:** `docs/state-machine.md` for milestone state transitions, multi-asset release logic,
/// and complete authorization matrix.
///
/// **Precondition:** The caller (`#[contractimpl]` wrapper) MUST have already
/// verified `creator.require_auth()` before calling this function.
///
/// Issue #242 – Reentrancy protection: acquires lock at entry, releases at exit.
/// Issue #244 – Balance verification: checks contract balance before each transfer.
///
/// Security properties:
/// - Milestone must be in `Unlocked` state (exactly once).
/// - Proportional math uses checked integer arithmetic — no overflows.
/// - Status is written to storage BEFORE transfers (CEI pattern) so a
/// re-entrant call on the same milestone index fails immediately.
/// - Recipient must be non-zero (validated before any transfer).
/// - Per-asset actual balances are used, not stored estimates, so the
/// contract can never release more than it actually holds.
/// - Dust amounts below MIN_TRANSFER_AMOUNT are skipped rather than
/// causing the whole release to fail.
/// - Per-asset actual balances are used, not stored estimates.
/// - Dust amounts below MIN_TRANSFER_AMOUNT are skipped.
pub fn release_milestone_multi_asset(env: &Env, milestone_index: u32, recipient: Address) {
// Issue #242 – Reentrancy protection: acquire lock
acquire_lock(env);
Expand Down
28 changes: 10 additions & 18 deletions campaign/src/release_milestone.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,20 @@ use crate::storage::{
use crate::types::{Error, MilestoneStatus};
use soroban_sdk::{panic_with_error, token, Address, Env};

/// Issue #207 – `release_milestone` function
/// Issue #207, #242, #244 – Release funds for an unlocked milestone.
///
/// Releases funds for an unlocked milestone to the recipient.
/// **Authorization:** Creator must call `require_auth()` (via wrapper in `lib.rs`).
/// **Freeze Gate:** Panics with `Error::ContractFrozen` if contract is frozen.
/// **Reentrancy:** Acquires lock at entry, releases at exit.
///
/// **Precondition:** The caller (`#[contractimpl]` wrapper) MUST have already
/// verified `creator.require_auth()` before calling this function.
///
/// Validates milestone status is `Unlocked`.
/// Prevents double release — `Released` milestones panic with `MilestoneAlreadyReleased`.
/// Prevents skipping milestones — previous milestone must be Released.
/// Transfers tokens from the campaign's primary (first) accepted asset to recipient.
/// Sets milestone status to `Released`.
/// Emits `milestone_released` event.
/// Respects the freeze flag — panics with `ContractFrozen` if frozen.
/// Releases funds for an unlocked milestone to the recipient using the campaign's
/// primary (first) accepted asset.
///
/// For campaigns accepting multiple assets, use `release_milestone_multi_asset`
/// instead, which distributes the release proportionally across all assets.
/// **See also:** `docs/state-machine.md` for milestone state transitions, release ordering,
/// and complete authorization matrix.
///
/// ## Security
///
/// Issue #242 – Reentrancy protection: acquires lock at entry, releases at exit.
/// Issue #244 – Balance verification: checks contract balance before each transfer.
/// **Precondition:** The caller (`#[contractimpl]` wrapper) MUST have already
/// verified `creator.require_auth()` before calling this function.
///
/// # Panics
/// - `Error::NotInitialized` if campaign not initialized
Expand Down
Loading
Loading