Skip to content

Textile Protocol v2.1 — Auditor Introduction

Document Overview

  • Scope: StructuredPool, Tranche (ERC4626), Reserve, Registry, PoolFactory, WithdrawalRegistry, UnderwriterRegistry, ProtocolConfiguration, core libraries (Errors, ValidationLogic, MathUtils, SettingsProposal, Roles).
  • Design Goals: Separation of concerns, immutable custody, minimal surface area on stateful components, standardized ERC4626 vault semantics, predictable access control, explicit custom errors, pausable and reentrancy safe, settings proposal pattern for secure parameter updates.
  • Current Limitations:
    • Single tranche per pool (MAX_TRANCHES = 1). Multi-tranche waterfall infrastructure is in place for future expansion.
    • One borrower per pool: Each StructuredPool serves exactly one borrower (set immutably at creation). To support multiple borrowers, deploy multiple pools.

High-Level Architecture

Component Overview

StructuredPool

Strategy-level coordinator and credit line manager that:

  • Manages a single tranche (currently; expandable to multiple)
  • Integrated credit line management for a single borrower
  • Per-second interest compounding at RAY precision (10^27)
  • 4th order binomial approximation for interest accrual
  • Deploys capital to the pool's designated borrower via Reserve
  • Distributes repayments (interest + principal) from borrower to tranches
  • Manages withdrawal approvals and queue processing
  • Tracks pool-wide metrics: outstandingDebt, totalInterestEarned, totalPrincipalRepaid
  • Integrates with underwriter for credit decisions
  • Exposes utilization and performance metrics
  • Uses settings proposal pattern for rate/limit updates (borrower address cannot change)

Tranche (ERC4626)

Pure share accounting vault that:

  • Implements full ERC4626 interface: deposit/mint/withdraw/redeem
  • Holds no funds directly - all custody delegated to Reserve
  • Maintains virtual accounting: virtualBalance, virtualDeployed, totalInterestEarned
  • Includes inflation attack protection via _decimalsOffset() = 7
  • Supports optional withdrawal approval workflow
  • Uses settings proposal pattern for secure parameter updates

Contract Architecture:

The Tranche implementation is consolidated into two files (located in contracts/v2.1/core/):

  • TrancheStorage (abstract) - Pure storage layer with state variables only, no logic (located in modules/)
  • Tranche (concrete) - All implementation logic consolidated into a single contract:
    • Access control modifiers
    • Virtual balance tracking and interest recording (recordInterest, recordPrincipal, recordDeployment)
    • Deposit/withdraw operations (ERC4626 core: deposit, mint, withdraw, redeem)
    • Checkpoint-based auto-interest compounding (v2.1: dilution-proof)
    • Two-step withdrawal approval workflow

The interface is consolidated into a single ITranche.sol (located in contracts/v2.1/interfaces/) that extends IERC4626 and declares all Tranche-specific functions and events.

Reserve (Immutable)

Custody contract with strict access control:

  • Deployed once per Tranche (via constructor, non-upgradeable)
  • Only Tranche can call: receiveFunds(), withdrawTo()
  • Only StructuredPool can call: deployTo()
  • Guardian-controlled emergency withdrawal with 3-day timelock
  • No upgrade mechanism - pure custody layer

Registry

Central infrastructure hub and pool lifecycle manager:

  • Source of Truth: References global infrastructure: ProtocolConfiguration, WithdrawalRegistry, UnderwriterRegistry
  • Factory Whitelisting: Manages approved factories allowed to deploy and register pools
  • Pool Lifecycle: Registers new pools and enables protocol admins to deactivate/reactivate them
  • Service Discovery: Eliminates circular dependencies by providing central lookup for protocol services
  • Metadata Tracking: Stores basic pool information (asset, name, deployment time) for discovery

WithdrawalRegistry

Centralized withdrawal management:

  • Stores pending withdrawal requests with timestamps
  • Manages approved shares per user per tranche
  • Role-based access:
    • TRANCHE_ROLE: Creates requests
    • MANAGER_ROLE: Approves requests
    • Only originating tranche can consume approvals
  • Enables orderly liquidity management

UnderwriterRegistry

Self-service underwriter registry:

  • Self-service registration (no admin approval required)
  • Underwriters set global fee rates (deposit max 30%, interest max 50%)
  • Only underwriters can update their own profiles/fees
  • Referenced by StructuredPool for underwriter validation
  • Upgradeable (UUPS) with timelock protection

ProtocolConfiguration

Global protocol settings:

  • Fee configuration: deposit fees, interest fees
  • Treasury address management
  • Emergency shutdown mechanism
  • Protocol-wide parameters

PoolFactory

Deployment coordinator using BeaconProxy pattern:

  • Deploys StructuredPool and Tranche as BeaconProxy instances (~200k gas per deployment)
  • Each contract type has its own upgradeable beacon (poolBeacon, trancheBeacon)
  • Enables mass upgrades of all instances via beacon upgrades
  • Initializes all contracts in correct sequence
  • Auto-registers pool in Registry
  • Enforces factory approval via Registry

Architecture Diagram


Access Control and Trust Boundaries

Role System

Contracts use OpenZeppelin's AccessControl with custom roles defined in Roles.sol:

solidity
// Custom roles from Roles.sol
PROTOCOL_ADMIN = keccak256("PROTOCOL_ADMIN")
TIMELOCK_ADMIN = keccak256("TIMELOCK_ADMIN")
REGISTRAR_ROLE = keccak256("REGISTRAR_ROLE")
TRANCHE_ROLE = keccak256("TRANCHE_ROLE")

Note: v2.1 does NOT use DEFAULT_ADMIN_ROLE or MANAGER_ROLE. Instead:

  • protocolAdmin() replaces DEFAULT_ADMIN_ROLE
  • Pool manager is tracked via manager address (direct checks, not role-based)

AccessControlHelpers Pattern

Purpose: Prevent governance self-bricking and enable secure role rotation

All core contracts inherit from AccessControlHelpers (or AccessControlHelpersUpgradeable for upgradeable contracts), which provides:

Key Features:

  1. Self-Admin Protocol Admin: _grantProtocolAdminWithSelfAdmin(admin_) sets protocolAdmin as its own admin, enabling role rotation without external dependencies
  2. Governance Self-Bricking Prevention: Tracks _protocolAdminCount to prevent renouncing the sole protocol admin role
  3. Role Rotation Support: Protocol admins can grant the role to new addresses and then renounce their own role for complete handover

Implementation Details:

solidity
// Overridden methods that track protocol admin count:
- renounceRole() - Prevents renouncing if only 1 protocol admin remains
- grantRole() - Increments count when granting protocol admin
- revokeRole() - Decrements count when revoking protocol admin

