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;
}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()
}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());
}
}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");
}// 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();// 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// 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)require(
accessController.hasRole(DEPLOYER_ROLE, msg.sender),
"Not deployer"
);function setApplyProtocolFees(bool apply) external {
require(
accessController.hasRole(FEE_SETTER_ROLE, msg.sender),
"Not fee setter"
);
applyProtocolFees = apply;
}function registerClaimTarget(address asset, address target) external {
require(
factory.accessController().hasRole(CLAIM_TARGET_ADMIN_ROLE, msg.sender),
"Not claim target admin"
);
claimTargets[asset] = target;
}// Admin grants deployer role
accessController.grantRole(DEPLOYER_ROLE, deployerAddress);
// Admin grants rescue role
accessController.grantRole(RESCUE_ROLE, rescueMultisig);// Admin revokes compromised deployer
accessController.revokeRole(DEPLOYER_ROLE, compromisedAddress);bool isDeployer = accessController.hasRole(DEPLOYER_ROLE, address);
bool isAdmin = accessController.hasRole(DEFAULT_ADMIN_ROLE, address);// An address can renounce its own role
accessController.renounceRole(DEPLOYER_ROLE, msg.sender);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...
}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;
}
// ...
}function depositSenior(uint256 amount, address receiver) external {
_capturePreDepositSeniorState();
// Recalculates capacity
uint256 currentDeposits = seniorTranche.totalDeposits();
uint256 capacity = seniorCapacity();
require(currentDeposits + amount <= capacity, "Exceeds capacity");
// ...
}require(block.timestamp < tStart + dPeriod, "Pool matured");for (uint256 i = 0; i < activeSpectrumRates.length; i++) {
uint128 rate = activeSpectrumRates[i];
// Calculate cap, distribute surplus
}