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
Bridge Failures
Timeout scenarios
Asset recovery process
State reconciliation
Oracle Failures
Price feed delays
Stale price handling
Fallback mechanisms
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
Total assets = idle assets + allocated assets
Pending requests must be processed in order
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