Security Properties:

  • ✅ Prevents accidental governance lockout
  • ✅ Enables emergency role rotation (add new admin, remove compromised admin)
  • ✅ Supports multisig transitions (multiple protocol admins can coexist temporarily)
  • ✅ Custom error CannotRenounceSoleAdmin() provides clear feedback

Contracts Using AccessControlHelpers:

  • StructuredPool, Tranche, Registry, WithdrawalRegistry, ProtocolConfiguration, PoolFactory

StructuredPool

Roles:

  • protocolAdmin() - Pause, emergency functions, role management
  • registrarRole() - Tranche registration/deactivation (granted to factory during init)

Direct Access Control:

  • manager address - Operational functions (withdrawal approvals, strategy updates, underwriter management, settings proposals)
  • Uses onlyPoolManager modifier checking msg.sender == manager

Special Authorization:

  • Controller validation via registry.getController(address(this))
  • Underwriter validation via _underwriter address

Tranche

Roles:

  • protocolAdmin() - Pause, emergency functions, role management

Direct Access Control:

  • _poolManager address - Settings proposals, withdrawal approvals
  • Uses onlyPoolManager modifier checking msg.sender == _poolManager

StructuredPool-Only Hooks (no role, checked via msg.sender == address(_structuredPool)):

  • recordInterest() - Update interest earned
  • recordPrincipal() - Record principal repayment
  • recordDeployment() - Record capital deployment
  • processWithdrawal() - Pool-managed withdrawal processing

Reserve

NO ROLES - Pure address-based access control:

  • guardian (immutable) - Emergency withdrawal initiation/execution/cancellation only
  • Only Tranche can call (checked via msg.sender == address(_tranche)):
    • receiveFunds() - Accept deposits
    • withdrawTo() - Process withdrawals
  • Only StructuredPool can call (checked via msg.sender == _structuredPool):
    • deployTo() - Deploy capital to borrower

StructuredPool

Roles:

  • protocolAdmin() - Settings management, pause, forced interest accrual
  • borrowerRole() - Drawdown and repayment operations

Direct Access Control:

  • Pool manager can propose settings (checked via structuredPool.manager())

WithdrawalRegistry

Roles:

  • protocolAdmin() - Role management, pause, admin functions
  • registrarRole() - Tranche registration (granted to factories)
  • trancheRole() - Request creation (granted to registered tranches)
    • createWithdrawRequest() - Create withdrawal request
    • createRedeemRequest() - Create redemption request
    • consumeApproval() - Consume approved shares

