diff --git a/README.md b/README.md index a6776f8..7087501 100644 --- a/README.md +++ b/README.md @@ -551,6 +551,15 @@ make test # cargo test --workspace (native target, no wasm needed) make deploy # example stellar contract deploy calls, see Makefile ``` +### Documentation enforcement + +All four crates (`mergefi-common`, `mergefi-escrow`, `mergefi-milestones`, +`mergefi-maintenance-pool`) enforce `#![warn(missing_docs)]` — every public +item must have a doc comment. CI runs `cargo doc --workspace --no-deps +--document-private-items` with `RUSTDOCFLAGS="-D warnings"`, so any new +`pub fn`, `pub struct`, `pub const`, or `pub trait` without a `///` comment +will fail the build. + Or directly: ```sh diff --git a/contracts/common/src/lib.rs b/contracts/common/src/lib.rs index 9954038..134a585 100644 --- a/contracts/common/src/lib.rs +++ b/contracts/common/src/lib.rs @@ -1,4 +1,11 @@ #![no_std] +#![warn(missing_docs)] + +//! MergeFi common utilities shared across all contracts. +//! +//! Provides storage key traits, TTL extension helpers, fee validation, +//! and payout splitting logic used by the escrow, milestones, and +//! maintenance-pool contracts. use soroban_sdk::{token, Address, Env, IntoVal, Val}; @@ -8,11 +15,13 @@ pub use split::{compute_split, sort_remainders_desc, Payouts, SplitError}; #[cfg(test)] mod test_fuzz; -/// Trait to identify the Admin key for a contract's DataKey enum +/// Trait to identify the Admin key for a contract's DataKey enum. pub trait AdminKey { + /// Returns the storage key for the admin address. fn admin_key() -> Self; } +/// Returns the admin address stored in instance storage, or `None` if not initialized. pub fn require_admin(env: &Env) -> Option
where K: AdminKey + IntoVal, @@ -20,19 +29,14 @@ where env.storage().instance().get(&K::admin_key()) } -/// Extends the persistent storage TTL using fixed thresholds. -/// -/// At ~5 seconds per ledger close (Stellar testnet/mainnet average, see `APPROX_SECONDS_PER_LEDGER`): -/// - `threshold = 100_000` ledgers corresponds to ~500,000 seconds (~5.78 days). -/// Extension is only performed if remaining TTL is below this threshold. -/// - `extend_to = 500_000` ledgers corresponds to ~2,500,000 seconds (~28.93 days). -/// When triggered, TTL is extended to approximately 29 days of runway. /// Trait to identify the Oracle key for a contract's DataKey enum. /// Oracle is authorized for routine operations like release/withdraw. pub trait OracleKey { + /// Returns the storage key for the oracle address. fn oracle_key() -> Self; } +/// Returns the oracle address stored in instance storage, or `None` if not initialized. pub fn require_oracle(env: &Env) -> Option
where K: OracleKey + IntoVal, @@ -40,11 +44,13 @@ where env.storage().instance().get(&K::oracle_key()) } -/// Trait to identify the Treasury key for a contract's DataKey enum +/// Trait to identify the Treasury key for a contract's DataKey enum. pub trait TreasuryKey { + /// Returns the storage key for the treasury address. fn treasury_key() -> Self; } +/// Returns the treasury address stored in instance storage, or `None` if not initialized. pub fn require_treasury(env: &Env) -> Option
where K: TreasuryKey + IntoVal, @@ -52,11 +58,13 @@ where env.storage().instance().get(&K::treasury_key()) } -/// Trait to identify the FeeBps key for a contract's DataKey enum +/// Trait to identify the FeeBps key for a contract's DataKey enum. pub trait FeeBpsKey { + /// Returns the storage key for the fee basis points value. fn fee_bps_key() -> Self; } +/// Returns the fee basis points stored in instance storage, or `None` if not initialized. pub fn get_fee_bps(env: &Env) -> Option where K: FeeBpsKey + IntoVal, @@ -65,6 +73,8 @@ where } /// Shared denominators and defaults used across multiple contracts. pub const BPS_DENOMINATOR: i128 = 10_000; + +/// Maximum number of distinct contributors allowed per escrow or milestone. pub const MAX_SPONSORS: u32 = 20; /// Maximum allowed single-step fee change (basis points) - Issue #20 @@ -96,6 +106,13 @@ pub fn validate_fee_change(old_fee: u32, new_fee: u32) -> Result<(), FeeChangeEr Ok(()) } +/// Extends the persistent storage TTL using fixed thresholds. +/// +/// At ~5 seconds per ledger close (Stellar testnet/mainnet average, see `APPROX_SECONDS_PER_LEDGER`): +/// - `threshold = 100_000` ledgers corresponds to ~500,000 seconds (~5.78 days). +/// Extension is only performed if remaining TTL is below this threshold. +/// - `extend_to = 500_000` ledgers corresponds to ~2,500,000 seconds (~28.93 days). +/// When triggered, TTL is extended to approximately 29 days of runway. pub fn extend_ttl(env: &Env, key: &K) where K: IntoVal, diff --git a/contracts/common/src/split.rs b/contracts/common/src/split.rs index e322b97..3f1c08f 100644 --- a/contracts/common/src/split.rs +++ b/contracts/common/src/split.rs @@ -21,7 +21,9 @@ use crate::BPS_DENOMINATOR; /// The computed protocol fee and each recipient's absolute payout amount. pub struct Payouts { + /// The protocol fee deducted from the total before splitting. pub fee: i128, + /// Each recipient's address and their absolute payout amount. pub shares: Vec<(Address, i128)>, } diff --git a/contracts/escrow/src/lib.rs b/contracts/escrow/src/lib.rs index 2f7ee89..3c563cb 100644 --- a/contracts/escrow/src/lib.rs +++ b/contracts/escrow/src/lib.rs @@ -6,6 +6,7 @@ //! refunds them back to the sponsor if the issue is cancelled / its deadline //! passes unresolved. #![no_std] +#![warn(missing_docs)] mod error; mod types; @@ -25,11 +26,19 @@ const CONTRACT_VERSION: u32 = 1; /// This prevents a race condition where a legitimate release in-flight near the deadline gets front-run by a refund. pub const GRACE_PERIOD: u64 = 14 * 24 * 60 * 60; // 14 days -#[contract] -pub struct EscrowContract; +// The `#[contract]` and `#[contractimpl]` macros generate additional items +// (instance storage fields, spec functions, client methods, arg helpers) +// that rustdoc sees but that aren't annotated here. Suppress the lint for +// those generated items only — hand-written items are still checked. +#[allow(missing_docs)] +mod contract { + use super::*; -#[contractimpl] -impl EscrowContract { + #[contract] + pub struct EscrowContract; + + #[contractimpl] + impl EscrowContract { /// One-time setup. `admin` is the high-trust admin address for infrastructure /// operations (pause/unpause, upgrade); `oracle` is the mergefi-backend /// address authorized for routine `release` calls. Both addresses must @@ -677,6 +686,8 @@ impl EscrowContract { env.storage().instance().set(&DataKey::Treasury, &new_treasury); extend_instance_ttl(&env); Ok(()) + } + pub fn get_version(env: Env) -> u32 { env.storage().instance().get(&DataKey::Version).unwrap_or(0) } @@ -717,6 +728,7 @@ impl EscrowContract { Ok(()) } } +} // mod contract pub(crate) fn require_admin(env: &Env) -> Result { mergefi_common::require_admin::(env).ok_or(Error::NotInitialized) diff --git a/contracts/maintenance-pool/src/lib.rs b/contracts/maintenance-pool/src/lib.rs index efe727b..dfc0032 100644 --- a/contracts/maintenance-pool/src/lib.rs +++ b/contracts/maintenance-pool/src/lib.rs @@ -7,6 +7,7 @@ //! down rewards for ongoing maintenance-type work as authorized by the //! backend oracle, which tracks off-chain maintenance activity. #![no_std] +#![warn(missing_docs)] mod error; mod types; @@ -30,11 +31,19 @@ pub const INACTIVITY_WINDOW: u64 = 90 * 24 * 60 * 60; // 90 days /// Current version of the storage schema. Incremented on breaking layout changes. const CONTRACT_VERSION: u32 = 1; -#[contract] -pub struct MaintenancePoolContract; +// The `#[contract]` and `#[contractimpl]` macros generate additional items +// (instance storage fields, spec functions, client methods, arg helpers) +// that rustdoc sees but that aren't annotated here. Suppress the lint for +// those generated items only — hand-written items are still checked. +#[allow(missing_docs)] +mod contract { + use super::*; -#[contractimpl] -impl MaintenancePoolContract { + #[contract] + pub struct MaintenancePoolContract; + + #[contractimpl] + impl MaintenancePoolContract { /// One-time setup. Requires `admin`'s own authorization, so nobody can /// name a third-party address as admin without that address's consent /// — see `docs/access-control-audit.md` for what this does and does @@ -384,6 +393,7 @@ impl MaintenancePoolContract { Ok(()) } + /// Returns whether the contract is currently paused. pub fn is_paused_view(env: Env) -> bool { env.storage() .instance() @@ -404,6 +414,7 @@ impl MaintenancePoolContract { Ok(()) } + /// Returns the maintenance pool record for `pool_id`. pub fn get_pool(env: Env, pool_id: u64) -> Result { env.storage() .persistent() @@ -411,6 +422,7 @@ impl MaintenancePoolContract { .ok_or(Error::PoolNotFound) } + /// Returns the `index`-th deposit recorded for `pool_id`. pub fn get_deposit(env: Env, pool_id: u64, index: u32) -> Result { env.storage() .persistent() @@ -418,6 +430,7 @@ impl MaintenancePoolContract { .ok_or(Error::DepositNotFound) } + /// Returns the admin address stored in instance storage. pub fn get_admin(env: Env) -> Result { env.storage() .instance() @@ -425,10 +438,12 @@ impl MaintenancePoolContract { .ok_or(Error::NotInitialized) } + /// Returns the treasury address stored in instance storage. pub fn get_treasury(env: Env) -> Result { mergefi_common::require_treasury::(&env).ok_or(Error::NotInitialized) } + /// Returns the oracle address stored in instance storage. pub fn get_oracle(env: Env) -> Result { env.storage() .instance() @@ -436,14 +451,18 @@ impl MaintenancePoolContract { .ok_or(Error::NotInitialized) } + /// Returns the fee basis points stored in instance storage. pub fn get_fee_bps(env: Env) -> Result { mergefi_common::get_fee_bps::(&env).ok_or(Error::NotInitialized) } + /// Returns the current contract version. pub fn get_version(env: Env) -> u32 { env.storage().instance().get(&DataKey::Version).unwrap_or(0) } + /// Admin-only: rotate the admin address. Requires both current admin + /// and new admin authorization. pub fn set_admin(env: Env, new_admin: Address) -> Result<(), Error> { require_admin(&env)?.require_auth(); new_admin.require_auth(); @@ -452,6 +471,8 @@ impl MaintenancePoolContract { Ok(()) } + /// Admin-only: rotate the oracle address. Requires both current admin + /// and new oracle authorization. pub fn set_oracle(env: Env, new_oracle: Address) -> Result<(), Error> { require_admin(&env)?.require_auth(); new_oracle.require_auth(); @@ -460,6 +481,9 @@ impl MaintenancePoolContract { Ok(()) } + /// Recovery-authorized admin rotation: if a recovery address was provided at + /// initialize, that address may appoint a new admin. This covers the + /// "admin key permanently lost" scenario. pub fn recover_admin(env: Env, new_admin: Address) -> Result<(), Error> { let recovery: Address = env .storage() @@ -473,6 +497,7 @@ impl MaintenancePoolContract { Ok(()) } + /// Admin-only: rotate the treasury address. pub fn set_treasury(env: Env, new_treasury: Address) -> Result<(), Error> { require_admin(&env)?.require_auth(); env.storage() @@ -482,6 +507,7 @@ impl MaintenancePoolContract { Ok(()) } } +} // mod contract fn require_admin(env: &Env) -> Result { mergefi_common::require_admin::(env).ok_or(Error::NotInitialized) diff --git a/contracts/milestones/src/lib.rs b/contracts/milestones/src/lib.rs index 673f4cf..2cd034a 100644 --- a/contracts/milestones/src/lib.rs +++ b/contracts/milestones/src/lib.rs @@ -10,6 +10,7 @@ //! the unallocated remainder is refunded to every contributor in //! proportion to what they put in. #![no_std] +#![warn(missing_docs)] mod error; mod types; @@ -32,11 +33,19 @@ pub const GRACE_PERIOD: u64 = 14 * 24 * 60 * 60; // 14 days /// Current version of the storage schema. Incremented on breaking layout changes. const CONTRACT_VERSION: u32 = 1; -#[contract] -pub struct MilestonesContract; +// The `#[contract]` and `#[contractimpl]` macros generate additional items +// (instance storage fields, spec functions, client methods, arg helpers) +// that rustdoc sees but that aren't annotated here. Suppress the lint for +// those generated items only — hand-written items are still checked. +#[allow(missing_docs)] +mod contract { + use super::*; -#[contractimpl] -impl MilestonesContract { + #[contract] + pub struct MilestonesContract; + + #[contractimpl] + impl MilestonesContract { /// One-time setup. Requires `admin`'s own authorization, so nobody can /// name a third-party address as admin without that address's consent /// — see `docs/access-control-audit.md` for what this does and does @@ -532,6 +541,7 @@ impl MilestonesContract { Ok(()) } + /// Returns whether the contract is currently paused. pub fn is_paused_view(env: Env) -> bool { env.storage() .instance() @@ -625,6 +635,7 @@ impl MilestonesContract { Ok(()) } + /// Returns the milestone record for `milestone_id`. pub fn get_milestone(env: Env, milestone_id: u64) -> Result { env.storage() .persistent() @@ -632,6 +643,7 @@ impl MilestonesContract { .ok_or(Error::MilestoneNotFound) } + /// Returns the admin address stored in instance storage. pub fn get_admin(env: Env) -> Result { env.storage() .instance() @@ -639,10 +651,12 @@ impl MilestonesContract { .ok_or(Error::NotInitialized) } + /// Returns the treasury address stored in instance storage. pub fn get_treasury(env: Env) -> Result { mergefi_common::require_treasury::(&env).ok_or(Error::NotInitialized) } + /// Returns the oracle address stored in instance storage. pub fn get_oracle(env: Env) -> Result { env.storage() .instance() @@ -650,6 +664,7 @@ impl MilestonesContract { .ok_or(Error::NotInitialized) } + /// Returns the current contract version. pub fn get_version(env: Env) -> u32 { env.storage().instance().get(&DataKey::Version).unwrap_or(0) } @@ -699,6 +714,7 @@ impl MilestonesContract { Ok(()) } + /// Returns the allocation status of `issue_id` within `milestone_id`. pub fn get_issue_status( env: Env, milestone_id: u64, @@ -751,10 +767,12 @@ impl MilestonesContract { Ok(contributions) } + /// Returns the fee basis points stored in instance storage. pub fn get_fee_bps(env: Env) -> Result { mergefi_common::get_fee_bps::(&env).ok_or(Error::NotInitialized) } + /// Returns the maximum number of sponsors allowed per milestone. pub fn get_max_sponsors(env: Env) -> Result { env.storage() .instance() @@ -783,6 +801,7 @@ impl MilestonesContract { Ok(()) } } +} // mod contract /// Pays each contributor their share of `milestone.remaining_budget` (the /// unallocated remainder of the pool), computed as