For the complete documentation index, see llms.txt. This page is also available as Markdown.

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

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,
}

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, independent of the tranche split.

VaultPhase

Lifecycle Diagram

See Vault Lifecycle for the full phase reference, including Cancelled (which replaced the old direct-to-Finalized push refund with a pull-based claim_refund per investor).

Functions

Constructor

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 for the worked example.

Events

See Common Types and Events 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

Codes 10, 23, and 24 are intentionally unused (numbering carried over from earlier iterations of the enum).

As of September 2026.

Last updated