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
Total Assets = Idle + Debt + Pending Investments
Share Price Monotonicity (never decreases under normal operation)
Fee Calculations Maintain Precision
Asset/Share Ratio Consistency
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