← Documentation home
Build · Contracts

TranchedMetaVault

The main contract. One per (chain, underlying ERC4626). Tracks senior/junior NAV, mints lcSEN/lcJUN shares, accrues premium, applies the loss waterfall.

Constants

NameValuePurpose
BPS10_000Basis-point denominator.
YEAR365 daysUsed to annualise bps rates.
SENIOR_PROTECTION_BPS2_000Junior NAV target for senior first-loss coverage (20%).
MAX_CORRELATED_SENIOR_LEVERAGE_BPS40_000Senior NAV ≤ 4× junior NAV. Derived from SENIOR_PROTECTION_BPS.
SENIOR_WITHDRAWAL_NOTICE5 daysWait between requestSeniorWithdrawal and withdrawSenior.
JUNIOR_WITHDRAWAL_NOTICE10 daysWait between requestJuniorWithdrawal and withdrawJunior.
MAX_PERFORMANCE_FEE_BPS2_000Hard cap on the performance fee charged on positive NAV deltas (20%).
KEEPER_FEE_BPS5Reward paid to a third party that cranks a matured notice on behalf of the holder. Skimmed from the holder's own proceeds; holder self-execution pays zero.
NOTICE_EXECUTION_WINDOW3 daysActive execution window after a notice matures. Past this window the notice goes stale and can only be cancelled (or extended via refile), never executed.
UNDERLYING_VALUE_TOLERANCE1One-wei slack on the deposit/withdrawal value-preservation check. Approved underlyings must be no-fee, no-slippage.
TRANCHE_SENIOR0Stable senior tranche id for metadata and indexers.
TRANCHE_JUNIOR1Stable junior tranche id for metadata and indexers.

Immutables

NameTypeNotes
vaultIERC4626The wrapped underlying ERC4626. Must be a well-behaved 4626 (strict maxRedeem semantics, plain ERC-20 asset). The factory's approvedUnderlyings allowlist enforces this.
assetIERC20Resolved at construction from vault.asset().
seniorTokenPooledTrancheTokenlcSEN shares. Fungible ERC20 receipt token; this contract is the only minter/burner/queue controller.
juniorTokenPooledTrancheTokenlcJUN shares. Same as senior.

Mutable state

NameTypePurpose
seniorNavuint256Asset value claimable by lcSEN holders.
juniorNavuint256Asset value claimable by lcJUN holders.
totalVaultSharesuint256Underlying-vault shares held by this contract (senior + junior aggregated).
protectionPremiumBpsuint256Active senior premium rate, annualised in bps. Updated atomically by setProtectionPremiumRate; any pre-update delay lives in an external TimelockController that holds OPERATOR_ROLE.
lastPremiumAccrualuint64Last _accruePremium() timestamp.
performanceFeeBpsuint256Active performance-fee rate. Charged on positive NAV deltas only; capped at MAX_PERFORMANCE_FEE_BPS. Updated atomically by setPerformanceFeeRate.
feeRecipientaddressRecipient of the junior shares minted to capture each performance-fee accrual.
seniorWithdrawalNotices[holder](shares, executableAt)Outstanding senior notice, if any.
juniorWithdrawalNotices[holder](shares, executableAt)Outstanding junior notice, if any.
totalSeniorWithdrawalNoticeSharesuint256Senior shares currently locked across all outstanding notices. Reserves underlying liquidity for senior exits before junior.
depositsPausedboolGlobal deposit pause (affects both tranches; withdrawals stay open).
keeperRestrictedboolWhen true, only PROTOCOL_ROLE holders can crank a matured notice on behalf of a holder; permissionless keeping is disabled. Self-execution by the holder is always allowed. Toggled via setKeeperRestricted.

Deposits

function depositSenior(uint256 assets, address receiver)
  external nonReentrant returns (uint256 shares);
 
function depositJunior(uint256 assets, address receiver)
  external nonReentrant returns (uint256 shares);

Each deposit pulls assets from msg.sender, wraps them into the underlying vault, credits the relevant tranche NAV, and mints lcSEN / lcJUN shares to receiver at the current per-tranche unit price. There is no merging or per-position metadata; the receipts are fungible.

Both calls run _syncAccounting() and _accruePremium() first, so the share-price quote reflects up-to-the-block NAV.

Senior deposits revert with ProtectionTooThin if the post-state would push senior NAV past 4× junior NAV. Both functions revert with DepositsPaused while depositsPaused is set.

Senior withdrawal

Two-step. Notice first, then redemption.

function requestSeniorWithdrawal(uint256 shares) external nonReentrant;
function cancelSeniorWithdrawal() external nonReentrant;
function clearStaleSeniorWithdrawal(address owner) external nonReentrant;
function withdrawSenior(uint256 shares, address owner, address receiver)
  external nonReentrant returns (uint256 assets);

