Core Operations

Error Types

error InsufficientAssets();
error VaultNotListed();
error RequestNotFound();
error InvalidController();
error MinimumBalanceNotMet();
error InvalidSuperformId();
error InvalidRecoveryAddress();
error InvalidAmount();
error TotalAmountMismatch();
error SharesLocked();
error VaultShutdown();
error Unauthorized();
error ExceedsMaxDeposit();
error ExceedsMaxMint();

Events

event Invest(uint256 amount);
event Divest(uint256 amount);
event SettleXChainInvest(uint256 indexed superformId, uint256 assets);
event SettleXChainDivest(uint256 assets);
event ProcessRedeemRequest(address indexed controller, uint256 shares);
event FulfillRedeemRequest(address indexed controller, uint256 shares, uint256 assets);
event RequestSettled(bytes32 indexed key, address indexed controller, uint256 settledAmount);

Constants

uint256 public constant WITHDRAWAL_QUEUE_SIZE = 30;
uint256 public constant SECS_PER_YEAR = 31_556_952;
uint256 public constant MAX_BPS = 10_000;

Deposit Operations

Atomic Deposit Flow

// Step 1: Request Deposit
requestId = vault.requestDeposit(
    1000e6,     // Amount
    msg.sender, // Controller
    msg.sender  // Owner
);

// Step 2: Execute Deposit (same transaction)
shares = vault.deposit(
    1000e6,     // Amount
    recipient,  // Share recipient
    msg.sender  // Controller
);
// Deposit validation
function _deposit(
    uint256 assets,
    uint256 shares,
    address receiver,
    address controller
)
    internal
    override
    returns (uint256 assetsReturn, uint256 sharesReturn)
{
    _updatePosition(controller, shares);
    if (lastRedeem[controller] == 0) lastRedeem[controller] = block.timestamp;
    return super._deposit(assets, shares, receiver, controller);
}

function _afterDeposit(uint256 assets, uint256 /*uint shares*/ ) internal override {
    uint128 assetsUint128 = assets.toUint128();
    _totalIdle += assetsUint128;
}

Key Features:

  • Atomic execution via multicall

  • Immediate request fulfillment

  • Direct share minting

  • No asynchronous operations

Security Considerations:

  • Emergency shutdown check

  • Share lock enforcement

  • Balance verification

Alternative Deposit Methods

// Direct minting flow
function mint(
    uint256 shares,
    address to,
    address controller
) public override noEmergencyShutdown returns (uint256 assets) {
    uint256 sharesBalance = balanceOf(to);
    assets = super.mint(shares, to, controller);
    _lockShares(to, sharesBalance, shares);
    _afterDeposit(assets, shares);
}

// Share locking mechanism
function _lockShares(
    address to, 
    uint256 sharesBalance, 
    uint256 newShares
) private {
    uint256 newBalance = sharesBalance + newShares;
    if (sharesBalance == 0) {
        _depositLockCheckPoint[to] = block.timestamp;
    } else {
        _depositLockCheckPoint[to] = 
            ((_depositLockCheckPoint[to] * sharesBalance) / newBalance) +
            ((block.timestamp * newShares) / newBalance);
    }
}

Investment Operations

Direct Investment (Same Chain)

function investSingleDirectSingleVault(
    address vaultAddress,
    uint256 assets,
    uint256 minSharesOut
)
    public
    onlyRoles(MANAGER_ROLE)
    returns (uint256 shares)
{
    // Ensure the target vault is in the approved list
    if (!isVaultListed(vaultAddress)) revert VaultNotListed();

    // Record the balance before deposit to calculate received shares
    uint256 balanceBefore = vaultAddress.balanceOf(address(this));

    // Deposit assets into the target vault
    ERC4626(vaultAddress).deposit(assets, address(this));

    // Calculate the number of shares received
    shares = vaultAddress.balanceOf(address(this)) - balanceBefore;

    // Ensure the received shares meet the minimum expected assets
    if (shares < minSharesOut) {
        revert InsufficientAssets();
    }

    // Update the vault's internal accounting
    uint128 amountUint128 = assets.toUint128();
    _totalIdle -= amountUint128;
    _totalDebt += amountUint128;
    vaults[_vaultToSuperformId[vaultAddress]].totalDebt += amountUint128;

    emit Invest(assets);
    return shares;
}

