> For the complete documentation index, see [llms.txt](https://docs.trilobyte.finance/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.trilobyte.finance/technical-documentation/vault-contract.md).

# Vault Contract

The core lending contract. Each vault represents a single loan — created with negotiated terms, funded by investors, disbursed to the borrower, and repaid through incoming cash-flow payments.

**Source:** `contracts/vault/`

## Source Modules

| File          | Purpose                                                                                                                                     |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `contract.rs` | All public entry points, SEP-41 FungibleToken + FungibleBurnable impls, OZ Upgradeable, inline `GlobalsClient` and `CollateralEscrowClient` |
| `storage.rs`  | Data types, storage keys, getters/setters, phase transitions                                                                                |
| `math.rs`     | Fixed-point integer EMI calculation, split logic, gross-up, late fees                                                                       |
| `errors.rs`   | `#[contracterror]` enum (34 is the highest code)                                                                                            |
| `events.rs`   | `#[contractevent]` structs (20 events)                                                                                                      |
| `token.rs`    | Documentation module — explains the FungibleToken impl approach                                                                             |
| `test.rs`     | 64 contract tests (+ 19 math tests in `math.rs` = 83 total)                                                                                 |

## Data Model

### VaultConfig (21 fields)

```rust
VaultConfig {
    borrower: Address,                    // Business receiving the loan
    lent_token: Address,                  // Token being lent (e.g. USDC)
    manager: Address,                     // Pool Manager who underwrote this vault
    principal: i128,                      // Requested loan amount (7-decimal)
    interest_rate: u32,                   // Annual rate in basis points (1000 = 10%)
    loan_term: u32,                       // Term in months
    split_ratio: u32,                     // % of payments routed to EMI pool (1–99)
    permissioned: bool,                   // Whether only allowlisted investors can deposit
    grace_period: u64,                    // Seconds after due date before default
    phase: VaultPhase,                    // Current lifecycle phase
    created_at: u64,                      // Creation timestamp
    disbursed_at: u64,                    // Disbursement timestamp (0 if not yet)
    funding_deadline: u64,                // Deadline for full funding (0 = no deadline)
    approval_deadline: u64,               // Deadline for manager approval (0 = no deadline)
    collateral_amount: i128,              // Manager collateral snapshotted at creation
    collateral_escrow: Option<Address>,   // Borrower-collateral escrow (None = none)
    collateral_token: Option<Address>,    // Asset the borrower must pledge
    collateral_recovery: Option<Address>, // Receives the pledge on default
    collateral_min: i128,                 // On-chain floor the pledge must meet at disbursement
    pledge_id: u64,                       // Escrow pledge id, recorded at disbursement (0 until then)
}
```

{% hint style="info" %}
`collateral_amount` is the manager (skin-in-the-game) collateral, snapshotted at creation so release/slash always use a fixed amount rather than a later (mutable) global ratio. The `collateral_*` fields concern **borrower-posted** collateral held in the [CollateralEscrow](/technical-documentation/collateral-escrow-contract.md).
{% endhint %}

### VaultPhase

```rust
VaultPhase {
    RaisingFunds = 0,     // Open for investor deposits
    AwaitingApproval = 1, // Fully funded, awaiting manager approval & disbursement
    Active = 2,           // Loan disbursed, borrower is repaying
    FullyRepaid = 3,      // Debt cleared — investors can claim remaining yield
    Finalized = 4,        // Vault closed, all funds distributed
    Renegotiation = 5,    // Borrower proposed new terms — under review
    Defaulted = 6,        // Borrower failed to repay within grace period
}
```

### Lifecycle Diagram

```
RaisingFunds → AwaitingApproval → Active → FullyRepaid
     │                  │            ↕
     │ (funding         │ (approval  Renegotiation
     │  expiry)         │  expiry)
     ▼                  ▼            Active → Defaulted
  Finalized          Finalized
```

The valid transitions enforced by `transition_phase` are:

* `RaisingFunds → AwaitingApproval` (auto, on full funding)
* `AwaitingApproval → Active` (manager approves & disburses)
* `Active → FullyRepaid` (debt cleared)
* `Active → Renegotiation` and `Renegotiation → Active`
* `Active → Defaulted`
* `FullyRepaid → Finalized` (defined as valid, but see note)

{% hint style="warning" %}
**Renegotiation is only reachable from `Active`** — not from `Defaulted` (a defaulted loan has already had its manager collateral slashed, so re-activating would leave it unbacked).

**No function performs `FullyRepaid → Finalized`.** The transition is defined in `transition_phase` but never triggered; the only writes to `Finalized` are the funding- and approval-expiry paths (which set the phase directly: `RaisingFunds → Finalized` and `AwaitingApproval → Finalized`).
{% endhint %}

## Functions

### Constructor

```
__constructor(globals, params: VaultParams)
```

Deploy a new vault. Validates split ratio, principal, term, rate, and amount against Globals bounds; if `collateral_escrow` is set, asserts the other borrower-collateral fields are present. Sets debt-token metadata (SEP-41) and cap (= principal), and snapshots the manager collateral (`principal × default_collateral_ratio / 100`). The admin is set to the Globals admin (governor/timelock) for upgradeability.

### Investor Actions (RaisingFunds)

| Function                     | Description                                                                                                                                                                                                                               |
| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `deposit(investor, amount)`  | Deposit funds; deducts the protocol fee (sent to treasury), credits the net amount, mints debt tokens 1:1 with the net. For permissioned vaults the investor must be allowlisted. Auto-transitions to AwaitingApproval when fully funded. |
| `withdraw(investor, amount)` | Withdraw before fully funded; burns debt tokens.                                                                                                                                                                                          |

### Manager / Borrower Actions

| Function                                               | Description                                                                                                                                                                                                                                                                                                                                                                      |
| ------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `approve_and_disburse(caller, pledge_id: Option<u64>)` | Manager approves the loan and disburses to the borrower; calculates EMI and initialises the schedule. AwaitingApproval → Active. **Borrower-collateral gate:** if the vault has a `collateral_escrow`, `pledge_id` is required and the escrow pledge is verified against this vault (controller, token, borrower, recovery party, and `amount`/live balance ≥ `collateral_min`). |
| `add_to_allowlist(caller, investor)`                   | Add an investor to a permissioned vault's allowlist (manager only).                                                                                                                                                                                                                                                                                                              |
| `remove_from_allowlist(caller, investor)`              | Remove an investor from the allowlist (manager only).                                                                                                                                                                                                                                                                                                                            |

### Payment Actions (Active)

| Function                         | Description                                                                                                                                                                                                                                       |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `receive_payment(payer, amount)` | Accept a **gross** payment (see math below). Splits gross → EMI pool + cash pool, takes the protocol fee from the cash share only, amortises outstanding on the EMI-pool principal component. Transitions to FullyRepaid when `outstanding == 0`. |
| `claim_yield(investor)`          | Claim pro-rata share of the EMI pool (Active or FullyRepaid).                                                                                                                                                                                     |
| `withdraw_cash(amount)`          | Borrower withdraws from the cash pool (Active or FullyRepaid).                                                                                                                                                                                    |

### Deadline Enforcement (Permissionless)

| Function                  | Description                                                                                                                                                           |
| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `check_funding_expiry()`  | Cancel if the funding deadline passed: refund investors, release manager collateral, decrement outstanding, refund any borrower collateral. RaisingFunds → Finalized. |
| `check_approval_expiry()` | Cancel if the approval deadline passed: same teardown. AwaitingApproval → Finalized.                                                                                  |

### Risk & Default (Active)

| Function                 | Description                                                                                                                                                                                                                           |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `apply_late_fee(caller)` | **Manager only.** Applies a late fee when overdue. The penalty rate is **hardcoded at 1800 bps (18% p.a.)**, not read from settings. Increments the missed-payment counter and advances `next_due`.                                   |
| `check_default()`        | Anyone can call. If past the contractual due date + grace period: slash manager collateral to the vault, seize any borrower collateral to the recovery party, mark the manager delinquent, decrement outstanding. Active → Defaulted. |

### Renegotiation (Active only)

| Function                                    | Description                                                                                                            |
| ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `propose_renegotiation(caller, rate, term)` | Manager proposes new terms (from Active only). Saves the previous phase. Active → Renegotiation.                       |
| `approve_renegotiation(caller)`             | Apply new terms, recalculate EMI on the **current outstanding** principal, reset the schedule. Renegotiation → Active. |
| `reject_renegotiation(caller)`              | Cancel the proposal, restore the previous phase (always Active).                                                       |

### Queries

| Function                             | Returns                                                                              |
| ------------------------------------ | ------------------------------------------------------------------------------------ |
| `get_config()`                       | `VaultConfig`                                                                        |
| `get_admin()`                        | OZ admin (governor/timelock) — `Option<Address>`                                     |
| `get_lending_pool()`                 | Total funded amount                                                                  |
| `get_investor_deposit(investor)`     | Individual deposit                                                                   |
| `get_investors()`                    | List of all investors                                                                |
| `get_emi()`                          | Investor installment (EMI) amount                                                    |
| `get_required_payment()`             | Standard-period **gross** payment: `ceil(EMI × 100 / split_ratio)`                   |
| `get_current_period_payment()`       | Gross payment for the current period (smaller on the final stretch); 0 if not Active |
| `get_emi_pool()` / `get_cash_pool()` | Pool balances                                                                        |
| `get_payments_made()`                | Payments-made counter (schedule timing only — does **not** gate completion)          |
| `get_outstanding()`                  | Remaining principal                                                                  |
| `get_next_due()`                     | Next payment due date                                                                |
| `get_claimable(investor)`            | Available yield to claim                                                             |
| `get_late_fees()`                    | Accumulated late fees                                                                |
| `get_missed_payments()`              | Consecutive missed payments                                                          |
| `get_grace_period()`                 | Grace period in seconds                                                              |
| `get_reneg_proposal()`               | Current renegotiation proposal                                                       |
| `get_token_cap()`                    | Debt-token supply cap                                                                |
| `is_allowlisted(investor)`           | Whether an investor is allowlisted                                                   |
| `get_allowlist()`                    | Allowlisted investors                                                                |

## SEP-41 Debt Token

The vault itself is the debt-token contract — it implements OZ `FungibleToken` and `FungibleBurnable` directly. Tokens are minted 1:1 with the **net** deposit and represent the investor's share. The cap equals the principal.

Debt tokens are **non-transferable** — `transfer`, `transfer_from`, and `approve` panic on call. This keeps yield claims tied to the original depositor.

| Function                                             | Description                |
| ---------------------------------------------------- | -------------------------- |
| `total_supply()` / `balance(account)`                | Supply and balance queries |
| `transfer(from, to, amount)`                         | ❌ Disabled — panics        |
| `transfer_from(spender, from, to, amount)`           | ❌ Disabled — panics        |
| `approve(owner, spender, amount, live_until_ledger)` | ❌ Disabled — panics        |
| `allowance(owner, spender)`                          | Allowance query (always 0) |
| `decimals()` → 7                                     | Token decimals             |
| `name()` → "Trilobyte Vault Token"                   | Token name                 |
| `symbol()` → "tVLT"                                  | Token symbol               |
| `burn(from, amount)` / `burn_from(...)`              | Burn tokens                |

## Payment Math (`math.rs`)

All math is integer-only with 10¹² internal scaling.

| Function                                       | Description                                                                                                           |
| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| `calculate_emi(principal, annual_bps, term)`   | Standard amortisation, ceiling-rounded (handles 0% as equal installments)                                             |
| `required_gross_payment(emi_inv, split_ratio)` | **Gross-up:** minimum gross so the EMI-pool share covers a full installment: `ceil(emi_inv × 100 / split_ratio)`      |
| `split_payment(amount, split_ratio)`           | Split a gross payment into EMI portion (`amount × split_ratio / 100`) + cash portion (remainder favours the borrower) |
| `split_emi(outstanding, emi, annual_bps)`      | Break an EMI into interest + principal components                                                                     |
| `total_interest(principal, annual_bps, term)`  | Total interest over the lifetime                                                                                      |
| `total_repayment(principal, annual_bps, term)` | Total borrower repayment                                                                                              |
| `calculate_protocol_fee(amount, fee)`          | Protocol fee (`fee` in 7-decimal format)                                                                              |
| `late_fee(overdue, penalty_bps)`               | One month's penalty interest                                                                                          |

### How `receive_payment` settles a payment

The borrower pays a **gross** amount because investors only receive `split_ratio %` of each payment:

1. **Required gross:** `g_min = required_gross_payment(needed_emi_share, split_ratio)` where `needed_emi_share` is a full investor installment, or — on the final stretch — just enough to clear `outstanding + interest`. A **min-payment tolerance band** is applied: `min_payment = g_min − g_min × min_emi_tolerance / 10_000`. Payments below `min_payment` are rejected (`InvalidAmount`).
2. **Split first:** the gross is split into the EMI-pool share (`split_ratio %`) and the cash share (remainder).
3. **Fee from cash only:** the protocol fee is computed on the gross but **charged to the cash share**, never the EMI/investor share, so investor amortisation is independent of the fee. If the fee would exceed the cash share it is **clamped** and a `ProtocolFeeClamped` event is emitted (only possible if the protocol fee is raised above `(100 − split_ratio) %`).
4. **Amortise on EMI share:** outstanding is reduced by the principal component of the EMI-pool share only — the cash pool is the borrower's working capital, not debt repayment.
5. **Completion** is gated on `outstanding == 0` — **never** on the payments-made counter. Early payments (`now < next_due`) are rejected.

## Events (20)

| Event                          | Key Fields                                                                  | When                      |
| ------------------------------ | --------------------------------------------------------------------------- | ------------------------- |
| `VaultCreated`                 | borrower, principal, loan\_term, interest\_rate                             | Constructor               |
| `InvestorDeposited`            | investor, amount, total\_funded                                             | Deposit                   |
| `InvestorWithdrew`             | investor, amount                                                            | Withdrawal                |
| `VaultFullyFunded`             | total\_funded                                                               | Fully funded              |
| `LoanDisbursed`                | borrower, amount                                                            | Disbursement              |
| `PhaseTransition`              | from, to                                                                    | Phase change              |
| `PaymentReceived`              | payer, total\_amount, emi\_share, cash\_share, payment\_number, outstanding | Payment                   |
| `YieldClaimed`                 | investor, amount                                                            | Claim                     |
| `CashWithdrawn`                | borrower, amount                                                            | Cash withdrawal           |
| `LateFeeApplied`               | fee, total\_late\_fees, missed\_payments                                    | Late fee                  |
| `VaultDefaulted`               | borrower, outstanding, missed\_payments                                     | Default                   |
| `RenegotiationProposed`        | manager, new\_interest\_rate, new\_loan\_term                               | Proposal                  |
| `RenegotiationFinalized`       | new\_interest\_rate, new\_loan\_term, new\_emi                              | Terms applied             |
| `RenegotiationRejected`        | investor                                                                    | Proposal cancelled        |
| `ProtocolFeeCollected`         | fee\_amount, treasury, source                                               | Fee taken                 |
| `ProtocolFeeClamped`           | configured\_fee, charged\_fee                                               | Fee clamped to cash share |
| `InvestorAllowlisted`          | investor                                                                    | Added to allowlist        |
| `InvestorRemovedFromAllowlist` | investor                                                                    | Removed from allowlist    |
| `VaultFundingExpired`          | deadline, total\_funded, refunded\_investors                                | Funding expired           |
| `VaultApprovalExpired`         | deadline, total\_funded, refunded\_investors                                | Approval expired          |

## Error Codes

| Code | Name                         | Description                                                                      |
| ---- | ---------------------------- | -------------------------------------------------------------------------------- |
| 1    | `InvalidPhase`               | Wrong lifecycle phase                                                            |
| 2    | `InvalidAmount`              | Amount out of bounds / below min payment                                         |
| 3    | `InvalidTerm`                | Term outside limits                                                              |
| 4    | `InvalidRate`                | Rate below minimum                                                               |
| 5    | `AlreadyFunded`              | Already fully funded                                                             |
| 6    | `ExceedsPrincipal`           | Deposit exceeds principal                                                        |
| 7    | `NoDeposit`                  | No deposit found                                                                 |
| 8    | `InsufficientDeposit`        | Withdrawing too much                                                             |
| 9    | `NotFullyFunded`             | Not yet fully funded                                                             |
| 10   | `InvalidSplitRatio`          | Not 1–99                                                                         |
| 11   | `AssetNotSupported`          | Token not whitelisted                                                            |
| 12   | `BorrowerNotApproved`        | Caller is not the manager                                                        |
| 13   | `PaymentNotDue`              | Payment not yet due (early payment)                                              |
| 14   | `LoanFullyRepaid`            | All payments made                                                                |
| 15   | `Unauthorized`               | Not authorised                                                                   |
| 16   | `NothingToClaim`             | No yield available                                                               |
| 17   | `InsufficientCashPool`       | Cash pool too low                                                                |
| 18   | `NotOverdue`                 | Can't apply late fee                                                             |
| 19   | `AlreadyDefaulted`           | Already defaulted                                                                |
| 20   | `WithinGracePeriod`          | Too early for default                                                            |
| 21   | `NoProposalExists`           | No renegotiation proposal                                                        |
| 22   | `ProposalAlreadyExists`      | Proposal pending                                                                 |
| 25   | `NotAllowlisted`             | Not on allowlist                                                                 |
| 26   | `AlreadyAllowlisted`         | Already on allowlist                                                             |
| 27   | `FundingDeadlineNotReached`  | Too early                                                                        |
| 28   | `ApprovalDeadlineNotReached` | Too early                                                                        |
| 29   | `NoDeadlineSet`              | No deadline configured                                                           |
| 30   | `TooManyInvestors`           | Max investors (100) reached                                                      |
| 31   | `MathOverflow`               | Arithmetic overflow in a math helper                                             |
| 32   | `CollateralPledgeRequired`   | Collateral-backed vault but no `pledge_id` at disbursement                       |
| 33   | `CollateralPledgeNotFound`   | The supplied `pledge_id` has no active pledge                                    |
| 34   | `CollateralPledgeInvalid`    | Pledge mismatch (controller / token / borrower / recovery / insufficient amount) |

{% hint style="info" %}
Codes 23 and 24 are intentionally unused (the enum jumps from 22 to 25).
{% endhint %}
