On-Chain Vault Integration
How a KYB-approved third-party vault deposits into Kasu for its own users: allowlisting, approvals, deposit and withdrawal requests, fixed terms, losses.
A third-party smart contract vault can integrate with Kasu to give its depositors access to Kasu lending pool yields. The vault is KYB'd (allowlisted) on Kasu's side, so individual vault depositors do not need to complete KYC — the vault itself is the single, verified depositor.
Architecture Overview#
┌─────────────────────────────────────────────┐
│ Vault Contract │
│ │
│ ┌──────────┐ ┌────────────────────────┐ │
│ │ USDC │ │ Kasu Pool Allocations │ │
│ │ Buffer │ │ │ │
│ │ │ │ Pool A / Tranche 1 │ │
│ │ Instant │ │ Pool B / Tranche 2 │ │
│ │ In/Out │ │ Pool C / Tranche 1 │ │
│ └──────────┘ └───────────┬────────────┘ │
│ │ │
└──────────────────────────────┼───────────────┘
│
USDC approve + requestDeposit()
│
▼
┌──────────────────────────────┐
│ LendingPoolManager │
│ (Kasu Entry Point) │
└──────────────────────────────┘Key design points:
- The vault maintains a USDC liquidity buffer for instant user deposits and withdrawals, since Kasu deposits/withdrawals are processed during clearing (weekly epochs).
- The vault is a single address allowlisted via
KasuAllowList.allowUser(). No per-transaction KYC signature is needed. - The vault operator configures which pools and tranches the vault allocates to. This can be a one-time configuration or dynamically managed.
- All interactions go through
LendingPoolManager— the vault calls the same user-facing functions as any other depositor.
Prerequisites#
Before the vault can interact with Kasu:
1. Kasu Admin Allowlists the Vault#
The Kasu Admin must call allowUser() on the KasuAllowList contract to allowlist the vault address:
// Called by Kasu Admin (ROLE_KASU_ADMIN)
kasuAllowList.allowUser(vaultAddress);Once allowlisted, the vault can call requestDeposit() directly (without the WithKyc variant and without KYC signatures appended to calldata).
2. USDC Approval#
Before each deposit, the vault must approve the LendingPoolManager to spend USDC:
usdc.approve(lendingPoolManagerAddress, amount);3. Contract Addresses#
The vault needs to know the addresses of:
| Contract | Purpose |
|---|---|
LendingPoolManager | Entry point for all deposit/withdrawal operations |
USDC | The deposit token |
| Target lending pool(s) | The pools to deposit into |
Pool and tranche addresses can be discovered on-chain via ILendingPool.lendingPoolInfo() (see Reading Pool State).
For deployed addresses, see Deployed Addresses.
Core Operations#
All vault interactions go through LendingPoolManager. The vault calls these functions as msg.sender.
Requesting a Deposit#
Deposits are not instant. They are queued for the current epoch and processed at the next clearing.
function requestDeposit(
address lendingPool, // Target lending pool address
address tranche, // Target tranche address within the pool
uint256 maxAmount, // Maximum USDC amount to deposit
bytes calldata swapData, // Pass empty bytes (0x) — no swap needed for USDC
uint256 fixedTermConfigId, // 0 for flexible deposits, or a fixed-term config ID
bytes calldata depositData // Optional arbitrary data (emitted as event, not stored)
) external payable returns (uint256 dNftID);Parameters:
| Parameter | Value for Vault |
|---|---|
lendingPool | Address of the target lending pool |
tranche | Address of the target tranche (get from lendingPoolInfo().trancheAddresses) |
maxAmount | USDC amount to deposit (6 decimals) |
swapData | "" (empty bytes) — vault deposits USDC directly |
fixedTermConfigId | 0 for flexible deposit, or a valid config ID for fixed-term |
depositData | "" (empty bytes) or any custom data for off-chain tracking |
Returns: A D-NFT ID (dNftID) representing the pending deposit request. The vault receives this ERC-721 token from the pool's PendingPool contract.
Example:
// Approve USDC first
usdc.approve(address(lendingPoolManager), 100_000e6);
// Request deposit of 100,000 USDC to Pool A, Tranche 0 (Junior)
uint256 dNftId = lendingPoolManager.requestDeposit(
poolA,
poolATranches[0], // Junior tranche
100_000e6, // 100,000 USDC
"", // No swap
0, // Flexible deposit (no fixed term)
"" // No deposit data
);Important: Deposits submitted during the clearing period (~48 hours at the end of each epoch) are accepted but queued for the next epoch, not the current one. Cancellations are not allowed during the clearing period.
Cancelling a Deposit Request#
A pending deposit can be cancelled before clearing processes it. USDC is refunded to the vault.
function cancelDepositRequest(
address lendingPool, // The lending pool
uint256 dNftID // The D-NFT ID from requestDeposit
) external;Cancellations cannot be submitted during the clearing period.
Requesting a Withdrawal#
Withdrawals are also queued and processed at the next clearing.
function requestWithdrawal(
address lendingPool, // The lending pool
address tranche, // The tranche to withdraw from
uint256 amount // Amount of tranche shares to withdraw
) external returns (uint256 wNftID);Parameters:
| Parameter | Description |
|---|---|
lendingPool | Address of the lending pool |
tranche | Address of the tranche |
amount | Number of tranche shares to withdraw (not USDC). Use ILendingPoolTranche.userActiveShares(vault) to get the vault's share balance. |
Returns: A W-NFT ID (wNftID) representing the pending withdrawal request.
The USDC value of shares depends on the current exchange rate. Use ILendingPoolTranche.convertToAssets(shares) to estimate the USDC equivalent.
Important: Withdrawals may be partially filled during clearing if there isn't enough liquidity. Unfilled shares remain in the vault's position and can be re-requested in the next epoch.
Cancelling a Withdrawal Request#
function cancelWithdrawalRequest(
address lendingPool, // The lending pool
uint256 wNftID // The W-NFT ID from requestWithdrawal
) external;Cancellations cannot be submitted during the clearing period.
Fixed-Term Deposits#
The vault can lock deposits for a fixed duration with a guaranteed interest rate. Fixed-term deposits cannot be withdrawn early.
Locking an existing deposit into a fixed term:
function lockDepositForFixedTerm(
address lendingPool, // The lending pool
address tranche, // The tranche
uint256 amount, // Tranche shares to lock
uint256 fixedTermConfigId // The fixed-term configuration ID
) external;Requesting withdrawal of a matured fixed-term deposit:
function requestFixedTermDepositWithdrawal(
address lendingPool, // The lending pool
uint256 fixedTermDepositId // The fixed-term deposit ID
) external;Cancelling a fixed-term withdrawal request:
function cancelFixedTermDepositWithdrawalRequest(
address lendingPool, // The lending pool
uint256 fixedTermDepositId // The fixed-term deposit ID
) external;See Fixed Term Deposits for details on fixed-term configurations and maturity.
Claiming Repaid Losses#
If a loss event occurs and is later repaid by the pool operator, the vault can claim its share:
function claimRepaidLoss(
address lendingPool, // The lending pool
address tranche, // The tranche
uint256 lossId // The loss event ID
) external returns (uint256 claimedAmount);See Losses & Recovery for the full loss lifecycle.
Reading Pool State#
The vault should read on-chain state to make informed allocation decisions and track its positions.
Pool Information#
// Get tranche addresses and pending pool address
ILendingPool(lendingPool).lendingPoolInfo()
returns (LendingPoolInfo memory)
// .trancheAddresses — address[] of tranches (index 0 = Junior)
// .pendingPool — PendingPool contract address
// Get full pool configuration
ILendingPool(lendingPool).poolConfiguration()
returns (PoolConfiguration memory)
// .tranches[] — tranche configs (ratio, interestRate, minDeposit, maxDeposit)
// .drawRecipient — where drawn funds go
// .desiredDrawAmount — how much the pool wants to deploy
// .targetExcessLiquidityPercentage
// .minimumExcessLiquidityPercentage
// Check deposit limits for a tranche
ILendingPool(lendingPool).trancheConfigurationDepositLimits(tranche)
returns (uint256 minDepositAmount, uint256 maxDepositAmount)
// Available USDC in the pool (not drawn)
ILendingPool(lendingPool).availableFunds()
returns (uint256)
// Number of tranches
ILendingPool(lendingPool).lendingPoolTrancheCount()
returns (uint256)
// All tranche addresses
ILendingPool(lendingPool).lendingPoolTranches()
returns (address[] memory)
// Verify a pool exists
ILendingPoolManager(lendingPoolManager).isLendingPool(address)
returns (bool)Vault Position (Per Tranche)#
Each tranche is an ERC-4626 vault. The vault's position is tracked as tranche shares:
// Vault's active tranche shares (excludes shares locked in pending withdrawals)
ILendingPoolTranche(tranche).userActiveShares(vaultAddress)
returns (uint256)
// Vault's active position in USDC terms
ILendingPoolTranche(tranche).userActiveAssets(vaultAddress)
returns (uint256)
// Convert between shares and assets
ILendingPoolTranche(tranche).convertToAssets(shares)
returns (uint256 assets)
ILendingPoolTranche(tranche).convertToShares(assets)
returns (uint256 shares)
// Vault's total tranche share balance (including shares locked in pending withdrawals)
ILendingPoolTranche(tranche).balanceOf(vaultAddress)
returns (uint256)Pending Requests#
// Vault's pending deposit amount for a given epoch
IPendingPool(pendingPool).userPendingDepositAmount(vaultAddress, epochId)
returns (uint256)
// Total pending deposits across all users for current epoch
IPendingPool(pendingPool).pendingDepositAmountForCurrentEpoch()
returns (uint256)
// Details of a specific D-NFT
IPendingPool(pendingPool).trancheDepositNftDetails(dNftId)
returns (DepositNftDetails memory)
// .assetAmount — USDC amount
// .tranche — target tranche
// .epochId — request epoch
// Details of a specific W-NFT
IPendingPool(pendingPool).trancheWithdrawalNftDetails(wNftId)
returns (WithdrawalNftDetails memory)
// .sharesAmount — tranche shares
// .tranche — target tranche
// .epochId — request epochAPY & Yield#
Reading the current interest rate (APY):
Each tranche has an epoch interest rate stored on-chain. This is the rate applied per epoch (7 days), not annualized:
// Get the epoch interest rate for each tranche
PoolConfiguration memory config = ILendingPool(lendingPool).poolConfiguration();
uint256 epochRate = config.tranches[trancheIndex].interestRate;
// epochRate uses 18 decimals: 1e18 = 100%, 1e16 = 1%To convert to APY (annualized):
APY = (1 + epochRate / 1e18) ^ 52.17857 - 1Where 52.17857 is the number of 7-day epochs per year (365.25 / 7).
Accumulated yield for the vault:
There is no single on-chain view function that returns accumulated yield. Yield is embedded in the ERC-4626 share price — as interest accrues each epoch, each tranche share becomes worth more USDC.
To calculate the vault's accumulated yield, track deposits and withdrawals and compare against the current position value:
yieldEarned = currentAssetValue - totalDeposited + totalWithdrawnWhere:
currentAssetValue=ILendingPoolTranche(tranche).userActiveAssets(vaultAddress)totalDeposited= sum of all accepted deposit amounts (fromDepositAcceptedevents)totalWithdrawn= sum of all accepted withdrawal amounts (fromWithdrawalAcceptedevents)
The vault contract should maintain its own accounting of deposited and withdrawn amounts, or index the relevant events off-chain:
// Events emitted by PendingPool during clearing
event DepositRequestAccepted(
address indexed user, // vault address
address indexed tranche,
uint256 indexed dNftID,
uint256 acceptedAmount, // USDC deposited
uint256 trancheSharesMinted
);
event WithdrawalRequestAccepted(
address indexed user, // vault address
address indexed tranche,
uint256 indexed wNftID,
uint256 acceptedShares,
uint256 assetsWithdrawn // USDC withdrawn
);Overall pool yield:
Total interest distributed to a pool's LPs is available via InterestApplied events emitted by the LendingPool contract at each clearing:
event InterestApplied(
address indexed tranche,
uint256 indexed epoch,
uint256 interestAmount // USDC interest added to tranche
);Epoch Timing#
The vault should be aware of epoch boundaries and clearing periods. Deposits and withdrawals submitted during clearing are queued for the next epoch. Cancellations are blocked during clearing. The SystemVariables contract provides epoch timing:
// Current epoch number
ISystemVariables(systemVariables).getCurrentEpoch()
returns (uint256)
// Whether clearing is currently pending
// (check the lending pool's clearing state)Vault Lifecycle#
1. Setup#
- Deploy the vault contract with references to
LendingPoolManager,USDC, and target pool addresses - Complete KYB — the vault operator undergoes KYB verification with Kasu
- Kasu Admin allowlists the vault address via
kasuAllowList.allowUser(vaultAddress) - Configure allocations — define which pools and tranches to deposit into
2. Depositing into Kasu#
- Vault accumulates USDC from its own depositors
- Based on allocation strategy, vault calls
usdc.approve()thenrequestDeposit()for each target pool/tranche - Deposit is queued — vault receives a D-NFT
- At the next clearing (end of epoch), the deposit is processed:
- If accepted: USDC is transferred, vault receives tranche shares
- If rejected (e.g., pool capacity reached): USDC is returned, D-NFT is burned
- Vault can cancel before clearing via
cancelDepositRequest()
3. Earning Yield#
- Interest accrues automatically each epoch. Tranche shares increase in value relative to USDC.
- The vault's position value can be tracked via
userActiveAssets(vault). - No action is required from the vault to earn yield — it accrues passively.
4. Withdrawing from Kasu#
- Vault calls
requestWithdrawal()specifying the tranche shares to withdraw - Withdrawal is queued — vault receives a W-NFT
- At the next clearing:
- If accepted: tranche shares are burned, USDC is returned to the vault
- If partially accepted: remaining shares stay in the vault's position
- Vault distributes received USDC to its own users
5. Handling Losses#
If a loss event is reported on a pool where the vault has a position:
- Loss tokens (ERC-1155) may be minted to the vault address
- Loss tokens represent the vault's share of unrealized losses
- If the loss is later repaid, vault calls
claimRepaidLoss()to recover USDC - See Losses & Recovery
Important Considerations#
Timing#
| Operation | Timing |
|---|---|
| Deposit request | Instant (queued for current epoch) |
| Deposit execution | Next clearing (~weekly) |
| Withdrawal request | Instant (queued for current epoch) |
| Withdrawal execution | Next clearing (~weekly) |
| Interest accrual | Each epoch at clearing |
| Clearing period | ~48 hours at end of epoch — deposits/withdrawals queue for next epoch, cancellations blocked |
The vault should maintain a USDC buffer large enough to service its own user withdrawals between Kasu clearing cycles.
Deposit Limits#
Each tranche has minimum and maximum deposit amounts. The vault must respect these:
(uint256 min, uint256 max) = lendingPool.trancheConfigurationDepositLimits(tranche);These limits apply per deposit request, not cumulative. Multiple deposit requests can be submitted in the same epoch.
NFT Management#
The vault receives ERC-721 tokens (D-NFTs and W-NFTs) from the PendingPool contract representing pending requests. The vault contract must be able to receive ERC-721 tokens (implement IERC721Receiver or use onERC721Received).
Similarly, loss tokens are ERC-1155. The vault must implement IERC1155Receiver if it needs to handle loss scenarios.
Gas Considerations#
Each requestDeposit() and requestWithdrawal() call operates on a single pool and tranche. If the vault allocates across multiple pools/tranches, each requires a separate transaction.
Clearing Failures#
If clearing does not execute for a given epoch (e.g., the pool's clearing manager does not trigger it), pending requests remain queued. They carry over to the next epoch's clearing. The vault can cancel pending requests at any time outside of the clearing period.