From 2603657e0575fe24a3b3e0f0e486d2a0b0b95bf9 Mon Sep 17 00:00:00 2001 From: vida Date: Fri, 19 Jun 2026 12:55:31 +0100 Subject: [PATCH] docs(investment): add InvestmentStatus lifecycle guide, state transition diagram and code doc comments --- .../docs/investment-lifecycle.md | 100 ++++++++++++++++++ quicklendx-contracts/src/investment.rs | 25 +++-- quicklendx-contracts/src/types.rs | 8 +- 3 files changed, 125 insertions(+), 8 deletions(-) create mode 100644 quicklendx-contracts/docs/investment-lifecycle.md diff --git a/quicklendx-contracts/docs/investment-lifecycle.md b/quicklendx-contracts/docs/investment-lifecycle.md new file mode 100644 index 00000000..10121a29 --- /dev/null +++ b/quicklendx-contracts/docs/investment-lifecycle.md @@ -0,0 +1,100 @@ +# Investment Lifecycle and State Machine + +This developer guide describes the lifecycle of an investment in the QuickLendX protocol, detailing the `InvestmentStatus` state machine, legal transitions enforced by `InvestmentStatus::validate_transition()`, driving entrypoints, and coupling to invoice and escrow states. + +## Overview + +The `Investment` state tracks an investor's funded position in the protocol. It is governed by a strict state machine to prevent orphaned active investments, double-refunds, and other transition exploits. + +## Investment Statuses + +`InvestmentStatus` is defined as: + +```rust +pub enum InvestmentStatus { + Active, + Withdrawn, + Completed, + Defaulted, + Refunded, +} +``` + +* **`Active`**: The investment is active, funded, and tracked in the active investment index. This is the only non-terminal state. +* **`Withdrawn`** (Terminal): The investment has been withdrawn by the investor. +* **`Completed`** (Terminal): The associated invoice has been successfully settled/paid in full, and the investor has received their expected principal and returns. +* **`Defaulted`** (Terminal): The associated invoice has failed to settle within its grace period, transitioning the investment to a defaulted state and triggering insurance payouts. +* **`Refunded`** (Terminal): The associated invoice/escrow has been cancelled or refunded, returning the investment funds to the investor. + +--- + +## State Diagram + +```mermaid +stateDiagram-v2 + [*] --> Active : accept_bid_and_fund + + state Active { + [*] --> ActiveState + } + + Active --> Completed : settlement + Active --> Refunded : refund_escrow_funds + Active --> Defaulted : default handling + Active --> Withdrawn : withdrawal + + Completed --> [*] + Refunded --> [*] + Defaulted --> [*] + Withdrawn --> [*] + + note right of Active : Only non-terminal state + note right of Completed : Terminal state + note right of Refunded : Terminal state + note right of Defaulted : Terminal state + note right of Withdrawn : Terminal state +``` + +--- + +## Legal Transitions + +All state transitions are validated through `InvestmentStatus::validate_transition()`. + +* **`Active`** is the only state that can transition. It can transition to any of the terminal states: `Completed`, `Defaulted`, `Refunded`, or `Withdrawn`. +* All other states (`Completed`, `Defaulted`, `Refunded`, `Withdrawn`) are **terminal** and immutable. No transitions from these states are permitted. + +| From State | To State | Driving Entrypoint / Trigger | Description | +| :--- | :--- | :--- | :--- | +| **`Active`** | `Completed` | **`settlement`** (`settle_invoice` / `process_partial_payment` finalization) | Full invoice settlement is completed. Funds are distributed to investor (return) and platform (fees). | +| **`Active`** | `Refunded` | **`refund_escrow_funds`** | Escrow funds are refunded back to the investor. | +| **`Active`** | `Defaulted` | **`default handling`** (`handle_default` / `mark_invoice_defaulted`) | Invoice due date + grace period expires. Triggers default handling and deactivates/claims insurance policies. | +| **`Active`** | `Withdrawn` | **`withdrawal`** (e.g. withdrawal/cancellation flow simulation) | Investor initiates withdrawal of their investment funds. | +| **`Completed`** | *(none)* | *(immutable)* | Terminal state. | +| **`Refunded`** | *(none)* | *(immutable)* | Terminal state. | +| **`Defaulted`** | *(none)* | *(immutable)* | Terminal state. | +| **`Withdrawn`** | *(none)* | *(immutable)* | Terminal state. | + +--- + +## Status Coupling + +The investment state is coupled directly to the corresponding `InvoiceStatus` and escrow state: + +| Investment Status | Invoice Status | Escrow Status / State | Description | +| :--- | :--- | :--- | :--- | +| **`Active`** | `Funded` | `Held` | The bid has been accepted and escrow is funded. | +| **`Completed`** | `Paid` | `Released` (or empty) | Invoice is fully paid, and escrow is released to the business owner. | +| **`Refunded`** | `Refunded` | `Refunded` | Escrow is returned to the investor, and invoice status is updated to `Refunded`. | +| **`Defaulted`** | `Defaulted` | `Held` (or claimed/empty) | Grace period expired. Escrow is frozen or processed, and insurance claims are paid. | +| **`Withdrawn`** | `Cancelled` | `Refunded` | Invoice is cancelled, and investment funds are withdrawn/refunded. | + +--- + +## Storage & Invariants + +1. **Active Investment Index (`act_inv`)**: + * Contains ONLY investments with `status == InvestmentStatus::Active`. + * During any transition leaving the `Active` state, the investment is atomically removed from this index via `remove_from_active_index()`. +2. **Orphan Prevention**: + * `validate_no_orphan_investments()` runs checks to ensure that all investments in the active index are indeed in the `Active` state, protecting against index drift. diff --git a/quicklendx-contracts/src/investment.rs b/quicklendx-contracts/src/investment.rs index c5cc89b7..34d3c439 100644 --- a/quicklendx-contracts/src/investment.rs +++ b/quicklendx-contracts/src/investment.rs @@ -39,19 +39,30 @@ impl InvestmentStatus { /// Terminal states are immutable. Once an investment reaches Completed, /// Defaulted, Refunded, or Withdrawn, no further transition is permitted. /// + /// Detailed state machine design and couplings are documented in + /// [investment-lifecycle.md](file:///Users/backenddevopsdeveloper/Downloads/DRIPS/vida-quicklendx-protocol/quicklendx-contracts/docs/investment-lifecycle.md). + /// /// ### Allowed transitions - /// | From | To | - /// |-----------|----------------------------------| - /// | Active | Completed, Defaulted, Refunded, Withdrawn | - /// | Withdrawn | (terminal - no further moves) | - /// | Completed | (terminal) | - /// | Defaulted | (terminal) | - /// | Refunded | (terminal) | + /// | From | To | Driving Entrypoint | + /// |-----------|----------------------------------|--------------------| + /// | Active | Completed, Defaulted, Refunded, Withdrawn | accept_bid_and_fund -> Active;
settlement -> Completed;
refund_escrow_funds -> Refunded;
default handling -> Defaulted;
withdrawal -> Withdrawn | + /// | Withdrawn | (terminal - no further moves) | - | + /// | Completed | (terminal) | - | + /// | Defaulted | (terminal) | - | + /// | Refunded | (terminal) | - | /// /// ### Security /// Calling code **must** invoke this before persisting a status change so /// that no path (settlement, default, refund, or future code) can produce /// an orphan `Active` investment or an impossible backward transition. + /// + /// # Arguments + /// * `from` - The current status of the investment. + /// * `to` - The target status for the transition. + /// + /// # Returns + /// * `Ok(())` if the transition is legal. + /// * `Err(QuickLendXError::InvalidStatus)` if the transition is invalid. pub fn validate_transition( from: &InvestmentStatus, to: &InvestmentStatus, diff --git a/quicklendx-contracts/src/types.rs b/quicklendx-contracts/src/types.rs index 7c542f36..d8b38d9d 100644 --- a/quicklendx-contracts/src/types.rs +++ b/quicklendx-contracts/src/types.rs @@ -35,14 +35,20 @@ pub enum BidStatus { Cancelled, } -/// Investment status enumeration +/// Investment status enumeration tracking the lifecycle of investor positions. #[contracttype] #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum InvestmentStatus { + /// The investment is active, funded, and tracked in the active investment index. + /// This is the only non-terminal state. Active, + /// Investment funds were withdrawn by the investor (terminal status). Withdrawn, + /// Investment completed successfully, and the investor has received their payouts (terminal status). Completed, + /// The associated invoice defaulted due to non-payment, triggering default/insurance logic (terminal status). Defaulted, + /// Investment was refunded due to invoice cancellation (terminal status). Refunded, }