Direct Access Control:

  • Pool managers can approve/reject via their respective pools (pool calls registry on manager's behalf)

Registry

Roles:

  • protocolAdmin() - Infrastructure updates, admin functions, schedule/execute upgrades
  • timelockAdmin() - Manage upgrade delay period (separate multi-sig recommended)
  • registrarRole() - Pool registration (granted to approved factories)
    • registerPool() - Register new pool (called by factory)

Factory Approval:

  • Uses isApprovedFactory(address) mapping (not role-based)
  • Approved via approveFactory() by protocolAdmin

Upgradeability:

  • UUPS proxy pattern with timelock protection
  • scheduleUpgrade() / executeUpgrade() - Protocol admin only
  • setUpgradeDelay() - Timelock admin only
  • Default 2-day timelock (configurable 1 hour to 30 days)

UnderwriterRegistry

NO ROLES - Self-service public registry:

  • Anyone can call registerUnderwriter() to register
  • Only the underwriter themselves can update their own profile/fees
  • No admin approval required (open registration)

ProtocolConfiguration

Roles:

  • protocolAdmin() - Fee updates, treasury updates, shutdown control

PoolFactory

Roles:

  • protocolAdmin() - Registry updates, admin functions

Direct Access Control:

  • Uses immutable admin address for onlyProtocolAdmin modifier
  • Granted protocolAdmin() role in constructor for compatibility

Upgradeability and Immutability

Upgradeable Contracts (UUPS Pattern - Infrastructure):

  • Registry: Central pool registry (single instance)
  • WithdrawalRegistry: Withdrawal approval management (single instance)
  • UnderwriterRegistry: Underwriter profiles and fees (single instance)

Upgradeable Contracts (BeaconProxy Pattern - Pool Components):

  • StructuredPool: All instances upgradeable via poolBeacon (mass upgrade capability)
  • Tranche: All instances upgradeable via trancheBeacon (mass upgrade capability)
  • StructuredPool: All instances upgradeable via poolBeacon (mass upgrade capability)

Upgrade Protection:

  • 2-day timelock on all upgrades (default, configurable 1 hour to 30 days)
  • Two-party authorization: PROTOCOL_ADMIN schedules/executes, TIMELOCK_ADMIN manages delay
  • Separate multi-sig wallets required for both roles (5/9 and 3/5 recommended)
  • Compromise of both multi-sigs required to bypass timelock

Immutability Guarantees:

  • Reserve: Non-upgradeable, deployed once per Tranche constructor. Guardian cannot change, only emergency withdraw with timelock.
  • Registry Bindings: Pool-Controller pairs are immutable once registered.
  • Beacon Addresses: Immutable in PoolFactory after deployment

Custom Errors

All revert paths use explicit errors from Errors.sol for gas efficiency and clarity:

  • AccessControlViolation(), OnlyStructuredPool(), NotUnderwriter(), etc.
  • No generic require() strings for production code

Core Flows

1. Deposit Flow (LP → Tranche → Reserve)

Key Validations:

  • minimumDeposit <= assets <= maximumTVL
  • receiver != address(0)
  • Contract not paused
  • Protocol not shutdown
  • First deposit mints 1:1, subsequent use ERC4626 formula

2. Drawdown Flow (Borrower → Pool → Reserve)

Key Invariants:

  • outstandingDebt + availableCredit = drawLimit (after update)
  • outstandingDebt matches sum of all tranche virtualDeployed
  • Interest must accrue before principal changes

3. Repayment Flow (Borrower → Pool → Tranche/Reserve)

Current Implementation Note: Distribution currently loops over the first active tranche only. Multi-tranche waterfall is planned for future versions.

Key Accounting:

  • Interest increases virtualBalance and totalInterestEarned
  • Principal returns deployed capital: virtualDeployedvirtualBalance
  • Both interest and principal go to Reserve custody immediately

4. Withdrawal Flows (Three Paths)

Path A: Direct Withdrawal (No Approval Required)

Path B: Approval-Based Withdrawal

Reserved Shares & Credit Protection: When withdrawal requests are approved, approved shares are reserved in WithdrawalRegistry.totalReservedShares[tranche]. Reserved shares' asset value is excluded from getAvailableForBorrowing(), preventing borrowers from drawing funds that LPs have requested to withdraw. The reservation is decremented as withdrawals execute, and fully consumed requests are deleted from storage.

Path C: Pool-Managed Processing

Liquidity Bounds:

availableCash = min(virtualBalance, reserve.balance())

For owner with ownerShares and ownerAssets = previewRedeem(ownerShares):
- maxWithdraw(owner) = min(ownerAssets, ownerAssets * availableCash / totalAssets)
- maxRedeem(owner) = min(ownerShares, ownerShares * availableCash / ownerAssets)

5. Emergency Withdrawal (Guardian)

Safety Features:

  • 3-day timelock provides opportunity for tranche holders to react
  • Guardian cannot change post-deployment
  • Only guardian can initiate, execute, or cancel
  • Execution requires explicit recipient address (prevents accidental burns)

6. Settings Proposal Flow

Used for secure parameter updates in both Tranche and StructuredPool:

Parameters Updated via Proposal:

Tranche Settings (DataTypes.TrancheSettings):

  • seniority - Tranche priority (1=Senior, 2=Mezzanine, 3=Junior) - IMMUTABLE after deployment
  • minimumDeposit - Minimum deposit amount
  • maximumTVL - Maximum TVL cap
  • withdrawalPeriod - Withdrawal period in seconds
  • requireApprovalForWithdrawals - Enable/disable approval workflow
  • targetAllocation - Target allocation in basis points

Note on Seniority: While the protocol currently supports only one tranche per pool (MAX_TRANCHES = 1), the seniority field accepts values 1-3 to prepare for future multi-tranche support. The seniority value is immutable after deployment and determines repayment priority in multi-tranche scenarios.

CreditLineController Settings (DataTypes.CreditLineSettings):

  • borrower - Borrower address
  • interestRate - Annual interest rate in basis points
  • drawLimit - Maximum credit line amount
  • interestPaymentInterval - Payment interval in seconds

Note: Protocol-level fees (deposit/interest) are configured separately via ProtocolConfiguration.setProtocolFees(), not in these settings structures.


7. Factory Deployment Flow

Deployment Sequence Critical Points:

  1. Pool includes integrated credit line functionality (no separate controller)
  2. Pool must be registered before tranche deployment (tranche init validates factory via registry)
  3. Reserve is deployed in Tranche constructor (immutable binding)
  4. Tranche must be added to pool before it's operational

Mathematical Specifications

ERC4626 Share Calculations (Standard)

Share Price:

p = totalAssets / totalSupply  (when totalSupply > 0)

Deposit (assets → shares):

shares = assets * totalSupply / totalAssets  (round down)

Special case (first deposit):
  shares = assets  (1:1 minting)

Withdrawal (shares → assets):

assets = shares * totalAssets / totalSupply  (round down)

Mint (shares → assets):

assets = shares * totalAssets / totalSupply  (round up)

Redeem (shares → assets):

assets = shares * totalAssets / totalSupply  (round down)

Tranche Total Assets

totalAssets = virtualBalance + virtualDeployed + pendingInterest

where:
  virtualBalance    = funds in Reserve available for deployment/withdrawal
  virtualDeployed   = funds currently deployed to the pool's borrower
  pendingInterest   = accrued but not yet paid interest from the borrower (accrued in StructuredPool)

pendingInterest is fetched from StructuredPool.getCurrentInterestDebt()

Virtual Accounting State Transitions

On Deposit:

virtualBalance += assets
totalSupply += shares

On Deployment (Pool → Borrower):

virtualBalance -= amount
virtualDeployed += amount
outstandingDebt += amount  (pool-level)

On Interest Repayment:

virtualBalance += interestPaid
totalInterestEarned += interestPaid  (tranche-level)
totalInterestEarned += interestPaid  (pool-level)

On Principal Repayment:

virtualDeployed -= principalPaid
virtualBalance += principalPaid
outstandingDebt -= principalPaid  (pool-level)
totalPrincipalRepaid += principalPaid  (pool-level)

On Withdrawal:

virtualBalance -= assets
totalSupply -= shares

Liquidity-Bounded Withdrawals

availableCash = min(virtualBalance, reserve.balance())

For owner with:
  ownerShares = balanceOf(owner)
  ownerAssets = previewRedeem(ownerShares)

Calculations:
  if (availableCash >= ownerAssets):
    maxWithdraw(owner) = ownerAssets
    maxRedeem(owner) = ownerShares
  else:
    maxWithdraw(owner) = floor(ownerAssets * availableCash / totalAssets)
    maxRedeem(owner) = floor(ownerShares * availableCash / ownerAssets)

Credit Line Interest Accrual (RAY Precision)

Constants:

RAY = 10^27
SECONDS_PER_YEAR = 31536000

Rate Conversion (basis points → RAY):

r_bps → r_ray = (r_bps * RAY) / 10000

Example: 500 bps (5% APR)
  r_ray = (500 * 10^27) / 10000 = 5 * 10^25

4th Order Binomial Approximation (per-second compounding):

(1 + x)^n ≈ 1 + nx + n(n-1)/2 x² + n(n-1)(n-2)/6 x³ + n(n-1)(n-2)(n-3)/24 x⁴

where:
  x = r_ray / SECONDS_PER_YEAR (per-second rate in RAY)
  n = seconds elapsed since last accrual
  D = total debt base (outstandingDebt + accumulatedInterest)

A(t) = D * growth_factor

Implementation: Iterative calculation in RAY precision with 4th order terms

Interest Calculation:

total_debt_base = outstandingDebt + accumulatedInterest
growth_factor = 1 + nx + n(n-1)x²/2 + n(n-1)(n-2)x³/6 + n(n-1)(n-2)(n-3)x⁴/24
compounded_amount = total_debt_base * growth_factor / RAY
new_interest = max(0, compounded_amount - total_debt_base)

Interest is always accrued and state updated when _accrueInterest() is called, typically triggered by drawdown and repayment operations. The 4th order binomial provides excellent accuracy (<1% error even at extreme rates and periods) while maintaining gas efficiency.

Utilization Rate

utilizationRate = floor((outstandingDebt / poolTotalAssets) * 10000)

where:
  poolTotalAssets = sum of all tranche totalAssets in pool

Example:
  outstandingDebt = 75,000 USDC
  poolTotalAssets = 100,000 USDC
  utilizationRate = 7500 (75%)

Inflation Attack Protection

Tranche uses _decimalsOffset() = 7 to add virtual assets:

totalAssets = virtualAssets + 10^(_decimalsOffset())
           = virtualAssets + 10^7

First deposit (with offset):
  shares = assets  (1:1 still, offset cancels in ratio)

Attack scenario (without offset):
  Attacker deposits 1 wei, gets 1 share
  Attacker donates large amount to inflate price
  Next depositor gets rounded down to 0 shares

With offset:
  Virtual assets prevent price manipulation via donation
  Minimum effective deposit becomes meaningful

Auto-Interest Compounding and Precision Handling

Feature Overview:

The Tranche supports optional auto-interest compounding. LPs can choose:

  • Compounding Enabled (default): Interest automatically increases share price, simplifying LP experience
  • Compounding Disabled: Interest is reserved and must be claimed manually, providing regular income distribution

Checkpoint-Based Accounting:

To prevent dilution bugs, the protocol uses a checkpoint-based accumulator model:

solidity
// TrancheStorage.sol - Global accumulator (scaled by 1e18)
uint256 public reservedInterestPerShareAccumulated;

// Per-user checkpoint (prevents cross-LP dilution)
mapping(address => uint256) public userReservedInterestCheckpoint;

// Total reserved (for liquidity management)
uint256 public reservedForNonCompoundingInterest;

How It Works:

  1. When interest is recorded: The global accumulator increases by (reservedAmount * 1e18) / totalSharesWithDisabledCompounding
  2. When LP disables compounding: Their checkpoint is set to the current accumulator value
  3. Claimable calculation: claimable = (accumulator - userCheckpoint) * userShares / 1e18

Key Benefit: Each LP's claimable amount is isolated and immutable - one LP toggling compounding cannot affect others' claimable amounts.

The Precision Challenge:

When interest is recorded for a tranche with mixed compounding preferences, the protocol must:

  1. Reserve proportional interest for non-compounding LPs: reserved = (netInterest * nonCompoundingShares) / totalSupply
  2. Allow remaining interest to increase share price for compounding LPs

With naive Floor rounding, non-compounding LPs would lose funds on small interest payments:

Example (50/50 split, 1 wei interest):
  exactReserved = (1 * 100000e18) / (200000e18) = 0.5 wei
  floor(0.5) = 0 wei → Non-compounding LP loses 100% of interest ❌

Example (0.01% shares, 1 wei interest):
  exactReserved = (1 * 100e18) / (1000000e18) = 0.0001 wei
  floor(0.0001) = 0 wei → Non-compounding LP loses 100% of interest ❌

The Solution: Floor Rounding for Fairness:

The protocol implements a fairness mechanism to prevent favoritism:

  1. Favor All LPs (Floor rounding):

    solidity
    // Tranche.sol recordInterest()
    reservedForNonCompounding = Math.mulDiv(
      netInterest,
      totalSharesWithDisabledCompounding,
      totalSupply(),
      Math.Rounding.Floor  // ← Prevents favoritism toward non-compounding LPs
    )

    Any dust resulting from the floor rounding stays in the pool's virtualBalance, which benefits ALL tranches and LPs through share price appreciation. This prevents non-compounding LPs from extracting more than their fair share of interest.

Fairness Properties:

AspectNon-Compounding LPsCompounding LPs
Immediate Effect✅ Fair share (Floor rounding)✅ Benefit from rounding dust
Long-Term EffectFair share of NET interestFair share of NET interest
Edge Case (1 wei)✅ May receive 0 (stays in pool)✅ Benefit from rounding dust
Net ResultFAIR - No favoritismFAIR - Benefit from pool appreciation

Example Scenarios:

  1. 50/50 Split, 1-wei payment:

    • Both sides theoretically entitled to 0.5 wei
    • Floor(0.5) = 0 wei reserved
    • 1 wei remains in pool, increasing share price for all
  2. 0.01% Share, 1000x 1-wei payments:

    • Each payment entitling non-compounding LP to 0.0001 wei
    • Floor(0.0001) = 0 wei reserved
    • All interest stays in pool until cumulative interest warrants a 1-wei reservation

State Variables:

solidity
// TrancheStorage.sol

// Checkpoint-based accounting (v2.1)
uint256 public reservedInterestPerShareAccumulated; // Global accumulator (scaled by 1e18)
mapping(address => uint256) public userReservedInterestCheckpoint; // Per-user checkpoint
uint256 public reservedForNonCompoundingInterest; // Total reserved for liquidity management

// Legacy/tracking variables
mapping(address => bool) public autoInterestCompoundingDisabled; // false = enabled (default)
uint256 public totalSharesWithDisabledCompounding; // Count of non-compounding shares
uint256 internal _accumulatedRoundingDust; // Track rounding errors for fair redistribution

View Functions:

solidity
function getClaimableReservedInterest(address user) external view returns (uint256);
function getAccumulatedRoundingDust() external view returns (uint256);

Allows LPs to check their claimable amount and monitor accumulated dust before redistribution.

Gas Efficiency:

  • Dust redistribution only triggers when threshold (1 unit of underlying) is reached
  • Typical recordInterest gas cost: ~97,473 gas (includes dust tracking overhead)
  • Redistribution is amortized across many interest payments (not per-payment)

Security Considerations:

  • No dilution: Checkpoint model prevents cross-LP dilution when toggling compounding
  • Isolated claims: Each LP's claimable amount is immutable and unaffected by others' actions
  • Overflow protection: Accumulator uses 1e18 scaling with safe math operations
  • Dust threshold (1 USDC for 6-decimal tokens) prevents griefing attacks via spam
  • Ceil rounding cannot be exploited to drain funds (bounded by actual interest earned)
  • Proportional fairness: LPs cannot claim more than their earned reserved interest
  • Transparent tracking: All state changes emit events for off-chain monitoring

Invariants and Safety Properties

Critical Invariants

  1. Reserve Custody:

    • Only Tranche can call Reserve.receiveFunds() and Reserve.withdrawTo()
    • Only StructuredPool can call Reserve.deployTo()
    • Reserve never initiates token pulls except from explicit safeTransferFrom by authorized contracts
  2. Capital Accounting:

    • StructuredPool.outstandingDebt = sum(tranche.virtualDeployed) for all tranches
    • Reserve.balance() >= Tranche.virtualBalance (equality in steady state, inequality during same-block operations)
    • StructuredPool.deployCapital() must not exceed selected Reserve.balance()
  3. Credit Line Invariant:

    • outstandingDebt + availableCredit = drawLimit after any operation that changes principal
  4. Registry Integrity:

    • Active tranche must have registered Reserve
  5. Tranche Lifecycle:

    • Tranche deactivation requires: virtualBalance == 0 AND virtualDeployed == 0 AND pool.outstandingDebt == 0
  6. Withdrawal Bounds:

    • maxWithdraw(owner) <= min(previewRedeem(balanceOf(owner)), availableCash)
    • maxRedeem(owner) <= balanceOf(owner)
    • When requireApprovalForWithdrawals = true: maxRedeem also bounded by approved shares
  7. Approval Accounting:

    • Approved shares in WithdrawalRegistry cannot exceed user's actual share balance at withdrawal execution
    • Only originating tranche can consume its approvals
    • Approval consumption is atomic with share burning
  8. Pause States:

    • Pausable contracts disable: deposits, mints, redeems where applicable
    • Withdrawal bounds always respect availableCash regardless of pause state
    • Emergency functions remain accessible during pause

Access Control Boundaries

  1. No unauthorized fund movement from Reserve
  2. No direct token transfers except through defined flows
  3. No ETH acceptance (contracts don't implement receive() or fallback())
  4. Factory approval required for pool/tranche initialization
  5. Role-based operations strictly enforced with custom errors

Failure Modes and Mitigations

Liquidity Shortfall

Scenario: More withdrawal requests than available cash

Mitigation:

  • maxWithdraw/maxRedeem enforce proportional exits based on availableCash
  • Approval-based withdrawal queue enables orderly processing
  • Manager can prioritize specific withdrawals via approveWithdrawRequest()

Stuck Pool/Credit Line

Scenario: StructuredPool reverts on getCurrentInterestDebt() call

Mitigation:

  • StructuredPool reverts on getCurrentInterestDebt() call
  • Tranche totalAssets calculation gracefully degrades
  • Pool operations continue without blocked by interest queries

Guardian Misuse

Scenario: Guardian attempts unauthorized emergency withdrawal

Mitigation:

  • 3-day timelock provides reaction time for tranche holders
  • Guardian address immutable post-deployment
  • Only guardian can initiate, execute, or cancel (no role delegation)
  • Execution requires explicit recipient address

Reentrancy Attacks

Scenario: Malicious token with reentrant callbacks

Mitigation:

  • All state-mutating functions use OpenZeppelin ReentrancyGuard
  • State updates precede external calls where possible (checks-effects-interactions)
  • Reserve, Tranche, StructuredPool all protected

Settings Manipulation

Scenario: Admin attempts rug pull via parameter changes

Mitigation:

  • Settings proposal pattern with 2-step approval (if underwriter exists)
  • Separate propose/approve steps require underwriter consensus
  • Settings validation logic enforces bounds
  • Proposals have expiry window (default 7 days) to prevent indefinite pending state

Inflation/Donation Attacks

Scenario: Attacker inflates share price via donation

Mitigation:

  • _decimalsOffset() = 7 adds virtual assets
  • Effective minimum deposit becomes meaningful
  • Price manipulation via donation becomes economically infeasible

Protocol Shutdown

Scenario: Critical vulnerability discovered

Mitigation:

  • ProtocolConfiguration.setEmergencyShutdown(bool) emergency function
  • Contracts check requireNotShutdown() on critical operations
  • Asymmetric shutdown behavior (safe shutdown principles):
    • Blocked during shutdown: New deposits, new drawdowns
    • Allowed during shutdown: Repayments, withdrawals, exits
  • This ensures borrowers can repay debt and lenders can exit positions during emergencies
  • Prevents liquidity lock and continued interest accrual during shutdown

Event Emissions

Design Pattern

Events are centralized in ITranche.sol for the Tranche contract. This design provides:

  • Single source of truth for all Tranche events
  • Clear ABI expectations for external integrators
  • Organized by functionality with events grouped by category (storage, accounting, compounding, withdrawal, settings)

TrancheStorage inherits from ITranche, making all events available to the implementation contract.

Key Events

Tranche (ITranche.sol - Centralized):

Storage Events:

  • ReserveDeployed(address indexed reserve) - Reserve contract deployed

Accounting Events:

  • Deposit(sender, owner, assets, shares) - Standard ERC4626
  • Withdraw(sender, receiver, owner, assets, shares) - Standard ERC4626
  • InterestAccrued(uint256 amount) - Interest recorded
  • VirtualBalanceUpdated(uint256 newBalance) - Virtual balance changed
  • DepositFeesCollected(protocolFee, underwriterFee) - Fees collected on deposit
  • InterestFeesCollected(totalInterest, protocolFee, underwriterFee, netInterest) - Fees on interest
  • InterestReservedForNonCompounding(amount, totalSharesWithDisabledCompounding) - Interest reserved for non-compounding LPs
  • RoundingDustRedistributed(uint256 amount) - Accumulated rounding dust redistributed to compounding LPs
  • DepositEnhanced(sender, owner, assets, shares, netAmount, protocolFee, underwriterFee) - Enhanced deposit event with fee breakdown

Compounding Events:

  • AutoInterestCompoundingSet(address indexed user, bool enabled) - LP toggled compounding
  • ReservedInterestClaimed(address indexed user, uint256 amount) - LP claimed reserved interest

Withdrawal Events:

  • WithdrawRequested(user, amount) - LP requests withdrawal
  • RedeemRequested(user, shares) - LP requests redemption

Settings Events:

  • TrancheSettingsUpdated(settings) - Settings applied

StructuredPool (IStructuredPool.sol):

  • PoolInitialized(asset, poolName, strategy) - Pool created
  • TrancheRegistered(tranche, seniority, targetAllocation) - Tranche added
  • CapitalDeployed(borrower, amount) - Capital deployed to borrower
  • RepaymentProcessed(totalAmount) - Repayment received
  • InterestDistributed(tranche, amount) - Interest routed to tranche
  • PrincipalReturned(tranche, amount) - Principal returned to tranche
  • UnderwriterProposed(pendingUnderwriter) - Underwriter proposed
  • UnderwriterAccepted(underwriter, depositFeeBps, interestFeeBps) - Underwriter accepted

StructuredPool (IStructuredPool.sol):

  • Drawdown(borrower, amount) - Capital drawn
  • Repaid(payer, totalAmount, grossInterest, netInterest, principal, protocolFee, underwriterFee) - Repayment made
  • InterestCalculated(interestAmount, totalAccruedInterest) - Interest calculated
  • CreditLineSettingsProposed(settings, nonce) - Settings proposed
  • CreditLineSettingsUpdated(settings, nonce) - Settings updated

WithdrawalRegistry (IWithdrawalRegistry.sol):

  • WithdrawalRequested(tranche, user, shares) - Request created
  • WithdrawalApproved(tranche, user, shares) - Request approved
  • WithdrawalRejected(tranche, user) - Request rejected
  • TrancheRegistered(factory, tranche) - Tranche registered with registry

PoolFactory:

  • PoolDeployed(deploymentId, pool, asset, poolName, deployer) - Pool deployed
  • TrancheDeployed(pool, tranche, seniority, trancheName) - Tranche deployed

Testing Focus Areas

Test Suite Organization

Tests located in packages/protocol/test/v2.1/:

  1. Tranche/ (10 test files)

    • Tranche.test.ts: Basic initialization and configuration
    • Tranche.Deposit.test.ts: Deposit and mint flows
    • Tranche.Withdraw.test.ts: Withdraw and redeem flows
    • Tranche.Accounting.test.ts: Virtual accounting state transitions
    • Tranche.Fees.test.ts: Fee calculations and protocol fee routing
    • Tranche.Inflation.test.ts: Inflation attack protection
    • Tranche.InterestCompounding.test.ts: Auto-compounding feature and reserved interest claiming
    • Tranche.CompoundingPrecision.test.ts: Ceil rounding, dust tracking, and 1-wei edge cases
    • Tranche.AccessControl.test.ts: Role-based permissions
    • Tranche.SettingsManagement.test.ts: Settings proposal pattern
    • Tranche.Upgrade.test.ts: BeaconProxy upgrade workflow, timelock, state preservation
  2. StructuredPool/ (11 test files)

    • Integrated credit line operations (drawdown, repayment, interest)
    • Settings proposals and approvals
    • Upgradeability and state preservation
    • Tranche management and waterfall logic
    • Underwriter integration
  3. Registry/ (4 test files)

    • Registry.test.ts: Pool registration and lookup
    • Registry.ControllerUpdate.test.ts: Controller update flows
    • Registry.AccessControl.test.ts: Factory approval
    • Registry.Upgrade.test.ts: UUPS upgrade workflow, timelock, separate admin roles
  4. Reserve/ (2 test files)

    • Reserve.test.ts: Fund custody operations
    • Reserve.AccessControl.test.ts: Access control boundaries
  5. WithdrawalRegistry/ (2 test files)

    • WithdrawalRegistry.test.ts: Request/approval/consumption flows
    • WithdrawalRegistry.AccessControl.test.ts: Role enforcement
  6. Factory/ (3 test files)

    • PoolFactory.test.ts: Deployment flows
    • PoolFactory.AccessControl.test.ts: Factory permissions
    • Factory.Security.test.ts: Deployment security
  7. Libraries/ (3 test files)

    • MathUtils.test.ts: Interest calculations validation
    • ValidationLogic.test.ts: Settings validation
    • Roles.test.ts: Role constant verification

Mathematical Validation

  • Audit scripts: scripts/v2.1-math-audit.js
  • Results: scripts/v2.1-math-audit-results.csv
  • Scenarios: 68+ distinct scenarios validating interest calculations
  • Formula verification: Cross-checked against reference implementations

Critical Test Scenarios

  1. ERC4626 Compliance: Full OpenZeppelin test suite coverage
  2. Liquidity Shortfall: Proportional exit calculations
  3. Interest Accrual: Per-second compounding accuracy over various time periods
  4. Precision Handling: Ceil rounding protection for non-compounding LPs
    • 1-wei interest payments with 50/50 split (non-compounding LP receives 1 wei, not 0)
    • 1-wei interest payments with 0.01% share (non-compounding LP receives 1 wei per payment)
    • Dust accumulation and redistribution to compounding LPs when threshold reached
    • No loss for non-compounding LPs on tiny payments across 100+ iterations
  5. Reentrancy: Attempted attacks on all state-mutating functions
  6. Access Control: Unauthorized access attempts on all restricted functions
  7. Edge Cases: Zero amounts, first deposit, last withdrawal, same-block operations

Upgradeability and Deployment Topology

Upgradeability Patterns

The protocol uses two complementary upgradeability patterns:

UUPS Proxy (Infrastructure - Single Instances):

  • Registry, WithdrawalRegistry, UnderwriterRegistry
  • Individual upgrade control per contract
  • 2-day timelock with separate admin roles

BeaconProxy (Pool Components - Mass Upgrade):

  • StructuredPool, Tranche, StructuredPool
  • All instances share implementation via beacon
  • Upgrade beacon once → all instances upgraded
  • ~200k gas per pool deployment (~94% savings vs direct)

Contract Upgradeability Summary

ContractPatternUpgradeabilityTimelock
RegistryUUPS✅ Individual✅ 2 days
WithdrawalRegistryUUPS✅ Individual✅ 2 days
UnderwriterRegistryUUPS✅ Individual✅ 2 days
StructuredPoolBeaconProxy✅ Mass✅ 2 days
TrancheBeaconProxy✅ Mass✅ 2 days
ReserveDirect❌ ImmutableN/A
ProtocolConfigurationDirect❌ ImmutableN/A
PoolFactoryDirect❌ ImmutableN/A

Upgrade Workflow

BeaconProxy (Mass Upgrade):

  1. Deploy new implementation
  2. beacon.scheduleUpgrade(newImpl) (PROTOCOL_ADMIN)
  3. Wait 2 days (timelock)
  4. beacon.executeUpgrade(newImpl) (PROTOCOL_ADMIN)
  5. All instances immediately use new implementation

UUPS (Individual Upgrade):

  1. Deploy new implementation
  2. proxy.scheduleUpgrade(newImpl) (PROTOCOL_ADMIN)
  3. Wait 2 days (timelock)
  4. proxy.upgradeToAndCall(newImpl, '0x') (PROTOCOL_ADMIN)

Access Control for Upgrades

PROTOCOL_ADMIN:

  • Schedule/execute/cancel upgrades
  • Self-administering (can rotate role)
  • Recommended: 5/9 multi-sig

TIMELOCK_ADMIN:

  • Adjust upgrade delay (1 hour to 30 days)
  • Cannot schedule or execute upgrades
  • Self-administering (independent from PROTOCOL_ADMIN)
  • Recommended: 3/5 multi-sig with DIFFERENT key holders

Security Model: Requires compromise of BOTH multi-sigs to bypass timelock.

Immutability Guarantees

Non-Upgradeable:

  • Reserve (immutable custody, deployed per Tranche)
  • ProtocolConfiguration (direct deployment)
  • PoolFactory (direct deployment)

Immutable Bindings:

  • Pool-Controller pairs (Registry)
  • Tranche-Reserve pairs (constructor binding)
  • Guardian addresses (Reserve)
  • Beacon addresses (PoolFactory)

Storage Layout Safety

All upgradeable contracts follow OpenZeppelin storage layout rules:

  • Existing variables maintain order and types
  • New variables added at end only
  • Storage gaps (__gap) reduced when adding variables
  • Each upgradeable contract includes uint256[47-50] private __gap;

Upgrade Validation Script:

bash
npx hardhat run scripts/v2.1/validateUpgrade.ts --network <network>

Performs 8 checks: storage layout, upgrade safety, interface compatibility, access control, initialization, versioning, gas analysis, state validation.

Deployment Topology

Infrastructure (deployed once):

Registry (UUPS)
WithdrawalRegistry (UUPS)
UnderwriterRegistry (UUPS)
ProtocolConfiguration (Direct)
PoolFactory (Direct)
  ├─> poolBeacon (UpgradeableBeaconWithTimelock)
  └─> trancheBeacon (UpgradeableBeaconWithTimelock)

Per Pool (deployed via factory):

PoolFactory.deployPool()
  ├─> StructuredPool (BeaconProxy → poolBeacon)
  ├─> Tranche (BeaconProxy → trancheBeacon)
  │     └─> Reserve (Direct, immutable)
  └─> Registry.registerPool(pool, asset, poolName)

Gas Costs (Celo Mainnet, 1 Gwei)

OperationGasCost (USD)Notes
Infrastructure setup~15M~$0.15One-time
Pool deployment~1.6M~$0.016Per pool
Schedule upgrade~50k~$0.0005Per upgrade
Execute UUPS upgrade~100k~$0.001Single instance
Execute Beacon upgrade~80k~$0.0008All instances

Mass Upgrade Efficiency: Upgrading 100 pools via beacon costs ~$0.0008 vs ~$0.10 individually (99.2% savings).


Glossary

Core Terms

  • virtualBalance: Assets available for withdrawal/deployment, held in Reserve for the tranche
  • virtualDeployed: Assets currently deployed to the pool's borrower (tracked at tranche level)
  • pendingInterest: Interest accrued by StructuredPool but not yet transferred to reserves
  • availableCredit: Remaining credit within draw limit for the borrower (drawLimit - outstandingDebt)
  • availableCash: min(virtualBalance, reserve.balance()) - determines maximum withdrawal amount
  • totalAssets: ERC4626 vault total: virtualBalance + virtualDeployed + pendingInterest

Accounting Terms

  • outstandingDebt: Current principal owed by borrower (includes compounded interest converted to principal)
  • drawLimit: Maximum total principal borrower can have outstanding
  • utilizationRate: (outstandingDebt / poolTotalAssets) * 10000

Operational Terms

  • WithdrawalRequest: Registry struct {shares, timestamp, approved} for tracking user withdrawal approvals
  • requireApprovalForWithdrawals: Tranche setting enabling/disabling approval-based withdrawal flow
  • SettingsProposal: Pattern for 2-step approval parameter updates (underwriter consensus required)
  • Emergency Withdrawal: Guardian-controlled recovery mechanism with 3-day timelock
  • Auto-Interest Compounding: Optional feature allowing LPs to have interest automatically increase share price (enabled by default) or reserved for manual claiming

Precision Terms

  • RAY: 10^27, precision unit for interest rate calculations
  • BASIS_POINTS: 10000, standard percentage denominator (1 bps = 0.01%)
  • decimalsOffset: Virtual asset offset (7) for inflation attack protection
  • reservedInterestForNonCompounding: Assets reserved for LPs who have disabled auto-compounding, must be manually claimed
  • totalSharesWithDisabledCompounding: Total shares held by LPs with auto-compounding disabled
  • accumulatedRoundingDust: Tracked difference between exact and Ceil-rounded reserved interest, redistributed to compounding LPs when threshold (1 unit) is reached
  • dustThreshold: Amount of accumulated rounding dust (1 unit of underlying asset) required before redistribution to compounding LPs

Files of Interest

Core Contracts (v2.1)

Core Pool & Tranche Contracts (contracts/v2.1/core/):

  • StructuredPool.sol (692 lines) - Pool coordinator
  • Tranche.sol (889 lines) - ERC4626 vault
  • Reserve.sol (278 lines) - Immutable custody
  • StructuredPool.sol (516 lines) - Credit line manager

Infrastructure Contracts (contracts/v2.1/infrastructure/):

  • Registry.sol (513 lines) - Central registry (UUPS upgradeable)
  • UpgradeableBeaconWithTimelock.sol (210 lines) - Beacon with timelock for mass upgrades
  • WithdrawalRegistry.sol (326 lines) - Withdrawal management
  • UnderwriterRegistry.sol (239 lines) - Underwriter registry
  • ProtocolConfiguration.sol (147 lines) - Global configuration

Factory Contracts (contracts/v2.1/factory/):

  • PoolFactory.sol (197 lines) - Pool deployment factory

Abstract Base Contracts

  • contracts/v2.1/base/AccessControlHelpers.sol (106 lines) - Governance self-bricking prevention
  • contracts/v2.1/base/AccessControlHelpersUpgradeable.sol (109 lines) - Upgradeable version with governance protection
  • contracts/v2.1/base/UpgradeTimelock.sol (150 lines) - UUPS upgrade timelock functionality

Libraries

Helper Libraries (contracts/v2.1/libraries/helpers/):

  • Errors.sol (88 lines) - Custom error definitions

Math Libraries (contracts/v2.1/libraries/math/):

  • MathUtils.sol (114 lines) - Interest calculations and RAY precision

Logic Libraries (contracts/v2.1/libraries/logic/):

  • ValidationLogic.sol (155 lines) - Settings validation
  • SettingsProposal.sol (101 lines) - Settings proposal pattern

Access Control Libraries (contracts/v2.1/libraries/access/):

  • Roles.sol (48 lines) - Role definitions

Type Libraries (contracts/v2.1/libraries/types/):

  • DataTypes.sol (48 lines) - Data structures

Interfaces

Core Interfaces:

  • contracts/v2.1/interfaces/IStructuredPool.sol (307 lines)
  • contracts/v2.1/interfaces/ITranche.sol (68 lines) - Composite interface
  • contracts/v2.1/interfaces/IReserve.sol (142 lines)
  • contracts/v2.1/interfaces/IWithdrawalRegistry.sol (145 lines)
  • contracts/v2.1/interfaces/IProtocolConfiguration.sol
  • contracts/v2.1/interfaces/IUnderwriterRegistry.sol

Tranche Interface (contracts/v2.1/interfaces/):

  • ITranche.sol (~210 lines) - Consolidated interface extending IERC4626 with all Tranche-specific functions:
    • State access and view functions
    • Accounting operations (recordInterest, recordPrincipal, recordDeployment)
    • Auto-interest compounding feature
    • Two-step withdrawal approval workflow
    • Settings management
    • All Tranche events centralized in this interface

Deprecated Contracts (v2-deprecated)

Note: The contracts/v2-deprecated/ directory contains legacy contracts from earlier protocol versions. These are maintained for reference and backwards compatibility but are not recommended for new deployments.

Deprecated Core Contracts:

  • CreditLineController.sol - Legacy credit line controller
  • CreditLineLIController.sol - Legacy liquidity index controller
  • VaultV2.sol - Legacy ERC4626 vault
  • VaultV2LI.sol - Legacy liquidity index vault

Deprecated Libraries (contracts/v2-deprecated/libraries/):

  • helpers/Errors.sol (24 lines) - Minimal error set for deprecated contracts
  • logic/ValidationLogic.sol (36 lines) - Legacy validation
  • logic/InterestIndexLogic.sol (51 lines) - Legacy liquidity index
  • logic/RateLogic.sol (28 lines) - Legacy rate calculations
  • math/MathUtils.sol - Legacy math utilities
  • types/DataTypes.sol (32 lines) - Legacy data structures

Deprecated Interfaces (contracts/v2-deprecated/interfaces/):

  • ICreditController.sol
  • ICreditControllerLI.sol
  • IVaultV2.sol
  • IVaultV2LI.sol

Audit Checklist

Access Control (Priority: Critical)

  • [ ] Validate Reserve fund movement permissions (receiveFunds, withdrawTo, deployTo
  • [ ] Review WithdrawalRegistry approval flows (create, approve, consume)
  • [ ] Confirm role assignments across all contracts match documentation
  • [ ] Verify factory approval system prevents unauthorized deployments
  • [ ] Check guardian cannot change post-deployment in Reserve
  • [ ] Ensure underwriter validation works correctly in StructuredPool
  • [ ] Verify AccessControlHelpers prevents governance self-bricking (cannot renounce sole protocol admin)
  • [ ] Validate _protocolAdminCount tracking is correct across grantRole, revokeRole, and renounceRole
  • [ ] Test role rotation scenarios (add new admin, remove old admin) for all core contracts
  • [ ] Confirm _grantProtocolAdminWithSelfAdmin sets protocol admin as its own admin

Arithmetic Safety (Priority: Critical)

  • [ ] Verify totalAssets calculation: virtualBalance + virtualDeployed + pendingInterest
  • [ ] Check all rounding directions favor protocol/LP (not user)
  • [ ] Validate RAY precision conversions in interest calculations
  • [ ] Confirm no overflow/underflow in virtual accounting state transitions
  • [ ] Review proportional withdrawal math during liquidity shortfalls
  • [ ] Verify first deposit minting and inflation attack protection

Withdrawal Logic (Priority: High)

  • [ ] Confirm proportional exit math: maxWithdraw = ownerAssets * availableCash / totalAssets
  • [ ] Verify approval consumption is atomic with share burning
  • [ ] Check maxWithdraw/maxRedeem bounds respect both liquidity and approvals
  • [ ] Ensure direct withdrawal path cannot bypass approval requirements when enabled
  • [ ] Validate pool-managed processing doesn't affect user shares incorrectly

Interest Accrual (Priority: High)

  • [ ] Validate per-second compounding implementation matches specification
  • [ ] Confirm interest accrual on drawdown/repayment executes correctly
  • [ ] Check getCurrentInterestDebt() try/catch error handling
  • [ ] Verify interest vs principal split in repayment logic

Auto-Compounding & Precision (Priority: High)

  • [ ] Verify Floor rounding is used when reserving interest for non-compounding LPs (Math.Rounding.Floor)
  • [ ] Confirm no phantom reserves from interest distribution rounding
  • [ ] Test edge case: 1-wei interest payments with various share distributions (50/50, 0.01%, etc.)
  • [ ] Verify totalSharesWithDisabledCompounding tracking is accurate across deposits/withdrawals/transfers
  • [ ] Check that share transfers correctly update reserved interest accounting
  • [ ] Confirm claimReservedInterest() burns shares proportional to claimed amount
  • [ ] Validate no double-counting of interest between reserved and compounding pools
  • [ ] Test scenarios with all LPs disabling compounding (dust should not accumulate)
  • [ ] Test scenarios with all LPs enabling compounding (no reservation, no dust)

Emergency Procedures (Priority: High)

  • [ ] Verify timelock invariants on guardian emergency withdrawal (3 days)
  • [ ] Confirm pause states disable critical operations correctly
  • [ ] Check emergency withdrawal cannot bypass tranche holder interests without timelock
  • [ ] Ensure guardian-initiated actions are properly logged
  • [ ] Verify asymmetric shutdown behavior - repayments allowed, new drawdowns blocked
  • [ ] Confirm handleRepayment in StructuredPool does NOT have whenNotShutdown modifier
  • [ ] Validate that withdrawals and exits remain functional during emergency shutdown
  • [ ] Test that shutdown prevents new deposits and drawdowns but allows debt settlement

Factory & Registry (Priority: Medium)

  • [ ] Review clone initialization sequence for race conditions
  • [ ] Confirm factory registration in Registry is immutable post-deployment
  • [ ] Check for front-running vulnerabilities in deployment flow
  • [ ] Verify factory approval validation in all initialization functions

Settings Management (Priority: Medium)

  • [ ] Review settings proposal expiry window implementation (default 7 days, not a timelock)
  • [ ] Verify proposed settings are validated before acceptance
  • [ ] Check proposal expiry logic prevents stale proposals
  • [ ] Ensure only authorized roles can propose/accept/reject

Reentrancy (Priority: Medium)

  • [ ] Verify all state-mutating functions use ReentrancyGuard
  • [ ] Check checks-effects-interactions pattern is followed
  • [ ] Review external calls for reentrancy vectors
  • [ ] Validate state consistency across same-block operations

Integration (Priority: Medium)

  • [ ] Verify pool-tranche-reserve interactions maintain accounting invariants
  • [ ] Check repayment distribution logic between pool and tranches
  • [ ] Validate registry lookups return correct addresses
  • [ ] Ensure withdrawal registry integration doesn't introduce race conditions

Upgradeability (Priority: Critical)

  • [ ] Verify timelock enforcement on all upgrades (Registry UUPS and all Beacons)
  • [ ] Confirm PROTOCOL_ADMIN and TIMELOCK_ADMIN roles are separate and self-administering
  • [ ] Test that single admin compromise cannot bypass timelock
  • [ ] Validate storage layout compatibility rules are enforced
  • [ ] Check that beacon upgrades affect all proxy instances correctly
  • [ ] Verify scheduleUpgrade()executeUpgrade() workflow requires timelock wait
  • [ ] Confirm cancelUpgrade() works correctly before execution
  • [ ] Test setUpgradeDelay() is only callable by TIMELOCK_ADMIN
  • [ ] Verify upgrade delay bounds (1 hour minimum, 30 days maximum)
  • [ ] Check that pending upgrades expire or can be replaced
  • [ ] Validate that implementation address in executeUpgrade() matches scheduled address
  • [ ] Test mass upgrade scenario: deploy new implementation, upgrade beacon, verify all instances
  • [ ] Confirm storage gaps are correctly sized and reduced when adding variables
  • [ ] Verify version() function returns correct version string
  • [ ] Test rollback scenario: can old implementation be redeployed and upgraded to?

Edge Cases (Priority: Low)

  • [ ] Test zero amount operations
  • [ ] Verify first/last depositor scenarios
  • [ ] Check same-block multiple operations
  • [ ] Validate behavior when reserve balance != virtualBalance temporarily

This document provides auditors with a comprehensive entry point into the Textile Protocol v2.1 architecture, flows, and mathematical specifications. For detailed implementation questions, refer to the test suites cited above and inline code comments in the contracts.

Document Version: v2.1.0 Last Updated: 2026-01-20 Protocol Version: v2.1 (Single Tranche with BeaconProxy Upgradeability)