Appearance
Textile Protocol v2.1 — Developer Guide
A practical guide for working with the Textile Protocol v2.1
Table of Contents
- Quick Start
- Architecture Overview
- Core Concepts
- Common Operations
- Frontend Integration
- Backend Integration
- Event Handling
- Testing & Development
- Troubleshooting
Quick Start
🔑 Critical: One Pool = One Borrower
Each pool in v2.1 serves exactly ONE borrower (set at creation, immutable). To work with multiple borrowers, create multiple pools.
What's New in v2.1?
Protocol v2.1 is a complete rewrite with institutional-grade features:
- ✅ One Borrower Per Pool: Dedicated credit lines, immutable borrower address
- ✅ ERC4626 Standard: Tranches are now standard ERC4626 vaults
- ✅ Immutable Custody: Reserve contracts provide trustless asset custody
- ✅ Factory Pattern: Deploy pools via minimal proxies (~200k gas vs 3M+ gas)
- ✅ Withdrawal Approvals: Optional two-step withdrawal flow for better liquidity management
- ✅ Settings Proposals: 2-step approval for parameter changes (with underwriter consensus)
- ✅ Per-Second Interest: RAY precision (10^27) compounding every second
- ✅ Auto-Interest Compounding Control: Per-LP income distribution and liquidity management
- ✅ Modular Infrastructure: Registry, ProtocolConfiguration, WithdrawalRegistry
Deploy Your First Pool (5 minutes)
bash
# Start local Hardhat node
cd packages/protocol
yarn hardhat node
# Deploy v2.1 infrastructure (in another terminal)
yarn deploy:v2:localThe deployment script will:
- Deploy all infrastructure contracts (Registry, factories, etc.)
- Deploy a sample pool with USDC
- Save addresses to
packages/constants/src/addresses.localhost.json
Interact with a Pool
typescript
import { useContractRead, useContractWrite, useAccount } from 'wagmi'
import { parseUnits, formatUnits } from 'viem'
import { SAMPLE_POOL, USDC_ADDRESS } from '@textile/constants'
const { address } = useAccount()
const poolAddress = SAMPLE_POOL
// Read tranche address
const { data: trancheAddress } = useContractRead({
address: poolAddress,
abi: StructuredPoolABI,
functionName: 'trancheList',
args: [0n]
})
console.log('Tranche address:', trancheAddress)
// Approve USDC
const { write: approve } = useContractWrite({
address: USDC_ADDRESS,
abi: ERC20ABI,
functionName: 'approve',
args: [trancheAddress, parseUnits('1000', 6)]
})
// Deposit to tranche
const { write: deposit } = useContractWrite({
address: trancheAddress,
abi: TrancheABI,
functionName: 'deposit',
args: [parseUnits('1000', 6), address],
onSuccess: (data) => {
console.log('Deposit successful:', data)
}
})
// Check shares balance
const { data: shares } = useContractRead({
address: trancheAddress,
abi: TrancheABI,
functionName: 'balanceOf',
args: [address]
})
console.log('Your shares:', shares ? formatUnits(shares, 6) : '0')Architecture Overview
The Big Picture
Component Roles
| Component | Purpose | Who Interacts |
|---|---|---|
| StructuredPool | Coordinates capital deployment, repayment, and integrated credit line | Borrower, Manager |
| Tranche | ERC4626 vault for LP deposits/withdrawals | LPs, Frontend |
| Reserve | Holds actual assets (custody layer) | Internal only |
| Registry | Pool registry and infrastructure references | Internal lookups |
| WithdrawalRegistry | Manages withdrawal approvals | LPs, Manager, Frontend |
| PoolFactory | Deploys new pools | Pool creators, Frontend |
| UnderwriterRegistry | Central registry for underwriter profiles and fees | Underwriters, Frontend |
Core Concepts
1. Virtual Accounting
Tranches use virtual accounting to track assets without holding them:
typescript
import { useContractRead } from 'wagmi'
import { formatUnits } from 'viem'
// Get all virtual balances in one call (more efficient)
const { data: virtualBalances } = useContractRead({
address: trancheAddress,
abi: TrancheABI,
functionName: 'getVirtualBalances'
})
// Returns: [balance, deployed, interestEarned]
if (virtualBalances) {
console.log('Available:', formatUnits(virtualBalances[0], 6), 'USDC')
console.log('Deployed:', formatUnits(virtualBalances[1], 6), 'USDC')
console.log('Interest Earned:', formatUnits(virtualBalances[2], 6), 'USDC')
}
// Get total assets (includes pending interest)
const { data: totalAssets } = useContractRead({
address: trancheAddress,
abi: TrancheABI,
functionName: 'totalAssets' // virtualBalance + virtualDeployed + pendingInterest
})
console.log('Total Assets:', formatUnits(totalAssets || 0n, 6), 'USDC')Flow:
- Deposit: Assets go to Reserve,
virtualBalanceincreases - Deployment: Assets move to the pool's borrower,
virtualBalance→virtualDeployed - Repayment: Assets return from borrower to Reserve, updates virtual balances
- Withdrawal: Assets leave Reserve to lenders,
virtualBalancedecreases
2. ERC4626 Share Pricing
Tranches follow standard ERC4626:
typescript
import { useContractRead } from 'wagmi'
import { formatUnits } from 'viem'
// Read total assets and supply
const { data: totalAssets } = useContractRead({
address: trancheAddress,
abi: TrancheABI,
functionName: 'totalAssets'
})
const { data: totalSupply } = useContractRead({
address: trancheAddress,
abi: TrancheABI,
functionName: 'totalSupply'
})
// Calculate share price
const sharePrice = totalAssets && totalSupply && totalSupply > 0n
? Number(totalAssets) / Number(totalSupply)
: 1
console.log('Share Price:', sharePrice.toFixed(4))
// Preview conversions
const depositAmount = parseUnits('1000', 6)
const { data: expectedShares } = useContractRead({
address: trancheAddress,
abi: TrancheABI,
functionName: 'previewDeposit',
args: [depositAmount] // assets → shares (rounded down)
})
console.log('1000 USDC =', formatUnits(expectedShares || 0n, 6), 'shares')
// Reverse calculation
const { data: expectedAssets } = useContractRead({
address: trancheAddress,
abi: TrancheABI,
functionName: 'previewRedeem',
args: [expectedShares || 0n] // shares → assets (rounded down)
})
console.log(formatUnits(expectedShares || 0n, 6), 'shares =', formatUnits(expectedAssets || 0n, 6), 'USDC')3. Interest Accrual (RAY Precision)
Interest compounds every second at 10^27 precision:
typescript
// Interest rate in basis points (e.g., 500 = 5% APR)
const rateInBps = 500
// Converted to RAY precision internally
const rateInRay = (rateInBps * 10**27) / 10000
// Compound formula: A(t) = P * (1 + rate/RAY)^seconds
// Interest = A(t) - PDeveloper Note: Use the getDebtDetails() helper function for a complete breakdown:
typescript
import { useContractRead } from 'wagmi'
import { formatUnits } from 'viem'
// Get complete debt breakdown in one call
const { data: debtDetails } = useContractRead({
address: poolAddress,
abi: StructuredPoolABI,
functionName: 'getDebtDetails'
})
// Returns: [principal, accruedInterest, pendingInterest, totalDebt]
if (debtDetails) {
console.log('Principal owed:', formatUnits(debtDetails[0], 6), 'USDC')
console.log('Accrued interest:', formatUnits(debtDetails[1], 6), 'USDC')
console.log('Pending interest:', formatUnits(debtDetails[2], 6), 'USDC')
console.log('Total debt:', formatUnits(debtDetails[3], 6), 'USDC')
}
// Or use getTotalDebt() for just the total
const { data: totalDebt } = useContractRead({
address: poolAddress,
abi: StructuredPoolABI,
functionName: 'getTotalDebt'
})
console.log('Total owed:', formatUnits(totalDebt || 0n, 6), 'USDC')4. Withdrawal Modes
⚠️ IMPORTANT: Each tranche has a requireApprovalForWithdrawals setting that determines which withdrawal flow is used. Always check this setting before attempting to withdraw!
typescript
// First, check if approval is required for this specific tranche
const { data: requiresApproval } = useContractRead({
address: trancheAddress,
abi: TrancheABI,
functionName: 'requireApprovalForWithdrawals'
})
console.log('This tranche requires approval:', requiresApproval)Tranches support three withdrawal modes based on this setting:
A. Direct Withdrawal (No Approval Required)
Only works if: requireApprovalForWithdrawals = false for this tranche
typescript
import { useContractRead, useContractWrite, useAccount } from 'wagmi'
import { formatUnits } from 'viem'
const { address } = useAccount()
// ALWAYS check this first!
const { data: requiresApproval } = useContractRead({
address: trancheAddress,
abi: TrancheABI,
functionName: 'requireApprovalForWithdrawals'
})
if (requiresApproval) {
console.error('❌ This pool requires approval! Use the approval-based flow instead.')
// Don't attempt direct withdrawal!
return
}
console.log('✅ Direct withdrawal enabled')
// Check max withdrawal
const { data: maxCanWithdraw } = useContractRead({
address: trancheAddress,
abi: TrancheABI,
functionName: 'maxWithdraw',
args: [address]
})
console.log('Max withdrawal:', formatUnits(maxCanWithdraw || 0n, 6), 'USDC')
// Direct withdrawal
const { write: withdraw } = useContractWrite({
address: trancheAddress,
abi: TrancheABI,
functionName: 'withdraw',
args: [amount, address, address],
onSuccess: () => console.log('✅ Withdrawal successful!')
})
withdraw?.()B. Approval-Based (Two-Step)
Only works if: requireApprovalForWithdrawals = true for this tranche
typescript
import { useContractWrite, useContractRead, useContractEvent, useAccount } from 'wagmi'
import { formatUnits } from 'viem'
import { WITHDRAWAL_REGISTRY } from '@textile/constants'
const { address } = useAccount()
// ALWAYS check this first!
const { data: requiresApproval } = useContractRead({
address: trancheAddress,
abi: TrancheABI,
functionName: 'requireApprovalForWithdrawals'
})
if (!requiresApproval) {
console.error('❌ This pool does NOT require approval! Use direct withdrawal instead.')
// Use the direct withdrawal flow
return
}
console.log('✅ Approval-based withdrawal - following 3-step process')
// Step 1: Request withdrawal
const { write: requestWithdraw } = useContractWrite({
address: trancheAddress,
abi: TrancheABI,
functionName: 'requestWithdraw',
args: [amount],
onSuccess: () => console.log('✅ Withdrawal request submitted, waiting for approval...')
})
// Step 2: Listen for manager approval
useContractEvent({
address: WITHDRAWAL_REGISTRY,
abi: WithdrawalRegistryABI,
eventName: 'WithdrawalApproved',
listener: (logs) => {
const { tranche, user, shares } = logs[0].args
if (tranche === trancheAddress && user === address) {
console.log('✅ Withdrawal approved!')
console.log('Approved shares:', formatUnits(shares, 6))
}
}
})
// Step 3: Check approved shares and execute
const { data: withdrawalRequest } = useContractRead({
address: WITHDRAWAL_REGISTRY,
abi: WithdrawalRegistryABI,
functionName: 'getWithdrawalRequest',
args: [trancheAddress, address]
})
const approvedShares = withdrawalRequest?.approved || 0n
const { write: redeem } = useContractWrite({
address: trancheAddress,
abi: TrancheABI,
functionName: 'redeem',
args: [approvedShares, address, address],
onSuccess: () => console.log('✅ Withdrawal executed!')
})C. Pool-Managed Processing
Special case: Pool manager can process withdrawals directly (bypasses user shares). This is independent of the requireApprovalForWithdrawals setting—only the pool manager can use this.
typescript
import { useContractWrite } from 'wagmi'
import { formatUnits } from 'viem'
// Manager processes withdrawal directly (doesn't burn user shares)
// Use case: Emergency liquidity management, fee collection, etc.
const { write: processWithdrawal } = useContractWrite({
address: poolAddress,
abi: StructuredPoolABI,
functionName: 'processWithdrawal',
args: [trancheAddress, receiverAddress, amount],
onSuccess: () => {
console.log('✅ Withdrawal processed!')
console.log('Receiver:', receiverAddress)
console.log('Amount:', formatUnits(amount, 6), 'USDC')
}
})
processWithdrawal?.()
// Note: This is a manager-only function. Regular users cannot call this.5. Auto-Interest Compounding Control
Feature: Per-LP control over how interest earnings are received.
Purpose: Income generation and liquidity management strategies.
Key Mechanism: Reserved interest is a liquidity lock. Claiming performs a forced partial redemption (burns shares, gives cash).
How It Works
typescript
// Default: Auto-compounding ENABLED
// - Interest reinvests automatically
// - Share price grows
// - Simplest approach
// Optional: Disable compounding
// - User's share of interest is reserved as locked liquidity
// - Cannot be deployed to borrowers
// - LP can claim by burning shares (partial redemption)
// - Allows extracting interest as cash while keeping principal investedValue Conservation Example
typescript
// Initial: 100 shares @ $1.00 = $100
// Interest: $10 paid, user owns 50% → $5 reserved
// Share price increases to $1.10 for everyone
// User's 100 shares now worth: $110
// Claim $5 reserved interest:
// - Burns: $5 / $1.10 = 4.545 shares
// - Receives: $5 cash
// - Remaining: 95.455 shares @ $1.10 = $100
// - Total: $5 cash + $100 shares = $105 ✓
// No value created - just converted share value to cash!State Variables
typescript
// Per-LP setting (mapping)
const { data: compoundingDisabled } = useContractRead({
address: trancheAddress,
abi: TrancheABI,
functionName: 'autoInterestCompoundingDisabled',
args: [userAddress]
})
// Aggregate tracking
const { data: totalNonCompoundingShares } = useContractRead({
address: trancheAddress,
abi: TrancheABI,
functionName: 'totalSharesWithDisabledCompounding'
})
// Reserved interest pool (in assets)
const { data: reservedInterest } = useContractRead({
address: trancheAddress,
abi: TrancheABI,
functionName: 'reservedInterestForNonCompounding'
})Impact on Liquidity
Reserved interest reduces deployable liquidity:
typescript
const { data: availableForBorrowing } = useContractRead({
address: trancheAddress,
abi: TrancheABI,
functionName: 'getAvailableForBorrowing'
})
// Returns: virtualBalance - withdrawalReserves - reservedInterestForNonCompounding
// More non-compounding LPs = less liquidity for borrowersImportant: Pool managers should monitor reservedInterestForNonCompounding when planning capital deployment.
Events to Monitor
typescript
// LP changes their setting
event AutoInterestCompoundingSet(address indexed user, bool enabled)
// Interest reserved on payment
event InterestReservedForNonCompounding(uint256 amount, uint256 totalSharesWithDisabledCompounding)
// LP claims interest (or auto-claimed on re-enable)
event ReservedInterestClaimed(address indexed user, uint256 amount)Common Operations
Deploy a New Pool
typescript
import { useContractWrite, useWaitForTransaction, useAccount } from 'wagmi'
import { parseUnits, parseEventLogs } from 'viem'
import { POOL_FACTORY } from '@textile/constants'
// Prepare settings (must match DataTypes.sol)
const trancheSettings = {
seniority: 1, // 1=Senior, 2=Mezzanine, 3=Junior
minimumDeposit: parseUnits('100', 6), // 100 USDC minimum
maximumTVL: parseUnits('1000000', 6), // 1M USDC maximum
withdrawalPeriod: 0, // 0 = immediate withdrawals (in seconds)
requireApprovalForWithdrawals: true, // Enable approval workflow
targetAllocation: 10000 // 10000 = 100% target allocation in bps
}
const creditSettings = {
borrower: borrowerAddress, // ⚠️ IMMUTABLE: Set once at pool creation
interestRate: 800, // 8% APR in basis points
drawLimit: parseUnits('500000', 6), // 500k USDC credit line
interestPaymentInterval: 2592000 // 30 days in seconds
}
// Deploy pool
const { write: deployPool } = useContractWrite({
address: POOL_FACTORY,
abi: PoolFactoryABI,
functionName: 'deployPool',
args: [
USDC_ADDRESS, // Asset (USDC)
'SME Lending Pool', // Pool name
'Short-term loans to SMEs', // Strategy description
'Senior Tranche', // Tranche name
'ST-SME', // Tranche symbol
trancheSettings,
creditSettings,
managerAddress // Who can approve withdrawals
],
onSuccess: (data) => {
console.log('Pool deployment tx:', data)
}
})
// Listen for PoolDeployed event
useContractEvent({
address: POOL_FACTORY,
abi: PoolFactoryABI,
eventName: 'PoolDeployed',
listener: (logs) => {
const { deploymentId, pool, asset, poolName, deployer } = logs[0].args
console.log('✅ Pool deployed successfully!')
console.log('Deployment ID:', deploymentId)
console.log('Pool address:', pool)
console.log('Asset:', asset)
console.log('Name:', poolName)
console.log('Deployer:', deployer)
}
})Deposit (Liquidity Provider)
typescript
import { useContractRead, useContractWrite, useAccount } from 'wagmi'
import { parseUnits, formatUnits } from 'viem'
const { address } = useAccount()
const depositAmount = parseUnits('10000', 6) // 10k USDC
// Get max deposit allowed
const { data: maxDeposit } = useContractRead({
address: trancheAddress,
abi: TrancheABI,
functionName: 'maxDeposit',
args: [address]
})
console.log('Max deposit:', formatUnits(maxDeposit || 0n, 6), 'USDC')
// Preview shares you'll receive
const { data: expectedShares } = useContractRead({
address: trancheAddress,
abi: TrancheABI,
functionName: 'previewDeposit',
args: [depositAmount]
})
console.log('Expected shares:', formatUnits(expectedShares || 0n, 6))
// Approve USDC
const { write: approve } = useContractWrite({
address: USDC_ADDRESS,
abi: ERC20ABI,
functionName: 'approve',
args: [trancheAddress, depositAmount],
onSuccess: () => console.log('✅ USDC approved')
})
// Deposit
const { write: deposit } = useContractWrite({
address: trancheAddress,
abi: TrancheABI,
functionName: 'deposit',
args: [depositAmount, address],
onSuccess: (data) => {
console.log('✅ Deposit successful!')
console.log('Transaction hash:', data.hash)
}
})Withdraw (Liquidity Provider)
typescript
import { useContractRead, useContractWrite, useAccount } from 'wagmi'
import { parseUnits, formatUnits } from 'viem'
import { WITHDRAWAL_REGISTRY } from '@textile/constants'
const { address } = useAccount()
const withdrawAmount = parseUnits('5000', 6)
// STEP 1: Check if this pool requires approval (CRITICAL!)
const { data: requiresApproval } = useContractRead({
address: trancheAddress,
abi: TrancheABI,
functionName: 'requireApprovalForWithdrawals'
})
console.log('Requires approval:', requiresApproval)
// STEP 2: Check available liquidity
const { data: maxWithdraw } = useContractRead({
address: trancheAddress,
abi: TrancheABI,
functionName: 'maxWithdraw',
args: [address]
})
console.log('Can withdraw:', formatUnits(maxWithdraw || 0n, 6), 'USDC')
// STEP 3: Follow appropriate flow based on requiresApproval
if (requiresApproval) {
console.log('⚠️ This tranche requires manager approval for withdrawals')
console.log('You must: 1) Request, 2) Wait for approval, 3) Execute')
// Step 1: Request withdrawal
const { write: requestWithdraw } = useContractWrite({
address: trancheAddress,
abi: TrancheABI,
functionName: 'requestWithdraw',
args: [withdrawAmount],
onSuccess: () => console.log('✅ Withdrawal request submitted')
})
// Check approved shares
const { data: withdrawalRequest } = useContractRead({
address: WITHDRAWAL_REGISTRY,
abi: WithdrawalRegistryABI,
functionName: 'getWithdrawalRequest',
args: [trancheAddress, address]
})
const approvedShares = withdrawalRequest?.approved || 0n
console.log('Approved shares:', formatUnits(approvedShares, 6))
// Step 2: Execute after approval
if (approvedShares && approvedShares > 0n) {
const { write: redeem } = useContractWrite({
address: trancheAddress,
abi: TrancheABI,
functionName: 'redeem',
args: [approvedShares, address, address],
onSuccess: () => console.log('✅ Withdrawal executed!')
})
}
} else {
console.log('✅ Direct withdrawal enabled - no approval needed')
// Direct withdrawal (no approval needed)
const { write: withdraw } = useContractWrite({
address: trancheAddress,
abi: TrancheABI,
functionName: 'withdraw',
args: [withdrawAmount, address, address],
onSuccess: () => console.log('✅ Withdrawal successful!')
})
}
// Pro tip: Display this info in your UI so users know what to expect!Smart Withdrawal Component (Handles Both Modes)
Best practice: Build a component that automatically detects and handles the correct withdrawal flow:
typescript
import { useContractRead, useContractWrite, useAccount } from 'wagmi'
import { parseUnits, formatUnits } from 'viem'
import { WITHDRAWAL_REGISTRY } from '@textile/constants'
function SmartWithdrawal({ trancheAddress, amount }: Props) {
const { address } = useAccount()
const amountBigInt = parseUnits(amount, 6)
// Detect withdrawal mode
const { data: requiresApproval } = useContractRead({
address: trancheAddress,
abi: TrancheABI,
functionName: 'requireApprovalForWithdrawals'
})
// Check max withdrawal
const { data: maxWithdraw } = useContractRead({
address: trancheAddress,
abi: TrancheABI,
functionName: 'maxWithdraw',
args: [address]
})
// Check if user has pending approval (if applicable)
const { data: withdrawalRequest } = useContractRead({
address: WITHDRAWAL_REGISTRY,
abi: WithdrawalRegistryABI,
functionName: 'getWithdrawalRequest',
args: [trancheAddress, address],
enabled: requiresApproval === true
})
const approvedShares = withdrawalRequest?.approved || 0n
// Setup appropriate transaction based on mode
if (requiresApproval) {
// Approval-based mode
if (approvedShares && approvedShares > 0n) {
// Step 3: Execute approved withdrawal
const { write: executeWithdraw } = useContractWrite({
address: trancheAddress,
abi: TrancheABI,
functionName: 'redeem',
args: [approvedShares, address, address],
onSuccess: () => console.log('✅ Withdrawal executed!')
})
console.log('Ready to execute withdrawal')
console.log('Approved shares:', formatUnits(approvedShares, 6))
// Button: "Withdraw Now"
} else {
// Step 1: Request withdrawal
const { write: requestWithdraw } = useContractWrite({
address: trancheAddress,
abi: TrancheABI,
functionName: 'requestWithdraw',
args: [amountBigInt],
onSuccess: () => console.log('✅ Request submitted, waiting for approval')
})
console.log('⏳ No approval yet - must request first')
// Button: "Request Withdrawal"
}
} else {
// Direct withdrawal mode
const { write: withdraw } = useContractWrite({
address: trancheAddress,
abi: TrancheABI,
functionName: 'withdraw',
args: [amountBigInt, address, address],
onSuccess: () => console.log('✅ Withdrawal successful!')
})
console.log('⚡ Direct withdrawal available')
console.log('Max available:', formatUnits(maxWithdraw || 0n, 6), 'USDC')
// Button: "Withdraw Now"
}
}Display Withdrawal Requirements to Users
Always show this prominently in your UI:
typescript
import { useContractRead } from 'wagmi'
function WithdrawalRequirementsBadge({ trancheAddress }: Props) {
const { data: requiresApproval } = useContractRead({
address: trancheAddress,
abi: TrancheABI,
functionName: 'requireApprovalForWithdrawals'
})
if (requiresApproval === undefined) return null
return (
<div className={requiresApproval ? 'badge-warning' : 'badge-success'}>
{requiresApproval ? (
<>
⏳ <strong>Approval Required</strong>
<p>Withdrawals require manager approval (1-3 days)</p>
</>
) : (
<>
⚡ <strong>Instant Withdrawals</strong>
<p>Withdraw anytime (subject to available liquidity)</p>
</>
)}
</div>
)
}
// Usage in pool list
function PoolCard({ poolAddress, trancheAddress }: Props) {
const { data: settings } = useContractRead({
address: trancheAddress,
abi: TrancheABI,
functionName: 'getTrancheSettings'
})
console.log('Pool Withdrawal Settings:')
console.log('- Requires approval:', settings?.requireApprovalForWithdrawals)
console.log('- Withdrawal period:', settings?.withdrawalPeriod, 'seconds')
console.log('- Min deposit:', formatUnits(settings?.minimumDeposit || 0n, 6), 'USDC')
console.log('- Max deposit:', formatUnits(settings?.maximumTVL || 0n, 6), 'USDC')
}Auto-Interest Compounding (Income Distribution)
Purpose: Control how you receive interest earnings - as reinvested shares (default) or extractable cash for regular income.
Key Concept: Claiming reserved interest burns shares at current price. This is a forced partial redemption, not free money.
Check Compounding Status
typescript
// Check if user has compounding enabled
const { data: compoundingDisabled } = useContractRead({
address: trancheAddress,
abi: TrancheABI,
functionName: 'autoInterestCompoundingDisabled',
args: [address]
})
const isCompounding = !compoundingDisabled
console.log('Auto-compounding:', isCompounding ? 'Enabled' : 'Disabled')
// Check claimable reserved interest
const { data: claimable } = useContractRead({
address: trancheAddress,
abi: TrancheABI,
functionName: 'getClaimableReservedInterest',
args: [address]
})
console.log('Claimable interest:', formatUnits(claimable || 0n, 6), 'USDC')
// Check total reserved interest in pool
const { data: totalReserved } = useContractRead({
address: trancheAddress,
abi: TrancheABI,
functionName: 'reservedInterestForNonCompounding'
})
console.log('Total reserved:', formatUnits(totalReserved || 0n, 6), 'USDC')Disable Compounding (Lock Interest as Liquidity)
typescript
// Disable compounding - future interest will be reserved
const { write: disableCompounding } = useContractWrite({
address: trancheAddress,
abi: TrancheABI,
functionName: 'setAutoInterestCompounding',
args: [false], // false = disable compounding
onSuccess: () => {
console.log('✅ Compounding disabled')
console.log('Future interest will be reserved for claiming')
}
})Claim Reserved Interest
typescript
// Claim reserved interest (burns shares, gives cash)
const { write: claimInterest } = useContractWrite({
address: trancheAddress,
abi: TrancheABI,
functionName: 'claimReservedInterest',
onSuccess: (data) => {
console.log('✅ Interest claimed successfully!')
console.log('Note: Shares were burned equivalent to claimed amount')
}
})
// Listen for claim event to get exact amount
useContractEvent({
address: trancheAddress,
abi: TrancheABI,
eventName: 'ReservedInterestClaimed',
listener: (logs) => {
const { user, amount } = logs[0].args
console.log('User:', user)
console.log('Claimed:', formatUnits(amount, 6), 'USDC')
}
})Re-Enable Compounding
typescript
// Re-enable compounding (auto-claims any unclaimed interest)
const { write: enableCompounding } = useContractWrite({
address: trancheAddress,
abi: TrancheABI,
functionName: 'setAutoInterestCompounding',
args: [true], // true = enable compounding
onSuccess: () => {
console.log('✅ Compounding re-enabled')
console.log('Any unclaimed interest was automatically claimed')
}
})Complete Example: Regular Income Extraction
typescript
// Check current position
const { data: shares } = useContractRead({
address: trancheAddress,
abi: TrancheABI,
functionName: 'balanceOf',
args: [address]
})
const { data: sharesValue } = useContractRead({
address: trancheAddress,
abi: TrancheABI,
functionName: 'convertToAssets',
args: [shares]
})
console.log('Position:', formatUnits(shares || 0n, 6), 'shares')
console.log('Value:', formatUnits(sharesValue || 0n, 6), 'USDC')
// 1. Disable compounding
await disableCompounding()
// 2. Wait for interest to accrue...
// (borrower makes repayments with interest)
// 3. Check claimable amount
const { data: claimable } = useContractRead({
address: trancheAddress,
abi: TrancheABI,
functionName: 'getClaimableReservedInterest',
args: [address]
})
console.log('Claimable:', formatUnits(claimable || 0n, 6), 'USDC')
// 4. Claim when ready (burns shares, gives cash)
await claimInterest()
// 5. Verify outcome
const { data: sharesAfter } = useContractRead({
address: trancheAddress,
abi: TrancheABI,
functionName: 'balanceOf',
args: [address]
})
const { data: cashBalance } = useContractRead({
address: USDC_ADDRESS,
abi: ERC20ABI,
functionName: 'balanceOf',
args: [address]
})
console.log('Shares after:', formatUnits(sharesAfter || 0n, 6))
console.log('Cash balance:', formatUnits(cashBalance || 0n, 6), 'USDC')
console.log('Note: Shares were burned, cash was received')Important Notes:
- ✅ Share price still increases whether compounding is enabled or not
- ✅ Claiming burns shares at current price (forced partial redemption)
- ✅ No value is created - you're converting share value to cash
- ✅ Reserved interest reduces available liquidity for borrowers
- ✅ Re-enabling compounding auto-claims to prevent orphaned funds
- ✅ Use for regular income generation or accounting separation
Gas Costs:
- Disable compounding: ~50,000 gas
- Claim interest: ~80,000-120,000 gas
- Re-enable (with auto-claim): ~100,000-150,000 gas
Borrow (Borrower via Credit Line)
typescript
import { useContractRead, useContractWrite, useAccount } from 'wagmi'
import { parseUnits, formatUnits } from 'viem'
const { address } = useAccount()
const drawAmount = parseUnits('50000', 6) // 50k USDC
// Get available credit
const { data: availableCredit } = useContractRead({
address: poolAddress,
abi: StructuredPoolABI,
functionName: 'getAvailableCredit'
})
// Get credit limit
const { data: creditLimit } = useContractRead({
address: poolAddress,
abi: StructuredPoolABI,
functionName: 'getCreditLimit'
})
console.log('Available credit:', formatUnits(availableCredit || 0n, 6), 'USDC')
console.log('Credit limit:', formatUnits(creditLimit || 0n, 6), 'USDC')
// Check current balance
const { data: balance } = useContractRead({
address: USDC_ADDRESS,
abi: ERC20ABI,
functionName: 'balanceOf',
args: [address]
})
console.log('Current balance:', formatUnits(balance || 0n, 6), 'USDC')
// Drawdown
const { write: drawdown } = useContractWrite({
address: poolAddress,
abi: StructuredPoolABI,
functionName: 'drawdown',
args: [drawAmount],
onSuccess: () => {
console.log('✅ Borrowed 50,000 USDC successfully!')
}
})Repay (Borrower)
typescript
import { useContractRead, useContractWrite } from 'wagmi'
import { parseUnits, formatUnits } from 'viem'
const repayAmount = parseUnits('10000', 6)
// Get detailed debt breakdown (one call instead of multiple)
const { data: debtDetails } = useContractRead({
address: poolAddress,
abi: StructuredPoolABI,
functionName: 'getDebtDetails'
})
// debtDetails returns: { principal, accruedInterest, pendingInterest, totalDebt }
if (debtDetails) {
console.log('Principal owed:', formatUnits(debtDetails[0], 6), 'USDC')
console.log('Accrued interest:', formatUnits(debtDetails[1], 6), 'USDC')
console.log('Pending interest:', formatUnits(debtDetails[2], 6), 'USDC')
console.log('Total debt:', formatUnits(debtDetails[3], 6), 'USDC')
}
// Get available credit
const { data: availableCredit } = useContractRead({
address: poolAddress,
abi: StructuredPoolABI,
functionName: 'getAvailableCredit'
})
console.log('Available credit:', formatUnits(availableCredit || 0n, 6), 'USDC')
// Approve USDC
const { write: approve } = useContractWrite({
address: USDC_ADDRESS,
abi: ERC20ABI,
functionName: 'approve',
args: [poolAddress, repayAmount],
onSuccess: () => console.log('✅ USDC approved')
})
// Repay (interest is paid first, then principal)
const { write: repay } = useContractWrite({
address: poolAddress,
abi: StructuredPoolABI,
functionName: 'repay',
args: [repayAmount],
onSuccess: () => console.log('✅ Repayment successful!')
})Approve Withdrawal Request (Pool Manager)
typescript
import { useContractRead, useContractWrite } from 'wagmi'
import { formatUnits } from 'viem'
// Check if a specific user has a pending request (access WithdrawalRegistry directly)
const { data: hasPending } = useContractRead({
address: WITHDRAWAL_REGISTRY,
abi: WithdrawalRegistryABI,
functionName: 'hasPendingRequest',
args: [trancheAddress, userAddress]
})
console.log('User has pending request:', hasPending)
// Get pending request details
const { data: pendingRequest } = useContractRead({
address: WITHDRAWAL_REGISTRY,
abi: WithdrawalRegistryABI,
functionName: 'getWithdrawalRequest',
args: [trancheAddress, userAddress]
})
// Returns: { shares, timestamp, approved }
if (pendingRequest) {
const assets = await tranche.previewRedeem(pendingRequest.shares)
console.log('Pending assets:', formatUnits(assets, 6), 'USDC')
console.log('Pending shares:', formatUnits(pendingRequest.shares, 6))
console.log('Approved:', pendingRequest.approved)
console.log('Requested at:', new Date(Number(pendingRequest.timestamp) * 1000))
}
// Approve via pool (manager only)
const { write: approveRequest } = useContractWrite({
address: poolAddress,
abi: StructuredPoolABI,
functionName: 'approveWithdrawRequest',
args: [trancheAddress, userAddress, approvedShares],
onSuccess: () => console.log('✅ Withdrawal request approved')
})Get Pool Information
typescript
import { useContractRead } from 'wagmi'
// Get pool name and strategy
const { data: poolInfo } = useContractRead({
address: poolAddress,
abi: StructuredPoolABI,
functionName: 'getPoolInfo'
})
// Returns: [poolName, poolStrategy]
if (poolInfo) {
console.log('Pool Name:', poolInfo[0])
console.log('Strategy:', poolInfo[1])
}
// Get pool manager
const { data: managerAddress } = useContractRead({
address: poolAddress,
abi: StructuredPoolABI,
functionName: 'manager'
})
console.log('Manager:', managerAddress)
// Get active tranches
const { data: activeTranches } = useContractRead({
address: poolAddress,
abi: StructuredPoolABI,
functionName: 'getActiveTranches'
})
console.log('Active tranches:', activeTranches)
// Get protocol fees
const { data: protocolFees } = useContractRead({
address: poolAddress,
abi: StructuredPoolABI,
functionName: 'getProtocolFees'
})
// Returns: [treasury, depositFeeBps, interestFeeBps]
if (protocolFees) {
console.log('Treasury:', protocolFees[0])
console.log('Deposit fee:', Number(protocolFees[1]) / 100, '%')
console.log('Interest fee:', Number(protocolFees[2]) / 100, '%')
}
// Check underwriter status
const { data: hasUnderwriter } = useContractRead({
address: poolAddress,
abi: StructuredPoolABI,
functionName: 'hasUnderwriter'
})
console.log('Has underwriter:', hasUnderwriter)
if (hasUnderwriter) {
// Get underwriter address
const { data: underwriter } = useContractRead({
address: poolAddress,
abi: StructuredPoolABI,
functionName: 'getUnderwriter'
})
console.log('Underwriter:', underwriter)
// Get underwriter fees
const { data: underwriterFees } = useContractRead({
address: poolAddress,
abi: StructuredPoolABI,
functionName: 'getUnderwriterFees'
})
// Returns: [depositFeeBps, interestFeeBps]
if (underwriterFees) {
console.log('Underwriter deposit fee:', Number(underwriterFees[0]) / 100, '%')
console.log('Underwriter interest fee:', Number(underwriterFees[1]) / 100, '%')
}
}Check Tranche Settings and Limits
typescript
import { useContractRead } from 'wagmi'
import { formatUnits } from 'viem'
// Get all tranche settings in one call
const { data: settings } = useContractRead({
address: trancheAddress,
abi: TrancheABI,
functionName: 'getTrancheSettings'
})
if (settings) {
console.log('Tranche Settings:')
console.log('Seniority:', settings.seniority, '(1=Senior, 2=Mezzanine, 3=Junior)')
console.log('Min deposit:', formatUnits(settings.minimumDeposit, 6), 'USDC')
console.log('Max deposit:', formatUnits(settings.maximumTVL, 6), 'USDC')
console.log('Withdrawal period:', settings.withdrawalPeriod, 'seconds')
console.log('Requires approval:', settings.requireApprovalForWithdrawals)
console.log('Target allocation:', settings.targetAllocation, 'bps')
}
// Or use individual helper functions
const { data: requiresApproval } = useContractRead({
address: trancheAddress,
abi: TrancheABI,
functionName: 'requireApprovalForWithdrawals'
})
const { data: seniority } = useContractRead({
address: trancheAddress,
abi: TrancheABI,
functionName: 'seniority'
})
console.log('Requires approval:', requiresApproval)
console.log('Seniority:', seniority)
// Get available liquidity in Reserve
const { data: availableLiquidity } = useContractRead({
address: trancheAddress,
abi: TrancheABI,
functionName: 'getAvailableLiquidity'
})
console.log('Reserve balance:', formatUnits(availableLiquidity || 0n, 6), 'USDC')Update Tranche Settings (Pool Manager)
Note: Tranche settings are managed via StructuredPool. This allows centralized governance with underwriter approval for critical changes.
Propose Settings Change
typescript
import { useContractWrite, useContractRead } from 'wagmi'
import { parseUnits } from 'viem'
// Prepare new settings
const newSettings = {
seniority: 3, // IMMUTABLE - cannot change
minimumDeposit: parseUnits('200', 6), // Critical field
maximumTVL: parseUnits('2000000', 6), // Non-critical field
withdrawalPeriod: 7 * 24 * 60 * 60, // 7 days
requireApprovalForWithdrawals: true, // Critical field
targetAllocation: 10000 // 100% in basis points
}
// Propose via StructuredPool (not Tranche!)
const { write: proposeSettings } = useContractWrite({
address: poolAddress,
abi: StructuredPoolABI,
functionName: 'proposeTrancheSettingsChange',
args: [trancheAddress, newSettings],
onSuccess: () => {
console.log('✅ Settings proposed')
console.log('Note: Critical changes require underwriter approval')
}
})Check Pending Proposal
typescript
// Check if there's a pending proposal for this tranche
const { data: proposal } = useContractRead({
address: poolAddress,
abi: StructuredPoolABI,
functionName: 'getPendingTrancheSettingsProposal',
args: [trancheAddress]
})
if (proposal) {
console.log('Pending proposal:', proposal.isPending)
console.log('Proposer:', proposal.proposer)
console.log('Timestamp:', new Date(Number(proposal.timestamp) * 1000))
console.log('Expired:', proposal.expired)
console.log('Proposed settings:', proposal.settings)
}Approve Settings Change (Underwriter)
typescript
// Only the pool's underwriter can approve
const { write: approveSettings } = useContractWrite({
address: poolAddress,
abi: StructuredPoolABI,
functionName: 'approveTrancheSettingsChange',
args: [trancheAddress],
onSuccess: () => {
console.log('✅ Settings approved and applied')
}
})Reject Settings Proposal
typescript
// Manager or underwriter can reject
const { write: rejectSettings } = useContractWrite({
address: poolAddress,
abi: StructuredPoolABI,
functionName: 'rejectTrancheSettingsChange',
args: [trancheAddress],
onSuccess: () => {
console.log('✅ Proposal rejected')
}
})Important Notes:
- Non-critical changes (like
maximumTVL) apply immediately - Critical changes (like
minimumDeposit,requireApprovalForWithdrawals) require underwriter approval if pool has underwriter - Immutable fields (like
seniority) cannot be changed and will revert - If pool has no underwriter, all changes apply immediately
- Default proposal expiry: 7 days (configurable by protocol admin)
Frontend Integration
React Hook Example: useTrancheDeposit
typescript
import { useContractWrite } from 'wagmi'
import { parseUnits } from 'viem'
export function useTrancheDeposit(trancheAddress: string, amount: string, receiver: string) {
const { write: deposit, isLoading, isSuccess, error } = useContractWrite({
address: trancheAddress,
abi: TrancheABI,
functionName: 'deposit',
args: [
amount ? parseUnits(amount, 6) : 0n,
receiver
],
onSuccess: (data) => {
console.log('✅ Deposit successful!')
console.log('Transaction hash:', data.hash)
},
onError: (error) => {
console.error('❌ Deposit failed:', error.message)
}
})
return { deposit, isLoading, isSuccess, error }
}
// Usage
const { address } = useAccount()
const { deposit, isLoading } = useTrancheDeposit(TRANCHE_ADDRESS, '1000', address)
// Execute deposit
deposit?.()Display Pool Metrics
typescript
import { useContractRead } from 'wagmi'
import { formatUnits } from 'viem'
// Get pool metrics in one call
const { data: poolMetrics } = useContractRead({
address: poolAddress,
abi: StructuredPoolABI,
functionName: 'getPoolMetrics'
})
// poolMetrics returns: { totalAssets, totalAvailable, totalDeployed, totalReserved, utilizationRate }
// totalDeployed represents current outstandingDebt
if (poolMetrics) {
console.log('📊 Pool Metrics:')
console.log('TVL:', formatUnits(poolMetrics.totalAssets, 6), 'USDC')
console.log('Available Liquidity:', formatUnits(poolMetrics.totalAvailable, 6), 'USDC')
console.log('Outstanding Debt:', formatUnits(poolMetrics.totalDeployed, 6), 'USDC')
console.log('Utilization:', (Number(poolMetrics.utilizationRate) / 100).toFixed(1) + '%')
}
// Get virtual balances from tranche
const { data: trancheAddress } = useContractRead({
address: poolAddress,
abi: StructuredPoolABI,
functionName: 'trancheList',
args: [0n]
})
const { data: virtualBalances } = useContractRead({
address: trancheAddress,
abi: TrancheABI,
functionName: 'getVirtualBalances'
})
// virtualBalances returns: [balance, deployed, interestEarned]
if (virtualBalances) {
console.log('Virtual Balance:', formatUnits(virtualBalances[0], 6), 'USDC')
console.log('Virtual Deployed:', formatUnits(virtualBalances[1], 6), 'USDC')
console.log('Total Interest Earned:', formatUnits(virtualBalances[2], 6), 'USDC')
}
// Get share price
const { data: totalAssets } = useContractRead({
address: trancheAddress,
abi: TrancheABI,
functionName: 'totalAssets'
})
const { data: totalSupply } = useContractRead({
address: trancheAddress,
abi: TrancheABI,
functionName: 'totalSupply'
})
const sharePrice = totalAssets && totalSupply && totalSupply > 0n
? Number(totalAssets) / Number(totalSupply)
: 1
console.log('Share Price:', sharePrice.toFixed(4))Complete Withdrawal Example (With Approval Flow)
typescript
import { useContractRead, useContractWrite, useContractEvent, useAccount } from 'wagmi'
import { parseUnits, formatUnits } from 'viem'
import { WITHDRAWAL_REGISTRY } from '@textile/constants'
const { address } = useAccount()
const amount = parseUnits('5000', 6)
// Check if approval required
const { data: requiresApproval } = useContractRead({
address: trancheAddress,
abi: TrancheABI,
functionName: 'requireApprovalForWithdrawals'
})
// Check approved shares
const { data: withdrawalRequest } = useContractRead({
address: WITHDRAWAL_REGISTRY,
abi: WithdrawalRegistryABI,
functionName: 'getWithdrawalRequest',
args: [trancheAddress, address]
})
const approvedShares = withdrawalRequest?.approved || 0n
console.log('Approved shares:', formatUnits(approvedShares, 6))
// Step 1: Request withdrawal
const { write: requestWithdraw } = useContractWrite({
address: trancheAddress,
abi: TrancheABI,
functionName: 'requestWithdraw',
args: [amount],
onSuccess: () => console.log('✅ Withdrawal requested')
})
// Step 2: Listen for approval
useContractEvent({
address: WITHDRAWAL_REGISTRY,
abi: WithdrawalRegistryABI,
eventName: 'WithdrawalApproved',
listener: (logs) => {
const { tranche, user, shares } = logs[0].args
if (tranche === trancheAddress && user === address) {
console.log('✅ Withdrawal approved!')
console.log('Approved shares:', formatUnits(shares, 6))
}
}
})
// Step 3: Execute withdrawal
const { write: executeWithdraw } = useContractWrite({
address: trancheAddress,
abi: TrancheABI,
functionName: 'redeem',
args: [approvedShares || 0n, address, address],
onSuccess: () => console.log('✅ Withdrawal executed!')
})Backend Integration
The Graph Indexing
The protocol uses The Graph for efficient event indexing and querying. This provides a GraphQL API for historical data, real-time updates, and complex queries without running your own indexer.
Subgraph Schema
Define entities for all key events:
graphql
# schema.graphql
type Pool @entity {
id: ID! # Pool address
address: Bytes! # Contract address
asset: Bytes! # Asset token address
name: String!
strategy: String!
borrower: Bytes! # Borrower address
manager: Bytes! # Manager address
outstandingDebt: BigInt!
totalLent: BigInt!
totalRepaid: BigInt!
totalInterestPaid: BigInt!
tranches: [Tranche!]! @derivedFrom(field: "pool")
createdAt: BigInt!
lastActivityAt: BigInt!
}
type Tranche @entity {
id: ID! # Tranche address
pool: Pool!
name: String!
symbol: String!
seniority: Int!
virtualBalance: BigInt!
virtualDeployed: BigInt!
totalInterestEarned: BigInt!
totalSupply: BigInt!
minimumDeposit: BigInt!
maximumTVL: BigInt!
requireApprovalForWithdrawals: Boolean!
deposits: [Deposit!]! @derivedFrom(field: "tranche")
withdrawals: [Withdrawal!]! @derivedFrom(field: "tranche")
withdrawalRequests: [WithdrawalRequest!]! @derivedFrom(field: "tranche")
createdAt: BigInt!
updatedAt: BigInt!
}
type Deposit @entity {
id: ID! # tx hash + log index
tranche: Tranche!
sender: Bytes! # Caller of deposit (ERC4626 standard)
owner: Bytes! # Share recipient
assets: BigInt!
shares: BigInt!
blockNumber: BigInt!
timestamp: BigInt!
transactionHash: Bytes!
}
type Withdrawal @entity {
id: ID!
tranche: Tranche!
sender: Bytes! # Caller of withdraw (ERC4626 standard)
receiver: Bytes! # Asset recipient
owner: Bytes! # Share owner
assets: BigInt!
shares: BigInt!
blockNumber: BigInt!
timestamp: BigInt!
transactionHash: Bytes!
}
type WithdrawalRequest @entity {
id: ID! # tranche address + user address
tranche: Tranche!
user: Bytes!
shares: BigInt! # Requested shares
approved: BigInt! # Approved shares (from WithdrawalApproved event)
timestamp: BigInt!
status: WithdrawalRequestStatus!
}
enum WithdrawalRequestStatus {
PENDING
APPROVED
EXECUTED
REJECTED
}
type CapitalDeployment @entity {
id: ID!
pool: Pool!
borrower: Bytes!
amount: BigInt!
tranche: Bytes!
blockNumber: BigInt!
timestamp: BigInt!
transactionHash: Bytes!
}
type Repayment @entity {
id: ID!
pool: Pool!
payer: Bytes!
totalAmount: BigInt!
interestPaid: BigInt!
principalPaid: BigInt!
blockNumber: BigInt!
timestamp: BigInt!
transactionHash: Bytes!
}Subgraph Handlers
typescript
// src/mappings/tranche.ts
import { Deposit, Withdraw } from '../generated/Tranche/Tranche'
import {
InterestDistributed,
PrincipalReturned,
CapitalDeployed,
RepaymentProcessed
} from '../generated/StructuredPool/StructuredPool'
import {
WithdrawalRequested,
WithdrawalApproved
} from '../generated/WithdrawalRegistry/WithdrawalRegistry'
import {
Deposit as DepositEntity,
Withdrawal,
Tranche,
Pool,
CapitalDeployment as CapitalDeploymentEntity,
Repayment as RepaymentEntity,
WithdrawalRequest as WithdrawalRequestEntity
} from '../generated/schema'
import { Tranche as TrancheContract } from '../generated/Tranche/Tranche'
import { StructuredPool as StructuredPoolContract } from '../generated/StructuredPool/StructuredPool'
import { BigInt } from '@graphprotocol/graph-ts'
export function handleDeposit(event: Deposit): void {
let deposit = new DepositEntity(
event.transaction.hash.toHex() + '-' + event.logIndex.toString()
)
deposit.tranche = event.address.toHex()
deposit.caller = event.params.sender // Note: ERC4626 uses 'sender' not 'caller'
deposit.owner = event.params.owner
deposit.assets = event.params.assets
deposit.shares = event.params.shares
deposit.blockNumber = event.block.number
deposit.timestamp = event.block.timestamp
deposit.transactionHash = event.transaction.hash
deposit.save()
// Update tranche totals using helper functions
let tranche = Tranche.load(event.address.toHex())
if (tranche) {
let contract = TrancheContract.bind(event.address)
let virtualBalances = contract.getVirtualBalances()
tranche.virtualBalance = virtualBalances.value0
tranche.virtualDeployed = virtualBalances.value1
tranche.totalInterestEarned = virtualBalances.value2
tranche.totalSupply = contract.totalSupply()
tranche.updatedAt = event.block.timestamp
tranche.save()
}
}
export function handleDepositEnhanced(event: DepositEnhanced): void {
let deposit = new DepositEntity(
event.transaction.hash.toHex() + '-' + event.logIndex.toString()
)
deposit.tranche = event.address.toHex()
deposit.caller = event.params.sender
deposit.owner = event.params.owner
deposit.assets = event.params.assets
deposit.shares = event.params.shares
deposit.netAmount = event.params.netAmount
deposit.protocolFee = event.params.protocolFee
deposit.underwriterFee = event.params.underwriterFee
deposit.blockNumber = event.block.number
deposit.timestamp = event.block.timestamp
deposit.transactionHash = event.transaction.hash
deposit.save()
// Update tranche totals using helper functions
let tranche = Tranche.load(event.address.toHex())
if (tranche) {
let contract = TrancheContract.bind(event.address)
let virtualBalances = contract.getVirtualBalances()
tranche.virtualBalance = virtualBalances.value0
tranche.virtualDeployed = virtualBalances.value1
tranche.totalInterestEarned = virtualBalances.value2
tranche.totalSupply = contract.totalSupply()
tranche.updatedAt = event.block.timestamp
tranche.save()
}
}
export function handleWithdraw(event: Withdraw): void {
let withdrawal = new Withdrawal(
event.transaction.hash.toHex() + '-' + event.logIndex.toString()
)
withdrawal.tranche = event.address.toHex()
withdrawal.caller = event.params.sender // Note: ERC4626 uses 'sender' not 'caller'
withdrawal.receiver = event.params.receiver
withdrawal.owner = event.params.owner
withdrawal.assets = event.params.assets
withdrawal.shares = event.params.shares
withdrawal.blockNumber = event.block.number
withdrawal.timestamp = event.block.timestamp
withdrawal.transactionHash = event.transaction.hash
withdrawal.save()
// Update tranche totals using helper functions
let tranche = Tranche.load(event.address.toHex())
if (tranche) {
let contract = TrancheContract.bind(event.address)
let virtualBalances = contract.getVirtualBalances()
tranche.virtualBalance = virtualBalances.value0
tranche.virtualDeployed = virtualBalances.value1
tranche.totalInterestEarned = virtualBalances.value2
tranche.totalSupply = contract.totalSupply()
tranche.updatedAt = event.block.timestamp
tranche.save()
}
}
// Handle interest distribution
export function handleInterestDistributed(event: InterestDistributed): void {
let tranche = Tranche.load(event.params.tranche.toHex())
if (tranche) {
let contract = TrancheContract.bind(event.params.tranche)
let virtualBalances = contract.getVirtualBalances()
tranche.virtualBalance = virtualBalances.value0
tranche.totalInterestEarned = virtualBalances.value2
tranche.updatedAt = event.block.timestamp
tranche.save()
}
}
// Handle principal returns
export function handlePrincipalReturned(event: PrincipalReturned): void {
let tranche = Tranche.load(event.params.tranche.toHex())
if (tranche) {
let contract = TrancheContract.bind(event.params.tranche)
let virtualBalances = contract.getVirtualBalances()
tranche.virtualBalance = virtualBalances.value0
tranche.virtualDeployed = virtualBalances.value1
tranche.updatedAt = event.block.timestamp
tranche.save()
}
}
// Handle capital deployments (from pool)
export function handleCapitalDeployed(event: CapitalDeployed): void {
let deployment = new CapitalDeploymentEntity(
event.transaction.hash.toHex() + '-' + event.logIndex.toString()
)
deployment.pool = event.address.toHex()
deployment.borrower = event.params.borrower
deployment.amount = event.params.amount
deployment.blockNumber = event.block.number
deployment.timestamp = event.block.timestamp
deployment.transactionHash = event.transaction.hash
deployment.save()
// Update pool totals
let pool = Pool.load(event.address.toHex())
if (pool) {
let contract = StructuredPoolContract.bind(event.address)
let metrics = contract.getPoolMetrics()
pool.outstandingDebt = metrics.totalDeployed
pool.updatedAt = event.block.timestamp
pool.save()
}
}
// Handle repayments
export function handleRepaymentProcessed(event: RepaymentProcessed): void {
let repayment = new RepaymentEntity(
event.transaction.hash.toHex() + '-' + event.logIndex.toString()
)
repayment.pool = event.address.toHex()
repayment.totalAmount = event.params.totalAmount
repayment.blockNumber = event.block.number
repayment.timestamp = event.block.timestamp
repayment.transactionHash = event.transaction.hash
repayment.save()
// Update pool totals
let pool = Pool.load(event.address.toHex())
if (pool) {
let contract = StructuredPoolContract.bind(event.address)
let metrics = contract.getPoolMetrics()
pool.outstandingDebt = metrics.totalDeployed
pool.updatedAt = event.block.timestamp
pool.save()
}
}
// Handle withdrawal requests (from WithdrawalRegistry)
export function handleWithdrawalRequested(event: WithdrawalRequested): void {
let id = event.params.tranche.toHex() + '-' + event.params.user.toHex()
let request = new WithdrawalRequestEntity(id)
request.tranche = event.params.tranche.toHex()
request.user = event.params.user
request.shares = event.params.shares
request.approved = BigInt.fromI32(0)
request.timestamp = event.block.timestamp
request.status = 'PENDING'
request.save()
}
// Handle withdrawal approvals
export function handleWithdrawalApproved(event: WithdrawalApproved): void {
let id = event.params.tranche.toHex() + '-' + event.params.user.toHex()
let request = WithdrawalRequestEntity.load(id)
if (request) {
request.approved = event.params.shares
request.status = 'APPROVED'
request.save()
}
}Querying The Graph
typescript
import { useQuery } from '@apollo/client'
import { gql } from 'graphql-tag'
// Query pool data with deposits and withdrawals
const POOL_QUERY = gql`
query GetPool($poolAddress: ID!) {
pool(id: $poolAddress) {
id
name
strategy
asset
outstandingDebt
totalInterestEarned
totalPrincipalRepaid
tranches {
id
name
symbol
seniority
virtualBalance
virtualDeployed
totalInterestEarned
totalSupply
requireApprovalForWithdrawals
}
deployments(first: 10, orderBy: timestamp, orderDirection: desc) {
id
borrower
amount
timestamp
}
repayments(first: 10, orderBy: timestamp, orderDirection: desc) {
id
payer
totalAmount
interestPaid
principalPaid
timestamp
}
}
}
`
function PoolDashboard({ poolAddress }: Props) {
const { data, loading, error } = useQuery(POOL_QUERY, {
variables: { poolAddress: poolAddress.toLowerCase() }
})
if (loading) return <div>Loading...</div>
if (error) return <div>Error: {error.message}</div>
const pool = data?.pool
console.log('Pool data:', pool)
return (
<div>
<h2>{pool.name}</h2>
<p>Strategy: {pool.strategy}</p>
{/* Display pool data */}
</div>
)
}
// Query user deposits
const USER_DEPOSITS_QUERY = gql`
query GetUserDeposits($userAddress: Bytes!, $trancheAddress: Bytes!) {
deposits(
where: { owner: $userAddress, tranche: $trancheAddress }
orderBy: timestamp
orderDirection: desc
first: 100
) {
id
assets
shares
timestamp
transactionHash
}
}
`
// Query pending withdrawal requests
const PENDING_WITHDRAWALS_QUERY = gql`
query GetPendingWithdrawals($trancheAddress: ID!) {
withdrawalRequests(
where: { tranche: $trancheAddress, status: PENDING }
orderBy: timestamp
orderDirection: asc
) {
id
user
shares
approved
timestamp
status
}
}
`Benefits of The Graph
- ✅ No backend infrastructure needed for event indexing
- ✅ Real-time updates with GraphQL subscriptions
- ✅ Complex queries with filtering, sorting, pagination
- ✅ Historical data available instantly
- ✅ Type-safe with generated TypeScript types
- ✅ Decentralized hosted service or self-hosted
Setting Up The Subgraph
bash
# Install Graph CLI
npm install -g @graphprotocol/graph-cli
# Initialize subgraph
graph init --protocol ethereum \
--network celo \
--contract-name StructuredPool \
--from-contract 0xYourPoolAddress
# Generate types from ABI
graph codegen
# Build subgraph
graph build
# Deploy to hosted service
graph deploy --product hosted-service your-username/textile-protocol-v2
# Or deploy to decentralized network
graph deploy --node https://api.thegraph.com/deploy/ \
--ipfs https://api.thegraph.com/ipfs/ \
your-subgraph-nameImportant: Events are defined in interface files (ITranche.sol, IStructuredPool.sol, etc.), so use the interface ABIs when setting up The Graph to ensure all events are captured correctly.
Event Handling
Key Events to Listen For
Tranche Events
typescript
// Standard ERC4626 events (from OpenZeppelin)
event Deposit(
address indexed sender,
address indexed owner,
uint256 assets,
uint256 shares
)
event Withdraw(
address indexed sender,
address indexed receiver,
address indexed owner,
uint256 assets,
uint256 shares
)
// Custom Tranche events
event WithdrawRequested(
address indexed user,
uint256 amount
)
event RedeemRequested(
address indexed user,
uint256 shares
)
event TrancheSettingsProposed(
DataTypes.TrancheSettings settings
)
event TrancheSettingsUpdated(
DataTypes.TrancheSettings settings
)
event DepositFeesCollected(
uint256 protocolFee,
uint256 underwriterFee
)
// Enhanced Deposit event with fee information
event DepositEnhanced(
address indexed sender,
address indexed owner,
uint256 assets,
uint256 shares,
uint256 netAmount,
uint256 protocolFee,
uint256 underwriterFee
)
event InterestFeesCollected(
uint256 totalInterest,
uint256 protocolFee,
uint256 underwriterFee,
uint256 netInterest
)StructuredPool Events
typescript
event PoolInitialized(
address indexed asset,
string poolName,
string strategy
)
event TrancheRegistered(
address indexed tranche,
uint8 seniority,
uint256 targetAllocation
)
event Drawdown(
address indexed borrower,
uint256 amount
)
event Repaid(
address indexed payer,
uint256 totalAmount,
uint256 principal,
uint256 netInterest,
uint256 protocolFee,
uint256 underwriterFee
)
event InterestDistributed(
address indexed tranche,
uint256 amount
)
event PrincipalReturned(
address indexed tranche,
uint256 amount
)
event UnderwriterProposed(
address indexed pendingUnderwriter
)
event UnderwriterAccepted(
address indexed underwriter,
uint256 depositFeeBps,
uint256 interestFeeBps
)StructuredPool Credit Line Events
typescript
event Drawdown(
address indexed borrower,
uint256 amount
)
event Repaid(
address indexed payer,
uint256 amount,
uint256 interest,
uint256 principal
)
event InterestCalculated(
uint256 interestAmount,
uint256 totalAccruedInterest
)
event CreditLineSettingsProposed(
DataTypes.CreditLineSettings settings
)
event CreditLineSettingsUpdated(
DataTypes.CreditLineSettings settings
)WithdrawalRegistry Events
typescript
event WithdrawalRequested(
address indexed tranche,
address indexed user,
uint256 shares,
)
event WithdrawalApproved(
address indexed tranche,
address indexed user,
uint256 shares,
)
event WithdrawalRejected(
address indexed tranche,
address indexed user
)
event TrancheRegistered(
address indexed factory,
address indexed tranche
)UnderwriterRegistry Events
typescript
event UnderwriterRegistered(
address indexed underwriter,
uint256 depositFeeBps,
uint256 interestFeeBps,
string name
)
event UnderwriterFeesUpdated(
address indexed underwriter,
uint256 depositFeeBps,
uint256 interestFeeBps
)
event UnderwriterDeactivated(
address indexed underwriter
)
event UnderwriterReactivated(
address indexed underwriter
)
event UnderwriterProfileUpdated(
address indexed underwriter,
string name,
string website
)PoolFactory Events
typescript
event PoolDeployed(
uint256 indexed deploymentId,
address indexed pool,
address asset,
string poolName,
address deployer
)
event TrancheDeployed(
address indexed pool,
address indexed tranche,
uint8 seniority,
string trancheName
)Frontend Event Listener Example
typescript
import { useContractEvent } from 'wagmi'
import { formatUnits } from 'viem'
// Listen for withdrawal requests (from WithdrawalRegistry)
useContractEvent({
address: WITHDRAWAL_REGISTRY,
abi: WithdrawalRegistryABI,
eventName: 'WithdrawalRequested',
listener(logs) {
const { tranche, user, shares } = logs[0].args
if (tranche === trancheAddress && user === userAddress) {
console.log('✅ Withdrawal request submitted!')
console.log('Shares:', formatUnits(shares, 6))
}
}
})
// Listen for approvals
useContractEvent({
address: WITHDRAWAL_REGISTRY,
abi: WithdrawalRegistryABI,
eventName: 'WithdrawalApproved',
listener(logs) {
const { tranche, user, shares } = logs[0].args
if (tranche === trancheAddress && user === userAddress) {
console.log('✅ Withdrawal approved!')
console.log('Approved shares:', formatUnits(shares, 6))
}
}
})
// Listen for successful withdrawals (standard ERC4626 event)
useContractEvent({
address: trancheAddress,
abi: TrancheABI,
eventName: 'Withdraw',
listener(logs) {
const { sender, receiver, owner, assets, shares } = logs[0].args
if (owner === userAddress) {
console.log('✅ Withdrawal successful!')
console.log('Assets:', formatUnits(assets, 6), 'USDC')
console.log('Shares burned:', formatUnits(shares, 6))
}
}
})
// Listen for deposits (standard ERC4626)
useContractEvent({
address: trancheAddress,
abi: TrancheABI,
eventName: 'Deposit',
listener(logs) {
const { sender, owner, assets, shares } = logs[0].args
if (owner === userAddress) {
console.log('✅ Deposit successful!')
console.log('Assets:', formatUnits(assets, 6), 'USDC')
console.log('Shares minted:', formatUnits(shares, 6))
}
}
})
// Listen for deposits with fee information (enhanced)
useContractEvent({
address: trancheAddress,
abi: TrancheABI,
eventName: 'DepositEnhanced',
listener(logs) {
const { sender, owner, assets, shares, netAmount, protocolFee, underwriterFee } = logs[0].args
if (owner === userAddress) {
console.log('✅ Deposit successful!')
console.log('Assets:', formatUnits(assets, 6), 'USDC')
console.log('Shares minted:', formatUnits(shares, 6))
console.log('Net amount:', formatUnits(netAmount, 6), 'USDC')
console.log('Protocol fee:', formatUnits(protocolFee, 6), 'USDC')
console.log('Underwriter fee:', formatUnits(underwriterFee, 6), 'USDC')
}
}
})
// Listen for drawdowns (from pool)
useContractEvent({
address: poolAddress,
abi: StructuredPoolABI,
eventName: 'Drawdown',
listener(logs) {
const { borrower, amount } = logs[0].args
console.log('📤 Drawdown occurred')
console.log('Borrower:', borrower)
console.log('Amount:', formatUnits(amount, 6), 'USDC')
}
})
// Listen for repayments
useContractEvent({
address: poolAddress,
abi: StructuredPoolABI,
eventName: 'Repaid',
listener(logs) {
const { payer, totalAmount, principal } = logs[0].args
console.log('📥 Repayment received')
console.log('Amount:', formatUnits(totalAmount, 6), 'USDC')
console.log('Principal repaid:', formatUnits(principal, 6), 'USDC')
}
})
// Listen for withdrawal requests
useContractEvent({
address: WITHDRAWAL_REGISTRY,
abi: WithdrawalRegistryABI,
eventName: 'WithdrawalRequested',
listener(logs) {
const { tranche, user, shares } = logs[0].args
console.log('📝 Withdrawal requested')
console.log('Tranche:', tranche)
console.log('User:', user)
console.log('Shares:', formatUnits(shares, 6))
}
})
// Listen for withdrawal approvals
useContractEvent({
address: WITHDRAWAL_REGISTRY,
abi: WithdrawalRegistryABI,
eventName: 'WithdrawalApproved',
listener(logs) {
const { tranche, user, shares } = logs[0].args
if (user === address) {
console.log('✅ Your withdrawal was approved!')
console.log('Approved shares:', formatUnits(shares, 6))
console.log('You can now execute withdrawal')
}
}
})
// Listen for withdrawal rejections
useContractEvent({
address: WITHDRAWAL_REGISTRY,
abi: WithdrawalRegistryABI,
eventName: 'WithdrawalRejected',
listener(logs) {
const { tranche, user } = logs[0].args
if (user === address) {
console.log('❌ Your withdrawal request was rejected')
}
}
})
// Listen for new underwriter registrations
useContractEvent({
address: UNDERWRITER_REGISTRY,
abi: UnderwriterRegistryABI,
eventName: 'UnderwriterRegistered',
listener(logs) {
const { underwriter, depositFeeBps, interestFeeBps, name } = logs[0].args
console.log('🆕 New underwriter registered')
console.log('Name:', name)
console.log('Address:', underwriter)
console.log('Deposit Fee:', Number(depositFeeBps) / 100, '%')
console.log('Interest Fee:', Number(interestFeeBps) / 100, '%')
}
})
// Listen for underwriter fee updates
useContractEvent({
address: UNDERWRITER_REGISTRY,
abi: UnderwriterRegistryABI,
eventName: 'UnderwriterFeesUpdated',
listener(logs) {
const { underwriter, depositFeeBps, interestFeeBps } = logs[0].args
console.log('💰 Underwriter fees updated')
console.log('Underwriter:', underwriter)
console.log('New Deposit Fee:', Number(depositFeeBps) / 100, '%')
console.log('New Interest Fee:', Number(interestFeeBps) / 100, '%')
}
})Testing & Development
Local Development Setup
bash
# Terminal 1: Start local Hardhat node
cd packages/protocol
yarn hardhat node
# Terminal 2: Deploy protocol
yarn deploy:v2:local
# Terminal 3: Run frontend
cd ../../web
yarn dev
# Terminal 4: Run backend API
cd ../../api
yarn devRun Tests
bash
# All v2.1 tests
cd packages/protocol
yarn test
# Specific test suites
yarn hardhat test test/v2.1/Tranche/Tranche.Deposit.test.ts
yarn hardhat test test/v2.1/StructuredPool/
# With gas reporting
REPORT_GAS=true yarn hardhat test
# With coverage
yarn hardhat coverageTesting with Frontend
typescript
// Test helper: Deploy test pool
import { ethers } from 'hardhat'
import { deployV2Protocol } from '../scripts/v2.1/deploy'
export async function setupTestPool() {
const [deployer, lp, borrower, manager] = await ethers.getSigners()
// Deploy infrastructure
const addresses = await deployV2Protocol(true, deployer, false)
// Deploy USDC mock
const USDC = await ethers.getContractFactory('ERC20Mock')
const usdc = await USDC.deploy('USD Coin', 'USDC', 6)
// Mint tokens
await usdc.mint(lp.address, ethers.parseUnits('1000000', 6))
await usdc.mint(borrower.address, ethers.parseUnits('100000', 6))
// Deploy pool
const factory = await ethers.getContractAt('PoolFactory', addresses.poolFactory)
const tx = await factory.deployPool(
usdc.address,
'Test Pool',
'Test Strategy',
'Test Tranche',
'TST',
{
seniority: 1,
minimumDeposit: ethers.parseUnits('100', 6),
maximumTVL: ethers.parseUnits('1000000', 6),
withdrawalPeriod: 0,
requireApprovalForWithdrawals: false,
targetAllocation: 10000
},
{
borrower: borrower.address,
interestRate: 500,
drawLimit: ethers.parseUnits('500000', 6),
interestPaymentInterval: 2592000
},
manager.address
)
const receipt = await tx.wait()
const event = receipt.events.find(e => e.event === 'PoolDeployed')
return {
usdc,
poolAddress: event.args.pool,
trancheAddress: await (await ethers.getContractAt('StructuredPool', event.args.pool)).trancheList(0),
signers: { deployer, lp, borrower, manager }
}
}
// Use in tests
describe('Frontend Integration', () => {
it('should allow LP to deposit and withdraw', async () => {
const { usdc, trancheAddress, signers } = await setupTestPool()
const tranche = await ethers.getContractAt('Tranche', trancheAddress)
// LP deposits
const depositAmount = ethers.parseUnits('10000', 6)
await usdc.connect(signers.lp).approve(tranche.address, depositAmount)
await tranche.connect(signers.lp).deposit(depositAmount, signers.lp.address)
// Check shares
const shares = await tranche.balanceOf(signers.lp.address)
expect(shares).to.equal(depositAmount) // 1:1 first deposit
// LP withdraws
await tranche.connect(signers.lp).redeem(shares, signers.lp.address, signers.lp.address)
// Check balance
const finalBalance = await usdc.balanceOf(signers.lp.address)
expect(finalBalance).to.be.closeTo(
ethers.parseUnits('1000000', 6),
ethers.parseUnits('1', 6) // Allow 1 USDC rounding
)
})
})Troubleshooting
Common Issues
1. "OnlyApprovedFactory" Error
Problem: Trying to initialize a pool/tranche directly
Solution: Always use PoolFactory to deploy pools:
typescript
// ❌ Wrong - Don't deploy contracts directly
const pool = await PoolContract.deploy(...)
await pool.initialize(...)
// ✅ Correct - Use the factory
const { write: deployPool } = useContractWrite({
address: POOL_FACTORY,
abi: PoolFactoryABI,
functionName: 'deployPool',
args: [/* pool parameters */]
})
deployPool?.()2. Withdrawal Fails with "InsufficientLiquidity"
Problem: Not enough available cash in Reserve
Solution: Check available liquidity before attempting withdrawal:
typescript
const { data: maxWithdraw } = useContractRead({
address: trancheAddress,
abi: TrancheABI,
functionName: 'maxWithdraw',
args: [userAddress]
})
if (maxWithdraw && withdrawAmount > maxWithdraw) {
console.log('Insufficient liquidity. Max available:', formatUnits(maxWithdraw, 6))
// Show UI message or reduce withdrawal amount
}3. "ApprovalRequired" or "NoWithdrawalApproval" Error
Problem: Pool's withdrawal mode doesn't match your approach
Common scenarios:
- Tried direct
withdraw()on a pool that requires approval → Error:NoWithdrawalApproval() - Tried
requestWithdraw()on a pool that doesn't require approval → Error:InvalidRequest()
Solution: ALWAYS check requireApprovalForWithdrawals first!
typescript
const { data: requiresApproval } = useContractRead({
address: trancheAddress,
abi: TrancheABI,
functionName: 'requireApprovalForWithdrawals'
})
console.log('Requires approval:', requiresApproval)
if (requiresApproval) {
// Use two-step flow: requestWithdraw() → wait for approval → redeem()
const { write: requestWithdraw } = useContractWrite({
address: trancheAddress,
abi: TrancheABI,
functionName: 'requestWithdraw',
args: [amount]
})
requestWithdraw?.()
} else {
// Use direct withdrawal: withdraw() or redeem()
const { write: withdraw } = useContractWrite({
address: trancheAddress,
abi: TrancheABI,
functionName: 'withdraw',
args: [amount, address, address]
})
withdraw?.()
}Pro tip: Display the withdrawal mode clearly in your UI before users deposit!
4. Interest Calculation Seems Wrong
Problem: Interest not matching expected APR
Remember:
- Interest compounds every second, not daily/monthly
- Uses RAY precision (10^27), not basis points for calculations
- Check
getCurrentInterestDebt()for accrued but unpaid interest - State only updates when threshold conditions met (time + amount)
typescript
// Force interest accrual by calling drawdown(0) or repay(0)
const { write: forceAccrual } = useContractWrite({
address: poolAddress,
abi: StructuredPoolABI,
functionName: 'drawdown',
args: [0n] // Forces accrual without drawing
})
forceAccrual?.()5. Events Not Showing in Frontend
Problem: Event listener not catching events
Solution: Make sure you're listening to the correct contract and event name:
typescript
// Make sure you're using the correct ABI with event definitions
// Events are defined in interfaces: ITranche.sol, IStructuredPool.sol, etc.
useContractEvent({
address: trancheAddress,
abi: ITrancheABI, // Use interface ABI, not implementation
eventName: 'Deposit',
listener: (logs) => {
console.log('Deposit event:', logs)
}
})6. Transaction Reverts with No Error Message
Possible causes:
- Protocol shutdown: Check
ProtocolConfiguration.isShutdown() - Contract paused: Check
isPaused()on relevant contract - Insufficient allowance: Check token approval
- Gas limit too low: Increase gas limit in transaction
typescript
// Debug transaction with wagmi
const { write: deposit, error } = useContractWrite({
address: trancheAddress,
abi: TrancheABI,
functionName: 'deposit',
args: [amount, receiver],
onError: (error) => {
if (error.message.includes('paused')) {
console.log('Contract is paused')
} else if (error.message.includes('shutdown')) {
console.log('Protocol is in emergency shutdown')
}
// Check specific error from Errors.sol
console.error('Transaction error:', error)
}
})Error Handling
The protocol uses custom errors (defined in Errors.sol) for gas efficiency and clarity. All errors are descriptive and help debug issues quickly.
Common Errors and Solutions
Validation Errors
typescript
// InvalidAmount - Amount is zero or invalid
error InvalidAmount()
// Solution: Ensure amount > 0
// BelowMinimumDeposit - Deposit below minimum
error BelowMinimumDeposit()
// Solution: Check tranche.getTrancheSettings().minimumDeposit
// ExceedsMaximumTVL - Deposit above maximum
error ExceedsMaximumTVL()
// Solution: Check tranche.getTrancheSettings().maximumTVL
// InvalidReceiver - Receiver is zero address
error InvalidReceiver()
// Solution: Ensure receiver != address(0)Balance & Liquidity Errors
typescript
// InsufficientBalance - User doesn't have enough shares
error InsufficientBalance()
// Solution: Check tranche.balanceOf(user)
// InsufficientLiquidity - Not enough cash in Reserve
error InsufficientLiquidity()
// Solution: Check tranche.maxWithdraw(user) before withdrawal
// InsufficientPoolLiquidity - Pool lacks liquidity for deployment
error InsufficientPoolLiquidity()
// Solution: Wait for LP deposits or reduce drawdown amount
// ExceedsCreditLimit - Drawdown exceeds available credit
error ExceedsCreditLimit()
// Solution: Check pool.getRemainingCreditLimit()Access Control Errors
typescript
// OnlyStructuredPool - Function can only be called by StructuredPool
error OnlyStructuredPool()
// Solution: Don't call this function directly (internal use)
// OnlyTranche - Function can only be called by Tranche
error OnlyTranche()
// Solution: Don't call this function directly (internal use)
// OnlyStructuredPool - Function can only be called by the StructuredPool
error OnlyStructuredPool()
// Solution: These functions are for internal pool-tranche coordination
// NotBorrower - Caller is not the designated borrower
error NotBorrower()
// Solution: Check if user matches creditLineSettings.borrower address
// AccessControlViolation - Missing required role
error AccessControlViolation()
// Solution: Check user roles with hasRole()Withdrawal Errors
typescript
// NoWithdrawalApproval - Withdrawal not approved by manager
error NoWithdrawalApproval()
// Solution: Request withdrawal first, wait for manager approval
// NoWithdrawalRequest - No pending withdrawal request
error NoWithdrawalRequest()
// Solution: Check withdrawalRegistry.hasPendingRequest(tranche, user) first
// ExceedsMaximumWithdrawalAmount - Withdrawal exceeds approved amount
error ExceedsMaximumWithdrawalAmount()
// Solution: Check approved amount in WithdrawalRegistry
// InvalidRequest - Invalid withdrawal request (e.g., requesting when approval not required)
error InvalidRequest()
// Solution: Check tranche.requireApprovalForWithdrawals() firstState Errors
typescript
// TrancheNotActive - Tranche is deactivated
error TrancheNotActive()
// Solution: Check pool.getTrancheInfo(tranche).active
// FundsCurrentlyDeployed - Cannot deactivate tranche with deployed funds
error FundsCurrentlyDeployed()
// Solution: Wait for borrower to repay all deployed capital
// PoolNotActive - Pool is not active in registry
error PoolNotActive()
// Solution: Check registry.isPoolActive(pool)
// ProtocolEmergencyShutdown - Protocol is in emergency shutdown
error ProtocolEmergencyShutdown()
// Solution: Wait for protocol to resume operationsSettings & Proposal Errors
typescript
// NoPendingSettingsProposal - No proposal to accept/reject
error NoPendingSettingsProposal()
// Solution: Propose settings first with structuredPool.proposeTrancheSettingsChange()
// SettingsProposalExpired - Proposal has expired
error SettingsProposalExpired()
// Solution: Submit a new proposal
// CannotModifyImmutableField - Trying to change immutable field
error CannotModifyImmutableField()
// Solution: Check which fields can be modified in current stateFactory & Registry Errors
typescript
// OnlyApprovedFactory - Only approved factory can initialize
error OnlyApprovedFactory()
// Solution: Always use PoolFactory.deployPool() to create pools
// AlreadyRegistered - Pool/Controller already registered
error AlreadyRegistered()
// Solution: Each pool can only be registered once
// MaxTranchesReached - Pool has maximum tranches (currently 1)
error MaxTranchesReached()
// Solution: Current version only supports 1 tranche per poolError Handling Example
typescript
import { useContractWrite } from 'wagmi'
import { parseUnits } from 'viem'
const { write: deposit, error } = useContractWrite({
address: trancheAddress,
abi: TrancheABI,
functionName: 'deposit',
args: [parseUnits('1000', 6), userAddress],
onError: (error) => {
// Parse custom errors
const errorMessage = error.message
if (errorMessage.includes('BelowMinimumDeposit')) {
console.error('❌ Amount is below minimum deposit')
// Show user the minimum deposit amount
} else if (errorMessage.includes('ExceedsMaximumTVL')) {
console.error('❌ Amount exceeds maximum TVL')
// Show user the maximum deposit amount
} else if (errorMessage.includes('InsufficientBalance')) {
console.error('❌ Insufficient token balance')
// Ask user to get more tokens
} else if (errorMessage.includes('InvalidReceiver')) {
console.error('❌ Invalid receiver address')
// Check receiver address
} else if (errorMessage.includes('ProtocolEmergencyShutdown')) {
console.error('❌ Protocol is in emergency shutdown')
// Show emergency message to user
} else {
console.error('❌ Transaction failed:', errorMessage)
}
}
})
// Pre-flight validation to prevent errors
const { data: settings } = useContractRead({
address: trancheAddress,
abi: TrancheABI,
functionName: 'getTrancheSettings'
})
const { data: userBalance } = useContractRead({
address: USDC_ADDRESS,
abi: ERC20ABI,
functionName: 'balanceOf',
args: [userAddress]
})
const amount = parseUnits('1000', 6)
if (settings && userBalance) {
if (amount < settings.minimumDeposit) {
console.error('Amount below minimum:', formatUnits(settings.minimumDeposit, 6))
} else if (amount > settings.maximumTVL) {
console.error('Amount above maximum:', formatUnits(settings.maximumTVL, 6))
} else if (userBalance < amount) {
console.error('Insufficient balance:', formatUnits(userBalance, 6))
} else {
// Safe to deposit
deposit?.()
}
}All Protocol Errors
For a complete list of all custom errors, see packages/protocol/contracts/libraries/helpers/Errors.sol. Key categories:
- Validation:
InvalidAmount,InvalidAddress,InvalidAsset, etc. - Limits:
BelowMinimumDeposit,ExceedsMaximumTVL,ExceedsCreditLimit - Balance:
InsufficientBalance,InsufficientLiquidity,OutstandingDebt - Access Control:
AccessControlViolation,OnlyStructuredPool,OnlyReserve - State:
TrancheNotActive,PoolNotActive,FundsCurrentlyDeployed - Withdrawals:
NoWithdrawalApproval,NoWithdrawalRequest,InvalidRequest - Emergency:
TimelockNotMet,ProtocolEmergencyShutdown - Proposals:
NoPendingSettingsProposal,SettingsProposalExpired - Factory:
OnlyApprovedFactory,AlreadyRegistered,MaxTranchesReached - Fees:
ExcessiveDepositFee,ExcessiveInterestFee,ExcessiveTotalFees
Getting Help
- Check test files:
packages/protocol/test/v2.1/has comprehensive examples - Read flow diagrams:
packages/protocol/docs/v2.1/flows/ - Review auditor intro:
packages/protocol/docs/v2.1/protocol-v2.1-auditor-intro.md - Ask in team Slack: #protocol-dev channel
Flow Diagrams
Detailed flow diagrams showing the complete protocol architecture and core operations. These diagrams are also available as standalone .mmd files in packages/protocol/docs/v2.1/flows/.
1. Architecture Overview
Complete system architecture showing infrastructure layer, pool instances, and external actors.
2. Deposit Flow
Step-by-step LP deposit process: validation → share calculation → custody transfer → minting.
3. Drawdown Flow
Borrower capital deployment: interest accrual → credit check → capital deployment → accounting updates.
4. Repayment Flow
Repayment processing: interest accrual → payment split → routing to tranches → accounting updates.
5. Approval-Based Withdrawal
Three-step withdrawal with approvals: request → manager approval → execution.
Reserved Shares & Credit Protection
When withdrawal requests are approved, the approved shares become reserved and are tracked in WithdrawalRegistry.totalReservedShares[tranche]. This reservation system ensures LP funds are protected:
- Prevents Credit Drawdown: Reserved shares' asset value is excluded from
getAvailableForBorrowing(), preventing borrowers from drawing funds that LPs have requested to withdraw - Dynamic Valuation: Reserved shares maintain their count but their asset value fluctuates with share price (interest accrual increases reserved asset value)
- Manager Safeguard: Managers cannot approve more shares than available liquidity (factoring in already-deployed capital)
- Automatic Cleanup: Reserved shares are decremented as withdrawals execute, and fully consumed requests are deleted from storage for gas efficiency
Example: If a tranche has 15,000 USDC and 5,000 is approved for withdrawal, only 10,000 remains available for credit deployment.
6. Factory Deployment
Complete pool deployment sequence via PoolFactory with minimal proxies.
Note: This is a simplified version of the deployment flow. For the complete sequence diagram, see
flows/06-factory-deployment-flow.mmd.
Deployment Steps:
- Deploy StructuredPool - BeaconProxy instance per pool (~200k gas)
- Deploy Tranche - BeaconProxy instance per tranche (~200k gas)
- Deploy Reserve - Immutable custody contract in Tranche constructor (~1M gas)
- Register Pool in Registry - Immutable metadata record
- Register Tranche with Pool - Complete the setup
Total Gas: ~1.6M for complete pool deployment (96% savings vs direct deployment)
Quick Reference
DataTypes Structure
Settings must match the structure defined in DataTypes.sol:
TrancheSettings
typescript
{
seniority: number // 1=Senior, 2=Mezzanine, 3=Junior
minimumDeposit: bigint // Minimum deposit in asset decimals
maximumTVL: bigint // Maximum TVL in asset decimals
withdrawalPeriod: number // Withdrawal period in seconds (0 = immediate)
requireApprovalForWithdrawals: boolean // ⚠️ CRITICAL: Enable two-step withdrawal flow
// true = request → approve → execute
// false = instant withdrawal (subject to liquidity)
targetAllocation: number // Target allocation in bps (10000 = 100%)
}⚠️ About requireApprovalForWithdrawals:
This is a per-tranche setting that fundamentally changes how withdrawals work:
false(Instant):- Users call
withdraw()orredeem()directly - Executes immediately if liquidity available
- Best for: Retail pools, high liquidity needs
- Users call
true(Approval-Based):- Users must call
requestWithdraw()first - Wait for manager to approve via
approveWithdrawRequest() - Then execute with
redeem() - Best for: Institutional pools, managed liquidity
- Users must call
Always display this prominently in your UI! Users need to know before depositing.
CreditLineSettings
typescript
{
borrower: Address // ⚠️ IMMUTABLE: Borrower address (one per pool, set at creation)
interestRate: number // Annual interest rate in basis points (e.g., 800 = 8%)
drawLimit: bigint // Maximum credit line in asset decimals
interestPaymentInterval: number // Payment interval in seconds (e.g., 2592000 = 30 days)
}Critical Note: The borrower field is immutable once the pool is created. Each pool serves exactly one borrower. To work with multiple borrowers, create multiple pools.
Note: Protocol fees (deposit/interest) are configured globally via ProtocolConfiguration, not per-tranche.
ProtocolFeeConfiguration (Global)
typescript
// Fees are set at the protocol level, not per-tranche
// Read via ProtocolConfiguration contract
const { data: feeConfig } = useContractRead({
address: PROTOCOL_CONFIGURATION,
abi: ProtocolConfigurationABI,
functionName: 'getFeeConfiguration'
})
// Returns:
{
treasury: Address // Where protocol fees are sent
depositFeeBps: number // Fee on deposits in basis points (e.g., 100 = 1%)
interestFeeBps: number // Fee on interest in basis points (e.g., 1000 = 10%)
}To update fees (admin only):
typescript
const { write: setFees } = useContractWrite({
address: PROTOCOL_CONFIGURATION,
abi: ProtocolConfigurationABI,
functionName: 'setProtocolFees',
args: [
treasuryAddress,
100, // 1% deposit fee
1000 // 10% interest fee
]
})Contract Addresses
typescript
// Import from constants package
import {
POOL_FACTORY,
REGISTRY,
WITHDRAWAL_REGISTRY,
UNDERWRITER_REGISTRY,
PROTOCOL_CONFIGURATION
} from '@textile/constants'Withdrawal Registry
The WithdrawalRegistry is a centralized service for managing withdrawal requests and approvals across all tranches in the protocol. It enables orderly liquidity management for pools that require manager approval for withdrawals.
How It Works
For tranches with requireApprovalForWithdrawals = true:
- LP Requests → Calls
tranche.requestWithdraw(amount)ortranche.requestRedeem(shares) - Registry Records → Request stored with timestamp and shares amount
- Manager Approves → Calls
pool.approveWithdrawRequest(tranche, user, approvedShares) - LP Executes → Calls
tranche.withdraw()ortranche.redeem()to claim approved funds
Request Withdrawal (LP)
typescript
import { useContractWrite, useContractRead } from 'wagmi'
import { parseUnits, formatUnits } from 'viem'
// Check if approval is required for this tranche
const { data: requiresApproval } = useContractRead({
address: trancheAddress,
abi: TrancheABI,
functionName: 'requireApprovalForWithdrawals'
})
if (!requiresApproval) {
console.log('This tranche allows instant withdrawals')
// Use direct withdraw() instead
return
}
// Request withdrawal by assets
const withdrawAmount = parseUnits('5000', 6)
const { write: requestWithdraw } = useContractWrite({
address: trancheAddress,
abi: TrancheABI,
functionName: 'requestWithdraw',
args: [withdrawAmount],
onSuccess: () => {
console.log('✅ Withdrawal request submitted')
console.log('Waiting for manager approval...')
}
})
// Or request by shares
const { write: requestRedeem } = useContractWrite({
address: trancheAddress,
abi: TrancheABI,
functionName: 'requestRedeem',
args: [shareAmount],
onSuccess: () => {
console.log('✅ Redemption request submitted')
}
})Check Request Status (LP)
typescript
import { WITHDRAWAL_REGISTRY } from '@textile/constants'
// Check if user has a pending request
const { data: hasPending } = useContractRead({
address: WITHDRAWAL_REGISTRY,
abi: WithdrawalRegistryABI,
functionName: 'hasPendingRequest',
args: [trancheAddress, userAddress]
})
console.log('Has pending request:', hasPending)
// Get full request details
const { data: request } = useContractRead({
address: WITHDRAWAL_REGISTRY,
abi: WithdrawalRegistryABI,
functionName: 'getWithdrawalRequest',
args: [trancheAddress, userAddress]
})
if (request) {
console.log('Requested shares:', formatUnits(request.shares, 6))
console.log('Approved shares:', formatUnits(request.approved, 6))
console.log('Requested at:', new Date(Number(request.timestamp) * 1000))
if (request.approved > 0n) {
console.log('✅ Request approved! You can now execute withdrawal')
} else {
console.log('⏳ Waiting for manager approval')
}
}
// Just get approved amount
const { data: approvedShares } = useContractRead({
address: WITHDRAWAL_REGISTRY,
abi: WithdrawalRegistryABI,
functionName: 'getApprovedShares',
args: [trancheAddress, userAddress]
})
console.log('Approved shares:', formatUnits(approvedShares || 0n, 6))Approve Request (Pool Manager)
typescript
// Manager approves withdrawal request
const { write: approveRequest } = useContractWrite({
address: poolAddress,
abi: StructuredPoolABI,
functionName: 'approveWithdrawRequest',
args: [
trancheAddress,
userAddress,
approvedShares // Can approve partial or full amount
],
onSuccess: () => {
console.log('✅ Withdrawal request approved')
console.log('User can now execute withdrawal')
}
})
approveRequest?.()Reject Request (Pool Manager)
typescript
// Manager rejects withdrawal request
const { write: rejectRequest } = useContractWrite({
address: poolAddress,
abi: StructuredPoolABI,
functionName: 'rejectWithdrawRequest',
args: [trancheAddress, userAddress],
onSuccess: () => {
console.log('✅ Withdrawal request rejected')
}
})
rejectRequest?.()Execute Approved Withdrawal (LP)
typescript
// After approval, LP executes the withdrawal
const { data: approvedShares } = useContractRead({
address: WITHDRAWAL_REGISTRY,
abi: WithdrawalRegistryABI,
functionName: 'getApprovedShares',
args: [trancheAddress, userAddress]
})
if (approvedShares && approvedShares > 0n) {
const { write: redeem } = useContractWrite({
address: trancheAddress,
abi: TrancheABI,
functionName: 'redeem',
args: [approvedShares, userAddress, userAddress],
onSuccess: () => {
console.log('✅ Withdrawal executed!')
console.log('Approval consumed automatically')
}
})
redeem?.()
} else {
console.log('❌ No approved shares available')
}Get All Pending Requests (Manager Dashboard)
typescript
// Note: WithdrawalRegistry doesn't have a built-in function to list all requests
// Use The Graph subgraph or event indexing for this
// Example with event filtering
const { data: requests } = useContractEvent({
address: WITHDRAWAL_REGISTRY,
abi: WithdrawalRegistryABI,
eventName: 'WithdrawalRequested',
listener: (logs) => {
logs.forEach(log => {
const { tranche, user, shares, timestamp } = log.args
if (tranche === trancheAddress) {
console.log('Pending request:')
console.log('User:', user)
console.log('Shares:', formatUnits(shares, 6))
console.log('Time:', new Date(Number(timestamp) * 1000))
}
})
}
})Reserved Shares & Liquidity Protection
When withdrawal requests are approved, the approved shares become reserved:
typescript
// Get total reserved shares for a tranche
const { data: totalReserved } = useContractRead({
address: trancheAddress,
abi: TrancheABI,
functionName: 'getTotalReservedForWithdrawals'
})
console.log('Total reserved shares:', formatUnits(totalReserved || 0n, 6))
// Reserved shares reduce available liquidity for borrowing
const { data: availableForBorrowing } = useContractRead({
address: trancheAddress,
abi: TrancheABI,
functionName: 'getAvailableForBorrowing'
})
console.log('Available for borrowing:', formatUnits(availableForBorrowing || 0n, 6))
// This excludes reserved shares' asset valueImportant Notes:
- Centralized Management: All withdrawal requests are managed in one registry
- Tranche-Specific: Each tranche can enable/disable approval requirements independently
- Partial Approvals: Managers can approve less than requested amount
- Automatic Consumption: Approved shares are consumed when withdrawal executes
- Liquidity Protection: Reserved shares cannot be deployed to borrowers
- Dynamic Valuation: Reserved shares' asset value changes with share price
- Manager Control: Only pool managers can approve/reject requests
- Cleanup: Fully consumed requests are deleted from storage for gas efficiency
Underwriter Registry
The UnderwriterRegistry is a central registry where underwriters can register themselves and set their fee rates. Underwriters provide credit assessment and risk management services for pools.
Register as an Underwriter
typescript
import { useContractWrite } from 'wagmi'
import { UNDERWRITER_REGISTRY } from '@textile/constants'
// Register with your fee rates
const { write: register } = useContractWrite({
address: UNDERWRITER_REGISTRY,
abi: UnderwriterRegistryABI,
functionName: 'registerUnderwriter',
args: [
50, // depositFeeBps: 0.5% fee on deposits
500, // interestFeeBps: 5% fee on interest payments
'My Underwriting Firm',
'https://myunderwriting.com'
],
onSuccess: () => {
console.log('✅ Registered as underwriter')
}
})
register?.()Fee Limits:
- Deposit fee: Maximum 30% (3000 basis points)
- Interest fee: Maximum 50% (5000 basis points)
Update Underwriter Fees
typescript
// Update your fee rates (only you can update your own fees)
const { write: updateFees } = useContractWrite({
address: UNDERWRITER_REGISTRY,
abi: UnderwriterRegistryABI,
functionName: 'updateFees',
args: [
75, // New deposit fee: 0.75%
750 // New interest fee: 7.5%
],
onSuccess: () => {
console.log('✅ Fees updated')
}
})
updateFees?.()Update Profile Information
typescript
// Update your profile details
const { write: updateProfile } = useContractWrite({
address: UNDERWRITER_REGISTRY,
abi: UnderwriterRegistryABI,
functionName: 'updateProfile',
args: [
'Updated Firm Name',
'https://newwebsite.com'
]
})
updateProfile?.()Get Underwriter Profile
typescript
// Get complete underwriter profile
const { data: profile } = useContractRead({
address: UNDERWRITER_REGISTRY,
abi: UnderwriterRegistryABI,
functionName: 'getUnderwriterProfile',
args: [underwriterAddress]
})
if (profile) {
console.log('Underwriter:', profile.underwriter)
console.log('Name:', profile.name)
console.log('Website:', profile.website)
console.log('Deposit Fee:', Number(profile.depositFeeBps) / 100, '%')
console.log('Interest Fee:', Number(profile.interestFeeBps) / 100, '%')
console.log('Active:', profile.active)
console.log('Registered:', new Date(Number(profile.registeredAt) * 1000))
}
// Just get fees
const { data: fees } = useContractRead({
address: UNDERWRITER_REGISTRY,
abi: UnderwriterRegistryABI,
functionName: 'getUnderwriterFees',
args: [underwriterAddress]
})
if (fees) {
console.log('Deposit Fee:', Number(fees[0]) / 100, '%')
console.log('Interest Fee:', Number(fees[1]) / 100, '%')
}
// Check if registered and active
const { data: isRegistered } = useContractRead({
address: UNDERWRITER_REGISTRY,
abi: UnderwriterRegistryABI,
functionName: 'isRegisteredUnderwriter',
args: [underwriterAddress]
})
console.log('Is registered underwriter:', isRegistered)List All Underwriters
typescript
// Get all registered underwriters for discovery
const { data: allUnderwriters } = useContractRead({
address: UNDERWRITER_REGISTRY,
abi: UnderwriterRegistryABI,
functionName: 'getAllUnderwriters'
})
console.log('Total underwriters:', allUnderwriters?.length)
// Fetch profiles for all underwriters
if (allUnderwriters) {
for (const address of allUnderwriters) {
const { data: profile } = useContractRead({
address: UNDERWRITER_REGISTRY,
abi: UnderwriterRegistryABI,
functionName: 'getUnderwriterProfile',
args: [address]
})
if (profile?.active) {
console.log(`${profile.name}: ${Number(profile.depositFeeBps)/100}% deposit, ${Number(profile.interestFeeBps)/100}% interest`)
}
}
}Deactivate/Reactivate Profile
typescript
// Temporarily deactivate your profile
const { write: deactivate } = useContractWrite({
address: UNDERWRITER_REGISTRY,
abi: UnderwriterRegistryABI,
functionName: 'deactivate',
onSuccess: () => {
console.log('✅ Profile deactivated')
}
})
// Reactivate your profile
const { write: reactivate } = useContractWrite({
address: UNDERWRITER_REGISTRY,
abi: UnderwriterRegistryABI,
functionName: 'reactivate',
onSuccess: () => {
console.log('✅ Profile reactivated')
}
})Propose Underwriter for Pool
typescript
// Pool manager proposes an underwriter for their pool
const { write: proposeUnderwriter } = useContractWrite({
address: poolAddress,
abi: StructuredPoolABI,
functionName: 'proposeUnderwriter',
args: [
underwriterAddress, // Must be registered in UnderwriterRegistry
50, // Deposit fee (must match registry)
500 // Interest fee (must match registry)
],
onSuccess: () => {
console.log('✅ Underwriter proposed')
console.log('Underwriter must accept the proposal')
}
})
proposeUnderwriter?.()Accept Underwriter Proposal
typescript
// Underwriter accepts the proposal
const { write: acceptProposal } = useContractWrite({
address: poolAddress,
abi: StructuredPoolABI,
functionName: 'acceptUnderwriter',
onSuccess: () => {
console.log('✅ Underwriter accepted and active for pool')
}
})
acceptProposal?.()Important Notes:
- Self-Service Registration: Anyone can register as an underwriter
- Global Fee Profile: Fee rates apply to all pools where you're the underwriter
- Update Anytime: You can update fees anytime (applies to future deposits/interest)
- Discovery: Frontends can query
getAllUnderwriters()to show available underwriters - Two-Step Activation: Pool manager proposes → Underwriter accepts
- Fee Validation: Proposed fees must match your registered fees in the registry
- Active Status: Deactivated underwriters cannot be proposed for new pools
Key Functions
| Operation | Contract | Function | Returns |
|---|---|---|---|
| Deploy Pool | PoolFactory | deployPool(...) | Pool address (via event) |
| Deposit | Tranche | deposit(assets, receiver) | Shares minted |
| Withdraw | Tranche | withdraw(assets, receiver, owner) | Assets withdrawn |
| Request Withdrawal | Tranche | requestWithdraw(assets) | Event emitted |
| Approve Request | StructuredPool | approveWithdrawRequest(tranche, user, shares) | - |
| Drawdown | StructuredPool | drawdown(amount) | - |
| Repay | StructuredPool | repay(amount) | amount repaid |
| Repay Interest | StructuredPool | repayInterest() | amount repaid |
| Repay All | StructuredPool | repayAll() | total amount repaid |
| Check Balance | Tranche | balanceOf(user) | Share balance |
| Check Assets | Tranche | previewRedeem(shares) | Asset amount |
| Get Pool Metrics | StructuredPool | getPoolMetrics() | totalAssets, utilization, etc. |
| Get Virtual Balances | Tranche | getVirtualBalances() | balance, deployed, interestEarned |
| Get Debt Details | StructuredPool | getDebtDetails() | principal, interest, totalDebt |
| Get Available Credit | StructuredPool | getAvailableCredit() | Available credit amount |
| Get Credit Limit | StructuredPool | getCreditLimit() | Credit limit |
| Get Vault Settings | Tranche | getTrancheSettings() | TrancheSettings struct |
| Requires Approval | Tranche | requireApprovalForWithdrawals() | Boolean |
| Get Total Reserved | Tranche | getTotalReservedForWithdrawals() | Total reserved shares |
| Request Withdraw | Tranche | requestWithdraw(assets) | Event emitted (via WithdrawalRegistry) |
| Request Redeem | Tranche | requestRedeem(shares) | Event emitted (via WithdrawalRegistry) |
| Has Pending Request | WithdrawalRegistry | hasPendingRequest(tranche, user) | Boolean |
| Get Withdrawal Request | WithdrawalRegistry | getWithdrawalRequest(tranche, user) | WithdrawalRequest struct |
| Get Approved Shares | WithdrawalRegistry | getApprovedShares(tranche, user) | Approved shares amount |
| Approve Withdraw Request | StructuredPool | approveWithdrawRequest(tranche, user, shares) | - (calls WithdrawalRegistry) |
| Reject Withdraw Request | StructuredPool | rejectWithdrawRequest(tranche, user) | - (calls WithdrawalRegistry) |
| Set Approved Shares | WithdrawalRegistry | setApprovedShares(tranche, user, shares) | - (Manager/Pool only) |
| Consume Approval | WithdrawalRegistry | consumeApproval(tranche, user, shares) | - (Tranche only) |
| Get Protocol Fees | StructuredPool | getProtocolFees() | treasury, depositFeeBps, interestFeeBps |
| Get Utilization Rate | StructuredPool | getUtilizationRate() | Utilization in bps |
| Register Underwriter | UnderwriterRegistry | registerUnderwriter(depositFee, interestFee, name, website) | - |
| Update Underwriter Fees | UnderwriterRegistry | updateFees(depositFeeBps, interestFeeBps) | - |
| Get Underwriter Profile | UnderwriterRegistry | getUnderwriterProfile(underwriter) | UnderwriterProfile struct |
| Get Underwriter Fees | UnderwriterRegistry | getUnderwriterFees(underwriter) | depositFeeBps, interestFeeBps |
| Is Registered Underwriter | UnderwriterRegistry | isRegisteredUnderwriter(underwriter) | Boolean |
| Get All Underwriters | UnderwriterRegistry | getAllUnderwriters() | Array of addresses |
| Propose Underwriter | StructuredPool | proposeUnderwriter(underwriter, depositFee, interestFee) | - |
| Accept Underwriter | StructuredPool | acceptUnderwriter() | - |
Gas Estimates
| Operation | Estimated Gas | Notes |
|---|---|---|
| Deploy Pool | ~3.5M gas | One-time per pool |
| Deploy Tranche | ~1.5M gas | Included in pool deployment |
| First Deposit | ~150k gas | Higher due to state initialization |
| Subsequent Deposits | ~80k gas | Standard ERC4626 deposit |
| Withdraw (direct) | ~70k gas | Standard ERC4626 withdraw |
| Request Withdrawal | ~60k gas | Creates registry entry |
| Execute Withdrawal | ~90k gas | Consumes approval + withdrawal |
| Drawdown | ~120k gas | Includes forced interest accrual |
| Repayment | ~150k gas | Includes interest split + distribution |
Summary
This guide covers:
- ✅ Quick Start: Deploy and interact with v2.1 pools in 5 minutes
- ✅ Architecture: 9 core contracts with clear separation of concerns
- ✅ Common Operations: Deposit, withdraw, borrow, repay, approve requests
- ✅ Frontend Integration: viem/wagmi examples with helper functions
- ✅ Backend Integration: The Graph subgraph setup and queries
- ✅ Event Handling: Complete event reference with accurate signatures
- ✅ Error Handling: All 93 custom errors with solutions
- ✅ Flow Diagrams: Visual guides for all major operations
- ✅ Testing: Setup and examples for integration testing
Key Improvements in v2.1
- One Borrower Per Pool - Each pool has one immutable borrower (dedicated credit lines)
- ERC4626 Standard Compliance - Tranches are now standard vaults
- Immutable Custody - Reserve contracts with no upgrade path
- Factory Pattern - 94% gas savings on deployment (200k vs 3.5M)
- Helper Functions - Efficient multi-value getters:
getVirtualBalances()- Get all virtual accounting in one callgetPoolMetrics()- Get all pool metrics in one callgetDebtDetails()- Get complete debt breakdown in one callgetTrancheSettings()- Get all tranche settings in one call
- Withdrawal Approvals - Optional two-step withdrawal flow (per-pool setting)
- Settings Proposals - 2-step approval for parameter changes (underwriter consensus)
- Per-Second Interest - RAY precision compounding
Version Compatibility
- v2.1: Current version (single tranche per pool)
- v2.0: Deprecated (use v2.1 for new deployments)
- v1.x: Still supported (no migration required)
Note: v2.1 contracts are not upgradeable. New versions will be deployed via new factories, allowing opt-in migration.
Document Version: v1.0 Last Updated: 2026-01-20 Protocol Version: v2.1 (Single Tranche)