← Documentation home
Build · Contracts

PooledTrancheToken

Two plain ERC20 share tokens minted by every metavault. lcSEN for senior, lcJUN for junior. Fungible, transferable, and the source of truth for tranche balances.

What gets deployed

The TranchedMetaVault constructor deploys two PooledTrancheToken instances:

Each token is a standalone ERC20 receipt with EIP-2612 permit support (ERC20Permit). The metavault is the only minter/burner; depositors and withdrawers go through the metavault's depositSenior / withdrawSenior (or junior counterparts), which drive the tranche-token state. Notice locks use standard transferFrom / transfer, so a holder must approve (or sign a permit for) the metavault before filing a notice.

How positions are represented

Each holder's position is a balance on the matching tranche token: seniorToken.balanceOf(holder) is the holder's lcSEN share count. There is no per-deposit metadata, no NFT, no per-position cooldown. New depositors buy in at the prevailing share price (lens.seniorSharePrice(vault) or lens.juniorSharePrice(vault)), which embeds the full state of the tranche NAV up to that block.

The metavault tracks the aggregates separately:

Share-price math is trancheNav × 1e18 / trancheToken.totalSupply(), surfaced via the Lens's seniorSharePrice(vault) and juniorSharePrice(vault) views — see the Lens contract page.

ERC20 surface

// Standard ERC20
function name() external view returns (string memory);
function symbol() external view returns (string memory);
function decimals() external view returns (uint8);
function balanceOf(address account) external view returns (uint256);
function totalSupply() external view returns (uint256);
function transfer(address to, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
function approve(address spender, uint256 amount) external returns (bool);
 
// EIP-2612 permit (gasless approvals via signature)
function permit(
  address owner, address spender, uint256 value,
  uint256 deadline, uint8 v, bytes32 r, bytes32 s
) external;
function nonces(address owner) external view returns (uint256);
function DOMAIN_SEPARATOR() external view returns (bytes32);
 
// LayerCover-specific
function trancheId() external view returns (uint8);
function controller() external view returns (address);

Pricing and capacity views live on the Lens, not on the token or the metavault. The Lens is stateless, takes the metavault address as its first argument, and runs the same sync the metavault would, so every read reflects up-to-the-block NAV:

function previewDepositSenior(address vault, uint256 assets) external view returns (uint256 shares);
function previewDepositJunior(address vault, uint256 assets) external view returns (uint256 shares);
function previewWithdrawSenior(address vault, uint256 shares) external view returns (uint256 assets);
function previewWithdrawJunior(address vault, uint256 shares) external view returns (uint256 assets);
function maxSeniorDepositAssets(address vault, address receiver) external view returns (uint256 assets);
function maxJuniorDepositAssets(address vault, address receiver) external view returns (uint256 assets);
 
function seniorSharePrice(address vault) external view returns (uint256);
function juniorSharePrice(address vault) external view returns (uint256);

maxJuniorWithdrawAssets (the asset value a junior holder can currently exit before the 4× leverage cap or underlying liquidity binds) is exposed as a field on the VaultSnapshot struct returned by lens.snapshot(vault) rather than as a standalone function — one call returns the full UI payload.

This keeps the receipt token simple and avoids stale ERC4626-style previews on the token itself. Integrators use the Lens for pricing and capacity, the metavault for deposits / notices / withdrawals, and the token for ownership, transferability, and approvals.

Notice locks

Filing a withdrawal notice via the metavault calls transferFrom(holder, metavault, shares) on the tranche token. That requires either a prior approve from the holder, or an EIP-2612 permit signature consumed in the same transaction. The shares sit on the metavault's own balance while the notice is pending; cancelling moves them back via transfer(holder, shares); executing burns them.

There is no privileged lock path. The metavault uses the public ERC20 surface like any other spender. The token also blocks direct transfers landing on the controller from any other caller, so a holder can't strand shares by transfer(controller, X) outside the notice flow.

Mint / burn

function mint(address to, uint256 amount) external onlyController;
function burn(address from, uint256 amount) external onlyController;

onlyController checks msg.sender == controller, where controller is the metavault set in the constructor. Any other caller reverts with OnlyController().

The metavault calls:

Standard token behavior

PooledTrancheToken is a fully fungible ERC20 via OpenZeppelin's ERC20Permit base. balanceOf, transfer, transferFrom, approve, totalSupply, name, symbol, decimals, and the EIP-2612 permit / nonces / DOMAIN_SEPARATOR surface all behave standard. The tranche-specific behavior is intentionally kept on the metavault.

Indexing notes