Keeper path. withdrawSenior is permissionless once the notice matures: anyone can call it with owner set to the notice holder and earn KEEPER_FEE_BPS of the proceeds (skimmed from the holder's payout, not socialised across other LPs). When msg.sender != owner, receiver is forced to equal owner so keepers cannot redirect funds. Self-execution (msg.sender == owner) is always free and unconstrained by receiver. When keeperRestricted is on, only PROTOCOL_ROLE holders are allowed on the keeper path.

The payout is priced at execution-time NAV.

Junior withdrawal

function requestJuniorWithdrawal(uint256 shares) external nonReentrant;
function cancelJuniorWithdrawal() external nonReentrant;
function withdrawJunior(uint256 shares, address owner, address receiver)
  external nonReentrant returns (uint256 assets);

Symmetric with senior, but with a 10-day notice and an additional gate: withdrawJunior reverts with InsufficientJuniorProtection(requestedAssets, maxAssets) if the redemption would push senior past the 4× leverage cap. The same keeper-path semantics and NOTICE_EXECUTION_WINDOW stale-notice rule apply. Junior does not have a clearStale* variant — there is no global liquidity reservation to release on the junior side, so a stale junior notice is simply cancellable by the holder.

Privileged ops

The metavault uses OpenZeppelin's AccessControl with two self-administering roles, granted atomically in the constructor:

No DEFAULT_ADMIN_ROLE is granted, so neither role can override the other. Each role rotates itself via grantRole + renounceRole (the role's own admin is itself).

function setProtectionPremiumRate(uint256 newRateBps) external onlyRole(OPERATOR_ROLE);
function setDepositsPaused(bool paused)               external onlyRole(OPERATOR_ROLE);
 
function setPerformanceFeeRate(uint256 newRateBps)    external onlyRole(PROTOCOL_ROLE);
function setFeeRecipient(address newRecipient)        external onlyRole(PROTOCOL_ROLE);
function setKeeperRestricted(bool restricted)         external onlyRole(PROTOCOL_ROLE);
 
function syncAccounting() external nonReentrant;  // public — anyone can run

setProtectionPremiumRate updates the senior→junior premium atomically; _syncAccounting() runs first so the previous rate finishes accruing up to the call's block. Reverts InvalidRate if newRateBps > BPS. To buffer the change on-chain, deployments set the OPERATOR_ROLE holder to an external TimelockController.

setPerformanceFeeRate updates the performance fee atomically with the same pre-sync pattern. The fee is charged on positive NAV deltas only (during _syncAccounting's gain branch) and is implemented as a freshly-minted parcel of junior shares to feeRecipient. Existing junior holders are not diluted in unit-price terms because the mint is sized at the post-distribution junior unit price. Reverts InvalidFeeRate if newRateBps > MAX_PERFORMANCE_FEE_BPS. No fee is charged on losses or while junior is wiped.

setFeeRecipient redirects future fee mints. Takes effect immediately on the metavault; already-minted shares are unaffected.

setKeeperRestricted toggles whether the keeper path on withdrawSenior / withdrawJunior is permissionless. When true, only PROTOCOL_ROLE holders can crank a matured notice on behalf of a holder — useful for deployments where the public keeper market is too thin to rely on (small vaults, L1 with sub-economic fees). Self-execution by the holder remains open regardless of mode.

syncAccounting runs _syncAccounting() + _accruePremium() and is permissionless. Anyone can crank state forward to refresh share prices.

Views

The metavault's external read surface is kept minimal: just the public-state auto-getters listed under Mutable state plus one helper:

function underlyingAssets() external view returns (uint256);

underlyingAssets() returns vault.previewRedeem(totalVaultShares): the asset-value of the metavault's underlying-vault position at the current 4626 price. It's the single source the lens uses to drive its preview math.

Everything else a UI typically wants (seniorSharePrice, juniorSharePrice, maxJuniorWithdrawAssets, requiredJuniorCollateralAssets, per-user seniorAssets / juniorAssets, deposit and withdrawal previews) lives on the Lens. One LayerCoverLens.snapshot(vault) call returns every field; LayerCoverLens.userPosition(vault, user) returns the per-holder fields. Front-ends should batch-read through the lens rather than chaining N view calls on the metavault.

Internal flow

Every state-changing call runs the same accounting prefix:

  1. _syncAccounting(): reconciles seniorNav + juniorNav against vault.previewRedeem(totalVaultShares) and applies the delta via the gain-pro-rata or loss-waterfall path.
  2. _accruePremium(): moves NAV from senior to junior at the active rate.

Then the call's own logic runs (deposit, redeem, request, cancel, role setter). This means any lens read after a state-changing call sees up-to-the-block NAV.