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
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,
}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)
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
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)
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
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)
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
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
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):
Reserve fee → treasury
Senior interest
Senior principal
Junior interest
Junior principal
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
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
As of September 2026.
Last updated