Read state, deposit, and withdraw against a pooled tranched metavault. Examples in TypeScript (viem), JavaScript (ethers), and Python (web3.py).
npm install viemThe Lens is one stateless contract per chain. One snapshot(vault) call returns every field a UI needs, atomic to a single block.
import { createPublicClient, http } from "viem";
import { arbitrumSepolia } from "viem/chains";
const LENS = "0x..." as const; // see /deployments
const VAULT = "0x..." as const; // pooled metavault address
const client = createPublicClient({ chain: arbitrumSepolia, transport: http() });
const snapshot = await client.readContract({
address: LENS,
abi: lensAbi,
functionName: "snapshot",
args: [VAULT],
});
console.log({
asset: snapshot.asset,
protectionPremiumBps: snapshot.protectionPremiumBps,
seniorNav: snapshot.seniorNav,
juniorNav: snapshot.juniorNav,
seniorSharePrice: snapshot.seniorSharePrice,
juniorSharePrice: snapshot.juniorSharePrice,
maxJuniorWithdrawAssets: snapshot.maxJuniorWithdrawAssets,
});The Lens returns the user's lcSEN / lcJUN balances plus any outstanding withdrawal notice on each side.
const position = await client.readContract({
address: LENS,
abi: lensAbi,
functionName: "userPosition",
args: [VAULT, userAddress],
});
console.log({
seniorShares: position.senior.shares,
seniorAssets: position.senior.assetsAtCurrentNav,
seniorNotice: position.senior.notice, // { shares, executableAt }
juniorShares: position.junior.shares,
juniorAssets: position.junior.assetsAtCurrentNav,
juniorNotice: position.junior.notice,
});A user has at most one outstanding notice per (address, tranche) pair. notice.shares > 0 means a notice is currently queued.
Approve the metavault to pull the underlying asset, then call depositSenior. lcSEN is minted to receiver at the current senior unit price.
import { parseUnits, maxUint256 } from "viem";
// 1. Approve the metavault to pull USDC.
await walletClient.writeContract({
address: USDC,
abi: erc20Abi,
functionName: "approve",
args: [VAULT, maxUint256],
});
// 2. Deposit. lcSEN shares are minted to `receiver` at the current senior unit price.
const amount = parseUnits("1000", 6); // 1000 USDC
const shares = await walletClient.writeContract({
address: VAULT,
abi: trancheVaultAbi,
functionName: "depositSenior",
args: [amount, userAddress],
});Reverts with:
ProtectionTooThin(seniorAssets, juniorAssets, requiredJuniorAssets): the new senior NAV would push past 4× junior NAV.DepositsPaused(): global pause active (single flag; affects both tranches).InsufficientAssetReceived(expected, received): fee-on-transfer asset detected.Same shape as senior — only the function name changes. Junior deposits don't have the leverage-cap revert (more junior is always fine). Blocked only by DepositsPaused.
const shares = await walletClient.writeContract({
address: VAULT,
abi: trancheVaultAbi,
functionName: "depositJunior",
args: [amount, userAddress],
});Senior withdrawals are gated by a 5-day notice. File the notice, wait, then execute. The redemption is priced at execution-time NAV.
// 1. File the notice. This locks `shares` worth of lcSEN on the tranche token.
await walletClient.writeContract({
address: VAULT,
abi: trancheVaultAbi,
functionName: "requestSeniorWithdrawal",
args: [shares],
});
// 2. Wait 5 days (SENIOR_WITHDRAWAL_NOTICE).
// Cancel any time with cancelSeniorWithdrawal(); refile to stack the noticed
// amount and re-stamp executableAt.
// 3. Execute within the 3-day window after maturity.
const assets = await walletClient.writeContract({
address: VAULT,
abi: trancheVaultAbi,
functionName: "withdrawSenior",
args: [shares, userAddress, userAddress],
});Reverts with:
WithdrawalNoticePending(executableAt): called before the 5-day notice has matured.WithdrawalNoticeStale(staleAt): matured, but the 3-day execution window has elapsed. Cancel or refile.WithdrawalNoticeTooSmall(requested, noticed): execution requested more shares than the notice covers.WithdrawalNoticeExpired(): execution attempted with no active notice.InsufficientVaultLiquidity(requestedShares, availableShares): underlying vault can't redeem right now (transient).To preview the expected payout without executing:
const expectedAssets = await client.readContract({
address: LENS,
abi: lensAbi,
functionName: "previewWithdrawSenior",
args: [VAULT, shares],
});The loss waterfall is applied to NAV continuously, so the senior unit price already reflects whatever protection has been consumed — there is no separate "claim" path on senior exit.
Junior withdrawals are gated by a 10-day notice. Same shape as senior, with an additional check at execution: the redemption must not push senior past 4× junior NAV.
// 1. File the notice.
await walletClient.writeContract({
address: VAULT,
abi: trancheVaultAbi,
functionName: "requestJuniorWithdrawal",
args: [shares],
});
// 2. Wait 10 days (JUNIOR_WITHDRAWAL_NOTICE).
// 3. Execute within the 3-day window after maturity.
await walletClient.writeContract({
address: VAULT,
abi: trancheVaultAbi,
functionName: "withdrawJunior",
args: [shares, userAddress, userAddress],
});Additional revert (on top of the senior list above):
InsufficientJuniorProtection(requestedAssets, maxAssets): the redemption would breach the leverage cap. Use maxJuniorWithdrawAssets on the Lens snapshot to preview the current cap-respecting maximum.The two PooledTrancheToken instances (lcSEN, lcJUN) are plain ERC20Permit receipt tokens. The metavault is the only minter/burner; holder-side transfers (including notice locks) go through the standard ERC20 surface, plus EIP-2612 permit so a holder can authorise the metavault to pull shares for a withdrawal notice without a separate approve tx.
const seniorToken = "0x..." as const; // metavault.seniorToken()
await walletClient.writeContract({
address: seniorToken,
abi: erc20PermitAbi,
functionName: "permit",
args: [holder, metavault, value, deadline, v, r, s],
});Pricing and capacity views live on the metavault (or the Lens), not on the receipt tokens. There is no ERC4626 deposit / redeem on the tranche tokens themselves. Always go through TranchedMetaVault for deposits, notices, and withdrawals.