Cross-Chain Investment

function investSingleXChainSingleVault(SingleXChainSingleVaultStateReq calldata req)
    external
    payable
    onlyRoles(MANAGER_ROLE)
{
        gateway.investSingleXChainSingleVault{ value: msg.value }(req);

        // Update the vault's internal accounting
        uint256 amount = req.superformData.amount;
        uint128 amountUint128 = amount.toUint128();
        _totalIdle -= amountUint128;

        emit Invest(amount);
}

Multi-Vault Investment

function investSingleDirectMultiVault(
    address[] calldata vaultAddresses,
    uint256[] calldata assets,
    uint256[] calldata minSharesOuts
) external returns (uint256[] memory shares) {
    shares = new uint256[](vaultAddresses.length);
    for (uint256 i = 0; i < vaultAddresses.length; ++i) {
        shares[i] = investSingleDirectSingleVault(
            vaultAddresses[i], 
            assets[i], 
            minSharesOuts[i]
        );
    }
}

Gas Management

modifier refundGas() {
        uint256 balanceBefore;
        assembly {
            balanceBefore := sub(selfbalance(), callvalue())
        }
        _;
        assembly {
            let balanceAfter := selfbalance()
            switch lt(balanceAfter, balanceBefore)
            case true {
                mstore(0x00, 0x1c26714c) // `InsufficientGas()`.
                revert(0x1c, 0x04)
            }
            case false {
                // Transfer all the ETH to sender and check if it succeeded or not.
                if iszero(call(gas(), origin(), balanceAfter, codesize(), 0x00, codesize(), 0x00)) {
                    mstore(0x00, 0xb12d13eb) // `ETHTransferFailed()`.
                    revert(0x1c, 0x04)
                }
            }
        }
    }

Withdrawal Operations

Request Phase

function requestRedeem(
        uint256 shares,
        address controller,
        address owner
    )
        public
        override
        noEmergencyShutdown
        returns (uint256 requestId)
    {
        // Require deposited shares arent locked
        _checkSharesLocked(controller);
        requestId = super.requestRedeem(shares, controller, owner);
    }

Processing Phase

function processRedeemRequest(
        address controller,
        SingleXChainSingleVaultWithdraw calldata sXsV,
        SingleXChainMultiVaultWithdraw calldata sXmV,
        MultiXChainSingleVaultWithdraw calldata mXsV,
        MultiXChainMultiVaultWithdraw calldata mXmV
    )
        external
        payable
        nonReentrant
        onlyRoles(RELAYER_ROLE)
    {
        // Retrieve the pending redeem request for the specified controller
        // This request may involve cross-chain withdrawals from various ERC4626 vaults

        // Process the redemption request asynchronously
        // Parameters:
        // 1. pendingRedeemRequest(controller): Fetches the pending shares
        // 2. controller: The address initiating the redemption (used as both 'from' and 'to')
        _processRedeemRequest(
            ProcessRedeemRequestConfig(
                pendingRedeemRequest(
                    controller
                ), 
                controller, 
                controller, 
                sXsV, 
                sXmV, 
                mXsV, 
                mXmV
            )
        );
        // Note: After processing, the redeemed assets are held by this contract
        // The user can later claim these assets using `redeem` or `withdraw`
    }
// 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]++;
            }
        }
    }

Settlement Phase

function settleLiquidation(bytes32 key, bool force) external onlyRoles(RELAYER_ROLE) {
    if (!_requestsQueue.contains(key)) revert RequestNotFound();

    RequestData memory data = requests[key];
    if (data.controller == address(0)) revert InvalidController();

    ERC20Receiver receiverContract = ERC20Receiver(getReceiver(key));
    uint256 settledAssets = receiverContract.balance();

    _requestsQueue.remove(key);

    if (!force) {
        if (receiverContract.balance() < receiverContract.minExpectedBalance()) {
            revert MinimumBalanceNotMet();
        }
    }

    receiverContract.pull(settledAssets);
    asset.safeTransfer(address(vault), settledAssets);
    vault.fulfillSettledRequest(data.controller, data.requestedAssets, settledAssets);
    emit RequestSettled(key, data.controller, settledAssets);
}

