Skip to content

Textile Protocol v2.1 — Developer Guide

A practical guide for working with the Textile Protocol v2.1

Table of Contents


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:local

The deployment script will:

  1. Deploy all infrastructure contracts (Registry, factories, etc.)
  2. Deploy a sample pool with USDC
  3. 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

ComponentPurposeWho Interacts
StructuredPoolCoordinates capital deployment, repayment, and integrated credit lineBorrower, Manager
TrancheERC4626 vault for LP deposits/withdrawalsLPs, Frontend
ReserveHolds actual assets (custody layer)Internal only
RegistryPool registry and infrastructure referencesInternal lookups
WithdrawalRegistryManages withdrawal approvalsLPs, Manager, Frontend
PoolFactoryDeploys new poolsPool creators, Frontend
UnderwriterRegistryCentral registry for underwriter profiles and feesUnderwriters, 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, virtualBalance increases
  • Deployment: Assets move to the pool's borrower, virtualBalancevirtualDeployed
  • Repayment: Assets return from borrower to Reserve, updates virtual balances
  • Withdrawal: Assets leave Reserve to lenders, virtualBalance decreases

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) - P

Developer 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 invested

Value 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 borrowers

Important: 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-name

Important: 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 dev

Run 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 coverage

Testing 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:

  1. Protocol shutdown: Check ProtocolConfiguration.isShutdown()
  2. Contract paused: Check isPaused() on relevant contract
  3. Insufficient allowance: Check token approval
  4. 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() first

State 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 operations

Settings & 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 state

Factory & 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 pool

Error 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

  1. Check test files: packages/protocol/test/v2.1/ has comprehensive examples
  2. Read flow diagrams: packages/protocol/docs/v2.1/flows/
  3. Review auditor intro: packages/protocol/docs/v2.1/protocol-v2.1-auditor-intro.md
  4. 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:

  1. Deploy StructuredPool - BeaconProxy instance per pool (~200k gas)
  2. Deploy Tranche - BeaconProxy instance per tranche (~200k gas)
  3. Deploy Reserve - Immutable custody contract in Tranche constructor (~1M gas)
  4. Register Pool in Registry - Immutable metadata record
  5. 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() or redeem() directly
    • Executes immediately if liquidity available
    • Best for: Retail pools, high liquidity needs
  • 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

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:

  1. LP Requests → Calls tranche.requestWithdraw(amount) or tranche.requestRedeem(shares)
  2. Registry Records → Request stored with timestamp and shares amount
  3. Manager Approves → Calls pool.approveWithdrawRequest(tranche, user, approvedShares)
  4. LP Executes → Calls tranche.withdraw() or tranche.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 value

Important 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

OperationContractFunctionReturns
Deploy PoolPoolFactorydeployPool(...)Pool address (via event)
DepositTranchedeposit(assets, receiver)Shares minted
WithdrawTranchewithdraw(assets, receiver, owner)Assets withdrawn
Request WithdrawalTrancherequestWithdraw(assets)Event emitted
Approve RequestStructuredPoolapproveWithdrawRequest(tranche, user, shares)-
DrawdownStructuredPooldrawdown(amount)-
RepayStructuredPoolrepay(amount)amount repaid
Repay InterestStructuredPoolrepayInterest()amount repaid
Repay AllStructuredPoolrepayAll()total amount repaid
Check BalanceTranchebalanceOf(user)Share balance
Check AssetsTranchepreviewRedeem(shares)Asset amount
Get Pool MetricsStructuredPoolgetPoolMetrics()totalAssets, utilization, etc.
Get Virtual BalancesTranchegetVirtualBalances()balance, deployed, interestEarned
Get Debt DetailsStructuredPoolgetDebtDetails()principal, interest, totalDebt
Get Available CreditStructuredPoolgetAvailableCredit()Available credit amount
Get Credit LimitStructuredPoolgetCreditLimit()Credit limit
Get Vault SettingsTranchegetTrancheSettings()TrancheSettings struct
Requires ApprovalTrancherequireApprovalForWithdrawals()Boolean
Get Total ReservedTranchegetTotalReservedForWithdrawals()Total reserved shares
Request WithdrawTrancherequestWithdraw(assets)Event emitted (via WithdrawalRegistry)
Request RedeemTrancherequestRedeem(shares)Event emitted (via WithdrawalRegistry)
Has Pending RequestWithdrawalRegistryhasPendingRequest(tranche, user)Boolean
Get Withdrawal RequestWithdrawalRegistrygetWithdrawalRequest(tranche, user)WithdrawalRequest struct
Get Approved SharesWithdrawalRegistrygetApprovedShares(tranche, user)Approved shares amount
Approve Withdraw RequestStructuredPoolapproveWithdrawRequest(tranche, user, shares)- (calls WithdrawalRegistry)
Reject Withdraw RequestStructuredPoolrejectWithdrawRequest(tranche, user)- (calls WithdrawalRegistry)
Set Approved SharesWithdrawalRegistrysetApprovedShares(tranche, user, shares)- (Manager/Pool only)
Consume ApprovalWithdrawalRegistryconsumeApproval(tranche, user, shares)- (Tranche only)
Get Protocol FeesStructuredPoolgetProtocolFees()treasury, depositFeeBps, interestFeeBps
Get Utilization RateStructuredPoolgetUtilizationRate()Utilization in bps
Register UnderwriterUnderwriterRegistryregisterUnderwriter(depositFee, interestFee, name, website)-
Update Underwriter FeesUnderwriterRegistryupdateFees(depositFeeBps, interestFeeBps)-
Get Underwriter ProfileUnderwriterRegistrygetUnderwriterProfile(underwriter)UnderwriterProfile struct
Get Underwriter FeesUnderwriterRegistrygetUnderwriterFees(underwriter)depositFeeBps, interestFeeBps
Is Registered UnderwriterUnderwriterRegistryisRegisteredUnderwriter(underwriter)Boolean
Get All UnderwritersUnderwriterRegistrygetAllUnderwriters()Array of addresses
Propose UnderwriterStructuredPoolproposeUnderwriter(underwriter, depositFee, interestFee)-
Accept UnderwriterStructuredPoolacceptUnderwriter()-

Gas Estimates

OperationEstimated GasNotes
Deploy Pool~3.5M gasOne-time per pool
Deploy Tranche~1.5M gasIncluded in pool deployment
First Deposit~150k gasHigher due to state initialization
Subsequent Deposits~80k gasStandard ERC4626 deposit
Withdraw (direct)~70k gasStandard ERC4626 withdraw
Request Withdrawal~60k gasCreates registry entry
Execute Withdrawal~90k gasConsumes approval + withdrawal
Drawdown~120k gasIncludes forced interest accrual
Repayment~150k gasIncludes 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

  1. One Borrower Per Pool - Each pool has one immutable borrower (dedicated credit lines)
  2. ERC4626 Standard Compliance - Tranches are now standard vaults
  3. Immutable Custody - Reserve contracts with no upgrade path
  4. Factory Pattern - 94% gas savings on deployment (200k vs 3.5M)
  5. Helper Functions - Efficient multi-value getters:
    • getVirtualBalances() - Get all virtual accounting in one call
    • getPoolMetrics() - Get all pool metrics in one call
    • getDebtDetails() - Get complete debt breakdown in one call
    • getTrancheSettings() - Get all tranche settings in one call
  6. Withdrawal Approvals - Optional two-step withdrawal flow (per-pool setting)
  7. Settings Proposals - 2-step approval for parameter changes (underwriter consensus)
  8. 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)