Asset Management

Core Asset States

Global Asset Tracking

uint128 internal _totalIdle;    // Unallocated assets
uint128 internal _totalDebt;    // Allocated assets
uint256 public sharePriceWaterMark; // High watermark for performance

// Cross-chain tracking
uint256 public totalpendingXChainInvests;
uint256 public totalPendingXChainDivests;

Per-Vault Asset Tracking

/// @notice A struct describing the status of an underlying vault
/// @dev Contains data about a vault's chain ID, share price, oracle, and more
struct VaultData {
    /// @dev Fees to deduct when applying performance fees
    uint16 deductedFees;
    /// @dev The ID of the chain where the vault is deployed
    uint64 chainId;
    /// @dev The last reported share price of the vault
    uint192 lastReportedSharePrice;
    /// @dev The superform ID of the vault in the Superform protocol
    uint256 superformId;
    /// @dev The oracle that provides the share price for the vault
    ISharePriceOracle oracle;
    /// @dev The number of decimals used in the ERC4626 shares
    uint8 decimals;
    /// @dev The total assets invested in the vault
    uint128 totalDebt;
    /// @dev The address of the vault
    address vaultAddress;
}

mapping(uint256 => VaultData) public vaults;

Asset Operations

Direct Asset Management

    /// @notice Invests assets from this vault into a single target vault within the same chain
    /// @dev Only callable by addresses with the MANAGER_ROLE
    /// @param vaultAddress The address of the target vault to invest in
    /// @param assets The amount of assets to invest
    /// @param minSharesOut The minimum amount of shares expected to receive from the investment
    /// @return shares The number of shares received from the target vault
    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 Asset Management

    /// @notice Invests assets from this vault into a single target vault on a different chain
    /// @dev Only callable by addresses with the MANAGER_ROLE
    /// @param req Crosschain deposit request
    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);
    }

NFT Position Management

    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;
    }
    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]++;
            }
        }
    }

Fee Management

Fee Structure

uint16 public managementFee;    // Base management fee
uint16 public performanceFee;   // Success-based fee
uint16 public oracleFee;        // Price update incentive

mapping(address => uint256) public managementFeeExempt;
mapping(address => uint256) public performanceFeeExempt;
mapping(address => uint256) public oracleFeeExempt;

Fee Assessment

    /// @notice Charges global management, performance, and oracle fees on the vault's total assets
    /// @dev Fee charging mechanism works as follows:
    /// 1. Time-based fees (management & oracle) are charged on total assets, prorated for the time period
    /// 2. Performance fees are only charged if two conditions are met:
    ///    a) Current share price is above the watermark (high water mark)
    ///    b) Returns exceed the hurdle rate
    /// 3. The hurdle rate is asset-specific:
    ///    - For stablecoins (e.g., USDC): typically tied to T-Bill yields
    ///    - For ETH: typically tied to base staking returns (e.g., Lido APY)
    /// 4. Performance fees are only charged on excess returns above both:
    ///    - The watermark (preventing double-charging on same gains)
    ///    - The hurdle rate (ensuring fees only on excess performance)
    /// Example calculation:
    /// - If initial assets = $1M, current assets = $1.08M
    /// - Duration = 180 days, Management = 2%, Oracle = 0.5%, Performance = 20%
    /// - Hurdle = 5% APY
    /// Then:
    /// 1. Management Fee = $1.08M * 2% * (180/365) = $10,628
    /// 2. Oracle Fee = $1.08M * 0.5% * (180/365) = $2,657
    /// 3. Hurdle Return = $1M * 5% * (180/365) = $24,657
    /// 4. Excess Return = ($80,000 - $13,285 - $24,657) = $42,058
    /// 5. Performance Fee = $42,058 * 20% = $8,412
    /// @return uint256 Total fees charged
    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;
    }

Asset Accounting

    /// @dev The withdraw amount is limited by the claimable redeem requests of the user
    function maxWithdraw(address owner) public view virtual override returns (uint256 assets) {
        return convertToAssets(maxRedeem(owner));
    }

    /// @dev The redeem amount is limited by the claimable redeem requests of the user
    function maxRedeem(address owner) public view virtual override returns (uint256 shares) {
        return _claimableRedeemRequest[owner].shares;
    }

Total Assets Calculation

    /// @notice Returns the total amount of the underlying asset managed by the Vault.
    function totalAssets() public view override returns (uint256 assets) {
        return gateway.totalpendingXChainInvests() + gateway.totalPendingXChainDivests() + totalWithdrawableAssets();
    }

    /// @notice Returns the total amount of the underlying asset that have been deposited into the vault.
    function totalDeposits() public view returns (uint256 assets) {
        return totalIdle() + totalDebt();
    }

    /// @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;
    }

    /// @notice returns the assets that are sitting idle in this contract
    /// @return assets amount of idle assets
    function totalIdle() public view returns (uint256 assets) {
        return _totalIdle;
    }

    /// @notice returns the total issued debt of underlying vaulrs
    /// @return assets amount assets that are invested in vaults
    function totalDebt() public view returns (uint256 assets) {
        return _totalDebt;
    }

Share Price Updates

modifier updateGlobalWatermark() {
    _;
    uint256 sp = sharePrice();
    assembly {
        let spwm := sload(sharePriceWaterMark.slot)
        if lt(spwm, sp) { sstore(sharePriceWaterMark.slot, sp) }
    }
}

function sharePrice() public view returns (uint256) {
    if (data.chainId != _chainId()) {
        VaultReport memory report = data.oracle.getLatestSharePrice(
            data.chainId,
            data.vaultAddress
        );
        return report.sharePrice;
    }
    return ERC4626(data.vaultAddress).convertToAssets(10 ** data.decimals);
}

Asset Safety

Critical Invariants

  1. Total Assets = Idle + Debt + Pending Investments

  2. Share Price Monotonicity (never decreases under normal operation)

  3. Fee Calculations Maintain Precision

  4. Asset/Share Ratio Consistency

  5. Cross-Chain Position Reconciliation

Balance Validation

// Available assets check
if (cache.assets > cache.totalAssets - gateway.totalpendingXChainInvests()) {
    revert InsufficientAvailableAssets();
}

// Receiver balance validation
if (!force && receiverContract.balance() < receiverContract.minExpectedBalance()) {
    revert MinimumBalanceNotMet();
}

// Slippage protection
if (shares < minSharesOut) {
    revert InsufficientAssets();
}

Oracle Integration

Share Price Oracle

interface ISharePriceOracle {
    /// @notice Gets the latest share price of a vault
    /// @dev Returns a VaultReport with share price and timestamp
    /// @param chainId Chain ID of the vault
    /// @param vault Address of the vault
    /// @return VaultReport containing latest price data
    function getLatestSharePrice(
        uint64 chainId,
        address vault
    ) external view returns (VaultReport memory);
}

Hurdle Rate Oracle

interface IHurdleRateOracle {
    /// @notice Returns the base hurdle rate for performance fee calculations
    /// @dev The hurdle rate differs by asset:
    /// - For stablecoins (USDC): Typically set to T-Bills yield
    /// - For ETH: Typically set to base staking return
    /// @param asset The asset to get the rate for
    /// @return uint256 The current base hurdle rate in basis points
    function getRate(address asset) external view returns (uint256);
}
IHurdleRateOracle public _hurdleRateOracle; // Hurdle rate oracle

function setHurdleRateOracle(IHurdleRateOracle hurdleRateOracle) external onlyRoles(ADMIN_ROLE) {
    _hurdleRateOracle = hurdleRateOracle;
}

Last updated