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

  1. Prioritizes idle assets (no withdrawal cost)

  2. Processes local withdrawals before cross-chain

  3. Batches cross-chain operations

  4. Uses unchecked blocks for counters

  5. 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

  1. Estimated Costs

    • Local withdrawals: ~100k-200k gas

    • Cross-chain operations: varies by chain

    • Batch processing savings

  2. Queue Security Implications

    • Maximum queue size impact

    • Reordering attack vectors

    • Front-running mitigations

  3. Slippage Protection Analysis

    • Minimum output validation

    • Cross-chain slippage handling

    • Price impact considerations

Error Types

    error VaultAlreadyListed();
    error InvalidSuperformId();

Last updated