Structured
Income and Protected: the sleeves, the payoff curves, maturity, settle and the early-exit fee.
StructuredVault is an ERC-4626 note with a date on it. It has a maturity, a mode - Income or Protected - and a sleeveBps that says how every deposit is split between an LPVault and a second sleeve, and it unwinds the whole thing to its asset once the date passes. The fact to carry out of this chapter is that "protected" is arithmetic on an SGOV sleeve bought through a Uniswap pool, not a promise from anyone, and that an Income note's second sleeve is off chain and reported by the same hedger key a hedged vault trusts.
The two modes
sleeveBps is the share of each deposit that goes to the mode's own sleeve. In Income that is the LP sleeve: sleeveBps of the deposit goes into lpVault.deposit, and the remainder stays in the note as idle asset, the collateral for a long perp position the hedger runs off chain. In Protected it is the T-bill sleeve: sleeveBps of the deposit is swapped to SGOV through the asset/SGOV pool, and the remainder goes into the LP vault. initialize rejects a sleeve of zero or of 10 000 or more (BadSleeve), a maturity that is not in the future (Matured), an LP vault whose asset() is not the note's (NotAsset), and for a Protected note an sgovPool that is not the asset/SGOV pair (also NotAsset).
The two seed notes in KerfWiring:
| Note | Symbol | Mode | sleeveBps | LP vault | Second sleeve |
|---|---|---|---|---|---|
Kerf Income NVDA | kNVDAi | Income | 7000 (INCOME_SLEEVE_BPS) | Kerf NVDA Hedged | 30 % idle WETH, the long reported through reportSleeve |
Kerf Protected SPY | kSPYp | Protected | 8000 (PROTECTED_SLEEVE_BPS) | Kerf SPY Hedged | 80 % SGOV through the deepest WETH/SGOV pool |
Both are WETH notes with the 5 WETH beta cap (_betaCap follows the asset), a term of NOTE_TERM = 90 days from the deploy block, the ETH/USD feed as assetUsdFeed, and the same MAX_HEDGE_AGE (6 hours) and MAX_EQUITY_JUMP_BPS (2000) as the hedged vaults. A note whose LP vault or SGOV pool is missing at deploy time is skipped and left out of the manifest.
Two things about the LP sleeve follow from the wiring and are easy to miss. The seed notes sit on the hedged vaults, so the LP sleeve's own share price already includes a hedge report and its own pause; and each note deposits into that vault through the ordinary deposit, so the LP vault's cap, guard and pause apply to the note's deposits and withdrawals too. When Kerf NVDA Hedged is paused, lpVault.redeem inside the Income note reverts HedgePaused_, and so does the note's withdrawal unless its idle balance covers it.
totalAssets() is the idle asset plus lpVault.convertToAssets(lpVault.balanceOf(note)) plus the second sleeve: _sgovValueInAsset() for Protected, sleeveEquityInAsset() for Income. maxDeposit() is zero after maturity and while paused - the note is closed to new money either way - and cap − totalAssets() otherwise; deposit and mint check maturity, the pause and the cap themselves, ahead of the standard's maxDeposit check, so a refused deposit says which of Matured, HedgePaused_ or CapExceeded it hit.
The payoff curves
Both curves are per unit of deposit, as a function of s, the underlying's price multiple at maturity, and both use √s as the value of the LP sleeve: a full-range position after a price move of s is worth the geometric mean of what went in, which is impermanent loss written as a value rather than a loss.
income = sleeve × (√s + feeYield) + (1 − sleeve) × min(s, capMult)
protected = sleeve × (1 + sgovYield) + (1 − sleeve) × √s
min(s, capMult) is the cap on Income's directional sleeve: above PAYOFF_CAP_MULT_X18 = 1.5e18, 1.5×, the long stops participating and only the LP sleeve keeps rising. The cap is what pays for the fee yield. Protected's first term does not depend on s at all, which is where the protection comes from.
The on-chain payoffCurve(spotMultiplierX18) mirrors packages/core/src/payoff.ts with two pins: feeYield is 0, because fees are already inside the LP sleeve's own NAV, and capMult is PAYOFF_CAP_MULT_X18. Its sgovYield is not the annual figure but SGOV_ANNUAL_YIELD_X18 (0.045e18, 4.5 %) accrued over the time still to run - sgovYieldX18() returns 0.045 × (maturity − now) / 365 days and zero after maturity - so a Protected curve flattens towards par as the date approaches. √s is a Babylonian square root on 1e18 fixed point. Core's payoffCurve samples 50 points over PAYOFF_MIN_MULT 0.2 to PAYOFF_MAX_MULT 2, and contracts/fixtures/payoff.json pins eight points of each curve that both sides must reproduce. For the seed notes at the start of their term:
| Spot multiple | kNVDAi (70 / 30) | kSPYp (80 / 20) |
|---|---|---|
| 0.25× | 0.425 | 0.909 |
| 0.5× | 0.645 | 0.950 |
| 1× | 1.000 | 1.009 |
| 1.5× | 1.307 | 1.054 |
| 2× | 1.440 | 1.092 |
The Protected floor is 0.8 × (1 + 0.045 × 90 / 365), about 0.809 per unit, whatever the underlying does. At par the note is worth par plus the carry; test_payoffAtParIsPar and test_protectedPayoffNeverFallsBelowItsFloor check both.
Reporting the Income sleeve
The Income perp sleeve is off chain. Its collateral stays in the note as idle asset, and there is no fundHedge here: nothing the note holds ever leaves it for Lighter. What the hedger reports through reportSleeve(unrealizedPnlUsd, equityUsd, asOf) is the sleeve's excess equity - what the perp has earned on top of the collateral - because counting the whole Lighter equity would double-count the collateral that never left. equityUsd is unsigned, so a losing sleeve is reported as zero excess and its loss shows up when the collateral comes back short. That is a limitation the contract header assigns to the docs and to the beta caps rather than to a larger contract, and this is the paragraph it meant.
The gate is HedgedLPVault.reportHedge line for line, minus the notional: NotHedger for any other caller, BadSleeve on a Protected note (it has no such sleeve, and a report on one would be nothing but a lever on its pause, so the hedger key holds no pause over it), StaleReport for an asOf in the future or not later than the last, then the jump test against the previous stored equity - measured only when a previous report exists, with a zero-equity report counting as a real one - then storage, SleeveReported, and HedgePaused("jump") or HedgeResumed. paused() has the same shape: a jump not yet followed by an in-band report, or now − sleeveReportedAt > maxHedgeAge with no transaction needed, and never true for a note that has not been reported on. sleeveConfig() returns the live bounds and sleeveState() the last report and the pause. The event names are shared with the hedged vault on purpose: the indexer unions the three vault ABIs by signature and tracks paused off the same pair.
sleeveEquityInAsset() converts through assetUsdFeed when there is one - the seed notes are WETH notes and use ETH/USD - and one for one when assetUsdFeed is zero, the case of a 6-decimal USD asset. A feed that fails or answers non-positive values the sleeve at zero, for the reason the hedged vault gives.
While paused, deposit, mint, withdraw and redeem all revert HedgePaused_ (_requireOpen runs first in each). settle is not gated on the pause: it prices no shares, only converts holdings. And an Income note stays reportable after maturity, until its last holder has left, because its share price still carries the sleeve until then.
The hedger bot in the API only handles vaults whose manifest name contains "Hedged" and only calls reportHedge; nothing in the repository calls reportSleeve. An Income note's report is therefore an operator action with the hedger key today, and a note that is never reported on never pauses and never counts a sleeve.
The SGOV pool as an oracle
A Protected note values its T-bill sleeve through the asset/SGOV pool, and buys and sells through it, so that pool is an oracle and is guarded like one. _requireOpen(buying) runs TwapGuard.check on the SGOV pool before every deposit, every withdrawal while the note holds SGOV, and every settlement - spot within MAX_TWAP_DEVIATION_BPS of a mean measured over at least MIN_TWAP_WINDOW - and VaultFactory.createStructuredVault grows that pool's observation ring at creation, as it does an LP pool's. The sleeve is valued at the mean, not at spot: _sgovValueInAsset() quotes the balance at the TWAP sqrt price, net of the pool fee it will cost to sell (ZapMath.quoteAtSpot). A view cannot refuse to answer, so on a ring too short to hold a mean it falls back to spot; every path that moves value has already run _requireOpen, which reverts on that same ring, so the fallback is only ever what an off-chain reader sees. test_theProtectedNavIsPricedAtTheTwapNotAtSpot nudges spot about 2 % - inside the band, so the deposit goes through - and watches the NAV not follow it: pushing the pool buys nobody a cheaper entry.
The swaps' minOut comes from the mean too: _swapThroughSgovPool sets amountOutMinimum to SGOV_SLIPPAGE_BPS (100, one percent) under the quote at the TWAP price, and check reverts first if that TWAP is unavailable or spot has left its band. A minOut derived from a spot price the caller just set is not a bound at all. The cost is availability, which is the trade every other Kerf vault makes: while the pool sits more than the band off its mean, deposits into and withdrawals out of a Protected note wait, and so does settle, which is simply retried later.
Once the note holds no SGOV and is not about to buy any - after settle, in practice - the band is waived, so a settled note's exits never depend on that pool again. test_aSettledNotesExitsIgnoreTheSgovPool redeems with the pool 9.5 % off its mean. An Income note has no SGOV leg and never looks at the pool.
Maturity, settle and the early exit
Withdrawals work at any time, before or after maturity, by unwinding a proportional slice. settle() is the permissionless call that unwinds everything once, so that after maturity the note is simply a pile of asset and nobody's exit depends on the SGOV pool's depth or the LP vault's price band on the day they happen to leave. It reverts NotMatured before the date and AlreadySettled the second time; it redeems every LP share the note holds, sells every SGOV it holds under the guard, and emits Settled(assetsOut) with the balance that resulted. The product ending does not need permission from anyone. After settlement a full redeem pays the whole NAV to the wei and leaves nothing stranded (test_afterSettlementAFullRedeemPaysTheWholeNav).
Leaving before maturity costs EARLY_EXIT_BPS = 50, half a percent, which goes to the FeeRouter by the same transfer-then-take path a harvest uses. The fee is not a penalty for its own sake: an early exit forces the note to unwind part of a sleeve at whatever the market is doing that minute, and the cost of that lands on whoever stays if the leaver does not carry it. It is charged the ERC-4626-correct way rather than skimmed off the transfer:
| View | Before maturity | After maturity |
|---|---|---|
previewRedeem(shares) | gross value less ceil(gross × 50 / 10 000) | gross |
previewWithdraw(assets) | shares for assets + ceil(assets × 50 / 9 950) | shares for assets |
maxWithdraw(owner) | previewRedeem(balance) | the same |
So withdraw(assets) really does deliver assets, with the fee raised on top, and the withdraw drawer shows the net figure previewRedeem returns and says under it that a withdrawal before maturity pays 50 bps. After maturity the fee is zero and every preview says so.
Inside _withdraw, a partial exit calls _raise(assets + fee): if the idle balance covers it nothing moves; otherwise the shortfall is taken as a proportional slice of what is on chain - LP shares redeemed and, on a Protected note, SGOV sold, each in the ratio of the shortfall to their combined value, rounded up. The Income perp sleeve is not on chain and is not touched, so an exit larger than the rest of the note reverts Shortfall - the same limit the hedged vault has and the same answer, the hedger repatriates. A full exit unwinds everything, takes the fee on the gross balance, and pays the rest, for the reason LPVault gives: paying the preview would either revert on the closing swap's price impact or strand dust nobody can claim. redeem() returns what was paid.
A deposit after maturity reverts Matured, in deposit, mint and _deposit alike.
What "protected" does not mean
The floor is arithmetic, not a promise. It assumes the SGOV sleeve can be sold for what SGOV is worth, that the asset/SGOV pool it is sold through has depth on the day, and that the token tracks the fund it is named after. None of those is underwritten by anyone. SGOV_SLIPPAGE_BPS is the bound the contract puts on each sale, not a guarantee the sale clears at it, and the TWAP band that protects the sale from a manipulated price also stops it, and every exit with it, while the pool is off its mean. The floor also does not cover the LP sleeve, which is 20 % of a seed note and falls with √s; the sleeve equity, on an Income note, is a number one key reports; and the asset the whole note is measured in is WETH, so a protected note is protected in ETH, not in dollars.
Errors
| Error | Raised by | When |
|---|---|---|
AlreadyInitialized | initialize | A second initialisation, or the implementation itself. |
BadSleeve | initialize, reportSleeve | A sleeve of 0 or of 10 000 or more; a report on a Protected note. |
Matured | initialize, deposit, mint | A maturity not in the future; a deposit on or after the date. |
NotMatured | settle | Before the date. |
AlreadySettled | settle | A second settlement. |
NotAsset | initialize | The LP vault is on another asset, or sgovPool is not the asset/SGOV pair. |
NotHedger | reportSleeve | The caller is not the hedger. |
StaleReport | reportSleeve | asOf is in the future or not later than the last report. |
HedgePaused_ | deposit, mint, withdraw, redeem | paused() is true: a jump or a stale sleeve report. |
CapExceeded | deposit, mint | totalAssets() plus the deposit would pass cap. |
ZeroShares | deposit, mint | The deposit would mint nothing. |
Shortfall | withdraw, redeem | The on-chain sleeves cannot raise the amount; the perp sleeve is not reachable. |
TwapWindowTooShort, PriceDeviation | Protected entry, exit, settle | The SGOV pool's guard, from TwapGuard. |
Events
| Event | When |
|---|---|
CapUpdated(cap) | Once, at initialisation. |
SleeveReported(unrealizedPnlUsd, equityUsd, asOf) | Every accepted reportSleeve. |
HedgePaused(reason) / HedgeResumed() | The jump gate, with the hedged vault's signatures. |
Settled(assetsOut) | Once, after maturity. |
Deposit / Withdraw | The ERC-4626 events; Withdraw reports the net amount paid. |
VaultCreated(vault, sgovPool, asset, 2, name) | On the factory; the pool field is the SGOV pool, zero for an Income note. |
contracts/src/StructuredVault.solthe header, _deposit, _withdraw, _raise, settle, reportSleeve, payoffCurve, _requireOpen, _swapThroughSgovPoolpackages/core/src/payoff.tsincomePayoff, protectedPayoff, payoffCurve, the full-range approximationcontracts/fixtures/payoff.jsonthe eight pinned points per curve both sides reproducecontracts/script/KerfWiring.solINCOME_SLEEVE_BPS, PROTECTED_SLEEVE_BPS, NOTE_TERM and the two seed notescontracts/test/StructuredVault.t.solthe split in both modes, every report gate, the SGOV guard, the fee before and after maturity, settleapps/web/components/docs/PayoffDemo.tsxthe chart above, drawn from core with the default parametersapps/web/lib/yield/view.tspayoffPointsFor, applyEarlyExitFee, withdrawFeeNote, countdown