diff --git a/README.md b/README.md index 9e06ba0e..052e5caf 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ npm run dev - [Platform Fee & Treasury Split Operations Guide](file:///c:/Users/HP/quicklendx-protocol/docs/contracts/platform-fee-ops.md): Admin operations playbook for managing fee rates, treasury rotation, and revenue splits. - `docs/RUNBOOK_INCIDENT_RESPONSE.md`: Operator playbook for unexpected contract behavior and incident-mode recovery. - `docs/INVESTOR_TIER.md`: How the investor risk score, tier, and investment limit are computed — math, thresholds, and worked examples. +- `docs/KYC.md`: Business KYC vs investor KYC, what each gates. - `quicklendx-contracts/README.md`: Smart contract-specific documentation. - `quicklendx-contracts/docs/contracts/deterministic-time.md`: Smart contract deterministic ledger time semantics. - `quicklendx-backend/README.md`: Backend-specific documentation. diff --git a/docs/KYC.md b/docs/KYC.md new file mode 100644 index 00000000..b595a258 --- /dev/null +++ b/docs/KYC.md @@ -0,0 +1,39 @@ +# KYC Guide for Operators + +This document explains the two types of KYC (Know Your Customer) used in QuickLendX: Business KYC and Investor KYC. As an operator, you will manage both processes to ensure platform compliance. + +## Business KYC +Business KYC is required for businesses that want to create invoices on the platform. It gates the ability to list invoices and receive funds. + +**What it gates:** +- Creating new invoices +- Receiving funds from investors +- Updating business profile details + +**Concrete Example:** +When a new business signs up, they cannot list an invoice until their KYC status is updated to `Verified`. +```json +{ + "business_id": "B-12345", + "kyc_status": "Verified", + "max_invoice_limit": 50000 +} +``` + +## Investor KYC +Investor KYC is required for users who want to fund invoices. It gates the ability to place bids and earn yields. + +**What it gates:** +- Placing bids on invoices +- Withdrawing funds +- Viewing detailed business financials + +**Concrete Example:** +An investor attempting to place a bid of $10,000 on an invoice will be blocked if their KYC status is `Pending`. +```json +{ + "investor_id": "I-67890", + "kyc_status": "Verified", + "investment_limit": 100000 +} +``` diff --git a/quicklendx-contracts/src/profits.rs b/quicklendx-contracts/src/profits.rs index a78efb9e..5a097f0e 100644 --- a/quicklendx-contracts/src/profits.rs +++ b/quicklendx-contracts/src/profits.rs @@ -558,6 +558,15 @@ pub fn compute_yield(amount: i128, rate_bps: u32, duration_days: u32) -> i128 { numerator / (BPS_DENOMINATOR * 365) } +/// Compute the expected return on a principal amount. +/// +/// # Returns +/// Total expected return (principal + yield) +pub fn compute_expected_return(amount: i128, rate_bps: u32, duration_days: u32) -> i128 { + let yield_amount = compute_yield(amount, rate_bps, duration_days); + amount.max(0).saturating_add(yield_amount) +} + // ============================================================================ // Tests // ============================================================================ diff --git a/quicklendx-contracts/src/test_compute_yield_props.rs b/quicklendx-contracts/src/test_compute_yield_props.rs index 927b9b93..9328f119 100644 --- a/quicklendx-contracts/src/test_compute_yield_props.rs +++ b/quicklendx-contracts/src/test_compute_yield_props.rs @@ -12,7 +12,7 @@ #[cfg(all(test, feature = "fuzz-tests"))] mod test_compute_yield_props { - use crate::profits::compute_yield; + use crate::profits::{compute_expected_return, compute_yield}; use proptest::prelude::*; // Ranges chosen to stay well inside i128 bounds while exercising realistic @@ -100,5 +100,21 @@ mod test_compute_yield_props { prop_assert_eq!(compute_yield(amount, rate_bps, 0), 0, "zero duration must yield 0"); } + // ── Expected Return ──────────────────────────────────────────────── + + #[test] + fn expected_return_is_non_negative_and_bounded( + amount in 0i128..MAX_AMOUNT, + rate_bps in 0u32..=MAX_RATE_BPS, + duration_days in 0u32..=MAX_DURATION_DAYS, + ) { + let er = compute_expected_return(amount, rate_bps, duration_days); + prop_assert!(er >= 0, "expected return must be non-negative"); + + // Expected return must be bounded by the return at MAX_RATE_BPS + let max_er = compute_expected_return(amount, MAX_RATE_BPS, duration_days); + prop_assert!(er <= max_er, "expected return must be bounded by MAX_RATE"); + prop_assert!(er >= amount, "expected return must be >= amount"); + } } }