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
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
37 changes: 27 additions & 10 deletions contracts/common/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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};

Expand All @@ -8,55 +15,56 @@ 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<K>(env: &Env) -> Option<Address>
where
K: AdminKey + IntoVal<Env, Val>,
{
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<K>(env: &Env) -> Option<Address>
where
K: OracleKey + IntoVal<Env, Val>,
{
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<K>(env: &Env) -> Option<Address>
where
K: TreasuryKey + IntoVal<Env, Val>,
{
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<K>(env: &Env) -> Option<u32>
where
K: FeeBpsKey + IntoVal<Env, Val>,
Expand All @@ -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
Expand Down Expand Up @@ -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<K>(env: &Env, key: &K)
where
K: IntoVal<Env, Val>,
Expand Down
2 changes: 2 additions & 0 deletions contracts/common/src/split.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)>,
}

Expand Down
20 changes: 16 additions & 4 deletions contracts/escrow/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -717,6 +728,7 @@ impl EscrowContract {
Ok(())
}
}
} // mod contract

pub(crate) fn require_admin(env: &Env) -> Result<Address, Error> {
mergefi_common::require_admin::<DataKey>(env).ok_or(Error::NotInitialized)
Expand Down
34 changes: 30 additions & 4 deletions contracts/maintenance-pool/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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()
Expand All @@ -404,46 +414,55 @@ impl MaintenancePoolContract {
Ok(())
}

/// Returns the maintenance pool record for `pool_id`.
pub fn get_pool(env: Env, pool_id: u64) -> Result<MaintenancePool, Error> {
env.storage()
.persistent()
.get(&DataKey::Pool(pool_id))
.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<Deposit, Error> {
env.storage()
.persistent()
.get(&DataKey::Deposit(pool_id, index))
.ok_or(Error::DepositNotFound)
}

/// Returns the admin address stored in instance storage.
pub fn get_admin(env: Env) -> Result<Address, Error> {
env.storage()
.instance()
.get(&DataKey::Admin)
.ok_or(Error::NotInitialized)
}

/// Returns the treasury address stored in instance storage.
pub fn get_treasury(env: Env) -> Result<Address, Error> {
mergefi_common::require_treasury::<DataKey>(&env).ok_or(Error::NotInitialized)
}

/// Returns the oracle address stored in instance storage.
pub fn get_oracle(env: Env) -> Result<Address, Error> {
env.storage()
.instance()
.get(&DataKey::Oracle)
.ok_or(Error::NotInitialized)
}

/// Returns the fee basis points stored in instance storage.
pub fn get_fee_bps(env: Env) -> Result<u32, Error> {
mergefi_common::get_fee_bps::<DataKey>(&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();
Expand All @@ -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();
Expand All @@ -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()
Expand All @@ -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()
Expand All @@ -482,6 +507,7 @@ impl MaintenancePoolContract {
Ok(())
}
}
} // mod contract

fn require_admin(env: &Env) -> Result<Address, Error> {
mergefi_common::require_admin::<DataKey>(env).ok_or(Error::NotInitialized)
Expand Down
Loading
Loading