> For the complete documentation index, see [llms.txt](https://knox-fi.gitbook.io/docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://knox-fi.gitbook.io/docs/advanced-topics.md).

# 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

{% stepper %}
{% step %}

### Maturity Reached

`redeemShares()` called
{% endstep %}

{% step %}

### Request Redemption

`allocator.requestRedeem(shares)`
{% endstep %}

{% step %}

### Immediate Response

Returns: `bytes32 exitHandle` + immediate assets (if any)
{% endstep %}

{% step %}

### Track Pending Exit

Accountant stores handle in `pendingExitHandles` (enumerable set)
{% endstep %}

{% step %}

### Pool State Update

Pool enters `REDEEMED` state
{% endstep %}

{% step %}

### Wait for Cooldown

Cooldown period elapses...
{% endstep %}

{% step %}

### Claim Redeemed Assets

`claimRedeemedAssets()` called (permissionless)
{% endstep %}

{% step %}

### Iterate Pending Handles

Iterates `pendingExitHandles`
{% endstep %}

{% step %}

### Claim Each Exit

For each handle:

* Check `allocator.pendingRedeem(handle)` → claimable?
* If yes: `allocator.claimRedeem(handle)` → assets
* Mark completed, remove from set
  {% endstep %}

{% step %}

### Auto-Settle

When `pendingExitCount == 0` && no market shares: Auto-call `settlePool()`
{% endstep %}

{% step %}

### Final State

Pool transitions to `SETTLED`
{% endstep %}
{% endstepper %}

### Exit Handle Format

Opaque to accountant:

`bytes32`

identifier managed by allocator

Allocator implementation (Strata example):

```solidity
struct ExitRecord {
    uint256 requestedShares;
    uint256 unlockAt; // Timestamp when claimable
    bool completed;
}

mapping(bytes32 => ExitRecord) public exitRecords;

function requestRedeem(uint256 shares) external returns (bytes32 handle, uint256 immediate) {
    // Request unstake from Strata vault
    vault.requestRedeem(shares);

    // Query cooldown timestamp
    IUnstakeCooldown.Request memory req = unstakeCooldown.activeRequests(address(this));

    // Generate unique handle
    handle = keccak256(abi.encode(msg.sender, shares, block.timestamp));

    // Store exit record
    exitRecords[handle] = ExitRecord({
        requestedShares: shares,
        unlockAt: req.unlockAt,
        completed: false
    });

    immediate = 0; // No immediate assets for Strata
}

function pendingRedeem(bytes32 handle) external view returns (bool claimable, bool completed) {
    ExitRecord memory record = exitRecords[handle];
    completed = record.completed;
    claimable = !completed && block.timestamp >= record.unlockAt;
}

function claimRedeem(bytes32 handle) external returns (uint256 assets) {
    ExitRecord storage record = exitRecords[handle];
    require(!record.completed, "Already claimed");
    require(block.timestamp >= record.unlockAt, "Cooldown not elapsed");

    // Finalize unstake
    assets = unstakeCooldown.finalize();

    // Mark completed
    record.completed = true;
}
```

### Monitoring

Check pending exits:

```solidity
uint256 count = accountant.pendingExitCount();
uint256 latestUnlock = accountant.maxPendingExitUnlockAt();

if (count > 0) {
    uint256 timeUntilReady = latestUnlock > block.timestamp
        ? latestUnlock - block.timestamp
        : 0;

    // Wait `timeUntilReady` seconds before calling claimRedeemedAssets()
}
```

UI pattern:

```javascript
if (poolState === 'REDEEMED' && pendingExitCount > 0) {
    const waitSeconds = maxPendingExitUnlockAt - Date.now() / 1000;

    if (waitSeconds > 0) {
        showMessage(`Settlement in ${formatDuration(waitSeconds)}`);
    } else {
        showButton('Claim & Settle', () => accountant.claimRedeemedAssets());
    }
}
```

### 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

```solidity
function proxyCall(address target, bytes calldata data) external {
    // 1. Check time constraint
    require(block.timestamp >= tStart + dPeriod + 28 days, "Too early");

    // 2. Check role (resolved dynamically from factory)
    require(
        factory.accessController().hasRole(RESCUE_ROLE, msg.sender),
        "Not rescue role"
    );

    // 3. Safety checks if targeting pool asset
    if (target == asset) {
        bytes4 selector = bytes4(data[:4]);
        require(
            selector == IERC20.transfer.selector ||
            selector == IERC20.transferFrom.selector,
            "Only transfer/transferFrom"
        );

        address recipient = abi.decode(data[36:68], (address));
        require(
            recipient == seniorTranche ||
            recipient == juniorTranche ||
            _isTranche[recipient] || // Any spectrum vault
            recipient == address(this),
            "Invalid recipient"
        );
    }

    // 4. Execute call
    (bool success, bytes memory result) = target.call(data);
    require(success, "Call failed");
}
```

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

```solidity
// Transfer dust from allocator to accountant
bytes memory data = abi.encodeWithSelector(
    IERC20.transfer.selector,
    address(accountant),
    1e6 // 1 USDC
);

accountant.proxyCall(address(asset), data);

// Now settlePool() can proceed
accountant.settlePool();
```

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

```solidity
// Deploy new allocator
address newAllocator = new ERC4626Allocator(newMarket);

// Transfer assets from old allocator to new allocator
// (requires custom logic, may need multiple calls)

// Eventually settle with new allocator
```

#### Scenario 3: Distribute Stuck Assets Manually

Problem: Settlement failed due to unknown bug

Solution: Manually distribute to vaults based on waterfall:

```solidity
// Calculate waterfall off-chain
// Then transfer to each vault
bytes memory seniorTransfer = abi.encodeWithSelector(
    IERC20.transfer.selector,
    seniorVault,
    525_000e6 // Calculated senior payout
);

accountant.proxyCall(address(asset), seniorTransfer);

// Repeat for spectrum and junior
// Mark pool as settled (requires state manipulation or custom function)
```

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

```solidity
require(
    accessController.hasRole(DEPLOYER_ROLE, msg.sender),
    "Not deployer"
);
```

#### FEE\_SETTER\_ROLE

Scope:

* SpectrumFactory

Powers: Toggle `applyProtocolFees` (global on/off switch)

Function:

```solidity
function setApplyProtocolFees(bool apply) external {
    require(
        accessController.hasRole(FEE_SETTER_ROLE, msg.sender),
        "Not fee setter"
    );

    applyProtocolFees = apply;
}
```

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:

```solidity
function registerClaimTarget(address asset, address target) external {
    require(
        factory.accessController().hasRole(CLAIM_TARGET_ADMIN_ROLE, msg.sender),
        "Not claim target admin"
    );

    claimTargets[asset] = target;
}
```

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

### Role Management

#### Granting Roles

```solidity
// Admin grants deployer role
accessController.grantRole(DEPLOYER_ROLE, deployerAddress);

// Admin grants rescue role
accessController.grantRole(RESCUE_ROLE, rescueMultisig);
```

#### Revoking Roles

```solidity
// Admin revokes compromised deployer
accessController.revokeRole(DEPLOYER_ROLE, compromisedAddress);
```

#### Checking Roles

```solidity
bool isDeployer = accessController.hasRole(DEPLOYER_ROLE, address);
bool isAdmin = accessController.hasRole(DEFAULT_ADMIN_ROLE, address);
```

#### Renouncing Roles

```solidity
// An address can renounce its own role
accessController.renounceRole(DEPLOYER_ROLE, msg.sender);
```

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

```solidity
function settlePool() external {
    require(poolState == PoolState.REDEEMED);
    require(pendingExitCount == 0);
    require(marketShares == 0);

    uint256 assets = IERC20(asset).balanceOf(address(this));

    if (assets == 0) {
        // Don't settle yet, stay in REDEEMED
        return;
    }

    // Proceed with waterfall...
}
```

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

```solidity
function redeem(uint256 shares, address receiver, address owner) public {
    // ...
    uint256 balance = IERC20(asset).balanceOf(address(this));
    uint256 totalShares = totalSupply();

    if (shares == totalShares) {
        // Last redeemer gets all remaining assets
        assets = balance;
    } else {
        // Normal proportional calculation
        assets = (shares * balance) / totalShares;
    }

    // ...
}
```

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

```solidity
function depositSenior(uint256 amount, address receiver) external {
    _capturePreDepositSeniorState();

    // Recalculates capacity
    uint256 currentDeposits = seniorTranche.totalDeposits();
    uint256 capacity = seniorCapacity();

    require(currentDeposits + amount <= capacity, "Exceeds capacity");

    // ...
}
```

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

```solidity
require(block.timestamp < tStart + dPeriod, "Pool matured");
```

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

```solidity
for (uint256 i = 0; i < activeSpectrumRates.length; i++) {
    uint128 rate = activeSpectrumRates[i];

    // Calculate cap, distribute surplus
}
```

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
