Withdrawal Queue Mechanics
Overview
The withdrawal queue system orchestrates asset withdrawals across multiple chains and vaults through two fixed-size queues:
Local queue for same-chain vaults
Cross-chain queue for vaults on other networks
Queue Structure
// Fixed size for all queues
uint256 public constant WITHDRAWAL_QUEUE_SIZE = 30;
// Queue storage
uint256[WITHDRAWAL_QUEUE_SIZE] public localWithdrawalQueue;
uint256[WITHDRAWAL_QUEUE_SIZE] public xChainWithdrawalQueue;
Queue Management
Queue Registration
/// @notice Add a new vault to the portfolio
/// @param chainId chainId of the vault
/// @param superformId id of superform in case its crosschain
/// @param vault vault address
/// @param vaultDecimals decimals of ERC4626 token
/// @param oracle vault shares price oracle
function addVault(
uint64 chainId,
uint256 superformId,
address vault,
uint8 vaultDecimals,
uint16 deductedFees,
ISharePriceOracle oracle
)
external
onlyRoles(MANAGER_ROLE)
{
if (superformId == 0) revert();
// If its already listed revert
if (isVaultListed(vault)) revert VaultAlreadyListed();
// Save it into storage
vaults[superformId].chainId = chainId;
vaults[superformId].superformId = superformId;
vaults[superformId].vaultAddress = vault;
vaults[superformId].decimals = vaultDecimals;
vaults[superformId].oracle = oracle;
vaults[superformId].deductedFees = deductedFees;
uint192 lastSharePrice = vaults[superformId].sharePrice().toUint192();
if (lastSharePrice == 0) revert();
vaults[superformId].lastReportedSharePrice = lastSharePrice;
_vaultToSuperformId[vault] = superformId;
if (chainId == THIS_CHAIN_ID) {
// Push it to the local withdrawal queue
uint256[WITHDRAWAL_QUEUE_SIZE] memory queue = localWithdrawalQueue;
for (uint256 i = 0; i != WITHDRAWAL_QUEUE_SIZE; i++) {
if (queue[i] == 0) {
localWithdrawalQueue[i] = superformId;
break;
}
}
// If its on the same chain perfom approval to vault
asset().safeApprove(vault, type(uint256).max);
} else {
// Push it to the crosschain withdrawal queue
uint256[WITHDRAWAL_QUEUE_SIZE] memory queue = xChainWithdrawalQueue;
for (uint256 i = 0; i != WITHDRAWAL_QUEUE_SIZE; i++) {
if (queue[i] == 0) {
xChainWithdrawalQueue[i] = superformId;
break;
}
}
}
emit AddVault(chainId, vault);
}
Withdrawal Processing
State Tracking
struct ProcessRedeemRequestCache {
// Per-chain tracking
uint256[WITHDRAWAL_QUEUE_SIZE][N_CHAINS] dstVaults; // Target vaults
uint256[WITHDRAWAL_QUEUE_SIZE][N_CHAINS] sharesPerVault; // Shares to withdraw
uint256[WITHDRAWAL_QUEUE_SIZE][N_CHAINS] assetsPerVault; // Expected assets
uint256[N_CHAINS] lens; // Vaults per chain
// Global tracking
uint256 amountToWithdraw; // Remaining to withdraw
uint256 sharesFulfilled; // Processed shares
uint256 totalClaimableWithdraw; // Available for immediate withdrawal
uint256 totalAssets; // Total vault assets
uint256 totalIdle; // Unallocated assets
uint256 totalDebt; // Allocated assets
uint256 assets; // Requested withdrawal
// Operation type flags
bool isSingleChain;
bool isMultiChain;
bool isMultiVault;
}
Withdrawal Route Calculation
/// @dev Precomputes the withdrawal route following the order of the withdrawal queue
/// according to the needed assets
/// @param cache the memory pointer of the cache
/// @dev writes the route to the cache struct
///
/// Note: First it will try to fulfill the request with idle assets, after that it will
/// loop through the withdrawal queue and compute the destination chains and vaults on each
/// destionation chain, plus the shaes to redeem on each vault
function _prepareWithdrawalRoute(ProcessRedeemRequestCache memory cache) private view {
// Use the local vaults first
_exhaustWithdrawalQueue(cache, localWithdrawalQueue, false);
// Use the crosschain vaults after
_exhaustWithdrawalQueue(cache, xChainWithdrawalQueue, true);
}
Queue Processing Logic
// Withdrawal Queue Processing
function _exhaustWithdrawalQueue(
ProcessRedeemRequestCache memory cache,
uint256[WITHDRAWAL_QUEUE_SIZE] memory queue,
bool resetValues
)
private
view
{
// Cache how many chains we need and how many vaults in each chain
for (uint256 i = 0; i != WITHDRAWAL_QUEUE_SIZE; i++) {
// If we exhausted the queue stop
if (queue[i] == 0) {
if (resetValues) {
// reset values
cache.amountToWithdraw = cache.assets - cache.totalIdle;
}
break;
}
if (resetValues) {
// If its fulfilled stop
if (cache.amountToWithdraw == 0) {
break;
}
}
// Cache next vault from the withdrawal queue
VaultData memory vault = vaults[queue[i]];
// Calcualate the maxWithdraw of the vault
uint256 maxWithdraw = vault.convertToAssets(_sharesBalance(vault), true);
// Dont withdraw more than max
uint256 withdrawAssets = Math.min(maxWithdraw, cache.amountToWithdraw);
if (withdrawAssets == 0) continue;
// Cache chain index
uint256 chainIndex = chainIndexes[vault.chainId];
// Cache chain length
uint256 len = cache.lens[chainIndex];
// Push the superformId to the last index of the array
cache.dstVaults[chainIndex][len] = vault.superformId;
uint256 shares;
if (cache.amountToWithdraw >= maxWithdraw) {
uint256 balance = _sharesBalance(vault);
shares = balance;
} else {
shares = vault.convertToShares(withdrawAssets, true);
}
if (shares == 0) continue;
// Push the shares to redeeem of that vault
cache.sharesPerVault[chainIndex][len] = shares;
// Push the assetse to withdraw of that vault
cache.assetsPerVault[chainIndex][len] = withdrawAssets;
// Reduce the total debt by no more than the debt of this vault
uint256 debtReduction = Math.min(vault.totalDebt, withdrawAssets);
// Reduce totalDebt
cache.totalDebt -= debtReduction;
// Reduce needed assets
cache.amountToWithdraw -= withdrawAssets;
// Cache wether is single chain or multichain
if (vault.chainId != THIS_CHAIN_ID) {
uint256 numberOfVaults = cache.lens[chainIndex];
if (numberOfVaults != 0) {
if (!cache.isSingleChain) {
cache.isSingleChain = true;
}
if (cache.isSingleChain && !cache.isMultiChain) {
cache.isMultiChain = true;
}
if (numberOfVaults > 1) {
cache.isMultiVault = true;
}
}
}
// Increase index for iteration
unchecked {
cache.lens[chainIndex]++;
}
}
}
Asset Tracking
/// @notice Returns the total amount of the underlying assets that are settled.
function totalWithdrawableAssets() public view returns (uint256 assets) {
return totalLocalAssets() + totalXChainAssets();
}
/// @notice Returns the total amount of the underlying asset that are located on this
/// same chain and can be transferred synchronously
function totalLocalAssets() public view returns (uint256 assets) {
assets = _totalIdle;
for (uint256 i = 0; i != WITHDRAWAL_QUEUE_SIZE;) {
VaultData memory vault = vaults[localWithdrawalQueue[i]];
if (vault.vaultAddress == address(0)) break;
assets += vault.convertToAssets(_sharesBalance(vault), false);
++i;
}
return assets;
}
/// @notice Returns the total amount of the underlying asset that are located on
/// other chains and need asynchronous transfers
function totalXChainAssets() public view returns (uint256 assets) {
for (uint256 i = 0; i != WITHDRAWAL_QUEUE_SIZE;) {
VaultData memory vault = vaults[xChainWithdrawalQueue[i]];
if (vault.vaultAddress == address(0)) break;
assets += vault.convertToAssets(_sharesBalance(vault), false);
++i;
}
return assets;
}
Optimization Strategies
Gas Optimizations
Prioritizes idle assets (no withdrawal cost)
Processes local withdrawals before cross-chain
Batches cross-chain operations
Uses unchecked blocks for counters
Caches chain indexes and lengths
Slippage Protection
// In investment/divestment operations
if (shares < minSharesOut) {
revert InsufficientAssets();
}
Asset Validation
// Available assets check
if (cache.assets > cache.totalAssets - gateway.totalpendingXChainInvests()) {
revert InsufficientAvailableAssets();
}
Gas Analysis
Estimated Costs
Local withdrawals: ~100k-200k gas
Cross-chain operations: varies by chain
Batch processing savings
Queue Security Implications
Maximum queue size impact
Reordering attack vectors
Front-running mitigations
Slippage Protection Analysis
Minimum output validation
Cross-chain slippage handling
Price impact considerations
Error Types
error VaultAlreadyListed();
error InvalidSuperformId();
Last updated