> 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 with **two investor tranches** — created with negotiated terms, funded by junior then senior investors, disbursed to the borrower, and repaid through a strict payment waterfall.

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

## Source Modules

| File          | Purpose                                                                                                     |
| ------------- | ----------------------------------------------------------------------------------------------------------- |
| `contract.rs` | All public entry points, SEP-41 FungibleToken + FungibleBurnable impls, OZ Upgradeable                      |
| `storage.rs`  | Data types, storage keys, getters/setters, phase transitions                                                |
| `math.rs`     | Fixed-point integer amortisation, tranche-target derivation, tranche-share splitting, write-down, late fees |
| `errors.rs`   | `#[contracterror]` enum (56 is the highest code)                                                            |
| `events.rs`   | `#[contractevent]` structs, `("trilo", name, schema_version)` topics                                        |
| `token.rs`    | Documentation module — explains why the FungibleToken impls live in `contract.rs`                           |
| `test.rs`     | Contract + math tests, including the worked example and the waterfall invariants                            |

## Data Model

### VaultConfig

```rust
VaultConfig {
    borrower: Address,
    lent_token: Address,
    manager: Address,
    principal: i128,
    interest_rate: u32,        // Annual rate in basis points
    loan_term: u32,            // Term in months
    leverage_ratio: u32,       // x100 (e.g. 400 = 4.00x). Replaces split_ratio.
    junior_spread_bps: u32,    // Snapshotted from Globals at creation
    reserve_fee_bps: u32,      // Snapshotted from Globals at creation
    writedown_grace_days: u32, // Snapshotted from Globals at creation
    writedown_max_days: u32,   // Snapshotted from Globals at creation
    period_seconds: u64,       // Snapshotted from Globals' monthly_emi_interval at creation
    reporting_interval: u64,   // Seconds; 0 = no reporting covenant
    junior_target: i128,       // ceil(principal / (leverage_ratio/100 + 1))
    senior_target: i128,       // principal - junior_target
    permissioned: bool,
    grace_period: u64,
    phase: VaultPhase,
    created_at: u64,
    disbursed_at: u64,
    funding_deadline: u64,
    approval_deadline: u64,
    collateral_amount: i128,          // Manager collateral snapshot
    collateral_escrow: Option<Address>,
    collateral_token: Option<Address>,
    collateral_recovery: Option<Address>,
    collateral_min: i128,
    pledge_id: Option<u64>,
    max_investors: u32,        // Snapshotted from Globals' max_investors_per_vault
    related_party: bool,       // Manager-declared disclosure flag, immutable
}

Tranche { Senior = 0, Junior = 1 }

InvestorPosition {           // keyed by (investor, tranche); an investor may hold both
    tranche: Tranche,
    deposit: i128,
    claimed_interest: i128,
    claimed_principal: i128,
}

Arrears {                    // carried shortfall for waterfall buckets 1-5
    reserve: i128,
    senior_interest: i128,
    senior_principal: i128,
    junior_interest: i128,
    junior_principal: i128,
}

CovenantStatus {
    missed_payments: u32,
    reporting_overdue: bool,
    borrower_current: bool,
}
```