Balance Verification

function previewWithdrawalRoute(uint256 assets)
        public
        view
        returns (ProcessRedeemRequestCache memory cachedRoute)
    {
        cachedRoute.assets = assets;
        uint256 shares = convertToShares(assets);
        cachedRoute.totalIdle = _totalIdle;
        cachedRoute.totalDebt = _totalDebt;
        cachedRoute.totalAssets = totalAssets();

        // Cannot process more assets than the available
        if (cachedRoute.assets > cachedRoute.totalAssets - gateway.totalpendingXChainInvests()) {
            revert InsufficientAvailableAssets();
        }

        // If totalIdle can covers the amount fulfill directly
        if (cachedRoute.totalIdle >= cachedRoute.assets) {
            cachedRoute.sharesFulfilled = shares;
            cachedRoute.totalClaimableWithdraw = cachedRoute.assets;
        }
        // Otherwise perform Superform withdrawals
        else {
            // Cache amount to withdraw before reducing totalIdle
            cachedRoute.amountToWithdraw = cachedRoute.assets - cachedRoute.totalIdle;
            // Use totalIdle to fulfill the request
            if (cachedRoute.totalIdle > 0) {
                cachedRoute.totalClaimableWithdraw = cachedRoute.totalIdle;
                cachedRoute.sharesFulfilled = _convertToShares(cachedRoute.totalIdle, cachedRoute.totalAssets);
            }
            ///////////////////////////////// PREVIOUS CALCULATIONS ////////////////////////////////
            _prepareWithdrawalRoute(cachedRoute);
        }
        return cachedRoute;
    }

Failure Mode Analysis

  1. Bridge Failures

    • Timeout scenarios

    • Asset recovery process

    • State reconciliation

  2. Oracle Failures

    • Price feed delays

    • Stale price handling

    • Fallback mechanisms

  3. Gas-Related Failures

    • Cross-chain operation out of gas

    • Batch operation partial success

    • Recovery procedures

Cross-Chain Settlement

Investment Settlement

function settleXChainInvest(uint256 superformId, uint256 bridgedAssets) public {
        if (msg.sender != address(gateway)) revert Unauthorized();
        _totalDebt += bridgedAssets.toUint128();
        vaults[superformId].totalDebt += bridgedAssets.toUint128();
        emit SettleXChainInvest(superformId, bridgedAssets);
    }

Divestment Settlement

function settleDivest(bytes32 key, bool force) external onlyRoles(RELAYER_ROLE) {
        if (!_requestsQueue.contains(key)) revert();
        RequestData memory data = requests[key];
        _requestsQueue.remove(key);
        ERC20Receiver receiverContract = ERC20Receiver(getReceiver(key));
        if (data.controller != address(vault)) revert();
        if (!force) {
            if (receiverContract.balance() < receiverContract.minExpectedBalance()) revert();
        }
        uint256 settledAssets = receiverContract.balance();
        uint256 requestedAssets = data.requestedAssets;
        receiverContract.pull(settledAssets);
        totalPendingXChainDivests -= settledAssets;
        asset.safeTransfer(address(vault), settledAssets);
        vault.settleXChainDivest(requestedAssets);
    }

Refund Handling

function notifyRefund(uint256 superformId, uint256 value) external {
        bytes32 key = ERC20Receiver(msg.sender).key();
        if (requests[key].receiverAddress != msg.sender) revert();
        RequestData memory req = requests[key];
        uint256 currentExpectedBalance = ERC20Receiver(msg.sender).minExpectedBalance();
        uint256 vaultIndex;
        for (uint256 i = 0; i < req.superformIds.length; ++i) {
            if (req.superformIds[i] == superformId) {
                vaultIndex = i;
                break;
            }
        }
        uint256 vaultRequestedAssets = req.requestedAssetsPerVault[vaultIndex];
        if (req.controller == address(vault)) {
            totalPendingXChainDivests -= vaultRequestedAssets;
        }
        requests[key].requestedAssets -= vaultRequestedAssets;
        ERC20Receiver(msg.sender).setMinExpectedBalance(_sub0(currentExpectedBalance, vaultRequestedAssets));
        superPositions.safeTransferFrom(msg.sender, address(this), superformId, value, "");
        superPositions.safeTransferFrom(
            address(this), address(vault), superformId, value, abi.encode(vaultRequestedAssets)
        );
    }

