For the complete documentation index, see llms.txt. This page is also available as Markdown.

Advanced Topics

Deep dives into asynchronous redemptions, the rescue mechanism, access control, and edge cases.

Asynchronous Redemptions

Why Async?

Some yield markets (e.g., Strata Finance) require a cooldown period before withdrawals:

  • Strata: 7-day unstake period for USDe, NUSD

  • Future markets: May have similar withdrawal queues

Knox handles these natively without blocking settlement.

Async Flow

1

Maturity Reached

redeemShares() called

2

Request Redemption

allocator.requestRedeem(shares)

3

Immediate Response

Returns: bytes32 exitHandle + immediate assets (if any)

4

Track Pending Exit

Accountant stores handle in pendingExitHandles (enumerable set)

5

Pool State Update

Pool enters REDEEMED state

6

Wait for Cooldown

Cooldown period elapses...

7

Claim Redeemed Assets

claimRedeemedAssets() called (permissionless)

8

Iterate Pending Handles

Iterates pendingExitHandles

9

Claim Each Exit

For each handle:

  • Check allocator.pendingRedeem(handle) → claimable?

  • If yes: allocator.claimRedeem(handle) → assets

  • Mark completed, remove from set

10

Auto-Settle

When pendingExitCount == 0 && no market shares: Auto-call settlePool()

11

Final State

Pool transitions to SETTLED

Exit Handle Format

Opaque to accountant:

bytes32

identifier managed by allocator

Allocator implementation (Strata example):

Monitoring

Check pending exits:

UI pattern:

Permissionless Claims

Anyone can call claimRedeemedAssets() once cooldowns expire:

  • Keepers can automate this

  • Users can trigger for their own pool

  • No incentive (yet) — purely altruistic or self-serving

Rescue Mechanism

Purpose

Recover stuck assets from edge cases:

  • Allocator bugs

  • Market pauses

  • Unexpected state transitions

  • Upgrade/migration needs

Availability

When:

tStart + dPeriod + 28 days

after maturity

Why 28 days: Grace period for normal settlement + async claims

Function Signature

Safety Rails

Dynamic Role Resolution

Role is checked against factory's AccessController at call time, not deployment:

factory.accessController().hasRole(RESCUE_ROLE, msg.sender)

Benefit: Governance can rotate rescue responders without redeploying pools

Asset Transfer Restrictions