{% 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, and credited toward `junior_target`. The `collateral_*` fields concern **borrower-posted** collateral held in the [CollateralEscrow](/technical-documentation/collateral-escrow-contract.md), independent of the tranche split.
{% endhint %}

### VaultPhase

```rust
VaultPhase {
    RaisingFunds = 0, AwaitingApproval = 1, Active = 2, FullyRepaid = 3,
    Finalized = 4, Renegotiation = 5, Defaulted = 6, Cancelled = 7,
}
```

### Lifecycle Diagram

```mermaid
stateDiagram-v2
  [*] --> RaisingFunds
  RaisingFunds --> AwaitingApproval: both tranches reach target
  RaisingFunds --> Cancelled: check_funding_expiry
  AwaitingApproval --> Active: approve_and_disburse
  AwaitingApproval --> Cancelled: check_approval_expiry
  Cancelled --> Finalized: finalize (all claim_refund'd)
  Active --> FullyRepaid: both outstanding reach 0
  Active --> Renegotiation: propose_renegotiation
  Renegotiation --> Active: approve_renegotiation / reject_renegotiation
  Active --> Defaulted: check_default
  FullyRepaid --> Finalized: finalize
  Defaulted --> Finalized: finalize (post distribute_recovery)
```

See [Vault Lifecycle](/protocol-mechanics/vault-lifecycle.md) for the full phase reference, including [Cancelled](/protocol-mechanics/vault-lifecycle/cancelled.md) (which replaced the old direct-to-Finalized push refund with a pull-based `claim_refund` per investor).

## Functions

### Constructor

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

Deploy a new vault. Validates leverage ratio, principal, term, rate, and amount against Globals bounds; if `collateral_escrow` is set, asserts the other borrower-collateral fields are present. Derives `junior_target`/`senior_target` from `leverage_ratio`. Snapshots `junior_spread_bps`, `reserve_fee_bps`, `writedown_grace_days`, `writedown_max_days`, `period_seconds`, and `max_investors` from the live Globals settings. Sets debt-token metadata (SEP-41) and cap (= principal), and snapshots the manager collateral. The admin is set to the Globals admin (governor/timelock) for upgradeability.

### Capital Raise (RaisingFunds)

| Function                              | Description                                                                                                                                                                                                                                                                                                                                 |
| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `deposit(investor, amount, tranche)`  | Deposit into a tranche; deducts the protocol fee, mints debt tokens 1:1 net. Senior deposits before `lock_junior` fail with `JuniorNotLocked`; a deposit above a tranche's target fails with `TrancheFull`; below `min_investment` fails with `BelowMinimumInvestment`. Auto-transitions to AwaitingApproval when both tranches hit target. |
| `lock_junior(caller)`                 | Manager-only. Succeeds once `junior_raised + collateral_amount >= junior_target`. Junior deposits stay open afterward.                                                                                                                                                                                                                      |
| `withdraw(investor, amount, tranche)` | Withdraw before fully funded; burns debt tokens. Junior withdrawals fail with `JuniorLocked` once `lock_junior` has been called.                                                                                                                                                                                                            |
| `check_funding_expiry()`              | Permissionless. Releases manager collateral and any pledge once; moves to Cancelled.                                                                                                                                                                                                                                                        |
| `claim_refund(investor, tranche)`     | Pull your own refund after Cancelled; burns your own debt tokens.                                                                                                                                                                                                                                                                           |
| `bump_investor_ttls(start, count)`    | Permissionless, paginated TTL keep-alive for investor position records.                                                                                                                                                                                                                                                                     |

### Manager / Borrower Actions

| Function                                                                         | Description                                                                                                                                                                                                                        |
| -------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `approve_and_disburse(caller, pledge_id: Option<u64>)`                           | Manager approves and disburses to the borrower; calculates the shared installment and initialises the schedule. AwaitingApproval → Active. Verifies any collateral pledge and the compliance gate for the borrower, if configured. |
| `record_collateral_pledge(pledge_id)`                                            | Anyone, once — links an already-locked escrow pledge to this vault ahead of disbursement.                                                                                                                                          |
| `check_approval_expiry()`                                                        | Permissionless. Same teardown as funding expiry. AwaitingApproval → Cancelled.                                                                                                                                                     |
| `add_to_allowlist(caller, investor)` / `remove_from_allowlist(caller, investor)` | Manager manages a permissioned vault's allowlist.                                                                                                                                                                                  |

### Payment Actions (Active)

| Function                         | Description                                                                                                                                                                                                                                                            |
| -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `receive_payment(payer, amount)` | Accept **any** `amount > 0`; allocates through the six-bucket waterfall, arrears-first. Verified against the vault's own token-balance delta (`TransferShortfall` on a short-delivering token). Transitions to FullyRepaid when both tranches' outstanding reach zero. |
| `claim_yield(investor, tranche)` | Claim pro-rata share of that tranche's interest + principal pools (Active or FullyRepaid).                                                                                                                                                                             |
| `withdraw_cash(amount)`          | Borrower withdraws from the cash pool; blocked while `missed_payments > 0` (`BorrowerNotCurrent`).                                                                                                                                                                     |
| `apply_late_fee(caller)`         | Manager only. Reads `late_fee_bps` live from Globals; splits the fee across senior/junior arrears pro rata. At most once per still-open period (`LateFeeAlreadyApplied`).                                                                                              |
| `mark_report_received(caller)`   | Manager clears the reporting-overdue flag.                                                                                                                                                                                                                             |
| `check_reporting()`              | Anyone, after the reporting deadline — flags `reporting_overdue`.                                                                                                                                                                                                      |

### Default & Recovery

| Function                            | Description                                                                                                                                                                                                                                |
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `check_default()`                   | Anyone. Past the contractual due date + grace period: slash manager collateral into the vault, seize any borrower pledge, mark the manager delinquent, decrement outstanding. Active → Defaulted.                                          |
| `distribute_recovery()`             | Permissionless, once per Defaulted vault (`RecoveryAlreadyDistributed` on a second call). Allocates the vault's full balance: senior principal → senior interest → junior principal → junior interest → residual (manager, then borrower). |
| `claim_recovery(investor, tranche)` | Pro-rata claim from that tranche's recovery pool; junior structurally cannot recover before senior's pool is covered.                                                                                                                      |

### Renegotiation (Active only)

| Function                                                          | Description                                                                                                                                                                                 |
| ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `propose_renegotiation(caller, new_interest_rate, new_loan_term)` | Manager proposes new terms, bounded by `min_interest_rate`/`max_interest_rate` and `max_rate_cut_bps` below the vault's current rate (`RateCutTooLarge`).                                   |
| `approve_renegotiation(caller, borrower)`                         | **Both** `caller` (the manager) and `borrower` must `require_auth()`. Recomputes the shared installment from current total outstanding, resets the schedule anchor. Renegotiation → Active. |
| `reject_renegotiation(caller)`                                    | Manager cancels the proposal, restores Active.                                                                                                                                              |

### Finalization & Views

| Function                                | Description                                                                                                                                                         |
| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `finalize()`                            | Closes FullyRepaid (cash pool empty, all yield claimed), Defaulted (recovery distributed and fully claimed), or Cancelled (all refunds claimed) into **Finalized**. |
| `get_senior_nav()` / `get_junior_nav()` | Outstanding principal after the linear write-down, junior-first. View only.                                                                                         |
| `get_covenant_status()`                 | `CovenantStatus { missed_payments, reporting_overdue, borrower_current }`                                                                                           |

### Queries

| Function                                                                           | Returns                                          |
| ---------------------------------------------------------------------------------- | ------------------------------------------------ |
| `get_config()`                                                                     | `VaultConfig`                                    |
| `get_admin()`                                                                      | OZ admin (governor/timelock) — `Option<Address>` |
| `get_lending_pool()`                                                               | Total funded amount                              |
| `get_junior_raised()` / `get_senior_raised()`                                      | Raised per tranche                               |
| `is_junior_locked()`                                                               | Whether `lock_junior` has been called            |
| `get_position(investor, tranche)`                                                  | `InvestorPosition`                               |
| `get_investors()`                                                                  | List of all `(Address, Tranche)` pairs           |
| `get_emi()`                                                                        | Shared installment amount                        |
| `get_arrears()`                                                                    | `Arrears` — carried shortfall per bucket         |
| `get_outstanding()` / `get_outstanding_senior()` / `get_outstanding_junior()`      | Remaining principal, total and per tranche       |
| `get_cash_pool()`                                                                  | Borrower cash pool balance                       |
| `get_payments_made()`                                                              | Fully-paid periods counter                       |
| `get_next_due()`                                                                   | Next payment due date                            |
| `get_claimable(investor, tranche)`                                                 | Available yield to claim, per tranche            |
| `get_late_fees()`                                                                  | Accumulated late fees                            |
| `get_missed_payments()`                                                            | Consecutive missed payments                      |
| `get_late_fee_charged_until()`                                                     | Guard value preventing a double late-fee charge  |
| `get_grace_period()`                                                               | Grace period in seconds                          |
| `get_reneg_proposal()`                                                             | Current renegotiation proposal, if any           |
| `get_token_cap()`                                                                  | Debt-token supply cap                            |
| `is_allowlisted(investor)` / `get_allowlist(start, count)` / `get_allowlist_len()` | Permissioned-vault allowlist, paginated          |

## 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, regardless of tranche — **tranche identity lives in `InvestorPosition`, not in the token balance.** The cap equals the principal.

Debt tokens are **non-transferable** — `transfer`, `transfer_from`, and `approve` panic on call.

## Payment Waterfall (`math.rs`)

All math is integer-only with 10¹² internal scaling. `receive_payment` accepts any `amount > 0`. Per period, buckets fill in this order (arrears from a prior period are added ahead of new dues, same order):

1. Reserve fee → treasury
2. Senior interest
3. Senior principal
4. Junior interest
5. Junior principal
6. Residual → borrower cash pool

Fresh dues for a new period accrue exactly once — when the period has started (`now >= next_due`) **and** no arrears are currently outstanding — so repeated payments against the same still-open period never double-count a period's interest. The schedule (`payments_made`, `next_due`) only advances once buckets 1–5 are fully cleared for that period. Senior/junior principal splits floor junior's share and give the exact remainder to senior. See [Payments & the Waterfall](/protocol-mechanics/payments-and-emi.md) for the worked example.

## Events

See [Common Types and Events](/technical-documentation/common-types-and-events.md) for the full, generated field-level schema of every vault event (`VaultCreated`, `InvestorDeposited`, `InvestorWithdrew`, `JuniorLocked`, `VaultFullyFunded`, `LoanDisbursed`, `PhaseTransition`, `PaymentReceived`, `YieldClaimed`, `CashWithdrawn`, `LateFeeApplied`, `VaultDefaulted`, `RecoveryDistributed`, `RecoveryClaimed`, `RenegotiationProposed`, `RenegotiationFinalized`, `RenegotiationRejected`, `ProtocolFeeCollected`, `ProtocolFeeClamped`, `InvestorAllowlisted`, `InvestorRemovedFromAllowlist`, `VaultFundingExpired`, `VaultApprovalExpired`, `RefundClaimed`, `ReportReceived`, `ReportingOverdue`).

## Error Codes

| Code | Name                                  | Description                                                              |
| ---- | ------------------------------------- | ------------------------------------------------------------------------ |
| 1    | `InvalidPhase`                        | Wrong lifecycle phase                                                    |
| 2    | `InvalidAmount`                       | Amount out of bounds                                                     |
| 3    | `InvalidTerm`                         | Term outside limits                                                      |
| 4    | `InvalidRate`                         | Rate below minimum                                                       |
| 5    | `AlreadyFunded`                       | Already fully funded                                                     |
| 6    | `ExceedsPrincipal`                    | Deposit would exceed principal                                           |
| 7    | `NoDeposit`                           | No deposit found                                                         |
| 8    | `InsufficientDeposit`                 | Withdrawing too much                                                     |
| 9    | `NotFullyFunded`                      | Not yet fully funded                                                     |
| 11   | `AssetNotSupported`                   | Token not whitelisted                                                    |
| 12   | `BorrowerNotApproved`                 | Caller is not the manager                                                |
| 13   | `PaymentNotDue`                       | Payment not yet due                                                      |
| 14   | `LoanFullyRepaid`                     | Both tranches already repaid                                             |
| 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` 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 / amount)                 |
| 35   | `ProtocolPaused`                      | The protocol is paused in Globals                                        |
| 36   | `CashPoolNotEmpty`                    | `finalize` requires the cash pool empty                                  |
| 37   | `EmiPoolNotFullyClaimed`              | `finalize` requires every tranche pool fully claimed                     |
| 38   | `PledgeAlreadyRecorded`               | `record_collateral_pledge` already called                                |
| 39   | `JuniorNotLocked`                     | Senior deposit before `lock_junior`                                      |
| 40   | `TrancheFull`                         | Deposit would exceed a tranche's target                                  |
| 41   | `BorrowerNotCurrent`                  | `withdraw_cash` blocked while behind on payments                         |
| 42   | `RecoveryAlreadyDistributed`          | `distribute_recovery` already called                                     |
| 43   | `ReportingNotOverdue`                 | No reporting covenant due, or covenant not configured                    |
| 44   | `JuniorAlreadyLocked`                 | `lock_junior` called twice                                               |
| 45   | `JuniorNotFunded`                     | Junior + collateral credit below `junior_target`                         |
| 46   | `NotCompliant`                        | Compliance gate rejected the caller                                      |
| 47   | `RecoveryNotDistributed`              | `claim_recovery`/`finalize` before `distribute_recovery`                 |
| 48   | `ManagerJuniorShareExceeded`          | Manager's own junior capital exceeds its capped share of `junior_target` |
| 49   | `CollateralAssetNotWhitelisted`       | Pledged/staked asset not on the collateral whitelist                     |
| 50   | `InsufficientBorrowerCollateralRatio` | Verified pledge below `principal × min_borrower_coll_ratio_bps`          |
| 51   | `BelowMinimumInvestment`              | Deposit below the Globals-configured minimum                             |
| 52   | `LateFeeAlreadyApplied`               | Late fee already charged against the current missed period               |
| 53   | `JuniorLocked`                        | Junior withdrawal attempted after locking                                |
| 54   | `InterestRateBelowMinimum`            | Renegotiated rate below the Globals minimum                              |
| 55   | `RateCutTooLarge`                     | Renegotiated rate cuts too far below the current rate                    |
| 56   | `TransferShortfall`                   | The transfer into the vault delivered less than requested                |

{% hint style="info" %}
Codes 10, 23, and 24 are intentionally unused (numbering carried over from earlier iterations of the enum).
{% endhint %}

*As of September 2026.*
