Appearance
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 inmodules/)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 requestsMANAGER_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()replacesDEFAULT_ADMIN_ROLE- Pool manager is tracked via
manageraddress (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:
- Self-Admin Protocol Admin:
_grantProtocolAdminWithSelfAdmin(admin_)setsprotocolAdminas its own admin, enabling role rotation without external dependencies - Governance Self-Bricking Prevention: Tracks
_protocolAdminCountto prevent renouncing the sole protocol admin role - 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 adminSecurity 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 managementregistrarRole()- Tranche registration/deactivation (granted to factory during init)
Direct Access Control:
manageraddress - Operational functions (withdrawal approvals, strategy updates, underwriter management, settings proposals)- Uses
onlyPoolManagermodifier checkingmsg.sender == manager
Special Authorization:
- Controller validation via
registry.getController(address(this)) - Underwriter validation via
_underwriteraddress
Tranche
Roles:
protocolAdmin()- Pause, emergency functions, role management
Direct Access Control:
_poolManageraddress - Settings proposals, withdrawal approvals- Uses
onlyPoolManagermodifier checkingmsg.sender == _poolManager
StructuredPool-Only Hooks (no role, checked via msg.sender == address(_structuredPool)):
recordInterest()- Update interest earnedrecordPrincipal()- Record principal repaymentrecordDeployment()- Record capital deploymentprocessWithdrawal()- 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 depositswithdrawTo()- Process withdrawals
- Only StructuredPool can call (checked via
msg.sender == _structuredPool):deployTo()- Deploy capital to borrower
StructuredPool
Roles:
protocolAdmin()- Settings management, pause, forced interest accrualborrowerRole()- Drawdown and repayment operations
Direct Access Control:
- Pool manager can propose settings (checked via
structuredPool.manager())
WithdrawalRegistry
Roles:
protocolAdmin()- Role management, pause, admin functionsregistrarRole()- Tranche registration (granted to factories)trancheRole()- Request creation (granted to registered tranches)createWithdrawRequest()- Create withdrawal requestcreateRedeemRequest()- Create redemption requestconsumeApproval()- 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 upgradestimelockAdmin()- 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 onlysetUpgradeDelay()- 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
adminaddress foronlyProtocolAdminmodifier - 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 <= maximumTVLreceiver != 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)outstandingDebtmatches sum of all tranchevirtualDeployed- 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
virtualBalanceandtotalInterestEarned - Principal returns deployed capital:
virtualDeployed→virtualBalance - 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 deploymentminimumDeposit- Minimum deposit amountmaximumTVL- Maximum TVL capwithdrawalPeriod- Withdrawal period in secondsrequireApprovalForWithdrawals- Enable/disable approval workflowtargetAllocation- 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 addressinterestRate- Annual interest rate in basis pointsdrawLimit- Maximum credit line amountinterestPaymentInterval- 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:
- Pool includes integrated credit line functionality (no separate controller)
- Pool must be registered before tranche deployment (tranche init validates factory via registry)
- Reserve is deployed in Tranche constructor (immutable binding)
- 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 += sharesOn 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 -= sharesLiquidity-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 = 31536000Rate 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^254th 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 termsInterest 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 meaningfulAuto-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:
- When interest is recorded: The global accumulator increases by
(reservedAmount * 1e18) / totalSharesWithDisabledCompounding - When LP disables compounding: Their checkpoint is set to the current accumulator value
- 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:
- Reserve proportional interest for non-compounding LPs:
reserved = (netInterest * nonCompoundingShares) / totalSupply - 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:
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:
| Aspect | Non-Compounding LPs | Compounding LPs |
|---|---|---|
| Immediate Effect | ✅ Fair share (Floor rounding) | ✅ Benefit from rounding dust |
| Long-Term Effect | Fair share of NET interest | Fair share of NET interest |
| Edge Case (1 wei) | ✅ May receive 0 (stays in pool) | ✅ Benefit from rounding dust |
| Net Result | FAIR - No favoritism | FAIR - Benefit from pool appreciation |
Example Scenarios:
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
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 redistributionView 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
recordInterestgas 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
Reserve Custody:
- Only Tranche can call
Reserve.receiveFunds()andReserve.withdrawTo() - Only StructuredPool can call
Reserve.deployTo() - Reserve never initiates token pulls except from explicit
safeTransferFromby authorized contracts
- Only Tranche can call
Capital Accounting:
StructuredPool.outstandingDebt = sum(tranche.virtualDeployed)for all tranchesReserve.balance() >= Tranche.virtualBalance(equality in steady state, inequality during same-block operations)StructuredPool.deployCapital()must not exceed selectedReserve.balance()
Credit Line Invariant:
outstandingDebt + availableCredit = drawLimitafter any operation that changes principal
Registry Integrity:
- Active tranche must have registered Reserve
Tranche Lifecycle:
- Tranche deactivation requires:
virtualBalance == 0 AND virtualDeployed == 0 AND pool.outstandingDebt == 0
- Tranche deactivation requires:
Withdrawal Bounds:
maxWithdraw(owner) <= min(previewRedeem(balanceOf(owner)), availableCash)maxRedeem(owner) <= balanceOf(owner)- When
requireApprovalForWithdrawals = true:maxRedeemalso bounded by approved shares
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
Pause States:
- Pausable contracts disable: deposits, mints, redeems where applicable
- Withdrawal bounds always respect
availableCashregardless of pause state - Emergency functions remain accessible during pause
Access Control Boundaries
- No unauthorized fund movement from Reserve
- No direct token transfers except through defined flows
- No ETH acceptance (contracts don't implement
receive()orfallback()) - Factory approval required for pool/tranche initialization
- Role-based operations strictly enforced with custom errors
Failure Modes and Mitigations
Liquidity Shortfall
Scenario: More withdrawal requests than available cash
Mitigation:
maxWithdraw/maxRedeemenforce proportional exits based onavailableCash- 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
totalAssetscalculation 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() = 7adds 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 ERC4626Withdraw(sender, receiver, owner, assets, shares)- Standard ERC4626InterestAccrued(uint256 amount)- Interest recordedVirtualBalanceUpdated(uint256 newBalance)- Virtual balance changedDepositFeesCollected(protocolFee, underwriterFee)- Fees collected on depositInterestFeesCollected(totalInterest, protocolFee, underwriterFee, netInterest)- Fees on interestInterestReservedForNonCompounding(amount, totalSharesWithDisabledCompounding)- Interest reserved for non-compounding LPsRoundingDustRedistributed(uint256 amount)- Accumulated rounding dust redistributed to compounding LPsDepositEnhanced(sender, owner, assets, shares, netAmount, protocolFee, underwriterFee)- Enhanced deposit event with fee breakdown
Compounding Events:
AutoInterestCompoundingSet(address indexed user, bool enabled)- LP toggled compoundingReservedInterestClaimed(address indexed user, uint256 amount)- LP claimed reserved interest
Withdrawal Events:
WithdrawRequested(user, amount)- LP requests withdrawalRedeemRequested(user, shares)- LP requests redemption
Settings Events:
TrancheSettingsUpdated(settings)- Settings applied
StructuredPool (IStructuredPool.sol):
PoolInitialized(asset, poolName, strategy)- Pool createdTrancheRegistered(tranche, seniority, targetAllocation)- Tranche addedCapitalDeployed(borrower, amount)- Capital deployed to borrowerRepaymentProcessed(totalAmount)- Repayment receivedInterestDistributed(tranche, amount)- Interest routed to tranchePrincipalReturned(tranche, amount)- Principal returned to trancheUnderwriterProposed(pendingUnderwriter)- Underwriter proposedUnderwriterAccepted(underwriter, depositFeeBps, interestFeeBps)- Underwriter accepted
StructuredPool (IStructuredPool.sol):
Drawdown(borrower, amount)- Capital drawnRepaid(payer, totalAmount, grossInterest, netInterest, principal, protocolFee, underwriterFee)- Repayment madeInterestCalculated(interestAmount, totalAccruedInterest)- Interest calculatedCreditLineSettingsProposed(settings, nonce)- Settings proposedCreditLineSettingsUpdated(settings, nonce)- Settings updated
WithdrawalRegistry (IWithdrawalRegistry.sol):
WithdrawalRequested(tranche, user, shares)- Request createdWithdrawalApproved(tranche, user, shares)- Request approvedWithdrawalRejected(tranche, user)- Request rejectedTrancheRegistered(factory, tranche)- Tranche registered with registry
PoolFactory:
PoolDeployed(deploymentId, pool, asset, poolName, deployer)- Pool deployedTrancheDeployed(pool, tranche, seniority, trancheName)- Tranche deployed
Testing Focus Areas
Test Suite Organization
Tests located in packages/protocol/test/v2.1/:
Tranche/ (10 test files)
Tranche.test.ts: Basic initialization and configurationTranche.Deposit.test.ts: Deposit and mint flowsTranche.Withdraw.test.ts: Withdraw and redeem flowsTranche.Accounting.test.ts: Virtual accounting state transitionsTranche.Fees.test.ts: Fee calculations and protocol fee routingTranche.Inflation.test.ts: Inflation attack protectionTranche.InterestCompounding.test.ts: Auto-compounding feature and reserved interest claimingTranche.CompoundingPrecision.test.ts: Ceil rounding, dust tracking, and 1-wei edge casesTranche.AccessControl.test.ts: Role-based permissionsTranche.SettingsManagement.test.ts: Settings proposal patternTranche.Upgrade.test.ts: BeaconProxy upgrade workflow, timelock, state preservation
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
Registry/ (4 test files)
Registry.test.ts: Pool registration and lookupRegistry.ControllerUpdate.test.ts: Controller update flowsRegistry.AccessControl.test.ts: Factory approvalRegistry.Upgrade.test.ts: UUPS upgrade workflow, timelock, separate admin roles
Reserve/ (2 test files)
Reserve.test.ts: Fund custody operationsReserve.AccessControl.test.ts: Access control boundaries
WithdrawalRegistry/ (2 test files)
WithdrawalRegistry.test.ts: Request/approval/consumption flowsWithdrawalRegistry.AccessControl.test.ts: Role enforcement
Factory/ (3 test files)
PoolFactory.test.ts: Deployment flowsPoolFactory.AccessControl.test.ts: Factory permissionsFactory.Security.test.ts: Deployment security
Libraries/ (3 test files)
MathUtils.test.ts: Interest calculations validationValidationLogic.test.ts: Settings validationRoles.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
- ERC4626 Compliance: Full OpenZeppelin test suite coverage
- Liquidity Shortfall: Proportional exit calculations
- Interest Accrual: Per-second compounding accuracy over various time periods
- 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
- Reentrancy: Attempted attacks on all state-mutating functions
- Access Control: Unauthorized access attempts on all restricted functions
- 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
| Contract | Pattern | Upgradeability | Timelock |
|---|---|---|---|
| Registry | UUPS | ✅ Individual | ✅ 2 days |
| WithdrawalRegistry | UUPS | ✅ Individual | ✅ 2 days |
| UnderwriterRegistry | UUPS | ✅ Individual | ✅ 2 days |
| StructuredPool | BeaconProxy | ✅ Mass | ✅ 2 days |
| Tranche | BeaconProxy | ✅ Mass | ✅ 2 days |
| Reserve | Direct | ❌ Immutable | N/A |
| ProtocolConfiguration | Direct | ❌ Immutable | N/A |
| PoolFactory | Direct | ❌ Immutable | N/A |
Upgrade Workflow
BeaconProxy (Mass Upgrade):
- Deploy new implementation
beacon.scheduleUpgrade(newImpl)(PROTOCOL_ADMIN)- Wait 2 days (timelock)
beacon.executeUpgrade(newImpl)(PROTOCOL_ADMIN)- All instances immediately use new implementation
UUPS (Individual Upgrade):
- Deploy new implementation
proxy.scheduleUpgrade(newImpl)(PROTOCOL_ADMIN)- Wait 2 days (timelock)
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)
| Operation | Gas | Cost (USD) | Notes |
|---|---|---|---|
| Infrastructure setup | ~15M | ~$0.15 | One-time |
| Pool deployment | ~1.6M | ~$0.016 | Per pool |
| Schedule upgrade | ~50k | ~$0.0005 | Per upgrade |
| Execute UUPS upgrade | ~100k | ~$0.001 | Single instance |
| Execute Beacon upgrade | ~80k | ~$0.0008 | All 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 coordinatorTranche.sol(889 lines) - ERC4626 vaultReserve.sol(278 lines) - Immutable custodyStructuredPool.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 upgradesWithdrawalRegistry.sol(326 lines) - Withdrawal managementUnderwriterRegistry.sol(239 lines) - Underwriter registryProtocolConfiguration.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 preventioncontracts/v2.1/base/AccessControlHelpersUpgradeable.sol(109 lines) - Upgradeable version with governance protectioncontracts/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 validationSettingsProposal.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 interfacecontracts/v2.1/interfaces/IReserve.sol(142 lines)contracts/v2.1/interfaces/IWithdrawalRegistry.sol(145 lines)contracts/v2.1/interfaces/IProtocolConfiguration.solcontracts/v2.1/interfaces/IUnderwriterRegistry.sol
Tranche Interface (contracts/v2.1/interfaces/):
ITranche.sol(~210 lines) - Consolidated interface extendingIERC4626with 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 controllerCreditLineLIController.sol- Legacy liquidity index controllerVaultV2.sol- Legacy ERC4626 vaultVaultV2LI.sol- Legacy liquidity index vault
Deprecated Libraries (contracts/v2-deprecated/libraries/):
helpers/Errors.sol(24 lines) - Minimal error set for deprecated contractslogic/ValidationLogic.sol(36 lines) - Legacy validationlogic/InterestIndexLogic.sol(51 lines) - Legacy liquidity indexlogic/RateLogic.sol(28 lines) - Legacy rate calculationsmath/MathUtils.sol- Legacy math utilitiestypes/DataTypes.sol(32 lines) - Legacy data structures
Deprecated Interfaces (contracts/v2-deprecated/interfaces/):
ICreditController.solICreditControllerLI.solIVaultV2.solIVaultV2LI.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
AccessControlHelpersprevents governance self-bricking (cannot renounce sole protocol admin) - [ ] Validate
_protocolAdminCounttracking is correct acrossgrantRole,revokeRole, andrenounceRole - [ ] Test role rotation scenarios (add new admin, remove old admin) for all core contracts
- [ ] Confirm
_grantProtocolAdminWithSelfAdminsets protocol admin as its own admin
Arithmetic Safety (Priority: Critical)
- [ ] Verify
totalAssetscalculation: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/maxRedeembounds 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
totalSharesWithDisabledCompoundingtracking 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
handleRepaymentinStructuredPooldoes NOT havewhenNotShutdownmodifier - [ ] 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)