Glossary
Names for the same thing, reconciled. This page exists because the NAV work surfaced three recurring failures: a Korean term in a meeting whose code name nobody could produce, an intermediate value in a formula that had no name at all, and — worst — one word used for two genuinely different quantities.
Use this page when writing a formula, a column name, or investor copy. It is a lookup table, not a design document: every entry points at the page that owns the concept.
⚠️ Terms that collide — resolve these first
Five words that have meant more than one thing in our own documents. Each row is the resolution, not a suggestion.
분모 / "denominator"
There are three, they are different formulas, and unifying them is a regression. This is the single most consequential collision on this page. ⚠️ And the yield one is a trap twice over: it excludes pool-held LP, which is what v3-104 Option A also did — but Option A meant "escrowed LP earns nothing" while v3-131 (3) means "it earns, and the booking is deferred to the escrow's boundary". Same expression, opposite rule. Read the Status column, not the formula.
| Context | Denominator | Decision | Status |
|---|---|---|---|
Ordinary NAV (nav_per_token) | totalSupply, read from pools.lp_total_supply (the indexer's on-chain mirror) | R9 | ✅ shipped — nav-formula.ts:242 divides by it, and usableTotalSupply declines to price the pool when the mirror is NULL or 0 rather than falling back |
| Yield accrual (recognised debt) | totalLpSupply − poolHeldLp — pool-held LP is parked, not waived: its entitlement is booked per request when the escrow ends | v3-131 (3) | ✅ shipped 2026-08-20. ⚠️ Same expression as v3-104 Option A but a different rule — Option A destroyed the accrual, this defers booking it. previewEscrowAccrual(requestId) is the deferred part |
| Wind-down NAV | claimingSupply = totalSupply − settledUnclaimedLp — settled-but-unclaimed only, pending/rolled-over LP stays in | R10 | ✅ shipped — deployed 2026-08-04, new pools only (see 06 → R10) |
If someone says "the denominator," ask which one. total_deposited is not a denominator anywhere after R9 — it survives only as the numerator's principal term.
The numerator's principal term: docs say total_deposited, code reads pools.tvl
Every NAV formula on these pages writes the numerator's principal as total_deposited. The code divides pools.tvl (nav-formula.ts:66-68, nav-suggest.ts:89). Read them as the same term, and read total_deposited as a label, not a column — pools has no such column, and the one that exists (pool_chain_deployments.total_deposited) has no writer anywhere.
The distinction is load-bearing, not cosmetic: pools.tvl is NET of redemption payouts (complete_redemption_atomic does tvl = tvl − payout), which is exactly what makes R9 invariant under an exit. A redemption of T tokens takes the numerator to V − T·p and the denominator to S − T, so the price is unchanged. A cumulative figure that "only grows" would keep the exited investor's principal in the numerator while their tokens left the denominator, inflating NAV on every exit. So where a decision card says total_deposited "only grows", the label is wrong and the column is right.
🔴 Decided 2026-08-27 (v3-153) (not yet reflected — the deployed engine does not work this way). Two decisions fix what "principal" means for interest accrual:
- Accrual does not track NAV — interest is charged on principal, not on the written-down value. The basis in IFRS 9 §5.4.1(b) is that impairment reduces the lender's interest revenue recognition to amortised cost, while the borrower's contractual interest obligation is unchanged. What this system keeps is the partner's liability ledger, so it follows the obligation.
- That principal is paid-in capital — money in minus money returned (
totalDeposited) — not the LP token count. The deployed engine accrues off the LP count.
totalSubscription turns up in older text as a fourth. It is a v3-16 leftover with no code behind it; do not revive it.
한 달 / "month"
There are three time bases, they measure different things, and unifying them is a regression (v3-124). The question "is one month 28 days or 30?" has no single answer because the three axes below are anchored to three different mechanisms, only one of which the word "month" should be used for at all.
| Axis | Base | How to say it | Code |
|---|---|---|---|
| Epoch redemption cycle | fixed 28 / 84 days | "every 28 days" — never "monthly" | epoch_duration_days; PoolCommonLib.epochFundingDateAt = previous funding date + that many days |
| Yield distribution | calendar month, 12 a year | "monthly" | computeNextYieldDue uses addUtcMonths with an end-of-month clamp (yield-schedule.ts) |
| Lock-up · maturity · duration display | 30 days (1 month = ×30) | "N months" = N × 30 days | firstYieldIntervalDays (30 / 90); investor PDP renders days / 30; backend CUSTOM months × 30 |
| Instant redemption | no cycle | settles per request | unit is not involved |
So: 28 exists in exactly one place (the epoch cycle, as a day count), and every value called a "month" is 30. The cadence selector in admin create therefore reads Every 28 days, which is what frees the word "month" to mean one thing everywhere else.
Why the cycle is a fixed day count. The contract does no calendar arithmetic at all — it advances by epochDurationDays × 1 days. 28 and 84 are multiples of 7, so every derived boundary (window open, cutoff, funding date) keeps the same weekday forever, which is what makes "the window opens on a Monday" something an operator can plan around. See 07 → Model B.
Do not "unify" these — two specific regressions
Do not lower firstYieldIntervalDays to 28. It is not a definition of "month"; it is the floor that guarantees a pool's first yield distribution has already happened before a lock-up lifts, which is what gives a YIELD_BASED penalty something to take. It tracks the calendar-month axis, where the first distribution lands 30-31 days out. At 28 the lock-up would end two days early with accrued_yield still at 0, so an early exit forfeits nothing — the exact hole v3-84 was created to close. Enforced in pools.post.create.ts and pools.patch.update.ts, mirrored in the admin wizard.
Do not change the epoch cadence to 30 / 90. It buys nothing and costs three things: 30 mod 7 = 2 and 90 mod 7 = 6, so weekday stability is gone and windows start drifting onto weekends; 90 sits exactly on MAX_EPOCH_DURATION_DAYS, leaving no headroom; and setEpochSchedule is create-only, so pools already deployed at 28 keep it and the estate splits. It would also reverse v3-93 without making the set uniform anyway, since yield would still be on the calendar.
Full unification is not available. It would require the yield axis to become a fixed day count too — 13 payouts a year on a date that drifts every period, which month-end NAV, statements and accounting cycles cannot absorb. DPD is not a blocker either way: it is already counted in days (30 / 60 / 90 DPD), which argues for day counts rather than against them.
완충자금 / "buffer"
Two funds, opposite roles. The word alone is ambiguous, so qualify it every time.
| Term | Means | Absorbs loss? |
|---|---|---|
| buffer / 파트너 완충자금 / manager equity buffer | The fund manager's own first-loss capital (R6). buffer_rate_bps / buffer_direction / buffer_basis are the computable form; equity_buffer_rule is its prose disclosure | ✅ yes — this is first-loss for standalone pools |
| reserve / 리저브 | reserve_bps share routed to external reserve_wallet | ❌ no (R8) — redemption liquidity only. It is investor money already inside the priced claim |
The old docs called the reserve a "first-loss buffer," which is what produced the double-count R8 removed. When you mean the manager's capital, say buffer; when you mean the deposit share routed to the reserve wallet, say reserve. Never "reserve buffer."
A rate alone does not describe the layer (migration 0118). Three columns do, and two of them decide things the rate cannot:
| Column | Values | What it settles |
|---|---|---|
buffer_rate_bps | 0–10000, 0 on every pool today | The size of the layer. At 0 the term vanishes |
buffer_direction | FIRST_LOSS (default) / EXCESS | Who takes the loss. 5% cap, 8% loss: investors take 3% under FIRST_LOSS, 5% under EXCESS |
buffer_basis | GROSS (default) / NET | Whether the reported cumulative_loss is before the partner's absorption or already after it. NET forces the cap to 0 — deducting twice is R8 one layer up |
equity_buffer_rule is subordinate to them, not parallel: 0121 refuses the prose unless buffer_rate_bps > 0, so the investor line "Manager first-loss commitment" cannot appear on a pool where nothing absorbs.
손실 / "loss" — the currency nobody stated
The costliest collision of the review, because both sides read as plain numbers. The partner reports in its own currency (Joob: EFIDR); the pool's book is USD.
| Term | Currency | Note |
|---|---|---|
cumulative_loss, total_subscribed, total_outstanding_principal | The fund's | Everything arriving in external_pool_data_snapshots |
total_deposited, uncoveredLoss, buffer | The pool's | Everything inside the NAV formula |
The only sanctioned crossing is a ratio, never an exchange rate (R7): lossRatioFromFundReport() = cumulative_loss ÷ total_subscribed, both from the same report, so the currency cancels and the result is dimensionless. Apply that to the pool's book.
Never cross it with pools.fx_rate
An FX conversion produces a believable number and puts a currency move straight back inside NAV. It also produced the live defect: rupiah subtracted from dollars returned NAV −1782, clamped to the floor, and the only NAV proposal dev ever generated was a total-loss write-down on a pool whose loans were all performing. fx_rate is for display conversion only.
회차추가분 / epoch top-up
Both names are real on-chain and mean different scopes:
| Name | Scope |
|---|---|
epochFundTopUp[epochId] | Per-cycle mapping — what the partner funded for one epoch |
totalEpochTopUp | Scalar mirror of the sum of unconsumed epochFundTopUp[*], because a mapping cannot be iterated on-chain |
The wind-down numerator uses totalEpochTopUp; the settlement spend order draws epochFundTopUp[id] first. Not interchangeable.
정산 NAV / settlement NAV
| Name | Where |
|---|---|
settleNav | On-chain local + the EpochSettled event's third argument (of five) |
settled_nav | The DB mirror of that value |
Same number, two spellings by layer. Neither is nav_at_request (instant pools) or navPerToken (the live price).
Scale axes — decimals, not currency
Three numeric scales are live at once, and they are orthogonal to the currency question above. Every amount is on exactly one.
| Axis | Decimals | Where | Convert with |
|---|---|---|---|
Raw (RawAmount) | The stablecoin's own (USDC = 6) | Anything that physically transfers | toRaw / fromRaw |
Normalized (NormalizedAmount) | 18 | The pool's internal ledger counters | toNormalized / fromNormalized / fromChainNormalized |
NAV price (NavPrice) | 6 (NAV_PRECISION = 1e6) | Per-token price only | toNavPrice / fromNavPrice |
These are branded TypeScript types (apps/infra/lib/shared/contract/units.ts) and a build guard (scripts/check-onchain-units.mjs) refuses casts outside that module. A cast asserts an axis; only a converter changes one.
The mistake the brands prevent
reserveBalance() returns a normalized counter. The indexer divided it by 1e6 as if it were raw, so a pool holding $2.10 mirrored as $2,100,000,000,000. It went unnoticed for weeks because the indexer was also dead. Same 6-vs-18 confusion as v3-101, opposite direction.
Liquidity buckets — disjoint by construction
The invariant lives in PlatformPoolStorage:
physicalBalance >= reserveBalance + redemptionCommitted + unclaimedYield| Term (KO) | Code name | Unit | Meaning |
|---|---|---|---|
| 리저브 · 상환 유동성 | reserve_wallet / reserveWallet | external wallet balance | In the current external-reserve implementation, the reserve_bps share of each deposit goes to reserve_wallet and the remainder goes to fund_wallet. The in-pool reserve counter and getter are removed. The ratio specifies routing, not retention. An external balance does not automatically fund a pool payout. |
| 회차 추가분 | epochFundTopUp[id] / totalEpochTopUp | Normalized | See epoch top-up above |
| 정산 예약분 | redemptionCommitted | Normalized | Settled-but-unclaimed redemption debt, in USD. Claims draw only from here |
| 정산 미청구 LP | settledUnclaimedLp | LP tokens | The token twin of the above. Added by R10 |
| 미분배 수익 | unclaimedYield | Normalized | ⚠️ Misleading name. Yield deposited but not yet distributed — depositYield credits it (YieldLib.sol:122), settleYield debits it (:179). It is not "distributed, nobody claimed"; that is pendingYield[investor], a separate obligation (see below) |
There is a fifth obligation the four-term form leaves out
SUM(pendingYield[investor]) — yield already distributed to holders and not yet claimed. unclaimedYield does not cover it (it was debited at distribution), yet the money is in the pool and owed. The Foundry invariant invariant_backingCoversObligations counts all five; the storage comment above counts four. Anything computing "free balance" from the four-term form is overstating it — see the 🔴 clamp note in 23-money-path §6.
The buckets are already net of redemptionCommitted
_reserveFilledGross debits the settled gross out of the three liquidity buckets and parks it in redemptionCommitted. Subtracting it from their sum removes the same dollars twice — which executeWindDown did, returning max(0, 50 − 100) = 0 for a pool holding $50 of unclaimed liquidity.
Redemption vocabulary
| Term (KO) | Code name | Meaning |
|---|---|---|
| 즉시 상환 | epoch_duration_days = 0 | Priced and settled per request |
| 회차 상환 | epoch_duration_days > 0 | Batched, pro-rata, forward-priced, pull-claim |
| 회차 | currentEpochId | One settlement cycle |
| 이월 수요 | epochCarryDemandLp | Unfilled demand carried forward. Served before new demand |
| 신규 수요 | epochNewDemandLp | Demand from this cycle's vintage |
| 체결 비율 | epochNewFillRatio / carry ladder | Share of demand one tier filled |
| 락업 | lockup_days | Redemption fully blocked. Independent of penalty type |
| 조기 이탈 | EARLY | Lockup ended, maturity not reached. Penalty may apply |
| 패널티 | penalty_rate_bps | Deducted from principal, paid to fund_wallet |
Ledger vocabulary
Two words carry most of the money-flow architecture, and neither is self-explanatory. Full model: 23-money-path §9.
fold
reduce, in the functional-programming sense: apply every event in order to produce a value. foldLedger(events, config) turns money_events into a balance.
The word is doing work as the opposite of increment, and the difference is what the whole record model rests on:
| increment | fold | |
|---|---|---|
| The write | tvl = tvl + 100 | tvl = f(every event) |
| Applied twice | 200 — wrong | the same answer |
| What is true | the number | the events; the number summarises them |
| A wrong rule | cannot be undone — the inputs are gone | fix the rule, fold again, history corrects |
| Checkable? | no; there is nothing to compare against | yes — re-fold and compare |
The folded value IS stored (money_pool_state, portfolio_positions) because recomputing on every read would be slow. The point is that the stored value is a rebuild, never an adjustment, so it can be thrown away and regenerated. scripts/money/replay.ts does exactly that and reports differences; a difference means either the fold is missing a rule or something other than the fold wrote the column.
fast path
The prompt of the two write paths into the ledger. Named against the sweep, which is the other.
| Trigger | Reads | |
|---|---|---|
| fast path | a handler that already knows the transaction hash | that one receipt, immediately |
| sweep | EventBridge, ~2 min | eth_getLogs over a block range |
When an investor deposits, the browser posts the transaction hash — at that moment we already know which transaction it is, so there is no reason to wait for a sweep to find it.
The redundancy is deliberate. A failed fast path is not an error the caller reports: the money is on-chain either way and the sweep will pick it up, and answering "deposit failed" to a confirmed transaction would be worse than the delay. Neither path needs to know what the other did, because UNIQUE (chain_id, tx_hash, log_index) decides what is new.
Ratios are basis points
Stored, transmitted and computed in bps. 1% = 100 bps, 100% = 10000 bps. Covers reserve_bps, redemption_gating_bps, penalty_rate_bps, buffer_rate_bps and net_yield_fee_config.*_bps.
Percent exists in exactly two places, both one-way: admin input (typed as %, converted once on save) and display (formatBpsAsPercent). Never store or transmit percent.
⚠️ apy_rate is the exception — it is a percent number, so comparing it against perf_hurdle_bps needs apy_rate × 100.
🔴 Decided 2026-08-27 (v3-154) (not yet reflected — no migration yet, apy_rate is still NOT NULL): the two rate fields become exclusive by accrual mode. A FIXED pool carries only accrual_rate_bps and leaves apy_rate empty; a TARGET pool carries only apy_rate. The sentence above therefore stops holding for FIXED pools, where the rate that matters is accrual_rate_bps — already bps, so no × 100. Feeding a hurdle comparison the empty apy_rate instead would silently compare against 0.
한글 ↔ 공식 이름 (symbols used in the NAV review)
The shorthand from the NAV working document, mapped to the names that appear in code, schema, and these docs. Use the right-hand column in anything durable — the symbols are for whiteboards.
| Symbol | Korean / meaning | Official name |
|---|---|---|
nav · S | 토큰 1개 가격 · 토큰 총량 (상환 시 감소) | nav_per_token · totalSupply |
Dep | 총 예치액 — the numerator's principal term, no longer a denominator (R9) | total_deposited |
R · R_avail | reserve 잔액 · 라이브 온체인 reserve — liquidity, not loss absorption | reserveBalance / pools.reserve_balance |
A · T | 예치 금액 · 토큰 수 | amount · tokens |
loss_cum · U_cum | 실현 손실 누계 (write-off 원금) · 흡수층이 못 막은 누적 손실 | cumulative_loss · uncoveredLoss |
B | first-loss buffer 한도 — the manager's equity buffer (R6). A cap, not a depleting balance | bufferCap from buffer_rate_bps (equity_buffer_rule = its prose form) |
E_b · ρ_b | 버킷 노출 · 상각률 — deferred automation template only (v3-13 OJK ladder), not live policy | exposure · rate |
C | reserve 흡수분 — retired by R8; always 0 (NO_RESERVE_CONSUMED) | reserveConsumed / nav_history.reserve_consumed |
U · 첨자 ⱼ | NAV로 넘어간 손실 · Junior 값 | uncovered · Junior tranche |
f · A_e · D_e | epoch 체결 비율 · 지급 가능 자금 · 요청 토큰 총량 | fillRatio · available · demand |
nav_req / nav_set / nav_wd | 요청 / 정산 / 청산 시점 NAV | nav_at_request · settleNav/settled_nav · wind-down navPerToken |
pen | 페널티 — goes to fund_wallet, never the reserve (v3-85) | penalty_amount |
fx(t) | 환율 — not an input to NAV (R7); applied at redemption | fx_rate / fx_rate_source |
FV · RNI · NPL · OUT | 펀드 총가치 · 실현 순수익 · 연체 잔액 · 미상환 원금 | partner template fields → external_pool_data_snapshots |
Data invariant the partner feed must satisfy: FV = Dep + RNI − NPL, and utilization = OUT / Dep.
Intermediate values that had no name
These appear in formulas and in conversation but were never named, which is how the same quantity ended up computed two ways. Names are now fixed.
| Name | Definition | Notes |
|---|---|---|
bufferAbsorbed | min(cumulativeLoss, bufferCap) — cumulative loss the manager's buffer has taken | ⚠️ Display only. See the warning below before using it in a formula |
netYield | gross − platform take − SPC mgmt − pool mgmt − perf fee − WHT — the amount actually credited per share | The figure settleYield's net leg carries. Distinct from gross (what depositYield receives) |
nplBalance | Exposure in buckets at or past npl_threshold_days that is not yet written off | On-book, unrealized. Feeds npl_ratio; never moves NAV (R5.5) |
depositCum | Cumulative deposits — the same figure as Dep / total_deposited | Named only to stop "cumulative deposits" reading as a second, different number |
settledUnclaimedLp | LP backing payouts that settled but were never claimed. + at settlement, − at claim/burn | The R10 counter. Deployed 2026-08-04 (commit 0abe168, new factory), new pools only — PlatformPoolStorage.sol:227, exposed by a same-named getter that reverts on a pre-deploy clone |
bufferCap | total_deposited × buffer_rate_bps / 10000, forced to 0 when buffer_basis = NET | Rate 0 ⇒ cap 0 ⇒ no buffer, which is every pool until a rate is confirmed |
claimingSupply | totalSupply − settledUnclaimedLp | Wind-down denominator only. Never the ordinary NAV one |
lossRatio | cumulative_loss ÷ total_subscribed, both from the same partner report | Dimensionless by construction — see 손실 |
distributable | reserveBalance + totalEpochTopUp | What an epoch settlement can actually pay. Already net of redemptionCommitted |
announcedNav | hasPendingNavUpdate ? pendingNav : navPerToken | PoolCommonLib.effectiveNav(). The price quoted during the 24h notice window by deposit (PlatformPool.sol:756), the instant reserve gate + request snapshot (RedemptionLib.sol:279, :327), epoch settlement (:762) and reinvest (YieldLib.sol:299) — reinvest from the 2026-08-06 implementation, the rest from 2026-08-04, and a Clones pool keeps whichever it was born with (R2·R3) |
bufferBalance as a depleting counter is a double-count
The tempting stateful form — bufferBalance = bufferCap − bufferAbsorbed, then uncovered = loss − bufferBalance — subtracts the absorbed amount twice: at loss = cap = 100 it returns 100 uncovered when the correct answer is 0. The formula is absolute, so the buffer needs no state at all: uncovered = max(0, loss − cap) under FIRST_LOSS, min(loss, cap) under EXCESS. bufferAbsorbed exists to show an operator how much of the layer is used, and a regression test pins the wrong version out.
NPL vs write-off — one dollar is never both
The most common ingestion error, so it belongs here as well as in 06:
- NPL (
npl_threshold_days, default 90) — the loan is non-performing but still on the book. Unrealized. Counts toward outstanding principal, feeds the risk badge, does not touch NAV. - Write-off (
write_off_policy, SEA P2P typically ~180 DPD) — the fund removes the loan from the book. Realized loss (cumulative_loss), and the only thing that moves NAV.
Aset mirrors what the fund reports and authors no provisioning curve of its own (R5).
The delinquency side has its own names, all of them signals rather than NAV inputs:
| Term (KO) | Code name | Unit | Meaning |
|---|---|---|---|
| 연체일수 | dpd_days | Days | Days past due on a loan |
| 연체 구간 | external_pool_dpd_buckets.lower_days | Days | Bucket lower bound. A dynamic array, not a fixed 30/60/90 |
| NPL 임계 | pools.npl_threshold_days | Days | At or past this bucket a loan is non-performing. Default 90 |
| NPL 비율 | npl_ratio | Ratio | Non-performing exposure ÷ outstanding principal |
| 미상환 잔액 | total_outstanding_principal | Fund's currency | The NPL denominator |
Status labels — built ≠ live
One label, because we kept re-litigating the same question in review: is this mechanism real?
| Label | Means | Does not mean |
|---|---|---|
| Dormant lever | The switch exists on-chain, is tested, and works — but no product surface pulls it, so the behaviour below it never runs on a live pool | Removed (the code is there) · broken (it works) · automatic (someone must pull it) |
| Retired / removed | The code is gone. See below | — |
| 🟡 Deploy-pending | Written and merged, not yet on-chain | Unbuilt |
Read a dormant lever as contract capability, not as live behaviour. Its counters read 0 forever, so any formula containing one is effectively that formula minus the term.
There is no live instance today. The label was written for the remainder hold-back (fundingRestricted / heldFundReleases), which was fully implemented and fuzz-tested on-chain with every dependent surface built and idle — and was removed in 0183 rather than wired up (v3-112). It is kept as a label because the situation it names recurs: a capability can outlive the decision to use it, and the honest thing is to say so rather than let a formula imply it is running.
Retired terms
Present in older documents and in the codebase's history. Each one now has exactly one correct replacement.
| Term | Status |
|---|---|
보류 자금 / hold-back (heldFundReleases, fundingRestricted) | Removed in 0183 (v3-112). The partner remainder always transfers to fund_wallet at deposit; reserveBalance is the pool's only liquidity bucket |
pool_type (AS_POOL / FUND_POOL) | Removed (v3-01). Branch on fund_id |
lp_issuance_model / FUND_ISSUED | Removed (v3-02). LP is always platform-issued |
DISTRESSED | Removed. Use ACTIVE + writedown, or IMPAIRED |
| Reserve as "first-loss" | Retired (R8). Redemption liquidity only |
reserve_consumed | Always 0. The contract reverts on any non-zero value |
totalSubscription as a NAV denominator | v3-16 leftover, no code behind it |
recovery_flag | Added and dropped inside two days. A NAV increase takes the approve button and nothing else |
| Escrow contract · Receipt NFT · D+7 refund | Removed in v3.0 |
See also: Writedown & NAV (the model these terms describe) · Core Concepts · Money Path (wallet naming) · v3-109 (the decisions this page reflects).