NFT Position Safety

function onERC1155Received(
        address operator,
        address from,
        uint256 superformId,
        uint256 value,
        bytes memory data
    )
        public
        returns (bytes4)
    {
        // Silence compiler warnings
        operator;
        value;
        if (from != address(gateway)) revert Unauthorized();
        if (data.length > 0) {
            uint256 refundedAssets = abi.decode(data, (uint256));
            if (refundedAssets != 0) {
                _totalDebt += refundedAssets.toUint128();
                vaults[superformId].totalDebt += refundedAssets.toUint128();
            }
        }
        return this.onERC1155Received.selector;
    }

Security Considerations

Critical Invariants

  1. Total assets = idle assets + allocated assets

  2. Pending requests must be processed in order

  3. Settlement amounts must meet minimum requirements

Fee Management

function chargeGlobalFees() external updateGlobalWatermark onlyRoles(MANAGER_ROLE) returns (uint256) {
        uint256 currentSharePrice = sharePrice();
        uint256 lastSharePrice = sharePriceWaterMark;
        uint256 duration = block.timestamp - lastFeesCharged;
        uint256 currentTotalAssets = totalAssets();
        uint256 lastTotalAssets = totalSupply().fullMulDiv(lastSharePrice, 10 ** decimals());

        // Calculate time-based fees (management & oracle)
        // These are charged on total assets, prorated for the time period
        uint256 managementFees = (currentTotalAssets * duration).fullMulDiv(managementFee, SECS_PER_YEAR) / MAX_BPS;
        uint256 oracleFees = (currentTotalAssets * duration).fullMulDiv(oracleFee, SECS_PER_YEAR) / MAX_BPS;
        uint256 totalFees = managementFees + oracleFees;
        uint256 performanceFees;

        currentTotalAssets += managementFees + oracleFees;

        lastFeesCharged = block.timestamp;

        // Calculate the asset's value change since entry
        // This gives us the raw profit/loss in asset terms
        int256 assetsDelta = int256(currentTotalAssets) - int256(lastTotalAssets);

        // Only calculate fees if there's a profit
        if (assetsDelta > 0) {
            uint256 excessReturn;

            // Calculate returns relative to hurdle rate
            uint256 hurdleReturn = (lastTotalAssets * hurdleRate()).fullMulDiv(duration, SECS_PER_YEAR) / MAX_BPS;
            uint256 totalReturn = uint256(assetsDelta);

            // Only charge performance fees if:
            // 1. Current share price is not below
            // 2. Returns exceed hurdle rate
            if (currentSharePrice > sharePriceWaterMark && totalReturn > hurdleReturn) {
                // Only charge performance fees on returns above hurdle rate
                excessReturn = totalReturn - hurdleReturn;

                performanceFees = excessReturn * performanceFee / MAX_BPS;
            }

            // Calculate total fees
            totalFees += performanceFees;
        }
        // Transfer fees to treasury if any were charged
        if (totalFees > 0) {
            _mint(treasury, convertToShares(totalFees));
            _afterDeposit(totalFees, 0);
        }
        assembly {
            let m := shr(96, not(0))

            // Emit the {AssessFees} event
            mstore(0x00, managementFees)
            mstore(0x20, performanceFees)
            mstore(0x40, oracleFees)
            log2(0x00, 0x60, 0xa443e1db11cb46c65620e8e21d4830a6b9b444fa4c350f0dd0024b8a5a6b6ef5, and(m, address()))
        }
        return totalFees;
    }

Access Control

modifier onlyRoles(uint256 role) {
    if (!hasRole(role, msg.sender)) revert Unauthorized();
    _;
}

State Protection

modifier nonReentrant() {
    require(_locked != 2, "ReentrancyGuard: reentrant call");
    _locked = 2;
    _;
    _locked = 1;
}

Emergency Controls

modifier noEmergencyShutdown() {
    if (emergencyShutdown) {
        revert VaultShutdown();
    }
    _;
}

Last updated