TranchedVault¶
Senior/junior tranche wrapper over MultiStrategyVault. Splits MSV yield: fixed-APR senior paid first; junior takes first loss + surplus.
Source: khomdev-keep/src/TranchedVault.vy (Vyper 0.4.x)
Concept guide: Tranches protocol page
Wraps: MultiStrategyVault · pairs with Tranche (skUSD / jkUSD)
Implementation overview¶
- Waterfall —
senior_value = min(principal + accrued APR, msv_value);junior_value = remainder. - Loss order — junior wiped first when MSV underpays senior claim.
- Maturity — APR stops at
maturity_timestamp; early-exit penalty waived after maturity. - Penalties — early senior/junior exits pay penalty into MSV (benefits remaining junior holders).
flowchart TD
ST[Senior Tranche skUSD] --> TV[TranchedVault]
JT[Junior Tranche jkUSD] --> TV
TV --> MSV[MultiStrategyVault]
Immutables¶
| Name | Role |
|---|---|
asset |
Underlying USDC |
msv |
Wrapped MultiStrategyVault |
senior_apr_bps |
Fixed senior APR (capped 50%) |
loss_cap_bps |
Informational risk label |
early_redemption_bps |
Early exit penalty (max 5%) |
start_timestamp / maturity_timestamp |
Accrual window |
Events¶
| Event | When |
|---|---|
Initialized |
Tranches wired |
SeniorAccrued |
APR tick |
SeniorDeposit / JuniorDeposit |
Capital in |
SeniorExit / JuniorExit |
Redemptions + penalties |
Init¶
wire_tranches¶
TranchedVault.wire_tranches(senior_tranche_, junior_tranche_)
One-shot registration of senior and junior Tranche contracts.
| Param | Type | Description |
|---|---|---|
senior_tranche_ |
address |
Senior tranche (skUSD) |
junior_tranche_ |
address |
Junior tranche (jkUSD) |
Returns / state: Sets senior_tranche, junior_tranche, initialized = True. Cannot call twice.
Access: DEFAULT_ADMIN_ROLE only. Must be distinct non-zero addresses.
Events: Initialized(senior, junior).
Source (khomdev-keep/src/TranchedVault.vy:165-178):
@external
def wire_tranches(senior_tranche_: address, junior_tranche_: address):
access_control._check_role(access_control.DEFAULT_ADMIN_ROLE, msg.sender)
assert not self.initialized, "tv: already inited"
self.senior_tranche = senior_tranche_
self.junior_tranche = junior_tranche_
self.initialized = True
log Initialized(senior=senior_tranche_, junior=junior_tranche_)
Example:
Cross-links: Tranche · deposit_senior · deposit_junior
Deposits (tranche-only callers)¶
deposit_senior¶
TranchedVault.deposit_senior(assets)
Senior tranche hook: pull USDC from senior Tranche, deposit into underlying MSV, track principal.
| Param | Type | Description |
|---|---|---|
assets |
uint256 |
USDC amount (senior tranche already pulled from user) |
Returns / state: Accrues senior APR, deposits to msv, increments senior_principal.
Access: senior_tranche only (_only_senior). Not when paused.
Events: SeniorDeposit(assets).
Source (khomdev-keep/src/TranchedVault.vy:184-202):
@external
@nonreentrant
def deposit_senior(assets: uint256):
self._only_senior()
pausable._require_not_paused()
self._accrue()
extcall IERC20(asset).transferFrom(msg.sender, self, assets, ...)
extcall IERC4626(msv).deposit(assets, self)
self.senior_principal += assets
Example:
Cross-links: Tranche · MultiStrategyVault deposit · deposit_junior
deposit_junior¶
TranchedVault.deposit_junior(assets)
Junior tranche hook: pull USDC from junior Tranche, deposit into underlying MSV. No separate principal ledger — value tracked via waterfall.
| Param | Type | Description |
|---|---|---|
assets |
uint256 |
USDC amount (junior tranche already pulled from user) |
Returns / state: Accrues senior APR first (_accrue), deposits to msv. Increases shared MSV position → lifts junior_value.
Access: junior_tranche only (_only_junior). Not when paused.
Events: JuniorDeposit(assets).
Source (khomdev-keep/src/TranchedVault.vy:204-216):
@external
@nonreentrant
def deposit_junior(assets: uint256):
self._only_junior()
pausable._require_not_paused()
self._accrue()
extcall IERC20(asset).transferFrom(msg.sender, self, assets, ...)
extcall IERC4626(msv).deposit(assets, self)
log JuniorDeposit(assets=assets)
Example:
Cross-links: deposit_senior · junior_value · MultiStrategyVault
Exits (tranche-only callers)¶
process_senior_exit¶
TranchedVault.process_senior_exit(shares_burned, supply_pre_burn, receiver) -> uint256
Senior tranche exit settlement after shares burned. Pro-rata claim vs senior_value, early penalty if pre-maturity.
| Param | Type | Description |
|---|---|---|
shares_burned |
uint256 |
Senior shares already burned by Tranche |
supply_pre_burn |
uint256 |
Total senior supply before burn |
receiver |
address |
USDC payout recipient |
Returns: uint256 — USDC paid (final_paid = value_share - penalty).
Returns / state: Decrements senior_principal and senior_accrued pro-rata. Withdraws from MSV. Penalty stays in MSV (benefits junior).
Access: senior_tranche only. Not blocked by pause.
Events: SeniorExit(shares_burned, paid, penalty).
Source (khomdev-keep/src/TranchedVault.vy:218-252):
@external
@nonreentrant
def process_senior_exit(shares_burned, supply_pre_burn, receiver) -> uint256:
self._only_senior()
self._accrue()
value_share = senior_value * shares_burned // supply_pre_burn
if block.timestamp < maturity_timestamp:
penalty = value_share * early_redemption_bps // MAX_BPS
final_paid = value_share - penalty
extcall IERC4626(msv).withdraw(final_paid, receiver, self)
return final_paid
Example:
Cross-links: senior_value · is_matured · process_junior_exit
process_junior_exit¶
TranchedVault.process_junior_exit(shares_burned, supply_pre_burn, receiver) -> uint256
Junior tranche exit settlement after shares burned. Pro-rata claim vs junior_value.
| Param | Type | Description |
|---|---|---|
shares_burned |
uint256 |
Junior shares already burned |
supply_pre_burn |
uint256 |
Total junior supply before burn |
receiver |
address |
USDC payout recipient |
Returns: uint256 — USDC paid after optional early penalty.
Returns / state: Withdraws final_paid from MSV. Penalty remains in MSV (increases residual junior_value for holders).
Access: junior_tranche only. Not blocked by pause.
Events: JuniorExit(shares_burned, paid, penalty).
Source (khomdev-keep/src/TranchedVault.vy:254-280):
@external
@nonreentrant
def process_junior_exit(shares_burned, supply_pre_burn, receiver) -> uint256:
self._only_junior()
self._accrue()
value_share = junior_value * shares_burned // supply_pre_burn
if block.timestamp < maturity_timestamp:
penalty = value_share * early_redemption_bps // MAX_BPS
final_paid = value_share - penalty
extcall IERC4626(msv).withdraw(final_paid, receiver, self)
return final_paid
Example:
Cross-links: junior_value · process_senior_exit · is_matured
Views¶
senior_value¶
TranchedVault.senior_value() -> uint256
Live senior tranche claim in USDC terms: min(principal + accrued_now, msv_value).
| Returns | Type | Description |
|---|---|---|
| — | uint256 |
Senior waterfall cap; never exceeds MSV mark |
Access: Public view. Does not mutate state (uses live accrual math, not stored senior_accrued).
Events: None.
Source (khomdev-keep/src/TranchedVault.vy:287-292, _senior_value_view 389-393):
@external
@view
def senior_value() -> uint256:
return self._senior_value_view()
# internal:
s_owed = senior_principal + _accrued_now()
return min(s_owed, _msv_value())
Example:
Cross-links: accrued_now · msv_value · junior_value · Tranches concept
junior_value¶
TranchedVault.junior_value() -> uint256
Live junior residual: msv_value - senior_value (floored at 0).
| Returns | Type | Description |
|---|---|---|
| — | uint256 |
Junior claim after senior waterfall |
Access: Public view.
Events: None.
Source (khomdev-keep/src/TranchedVault.vy:296-300, _junior_value_view 397-402):
@external
@view
def junior_value() -> uint256:
return self._junior_value_view()
# internal:
v = _msv_value()
s = _senior_value_view()
return 0 if v <= s else v - s
Example:
Cross-links: senior_value · msv_value · process_junior_exit
msv_value¶
TranchedVault.msv_value() -> uint256
Mark-to-market USDC value of the vault's MSV share balance.
| Returns | Type | Description |
|---|---|---|
| — | uint256 |
convertToAssets(msv.balanceOf(self)) |
Access: Public view.
Events: None.
Source (khomdev-keep/src/TranchedVault.vy:304-308, _msv_value 381-385):
@external
@view
def msv_value() -> uint256:
return self._msv_value()
# internal:
our_shares = IERC20(msv).balanceOf(self)
return 0 if our_shares == 0 else IERC4626(msv).convertToAssets(our_shares)
Example:
Cross-links: MultiStrategyVault · senior_value · junior_value
accrued_now¶
TranchedVault.accrued_now() -> uint256
Pending senior interest accrued through current block (view-only; does not call _accrue()).
| Returns | Type | Description |
|---|---|---|
| — | uint256 |
senior_accrued + linear APR delta since last_accrue, capped at maturity_timestamp |
Access: Public view.
Events: None.
Source (khomdev-keep/src/TranchedVault.vy:312-316, _accrued_now 370-377):
@external
@view
def accrued_now() -> uint256:
return self._accrued_now()
# internal:
now_capped = min(block.timestamp, maturity_timestamp)
if now_capped <= last_accrue:
return senior_accrued
dt = now_capped - last_accrue
return senior_accrued + senior_principal * senior_apr_bps * dt // SECONDS_PER_YEAR // MAX_BPS
Example:
Cross-links: senior_value · is_matured · process_senior_exit
is_matured¶
TranchedVault.is_matured() -> bool
Whether the tranche has reached maturity_timestamp (APR stops; early-exit penalties waived).
| Returns | Type | Description |
|---|---|---|
| — | bool |
block.timestamp >= maturity_timestamp |
Access: Public view.
Events: None.
Source (khomdev-keep/src/TranchedVault.vy:320-324):
Example:
Cross-links: accrued_now · process_senior_exit · process_junior_exit
Emergency¶
pause¶
TranchedVault.pause()
Pause new senior/junior deposits. Exits (process_senior_exit, process_junior_exit) stay open.
Returns / state: Sets paused = True (snekmate pausable).
Access: DEFAULT_ADMIN_ROLE only.
Events: Paused(account) (snekmate pausable).
Source (khomdev-keep/src/TranchedVault.vy:331-336):
@external
def pause():
access_control._check_role(access_control.DEFAULT_ADMIN_ROLE, msg.sender)
pausable._pause()
Blocked when paused: deposit_senior, deposit_junior.
Example:
Cross-links: unpause · deposit_senior · deposit_junior
unpause¶
TranchedVault.unpause()
Resume senior/junior deposits after emergency pause.
Returns / state: Sets paused = False (snekmate pausable).
Access: DEFAULT_ADMIN_ROLE only.
Events: Unpaused(account) (snekmate pausable).
Source (khomdev-keep/src/TranchedVault.vy:339-344):
@external
def unpause():
access_control._check_role(access_control.DEFAULT_ADMIN_ROLE, msg.sender)
pausable._unpause()
Example:
Cross-links: pause · deposit_senior · deposit_junior