# Knox Protocol — Product Description ## What is Knox? Knox lets you choose exactly how much risk you want to take on DeFi yield. Every Knox pool takes a single yield source — like a Morpho vault or Aave market — and splits its returns into a menu of risk/reward positions. You pick the one that matches your appetite: - **Want safety?** Deposit into the **senior tranche** and lock in a guaranteed fixed rate (e.g., 5% APY). You get paid first, no matter what happens to the market. You only lose money in a catastrophic scenario where even junior depositors are fully wiped out. - **Want moderate risk with a known ceiling?** Pick a **spectrum tranche** — say 8% or 12% APY cap. You earn up to that rate, with downside protection proportional to your position on the risk ladder. Lower caps are safer; higher caps offer more upside but absorb more losses. When surplus yield is available, spectrum tranches also participate in risk-weighted surplus sharing. - **Want maximum upside?** Deposit into the **junior tranche**. You absorb losses first, but you capture all the excess yield that nobody else is entitled to. If the market crushes it, you keep everything above the senior rate and spectrum caps — after earning your hurdle rate. **How it works in practice:** A pool opens, you deposit into your chosen position, and your capital goes to work in the underlying market. When the pool matures (e.g., after 30 days), the protocol runs a waterfall: senior gets paid first, then spectrum tranches from lowest to highest cap, then surplus is distributed among spectrum and junior via risk-weighted participation, and finally junior gets the rest. You withdraw your share. That's it. Your position is an ERC-20 token, so you can transfer it or trade it on secondary markets before maturity — even though you can't withdraw from the pool early. Spectrum tranches are created on demand. You don't have to pick from a pre-set menu — any rate on the grid between the senior rate and the maximum is available. The first person to deposit at a new grid point creates that tranche; everyone after deposits into the same vault. Use the [Spectrum Simulator](https://simulator.knox.finance/) to model pool mechanics, test yield scenarios, and see how the settlement waterfall distributes value across tranches under different market conditions. --- ## Technical Overview Knox is an on-chain **structured finance protocol** that splits yield from an underlying DeFi market into risk-tiered tranches. Each pool has a **senior tranche** with a fixed, guaranteed APY, a **junior tranche** that absorbs risk in exchange for uncapped residual yield, and — between the two — any number of lazily-created **spectrum tranches**, each with a specific APY cap chosen from a configurable grid. Each pool is a finite-duration instrument. It opens for deposits, deploys all capital into a single yield-bearing market (Morpho, Aave, Strata/Ethena, or any ERC4626 vault), and at maturity distributes proceeds according to a fixed priority waterfall. Capital is locked for the pool duration — there are no early withdrawals, though the tranche tokens are ERC-20s and can be transferred or traded on secondary markets. Every pool is fully independent: its own accountant, its own tranche vaults, its own allocator, its own parameters. There is no cross-pool interaction. --- ## The Tranches ### Senior Tranche - Receives a **fixed, pre-agreed APY** (`rSenior`, e.g., 5%) on their deposit regardless of what the underlying market actually earns. - Has **absolute priority** over pool assets at settlement. All subordinate tranches must be fully wiped out before the senior takes any loss. - Senior deposits are **capacity-constrained** — the protocol only allows as much senior capital as the combined spectrum + junior value can insure (see Collateralization section). Capacity also **decays over time** based on `cCapReductionFactor`. **Senior share pricing**: When a user deposits `assets` into the senior tranche, they receive `compoundAssets(assets, rSenior, secondsRemaining, dPeriod)` shares. The compounding factor is `(1 + rSenior/10000)^(timeRemaining / SECONDS_PER_YEAR)` computed via PRB Math's UD60x18 fixed-point exponentiation. This means: - A deposit of 100 USDC at pool start with 5% APY for a 365-day pool yields `100 * 1.05 = 105` shares. - A deposit of 150 USDC at day 183 with 182 days remaining yields `150 * 1.05^(182/365) ~ 153.70` shares. At settlement, the senior tranche receives exactly `seniorTrancheCurrentValue()` worth of assets, and total senior shares always equals this value. So 1 share = 1 asset at maturity — each depositor gets back their deposit compounded at the guaranteed rate for the time they were in the pool. ### Spectrum Tranches Spectrum tranches sit between senior and junior. Each has a **capped APY** chosen from a grid defined by `rSenior`, `rMaxSpectrum`, and `spectrumGridStep`. For example, with `rSenior = 500` (5%), `rMaxSpectrum = 2000` (20%), and `spectrumGridStep = 50` (0.5%), the available grid points are 5.5%, 6.0%, 6.5%, ... 19.5%, 20.0%. - **Lower-cap spectrum tranches** are safer — they get paid before higher-cap ones in the waterfall, and their collateral factor means they absorb less loss. - **Higher-cap spectrum tranches** offer more upside potential but absorb more losses. - Spectrum vaults are **created lazily** — the first deposit at a given grid point deploys a new TrancheVault clone via EIP-1167. Subsequent deposits at the same rate use the existing vault. - Spectrum deposits are **capacity-constrained** — each spectrum tranche has a capacity limit derived from the collateral backing of higher-risk tranches (see Collateralization section). Capacity also decays over time based on `cCapReductionFactor`. - Spectrum deposits can be **cut off** via `cSpectrumDepositCutoff` — a configurable cutoff expressed in basis points of elapsed pool duration. For example, `7500` means spectrum deposits close after 75% of the pool has elapsed. Set to `0` to disable. - When surplus yield exists beyond all caps, spectrum tranches **participate in surplus sharing** proportional to their risk (collateral factor), weighted by `kSurplusParticipation` and `nSurplusParticipation` (see Surplus Participation section). **Spectrum share pricing**: Uses the same compounding model as senior — `compoundAssets(assets, apyCap, secondsRemaining, dPeriod)`. Early depositors get more shares, correctly encoding time-value. At maturity, 1 share = 1 asset if the cap is reached. Each spectrum tranche also uses piecewise compounding with per-tranche snapshots (`_yieldLastSpectrumDeposit`, `_timeLastSpectrumDeposit`) to prevent rate inflation from late depositors. ### Junior Tranche - Receives **variable, residual yield**: everything remaining after senior, spectrum tranches, surplus participation, and the protocol fee. - Acts as the **ultimate loss buffer**: if the market underperforms, junior absorbs losses first, before any impact ripples up through spectrum tranches to senior. - In exchange for this risk, juniors capture all upside above the highest spectrum cap, after earning their hurdle rate. - Junior deposits can optionally be **role-gated** via `juniorDepositRole` — when set, only addresses holding the specified role in the factory's AccessController can deposit into the junior tranche. **Junior share pricing**: Works like a standard ERC4626 vault — shares are proportional to the current junior tranche value. `calculateJuniorShares(assets) = (assets * totalSupply) / juniorTrancheCurrentValue()`. First depositor receives `assets + 1` shares (bootstrap shares to prevent the ERC4626 inflation/donation attack vector). **Junior tranche value is always a residual**: computed via the full waterfall — it is whatever remains after senior, protocol fee, all spectrum payouts, and surplus participation. --- ## Pool Lifecycle ``` DEPLOYED --> ACTIVE --> REDEEMED --> SETTLED ``` ### 1. DEPLOYED The `SpectrumFactory` clones an accountant, a senior vault, and a junior vault via EIP-1167 minimal proxies. `initialize()` is called in the same transaction, which sets all parameters and immediately transitions the pool to ACTIVE. Spectrum vaults are NOT created at this stage — they are deployed lazily by the accountant on first deposit at each grid point. No user actions occur in this state. ### 2. ACTIVE Deposits are open. There are three deposit paths, listed from most common to least: **Router deposit (primary path)**: The `KnoxRouter` is the standard user-facing entry point. It provides dedicated functions for each tranche type, with both standard ERC20 approval and Permit2 signature variants: - `depositSeniorSpectrum(accountant, assets, receiver)` — deposits into the senior tranche - `depositSpectrumTranche(accountant, apyBps, assets, receiver)` — deposits into a spectrum tranche at a specific APY cap - `depositJuniorSpectrum(accountant, assets, receiver)` — deposits into the junior tranche Each has a Permit2 variant (e.g., `depositSeniorSpectrumPermit2`) that uses Permit2 signatures instead of on-chain approvals. The router flow: pull assets from user to router -> approve the accountant -> call the accountant's deposit function -> reset approval to 0. **Accountant-driven deposit (secondary path)**: The `SpectrumAccountant` can be called directly with three deposit functions: - `depositSenior(assets, receiver)` — validates senior capacity, computes senior shares, pulls assets from user to allocator, invests via `allocator.deposit()`, and mints shares on the senior vault using `accountantMint()`. - `depositSpectrum(apyBps, assets, receiver)` — validates grid alignment and spectrum deposit cutoff, checks spectrum capacity, creates the spectrum vault if new, computes shares, pulls/invests assets, and mints shares. - `depositJunior(assets, receiver)` — checks junior deposit role authorization, computes junior shares, pulls/invests assets, and mints shares on the junior vault. All three follow the same internal flow: pull assets from user -> transfer to allocator -> `allocator.deposit()` -> mint shares via `vault.accountantMint()`. **Direct vault deposit (ERC4626 path)**: Users can also call `vault.deposit(assets, receiver)` on any existing tranche vault. The vault transfers assets to the allocator, then calls `accountant.registerDeposit()` to invest. Shares are calculated via `accountant.previewDeposit()`. This path cannot create new spectrum vaults — only the accountant (via router or direct call) can do that. **Constraints during ACTIVE**: - Senior deposits are capped by `seniorCapacity()` (see Collateralization). - Spectrum deposits are capped by `spectrumCapacity(apyBps)` per tranche (see Collateralization). - Total deposits across all tranches are capped by `cMaxTotalDeposits` (set to `type(uint256).max` if initialized with 0). - Deposits are blocked after maturity (`block.timestamp >= tStart + dPeriod`). - Spectrum deposits are blocked after the cutoff (`block.timestamp >= tStart + dPeriod * cSpectrumDepositCutoff / 10000`), if `cSpectrumDepositCutoff > 0`. - Junior deposits are blocked after the cutoff (`block.timestamp >= tStart + dPeriod * cJuniorDepositCutoff / 10000`), if `cJuniorDepositCutoff > 0`. - Spectrum rates must be on the grid: `rSenior < apyBps <= rMaxSpectrum` and `(apyBps - rSenior) % spectrumGridStep == 0`. - Junior deposits may require role authorization when `juniorDepositRole` is set. - Withdrawals are not permitted. ### 3. REDEEMED After maturity, anyone can call `redeemShares(shares)` (permissionless). This function: - Accepts a specific share amount, or `0` to redeem the entire allocator position. - Can be called multiple times while the pool is ACTIVE and matured, allowing batched redemptions. - For **synchronous** allocators: calls `allocator.redeem()` and receives assets immediately. - For **asynchronous** allocators: calls `allocator.requestRedeem()`, receives an opaque `bytes32 exitHandle` and possibly some immediate assets. The handle is added to an enumerable tracking set (`pendingExitHandles`). Once all market shares have been redeemed (or requested), the pool transitions to REDEEMED. If there are no pending async exits, it automatically calls `settlePool()` and moves directly to SETTLED. For async markets, the pool stays in REDEEMED while cooldowns elapse. Anyone can call `claimRedeemedAssets()` (permissionless) to iterate over pending exit handles, claim any that have become claimable, and remove completed ones from tracking. Once `pendingExitCount` reaches 0 and no market shares remain, settlement is triggered automatically. `maxPendingExitUnlockAt()` returns the latest cooldown timestamp across all pending exits — useful for UIs to estimate when settlement will be possible. **Edge case**: If the accountant holds zero assets when `settlePool()` is called, it returns without settling and remains in REDEEMED, allowing a later rescue or claim call to provide the assets. ### 4. SETTLED `settlePool()` executes the settlement waterfall (see below) and transitions to SETTLED. After this: - Each tranche vault holds its final asset balance. - The vaults behave as standard ERC4626 — users call `tranche.redeem(shares, receiver, owner)` to withdraw their proportional share of the tranche's assets. - The last redeemer of each tranche receives the entire remaining balance (not a calculated amount) to prevent rounding dust from being trapped. --- ## Settlement Waterfall At maturity, assets are distributed in a multi-phase priority system with floors, caps, and surplus participation. ``` Withdrawn Assets | Yield > 0? --Yes--> Deduct Protocol Fee + Curator Fee | | No v | distributable v | distributable | | | v v Step 1: Reserve Floors | Covers all floors? | YES --> Each tranche gets its floor --> Step 2: Distribute Surplus Top-Down | NO --> Allocate floors pro-rata --> No surplus to distribute Step 2: Distribute Surplus Top-Down | Senior: up to compounded value | Spectrum (lowest to highest): up to capped return | Remaining pool > 0? | YES --> Surplus participation enabled? | | | YES --> Junior hurdle first, then risk-weighted sharing | | | NO --> Junior gets all remaining | NO --> Done ``` ### Phase 1: Fees Before any tranche distribution, the protocol fee and curator fee are deducted from total yield: ``` totalYield = withdrawnAssets - totalDeposits (floored at 0) protocolFee = totalYield * cProtocolFee / 10000 (only if applyProtocolFees is enabled) curatorFee = totalYield * cCuratorFee / 10000 (only if curatorFeeBeneficiary is set) distributable = withdrawnAssets - protocolFee - curatorFee ``` Both fees are taken on **total pool yield** (not just junior/spectrum profit), and are deducted upfront before the waterfall runs. The protocol fee goes to `protocolFeeBeneficiary`; the curator fee goes to `curatorFeeBeneficiary`. This means the fee cost is effectively borne by the riskiest positions (highest spectrum + junior) since they receive whatever remains after safer tranches are paid. ### Phase 2: Floor-Based Waterfall The remaining `distributable` amount is allocated via a two-step process: **Step 1 — Reserve floors**: Each tranche has a protected floor based on its collateral factor: - Senior floor = `totalDeposits` (senior always gets at least its deposits back, if possible) - Spectrum floor = `deposits * (1 - collateralFactor / 10000)` — the portion of deposits NOT pledged as collateral - Junior floor = `deposits * (1 - cCollateralFactorJunior / 10000)` — typically 0 when `cCollateralFactorJunior = 10000` (100%) If the distributable amount covers all floors, each tranche receives at least its floor. If not, floors are allocated pro-rata based on their relative size. **Step 2 — Distribute surplus top-down**: After floors are reserved, remaining surplus is distributed from safest to riskiest: 1. **Senior** gets up to its full compounded value (`seniorTrancheCurrentValue()`) 2. **Spectrum (lowest rate first)** — each gets up to its capped return: `compoundAssets(deposits, apyCap, elapsed, dPeriod)` 3. **Remaining pool** — handled by surplus participation (see below) ### Phase 3: Surplus Participation After the waterfall distributes up to each tranche's cap, any remaining pool assets are allocated based on whether surplus participation is enabled (`kSurplusParticipation > 0`) and whether junior deposits exist: **If junior deposits exist and surplus participation is enabled:** 1. **Junior hurdle**: Junior first earns up to `rJuniorHurdle` on its deposits before any sharing occurs. The hurdle value is `compoundAssets(juniorDeposits, rJuniorHurdle, elapsed, dPeriod)`. The gap between the hurdle value and junior's current waterfall value is filled first. 2. **Risk-weighted surplus sharing**: Remaining surplus after the hurdle is distributed among all spectrum tranches and junior using **participation units**. Each tranche's participation unit is: ``` M_i = 1 + k * (CF_i / 10000)^n PU_i = twDeposits_i * M_i share_i = PU_i / sum(PU) * surplus ``` Where `k = kSurplusParticipation`, `n = nSurplusParticipation`, and `CF_i` is the tranche's collateral factor. Higher-risk tranches (higher CF) get a larger multiplier, meaning they receive a proportionally larger share of surplus. Junior receives any rounding remainder. The participation basis (`twDeposits_i`) uses **time-weighted deposits** rather than raw deposits. Each deposit is scaled down based on how late in the pool's duration it arrives — early depositors receive full weight while late depositors receive progressively less. This prevents late capital from extracting a disproportionate share of surplus that was primarily generated by capital deployed earlier in the pool. **If junior deposits exist but surplus participation is disabled:** Junior gets all remaining pool assets. **If no junior deposits exist:** Remaining pool assets are sent to `protocolFeeBeneficiary` as swept surplus. **Outcome scenarios:** | Underlying performance | Senior | Spectrum | Junior | Fees | |---|---|---|---|---| | Strong yield (above all caps) | Full guaranteed return | Capped return + surplus share | Hurdle + surplus share + remainder | Protocol + curator fees charged on total yield | | Moderate yield | Full guaranteed return | Lower caps filled, higher caps partial | Reduced residual | Protocol + curator fees charged on total yield | | Below senior rate, positive | Full guaranteed return | Partial or floor only | Floor only or wiped | Zero (no yield) | | Severe loss | Partial — gets priority | Gets floor pro-rata | Wiped out | Zero | --- ## Senior Tranche Value Tracking The senior tranche must correctly account for deposits arriving at different times, each earning the guaranteed rate from their deposit timestamp to maturity. The accountant uses a **snapshot mechanism** with two state variables: - `_yieldLastSeniorDeposit`: accumulated yield on prior senior deposits at the time of the most recent senior deposit. - `_timeLastSeniorDeposit`: the timestamp of that snapshot. Before each senior deposit, `_capturePreDepositSeniorState()` computes: `_yieldLastSeniorDeposit = seniorTrancheCurrentValue() - seniorTranche.totalDeposits()`. Then `seniorTrancheCurrentValue()` computes: `compoundAssets(totalDeposits + _yieldLastSeniorDeposit, rSenior, timeSinceSnapshot, remainingPoolDurationAtSnapshot)`. This creates **piecewise compounding**: the accumulated value at each deposit becomes the new principal base, which then compounds for the remaining pool duration. The result is that each depositor's shares correspond to exactly `deposit * (1 + rSenior)^(timeInPool / year)` worth of assets at maturity. ## Spectrum Tranche Value Tracking Each spectrum tranche uses the same piecewise compounding snapshot mechanism as senior, with per-tranche state: - `_yieldLastSpectrumDeposit[apyBps]`: accumulated yield for that spectrum tranche at the time of its most recent deposit. - `_timeLastSpectrumDeposit[apyBps]`: the timestamp of that snapshot. Before each spectrum deposit, `_capturePreDepositSpectrumState(apyBps)` computes: `_yieldLastSpectrumDeposit[apyBps] = spectrumTrancheAccumulatedValue(apyBps) - spectrumVaults[apyBps].totalDeposits()`. Then `spectrumTrancheAccumulatedValue(apyBps)` computes: `compoundAssets(totalDeposits + _yieldLastSpectrumDeposit[apyBps], apyBps, timeSinceSnapshot, remainingPoolDurationAtSnapshot)`. This ensures that deposits at different times into the same spectrum tranche are correctly priced, preventing late depositors from inflating the tranche's accrued value. --- ## Collateralization and Capacity Both senior and spectrum tranches have dynamic capacity limits. Capacity is constrained by the combined value of all subordinate tranches, weighted by their collateral factors, and decays over time. ### Collateral Factors Each spectrum tranche and the junior tranche has a **collateral factor** that determines how much of its value can back higher-priority deposits. Collateral factors are interpolated linearly across the spectrum grid: - `cCollateralFactorFirst`: the collateral factor for the first (lowest) spectrum grid point - `cCollateralFactorJunior`: the collateral factor for the junior tranche (and the upper bound of the interpolation) For a spectrum tranche at position `p` on the grid (0-indexed, where the grid has `N` total points): ``` collateralFactor(p) = cCollateralFactorFirst + (cCollateralFactorJunior - cCollateralFactorFirst) * p / N ``` ### Senior Capacity The senior capacity formula considers all subordinate tranches: ``` seniorCapacity = sum(spectrumValue_i * collateralFactor_i + juniorValue * cCollateralFactorJunior) * SECONDS_PER_YEAR / (dPeriod * rSenior) ``` This ensures each subordinate position contributes to senior backing proportional to its risk level. ### Spectrum Capacity Each spectrum tranche has its own capacity limit, backed only by tranches with **higher risk** (higher APY cap or junior). The capacity for a spectrum tranche at rate `apyBps` considers only tranches with rates strictly above `apyBps`, plus junior: ``` spectrumCapacity(apyBps) = sum(spectrumValue_i * CF_i for rates_i > apyBps) + juniorValue * cCollateralFactorJunior) * SECONDS_PER_YEAR / (dPeriod * apyBps) ``` This means lower-cap spectrum tranches have more capacity (backed by more subordinate tranches), while higher-cap tranches have less (backed only by junior and higher-cap tranches above them). ### Cap Reduction Factor Both senior and spectrum capacity are subject to **time-based reduction** via `cCapReductionFactor`: ``` effectiveCap = collateralCap * (1 - cCapReductionFactor * timeElapsed / dPeriod) ``` When `cCapReductionFactor = 10000` (100%), capacity decays linearly to 0 at maturity. When `cCapReductionFactor = 0`, capacity does not decay. This prevents large late deposits from entering the pool when there is less time for subordinate tranches to absorb losses. ### Example With a 365-day pool, 5% senior rate, `cCollateralFactorFirst = 5000` (50%), `cCollateralFactorJunior = 10000` (100%): | Tranche | Collateral Factor | Backing per 1 USDC | |---|---|---| | Spectrum 5.5% (position 0) | 50% | 10 USDC senior | | Spectrum 10.0% (mid-grid) | ~75% | 15 USDC senior | | Junior | 100% | 20 USDC senior | Capacity is **dynamic** — it recalculates based on the current waterfall values (which include market gains/losses), not just deposits. As the underlying market appreciates, capacity grows. Senior capacity returns 0 after maturity, preventing deposits. --- ## Lazy Spectrum Vault Creation When `depositSpectrum(apyBps, ...)` is called for a new grid point: 1. The accountant clones `trancheVaultImpl` via `Clones.clone()` (EIP-1167 minimal proxy) 2. The new vault is initialized via a low-level call with the pool's asset, a derived name/symbol, and the accountant's address 3. The vault is registered: `spectrumVaults[apyBps] = vault`, `_vaultApyBps[vault] = apyBps`, `_isTranche[vault] = true` 4. The APY rate is inserted into the sorted `activeSpectrumRates` array (maintained low->high via insertion sort) The sorted array is bounded by the grid — for a typical config of `rSenior = 500`, `rMaxSpectrum = 2000`, `gridStep = 50`, there are at most 30 possible grid points. This keeps the O(n) waterfall computation gas-efficient. Once a spectrum vault exists, it supports both deposit paths: users can deposit via the accountant's `depositSpectrum()` or directly via the vault's standard ERC4626 `deposit()`. --- ## Yield Markets (Allocators) The `IAllocator` interface is the pluggable adapter between the accountant and any external yield source. Each pool references one allocator and one market address. | Allocator | Underlying market | Redemption | |---|---|---| | `ERC4626Allocator` | Standard ERC4626 vaults (e.g., Morpho Blue) | Synchronous | | `AaveV3L2Allocator` | Aave V3 on L2 chains (calldata-compressed) | Synchronous | | `ERC4626AsyncStrataMainnetAllocator` | Strata Finance (Ethena USDe, Neutrl NUSD on mainnet) | Async — cooldown unstake | | `ERC4626AsyncRedeemAllocatorExample` | Generic async ERC4626 reference implementation | Async | ### Synchronous allocators `allocator.deposit()` transfers assets from the allocator to the underlying market and returns market shares to the accountant. `allocator.redeem()` burns market shares and returns assets directly. ### Asynchronous allocators `allocator.requestRedeem()` initiates a withdrawal that may require a cooldown period (e.g., Strata's 7-day unstake). It returns an opaque `bytes32 exitHandle` and any immediately-received assets. The accountant tracks active handles in an enumerable set (swap-and-pop for O(1) removal). The Strata allocator specifically: when `requestRedeem` is called on a Strata vault, the vault creates an unstake cooldown request. The allocator queries `IUnstakeCooldown.activeRequests()` to get the `unlockAt` timestamp, stores it in the exit record, and returns the handle. Later, `claimRedeem` calls `IUnstakeCooldown.finalize()` to pull assets once the cooldown expires. The `pendingRedeem()` view function lets the accountant check whether each exit is claimable or completed, enabling the `claimRedeemedAssets()` loop to process only ready exits. --- ## Deposit Flow Details ### Router deposit (primary path) ``` User | +-> KnoxRouter.depositSeniorSpectrum() / depositSpectrumTranche() / depositJuniorSpectrum() | +-> Pull assets from User (transferFrom or Permit2) +-> forceApprove(accountant, assets) +-> accountant.depositSenior() / depositSpectrum() / depositJunior() | | | +-> Validate caps, compute shares | +-> transferFrom(router, allocator, assets) | +-> allocator.deposit() -> Invest in market | +-> vault.accountantMint(receiver, shares) | +-> forceApprove(accountant, 0) (cleanup) ``` 1. User calls a router function. Permit2 variants accept a signature instead of requiring on-chain approval. 2. Router pulls assets from user to itself (via `safeTransferFrom` or `PERMIT2.transferFrom`). 3. Router approves the `SpectrumAccountant` to spend the assets using `forceApprove`. 4. Router calls the corresponding accountant function. 5. The accountant validates caps, computes shares, pulls assets from the router to the allocator, invests via `allocator.deposit()`, and mints shares to the receiver. 6. Router resets the accountant's allowance to 0 (defensive cleanup). ### Accountant-driven deposit (secondary path) 1. User calls `accountant.depositSenior()`, `depositSpectrum()`, or `depositJunior()` directly. 2. Deposit cap is validated against `cMaxTotalDeposits`. 3. For senior: `_capturePreDepositSeniorState()` validates capacity and snapshots yield. 4. For spectrum: rate is validated on the grid, spectrum deposit cutoff is checked, spectrum capacity is validated, and vault is created if new. `_capturePreDepositSpectrumState()` snapshots yield for that tranche. 5. For junior: `_checkJuniorDepositRole()` validates role authorization if `juniorDepositRole` is set. 6. Shares are calculated using the appropriate pricing model. 7. `_pullAndDeposit()` transfers assets from user to allocator, then calls `allocator.deposit()` to invest in the underlying market. 8. `vault.accountantMint(receiver, shares, assets)` mints shares directly, bypassing the `previewDeposit` callback. ### Direct vault deposit (ERC4626 path) 1. User calls `trancheVault.deposit(assets, receiver)` on any existing tranche vault. 2. `TrancheVault._deposit()` pulls assets from user to allocator. 3. `accountant.registerDeposit(amount, receiver)` is called — validates cap, checks junior deposit role if applicable, snapshots senior/spectrum state if needed, validates spectrum capacity and cutoff if applicable, invests via `allocator.deposit()`. 4. Shares are calculated via `accountant.previewDeposit()` (which dispatches based on which vault is calling). 5. Tranche shares are minted to the receiver. Note: This path cannot create new spectrum vaults — only the accountant can do that (via the router or direct call). --- ## Underlying Asset Value Tracking `underlyingAssetCurrentValue()` reports differently depending on pool state: | Pool state | How value is computed | |---|---| | ACTIVE | `allocator.convertedBalanceOf(underlyingMarket, accountant)` — queries the market for the current asset value of the accountant's share position | | REDEEMED / SETTLED | `asset.balanceOf(accountant)` — the accountant holds raw assets, not market shares | --- ## Access Control and Roles The `AccessController` (OpenZeppelin `AccessControl` wrapper) governs permissions across the system. | Role | Where checked | Purpose | |---|---|---| | `DEFAULT_ADMIN_ROLE` | `AccessController` | Can grant/revoke all roles | | `DEPLOYER_ROLE` | `SpectrumFactory` | Authorized to deploy new pools | | `FEE_SETTER_ROLE` | `SpectrumFactory` | Toggles the global `applyProtocolFees` flag | | `RESCUE_ROLE` | `SpectrumAccountant` (via factory's AccessController) | Can execute `proxyCall` after maturity + 28 days | | `juniorDepositRole` | `SpectrumAccountant` (via factory's AccessController) | Per-pool configurable role that gates junior tranche deposits. Set to `bytes32(0)` to allow unrestricted junior deposits. | | `CLAIM_TARGET_ADMIN_ROLE` | `ERC4626AsyncStrataMainnetAllocator` | Registers new claim targets for new asset types | ### Junior Deposit Gating When `juniorDepositRole` is set (non-zero `bytes32`), the accountant checks whether the deposit receiver holds the specified role in the factory's `AccessController`. This check applies to both direct accountant deposits and ERC4626 vault deposits. If the role is `bytes32(0)`, junior deposits are unrestricted. ### Rescue mechanism Available only after `tStart + dPeriod + 28 days`. The `proxyCall` function lets RESCUE_ROLE holders make arbitrary external calls from the accountant to recover stuck assets. Safety rails: - The RESCUE_ROLE is resolved **dynamically** from the factory's AccessController at call time, allowing governance to rotate rescue responders after deployment. - When targeting the pool's asset token, only `transfer` and `transferFrom` selectors are permitted, and the recipient must be one of the tranche vaults (senior, junior, or any spectrum vault) or the accountant itself. This prevents a rescue caller from draining assets to an arbitrary address. - Calls to other targets (allocators, markets, etc.) are unrestricted, enabling recovery from allocator-level edge cases. --- ## Architecture: Diamond Pattern The `SpectrumAccountant` uses a **diamond-like pattern** to split its logic across multiple extension contracts while sharing a single storage layout. This is necessary because the combined logic exceeds the EVM contract size limit. ### Extension Contracts | Extension | Selectors | Purpose | |---|---|---| | `SpectrumDeposits` | `depositSenior`, `depositSpectrum`, `depositJunior`, `registerDeposit`, `previewDeposit`, `seniorCapacity`, `spectrumCapacity`, `getCurrentTrancheValuation`, `computeFullWaterfall`, `underlyingAssetCurrentValue`, `totalDeposits`, view helpers | Active-pool operations: deposits, share pricing, capacity constraints, valuation views | | `SpectrumSettlement` | `redeemShares`, `claimRedeemedAssets`, `settlePool`, `maxPendingExitUnlockAt` | Post-maturity operations: redeem, settlement waterfall execution, async exit tracking | | `SpectrumRescue` | `proxyCall` | Emergency operations: rescue stuck assets after maturity + 28 days | ### How it works 1. The `SpectrumAccountant` implementation is deployed with three extension addresses as immutable constructor arguments. 2. `SpectrumFactory` clones the implementation via EIP-1167 minimal proxies. Clones share the implementation's immutable extension addresses but have their own storage. 3. `initialize()` runs directly on the accountant (not delegated). All public state variable getters are served directly by the accountant. 4. All other function calls hit the `fallback()`, which routes the selector to the correct extension via `delegatecall`. The routing is explicit: settlement selectors go to `_settlementExtension`, rescue selectors go to `_rescueExtension`, everything else goes to `_depositsExtension`. 5. All extensions inherit `SpectrumAccountantStorage`, guaranteeing identical storage slot assignments across `delegatecall` boundaries. --- ## Key Parameters | Parameter | Type | Description | Example | |---|---|---|---| | `rSenior` | `uint128` | Senior APY guarantee (basis points) | `500` = 5% | | `rMaxSpectrum` | `uint128` | Maximum spectrum APY cap (basis points) | `2000` = 20% | | `spectrumGridStep` | `uint128` | APY increment between spectrum grid points (basis points) | `50` = 0.5% | | `dPeriod` | `uint128` | Pool duration (seconds) | `2_592_000` = 30 days | | `cCollateralFactorFirst` | `uint128` | Collateral factor for the first (lowest) spectrum grid point (basis points) | `5_000` = 50% | | `cCollateralFactorJunior` | `uint128` | Collateral factor for the junior tranche (basis points); upper bound of interpolation | `10_000` = 100% | | `cCapReductionFactor` | `uint128` | Rate at which capacity decays over pool lifetime (basis points); `0` = no decay, `10000` = full linear decay to 0 | `5_000` = 50% | | `cSpectrumDepositCutoff` | `uint128` | Fraction of pool duration (basis points) after which spectrum deposits are blocked; `0` = no cutoff, `10000` = at maturity | `7_500` = 75% elapsed | | `cJuniorDepositCutoff` | `uint128` | Fraction of pool duration (basis points) after which junior deposits are blocked; `0` = no cutoff, `10000` = at maturity | `2_500` = 25% elapsed | | `cProtocolFee` | `uint128` | Protocol fee on total pool yield (basis points) | `1_000` = 10% | | `cCuratorFee` | `uint128` | Curator fee on total pool yield (basis points); deducted alongside protocol fee before the waterfall | `500` = 5% | | `cMaxSettlementSlippage` | `uint128` | Maximum acceptable slippage on synchronous redemption (basis points, max 1000); reverts if withdrawn assets fall below expected by more than this | `100` = 1% | | `cMaxTotalDeposits` | `uint256` | Hard cap on total pool assets; `0` at init becomes `type(uint256).max` | `10_000_000e6` | | `kSurplusParticipation` | `uint128` | Surplus participation scaling factor; `0` = disabled (junior gets all surplus) | `2` | | `rJuniorHurdle` | `uint128` | Junior hurdle APY (basis points); junior earns this before surplus sharing. Must be >= `rMaxSpectrum` when surplus participation is enabled | `2_500` = 25% | | `nSurplusParticipation` | `uint128` | Exponent for collateral factor in participation unit calculation. Must be > 0 when surplus participation is enabled | `1` | | `juniorDepositRole` | `bytes32` | Role in factory's AccessController required to deposit into junior; `bytes32(0)` = unrestricted | `keccak256("JUNIOR_DEPOSITOR")` | | `underlyingMarket` | `address` | The yield-bearing market (ERC4626 vault, Aave aToken, Strata vault) | -- | | `allocator` | `address` | The `IAllocator` adapter that bridges the accountant to the market | -- | | `protocolFeeBeneficiary` | `address` | Receives protocol fees at settlement (and swept surplus when no junior deposits exist) | -- | | `curatorFeeBeneficiary` | `address` | Receives curator fees at settlement; set to `address(0)` to disable curator fees | -- | ### Validation constraints - `rSenior < rMaxSpectrum` - `spectrumGridStep > 0` - `(rMaxSpectrum - rSenior) % spectrumGridStep == 0` (grid must divide evenly) - `cCollateralFactorFirst <= cCollateralFactorJunior` - `cCapReductionFactor <= 10000` - `cSpectrumDepositCutoff <= 10000` - `cJuniorDepositCutoff <= 10000` - `cMaxSettlementSlippage <= 1000` - If `kSurplusParticipation > 0`: `nSurplusParticipation > 0` and `rJuniorHurdle >= rMaxSpectrum` --- ## Core Contracts | Contract | Role | |---|---| | **`KnoxRouter`** | User-facing entry point for deposits. Provides dedicated functions for each tranche type with both standard ERC20 approval and Permit2 signature variants. Handles the pull -> approve -> deposit -> cleanup flow so users interact with a single contract. | | **`SpectrumAccountant`** | Diamond-pattern router for a spectrum pool. Serves public state variable getters and `initialize()` directly. Routes all other calls via `fallback()` to one of three extensions: `SpectrumDeposits`, `SpectrumSettlement`, or `SpectrumRescue`. Extension addresses are immutable (embedded in implementation bytecode, shared by all clones). Deployed as an implementation and cloned by `SpectrumFactory`. | | **`SpectrumDeposits`** | Extension handling active-pool operations: deposits (senior/spectrum/junior), share pricing, capacity constraints (senior and per-spectrum-tranche), valuation views, lazy vault creation, and junior deposit role checking. Executed via delegatecall from `SpectrumAccountant`. | | **`SpectrumSettlement`** | Extension handling post-maturity operations: redeem from allocator, settlement waterfall execution with surplus participation, async exit tracking, and claim processing. Executed via delegatecall from `SpectrumAccountant`. | | **`SpectrumRescue`** | Extension handling emergency rescue: `proxyCall` with asset-transfer safety rails. Executed via delegatecall from `SpectrumAccountant`. | | **`SpectrumAccountantStorage`** | Abstract contract defining the shared storage layout, types, constants, events, errors, modifiers, and internal helpers inherited by `SpectrumAccountant` and all extensions. Guarantees identical storage slot assignments across delegatecall boundaries. | | **`TrancheVault`** | ERC4626 vault representing a single tranche. Each pool has one senior vault, one junior vault, and zero or more lazily-created spectrum vaults. Delegates share-pricing to the accountant via `previewDeposit()` / `getCurrentTrancheValuation()`. Deposits blocked after maturity; withdrawals blocked until SETTLED. Deployed as an implementation and cloned by `SpectrumFactory` (or by the accountant for spectrum vaults). | | **`SpectrumFactory`** | Deploys spectrum pools atomically via the EIP-1167 clone pattern. A single `deploy()` call creates one `SpectrumAccountant` clone, one senior `TrancheVault` clone, and one junior `TrancheVault` clone. Spectrum vaults are created lazily by the accountant. Roles (`DEPLOYER_ROLE`, `FEE_SETTER_ROLE`, `RESCUE_ROLE`) are managed via `AccessController`. | | **`AccessController`** | Thin wrapper around OpenZeppelin `AccessControl`. Shared by the factory and used by accountants for rescue role and junior deposit role checks. Provides `isMemberOfAny()` for checking membership across multiple roles. | | **`KnoxMath`** | Library providing `compoundAssets()` and `compoundingFactor()` using PRB Math's `UD60x18` fixed-point exponentiation. Used for senior, spectrum, and waterfall cap calculations. | | **`SpectrumWaterfallLib`** | Library providing extracted waterfall distribution math (`computeWaterfall`), surplus participation distribution (`distributeSurplus`), capacity computation (`computeCapacity`), and floor calculation (`trancheFloor`). | | **`IAllocator`** | Interface for pluggable yield market adapters. Implementations include `ERC4626Allocator`, `AaveV3L2Allocator`, and async variants. | --- ## Security ### Risks/Mitigations - **Senior Yield Risk (Protocol)**: Priority waterfall: Senior is paid first. Spectrum and Junior tranches absorb all yield shortfalls before Senior is affected. Senior is only impaired in extreme scenarios where all subordinate tranches are fully wiped out. - **Spectrum Tranche Risk**: Each Spectrum tranche has a defined collateral factor that determines how much loss it absorbs. Lower-cap tranches are paid earlier in the waterfall and are safer; higher-cap tranches offer more upside but absorb more downside. - **General Yield Risk (sources, underperformance, ...)**: - Allocate only into vetted / qualified yield sources (Morpho, Aave, Strata/Ethena) - Pluggable allocator architecture enables rapid response to market issues - Active monitoring and alerting on underlying positions - Rescue mechanism available 28 days post-maturity for edge case recovery - **Smart Contract Risk / Security**: - Knox: - Audit Knox Smart Contracts - Follow industry standards: ERC-4626, OpenZeppelin, PRBMath - Access control via centralized AccessController with role-based permissions - EIP-1167 minimal proxies for gas-efficient pool deployment - Rescue mechanism with strict safety rails — asset transfers restricted to tranche vaults only - Underlying: - Only use blue-chip yield sources - Monitor their security incidents ### Audits After a beta testing period, we will have our smart contracts audited by reputable audit firms and security experts. Until then, we'll provide allowlist access enforced through on-chain access control. Please contact us to obtain access. We clearly advise you to only make small investments pre-audit (i.e. before our public launch). --- ## Rewards & Tokenomics ### Points Program We will introduce a points program that assigns each protocol user points according to the following metrics: - Deposit size - Tranche: Junior (greatest factor), Spectrum higher caps (higher factor), Spectrum lower caps (moderate factor), Senior (lower factor) - Early bird factor: The earlier you are in using Knox, the higher will be the points multiplier ### Knox Token Based on the points assignment, a fraction of the future Knox token will be distributed to early users. Other (long-term vesting) assignments will be made to the team & investors. Knox will also keep distributing the token through enshrined incentives based on vault usage. --- ## Resources - Terms of Service - https://knox.finance/terms-of-service - Privacy Policy - https://knox.finance/privacy-policy ## Links - Website - https://knox.finance/ - App - https://app.knox.finance/explore - Simulator - https://simulator.knox.finance/ - X / Twitter - https://x.com/0xKnoxFi - Github - N/A ## Documentation - [Core Concepts](https://docs.knox.finance/Core-Concepts-321e01fa5517807e9579d55bd1fbf764): Understand the three tranche types (Senior, Spectrum, Junior), pool lifecycle states, and how holding period affects returns. - [Settlement Waterfall](https://docs.knox.finance/Settlement-Waterfall-321e01fa5517802caac9cf3349acce7b): Deep dive into the two-phase waterfall: protocol fee deduction, floor-based distribution, and loss absorption hierarchy with detailed scenarios. - [Technical Architecture](https://docs.knox.finance/Technical-Architecture-321e01fa551780d0abcde2562e642750): Smart contract system overview: KnoxRouter, SpectrumAccountant, TrancheVault, allocators, deposit flows, and lazy vault creation. - [Pool Configuration & Parameters](https://docs.knox.finance/Pool-Configuration-Parameters-321e01fa551780a884d8cb629233c231): Complete reference for rate configuration, collateral factors, capacity constraints, and validation rules with parameter examples. - [Integration Guide](https://docs.knox.finance/Integration-Guide-321e01fa551780278155d4a672556051): Developer guide: depositing via router/accountant/vault, querying positions, withdrawing after settlement, and common integration patterns. - [Advanced Topics](https://docs.knox.finance/Advanced-Topics-321e01fa551780089d5fdaed99b70dc2): Asynchronous redemptions with cooldown periods, rescue mechanism for edge cases, access control roles, and performance considerations. - [Spectrum Simulator](https://docs.knox.finance/Spectrum-Simulator-321e01fa551780ba8357c728389ac86b): Interactive tool for modeling pool mechanics and testing scenarios.