When target == asset (the pool's underlying token):

  • Allowed selectors: Only transfer(address,uint256) and transferFrom(address,address,uint256)

  • Allowed recipients: Senior vault, junior vault, any spectrum vault, or accountant itself

  • Blocked: Arbitrary addresses (prevents draining to attacker)

Unrestricted for Other Targets

Calls to allocators, markets, or other contracts are unrestricted:

Use case: Rescue from allocator-level bugs

Example: Call allocator.emergencyWithdraw() if available

Example Scenarios

Scenario 1: Allocator Holds Dust

Problem: Allocator has 1000 USDC but accounting shows 999 USDC (rounding error)

Solution:

Scenario 2: Market Paused

Problem: Underlying market is paused, blocking redemptions

Solution: Wait for market to unpause, or use rescue to migrate to new allocator:

Scenario 3: Distribute Stuck Assets Manually

Problem: Settlement failed due to unknown bug

Solution: Manually distribute to vaults based on waterfall:

Access Control

Roles

DEFAULT_ADMIN_ROLE

Scope:

  • AccessController

Powers:

  • Grant any role to any address

  • Revoke any role from any address

  • Renounce own admin role

Typical Holder: Multisig or governance contract

DEPLOYER_ROLE

Scope:

  • SpectrumFactory

Powers: Call factory.deploy() to create new pools

Typical Holder:

  • Deployment bot

  • Frontend contract

  • Governance contract

Check:

FEE_SETTER_ROLE

Scope:

  • SpectrumFactory

Powers: Toggle applyProtocolFees (global on/off switch)

Function:

Use Case:

  • Launch period: disable fees

  • Post-launch: enable fees

  • Emergency: disable fees

RESCUE_ROLE

Scope:

  • SpectrumAccountant

  • (via factory's AccessController)

Powers: Execute accountant.proxyCall() after maturity + 28 days

Dynamic Resolution: Role checked at call time, not deployment

Typical Holder:

  • Multisig

  • Emergency response team

  • Governance contract

CLAIM_TARGET_ADMIN_ROLE

Scope:

  • ERC4626AsyncStrataMainnetAllocator

Powers: Register new claim targets for new asset types

Function:

Use Case: Strata launches new vault for a new asset (e.g., sDAI)

Role Management

Granting Roles

Revoking Roles

Checking Roles

Renouncing Roles

Security Considerations

  • Admin is powerful: Can grant/revoke all roles → should be multisig

  • Deployer is semi-trusted: Can create pools with arbitrary parameters

  • Fee setter: Can disable revenue → should be governance

  • Rescue role: Can move assets post-maturity → should be multisig with timelock

  • Dynamic resolution: Allows role rotation without redeployment

Edge Cases

Edge Case 1: Zero Assets at Settlement

Scenario:

settlePool() called but accountant holds 0 assets

Behavior:

Resolution:

  • Wait for claimRedeemedAssets() to bring assets in

  • Or use rescue mechanism to transfer assets to accountant

Edge Case 2: Rounding Dust

Scenario: Waterfall leaves 1 wei in accountant after distribution

Behavior: Last redeemer of each tranche gets entire remaining balance

Effect: No dust left behind, all assets distributed

Edge Case 3: Senior Capacity Exceeded Mid-Deposit

Scenario:

  • Senior capacity = 1000 USDC

  • User A deposits 900 USDC (succeeds)

  • User B tries to deposit 200 USDC (should fail with 100 USDC remaining)

Behavior: Transaction reverts

Frontend: Should check capacity before prompting deposit

Edge Case 4: Spectrum Vault Exists But Has 0 Deposits

Scenario:

  • User A deposits into 7% spectrum (creates vault)

  • User A withdraws all shares before maturity (transfers to another address)

  • Settlement runs with 0 deposits in 7% vault

Behavior:

  • Vault exists in spectrumVaults[700]

  • Vault appears in activeSpectrumRates array

  • Waterfall skips it (0 value)

  • No issue

Edge Case 5: Multiple Async Exits, One Fails

Scenario:

  • Pool has 5 pending exit handles

  • 4 succeed, 1 fails (market bug)

  • pendingExitCount stuck at 1

Behavior: Pool stays in REDEEMED state

Resolution:

  • Wait for market fix

  • Or use rescue mechanism to manually claim

  • Or admin intervention to mark exit as completed

Edge Case 6: Deposit Exactly at Maturity

Scenario:

block.timestamp == tStart + dPeriod

Behavior: Deposit reverts

Effect: No deposits at exactly maturity timestamp

Edge Case 7: Grid with 1 Spectrum Tranche

Scenario:

  • rSenior = 5%

  • rMaxSpectrum = 6%

  • gridStep = 1%

Grid: Only 6%

Behavior: Valid, works as expected

Collateral factor: First spectrum = only spectrum = junior CF (same interpolation)

Performance Considerations

Gas Costs

Deployment

  • Factory deploy(): ~500k gas (3 clones + initialization)

  • Lazy spectrum vault: ~200k gas (clone + initialize + sorted insert)

Deposits

  • Router deposit: ~150k gas (transfer + approve + accountant call)

  • Direct accountant: ~120k gas

  • Direct vault: ~100k gas

Settlement

  • settlePool(): ~50k + (N × 10k) gas, where N = number of active spectrum tranches

  • Senior cap: 10k gas

  • Each spectrum cap: 10k gas

  • Junior residual: 5k gas

  • Protocol fee: 5k gas

Withdrawals

  • vault.redeem(): ~50k gas (transfer + burn)

Waterfall Iteration

Spectrum tranches are iterated in sorted order (O(n)):

Bound: Grid size is capped by (rMaxSpectrum - rSenior) / gridStep

Typical: 10-30 tranches

Max practical: ~50 tranches (still < 1M gas)

State Growth

  • Each spectrum vault: 1 storage slot in spectrumVaults mapping

  • Each active rate: 1 element in activeSpectrumRates array

  • Each async exit: 1 element in pendingExitHandles set

No unbounded growth: Grid size is fixed, async exits are bounded by market shares

Key Takeaways

  • Async redemptions are natively supported with handle tracking and permissionless claims

  • Rescue mechanism provides safety net with strict access control and safety rails

  • Roles are managed centrally via AccessController, dynamically resolved at call time

  • Edge cases are handled gracefully (rounding, zero assets, failed exits)

  • Gas costs scale linearly with spectrum tranche count, remain practical even for large grids

  • Security relies on multisig admin, time delays, and restricted rescue operations