The main contract. One per (chain, underlying ERC4626). Tracks senior/junior NAV, mints lcSEN/lcJUN shares, accrues premium, applies the loss waterfall.
| Name | Value | Purpose |
|---|---|---|
BPS | 10_000 | Basis-point denominator. |
YEAR | 365 days | Used to annualise bps rates. |
SENIOR_PROTECTION_BPS | 2_000 | Junior NAV target for senior first-loss coverage (20%). |
MAX_CORRELATED_SENIOR_LEVERAGE_BPS | 40_000 | Senior NAV ≤ 4× junior NAV. Derived from SENIOR_PROTECTION_BPS. |
SENIOR_WITHDRAWAL_NOTICE | 5 days | Wait between requestSeniorWithdrawal and withdrawSenior. |
JUNIOR_WITHDRAWAL_NOTICE | 10 days | Wait between requestJuniorWithdrawal and withdrawJunior. |
MAX_PERFORMANCE_FEE_BPS | 2_000 | Hard cap on the performance fee charged on positive NAV deltas (20%). |
KEEPER_FEE_BPS | 5 | Reward 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_WINDOW | 3 days | Active 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_TOLERANCE | 1 | One-wei slack on the deposit/withdrawal value-preservation check. Approved underlyings must be no-fee, no-slippage. |
TRANCHE_SENIOR | 0 | Stable senior tranche id for metadata and indexers. |
TRANCHE_JUNIOR | 1 | Stable junior tranche id for metadata and indexers. |
| Name | Type | Notes |
|---|---|---|
vault | IERC4626 | The wrapped underlying ERC4626. Must be a well-behaved 4626 (strict maxRedeem semantics, plain ERC-20 asset). The factory's approvedUnderlyings allowlist enforces this. |
asset | IERC20 | Resolved at construction from vault.asset(). |
seniorToken | PooledTrancheToken | lcSEN shares. Fungible ERC20 receipt token; this contract is the only minter/burner/queue controller. |
juniorToken | PooledTrancheToken | lcJUN shares. Same as senior. |
| Name | Type | Purpose |
|---|---|---|
seniorNav | uint256 | Asset value claimable by lcSEN holders. |
juniorNav | uint256 | Asset value claimable by lcJUN holders. |
totalVaultShares | uint256 | Underlying-vault shares held by this contract (senior + junior aggregated). |
protectionPremiumBps | uint256 | Active senior premium rate, annualised in bps. Updated atomically by setProtectionPremiumRate; any pre-update delay lives in an external TimelockController that holds OPERATOR_ROLE. |
lastPremiumAccrual | uint64 | Last _accruePremium() timestamp. |
performanceFeeBps | uint256 | Active performance-fee rate. Charged on positive NAV deltas only; capped at MAX_PERFORMANCE_FEE_BPS. Updated atomically by setPerformanceFeeRate. |
feeRecipient | address | Recipient 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. |
totalSeniorWithdrawalNoticeShares | uint256 | Senior shares currently locked across all outstanding notices. Reserves underlying liquidity for senior exits before junior. |
depositsPaused | bool | Global deposit pause (affects both tranches; withdrawals stay open). |
keeperRestricted | bool | When 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. |
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.
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);requestSeniorWithdrawal locks shares on lcSEN (via transferFrom, so prior approve or EIP-2612 permit is required) so they can't be transferred while pending. Re-calling stacks the noticed amount and re-stamps executableAt = block.timestamp + 5 days.cancelSeniorWithdrawal releases all currently-locked senior shares back to the holder.clearStaleSeniorWithdrawal is callable by anyone once the notice's NOTICE_EXECUTION_WINDOW has elapsed past executableAt. It returns the locked shares to owner and releases the senior liquidity reservation; the notice can no longer be executed (only re-filed).withdrawSenior reverts if the notice timer hasn't matured or if NOTICE_EXECUTION_WINDOW has elapsed since maturity. Otherwise it redeems the corresponding vault shares, transfers underlying assets to receiver, and burns the queued lcSEN. shares must be ≤ the noticed amount; partial redemptions are first-class.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.
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.
The metavault uses OpenZeppelin's AccessControl with two self-administering roles, granted atomically in the constructor:
OPERATOR_ROLE: held by the curator who called factory.deployVault. Controls the premium rate and the deposit pause.PROTOCOL_ROLE: held by factory.owner() at deploy time. Controls the performance fee and the fee-recipient slot.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 runsetProtectionPremiumRate 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.
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.
Every state-changing call runs the same accounting prefix:
_syncAccounting(): reconciles seniorNav + juniorNav against vault.previewRedeem(totalVaultShares) and applies the delta via the gain-pro-rata or loss-waterfall path._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.