State Management & Operations
State Management & Operations
Core State Structures
Vault State Structure
/// @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;
}
Request States
/// @notice Data structure for cross-chain requests
/// @param controller Address controlling the request
/// @param superformIds Array of Superform IDs involved in request
/// @param requestedAssetsPerVault Array of requested assets per vault
/// @param requestedAssets Total requested assets
/// @param receiverAddress Address to receive assets
struct RequestData {
address controller;
uint256[] superformIds;
uint256[] requestedAssetsPerVault;
uint256 requestedAssets;
address receiverAddress;
}
// Request tracking
mapping(address => ERC7540_Request) private _pendingDepositRequest;
mapping(address => ERC7540_FilledRequest) private _claimableDepositRequest;
mapping(address => ERC7540_Request) private _pendingRedeemRequest;
mapping(address => ERC7540_FilledRequest) private _claimableRedeemRequest;
// State tracking
mapping(bytes32 => RequestData) public requests;
mapping(address => uint256) public nonces;
mapping(address => uint256) public lastRedeem;
Withdrawal Queue State
/// @dev Internal cache struct to allocate in memory
struct ProcessRedeemRequestCache {
// List of vauts to withdraw from on each chain
uint256[WITHDRAWAL_QUEUE_SIZE][N_CHAINS] dstVaults;
// List of shares to redeem on each vault in each chain
uint256[WITHDRAWAL_QUEUE_SIZE][N_CHAINS] sharesPerVault;
// List of assets to withdraw on each vault in each chain
uint256[WITHDRAWAL_QUEUE_SIZE][N_CHAINS] assetsPerVault;
// Cache length of list of each chain
uint256[N_CHAINS] lens;
// Assets to divest from other vaults
uint256 amountToWithdraw;
// Shares actually used
uint256 sharesFulfilled;
// Save assets that were withdrawn instantly
uint256 totalClaimableWithdraw;
// Cache totalAssets
uint256 totalAssets;
// Cache totalIdle
uint256 totalIdle;
// Cache totalDebt
uint256 totalDebt;
// Convert shares to assets at current price
uint256 assets;
// Wether is a single chain or multichain withdrawal
bool isSingleChain;
bool isMultiChain;
// Wether is a single or multivault withdrawal
bool isMultiVault;
}
Request State Lifecycle
Request Configuration
/// @param shares to redeem and burn
/// @param controller controller that created the request
/// @param receiver address of the assets receiver in case its a
struct ProcessRedeemRequestConfig {
uint256 shares;
address controller;
SingleXChainSingleVaultWithdraw sXsV;
SingleXChainMultiVaultWithdraw sXmV;
MultiXChainSingleVaultWithdraw mXsV;
MultiXChainMultiVaultWithdraw mXmV;
}
Initial State → PendingRequest
/// @notice Transfers assets from sender into the Vault and submits a Request for asynchronous deposit.
/// @param assets the amount of deposit assets to transfer from owner
/// @param controller the controller of the request who will be able to operate the request
/// @param owner the source of the deposit assets
/// @return requestId
function requestDeposit(
uint256 assets,
address controller,
address owner
)
public
override
noEmergencyShutdown
returns (uint256 requestId)
{
requestId = super.requestDeposit(assets, controller, owner);
// fulfill the request directly
_fulfillDepositRequest(controller, assets, convertToShares(assets));
}
PendingRequest → ClaimableRequest
/// @notice Executes the redeem request for a controller
function _processRedeemRequest(ProcessRedeemRequestConfig memory config) private {
// Use struct to avoid stack too deep
ProcessRedeemRequestCache memory cache;
cache.totalIdle = _totalIdle;
cache.totalDebt = _totalDebt;
cache.assets = convertToAssets(config.shares);
cache.totalAssets = totalAssets();
// Cannot process more assets than the
if (cache.assets > cache.totalAssets - gateway.totalpendingXChainInvests()) {
revert InsufficientAvailableAssets();
}
// If totalIdle can covers the amount fulfill directly
if (cache.totalIdle >= cache.assets) {
cache.sharesFulfilled = config.shares;
cache.totalClaimableWithdraw = cache.assets;
}
// Otherwise perform Superform withdrawals
else {
// Cache amount to withdraw before reducing totalIdle
cache.amountToWithdraw = cache.assets - cache.totalIdle;
// Use totalIdle to fulfill the request
if (cache.totalIdle > 0) {
cache.totalClaimableWithdraw = cache.totalIdle;
cache.sharesFulfilled = _convertToShares(cache.totalIdle, cache.totalAssets);
}
///////////////////////////////// PREVIOUS CALCULATIONS ////////////////////////////////
_prepareWithdrawalRoute(cache);
//////////////////////////////// WITHDRAW FROM THIS CHAIN ////////////////////////////////
// Cache chain index
uint256 chainIndex = chainIndexes[THIS_CHAIN_ID];
if (cache.lens[chainIndex] > 0) {
if (cache.lens[chainIndex] == 1) {
// shares to redeem
uint256 sharesAmount = cache.sharesPerVault[chainIndex][0];
// assets to withdraw
uint256 assetsAmount = cache.assetsPerVault[chainIndex][0];
// superformId(take first element fo the array)
uint256 superformId = cache.dstVaults[chainIndex][0];
// get actual withdrawn amount
uint256 withdrawn = _liquidateSingleDirectSingleVault(
vaults[superformId].vaultAddress, sharesAmount, 0, address(this)
);
// cache shares to burn
cache.sharesFulfilled += _convertToShares(assetsAmount, cache.totalAssets);
// reduce vault debt
vaults[superformId].totalDebt = _sub0(vaults[superformId].totalDebt, assetsAmount).toUint128();
// cache instant total withdraw
cache.totalClaimableWithdraw += withdrawn;
// Increase idle funds
cache.totalIdle += withdrawn;
} else {
uint256 len = cache.lens[chainIndex];
// Prepare arguments for request using dynamic arrays
address[] memory vaultAddresses = new address[](len);
uint256[] memory amounts = new uint256[](len);
// Calculate requested amount
uint256 requestedAssets;
// Cast fixed arrays to dynamic ones
for (uint256 i = 0; i != len; i++) {
vaultAddresses[i] = vaults[cache.dstVaults[chainIndex][i]].vaultAddress;
amounts[i] = cache.sharesPerVault[chainIndex][i];
// Reduce vault debt individually
uint256 superformId = cache.dstVaults[chainIndex][i];
// Increase total assets requested
requestedAssets += cache.assetsPerVault[chainIndex][i];
// Reduce vault debt
vaults[superformId].totalDebt =
_sub0(vaults[superformId].totalDebt, cache.assetsPerVault[chainIndex][i]).toUint128();
}
// Withdraw from the vault synchronously
uint256 withdrawn = _liquidateSingleDirectMultiVault(
vaultAddresses, amounts, _getEmptyuintArray(amounts.length), address(this)
);
// Increase claimable assets and fulfilled shares by the amount withdran synchronously
cache.totalClaimableWithdraw += withdrawn;
cache.sharesFulfilled += _convertToShares(requestedAssets, cache.totalAssets);
// Increase total idle
cache.totalIdle += withdrawn;
}
}
//////////////////////////////// WITHDRAW FROM EXTERNAL CHAINS ////////////////////////////////
// If its not multichain
if (!cache.isMultiChain) {
// If its multivault
if (!cache.isMultiVault) {
uint256 superformId;
uint256 amount;
uint64 chainId;
uint256 chainIndex;
for (uint256 i = 0; i < N_CHAINS; ++i) {
if (DST_CHAINS[i] == THIS_CHAIN_ID) continue;
// The vaults list length should be 1(single-vault)
if (cache.lens[i] > 0) {
chainId = DST_CHAINS[i];
chainIndex = chainIndexes[chainId];
superformId = cache.dstVaults[i][0];
amount = cache.sharesPerVault[i][0];
// Withdraw from one vault asynchronously(crosschain)
_liquidateSingleXChainSingleVault(
chainId, superformId, amount, config.controller, config.sXsV, cache.assetsPerVault[i][0]
);
// reduce vault debt
vaults[superformId].totalDebt =
_sub0(vaults[superformId].totalDebt, cache.assetsPerVault[chainIndex][0]).toUint128();
break;
}
}
} else {
uint256[] memory superformIds;
uint256[] memory amounts;
uint64 chainId;
for (uint256 i = 0; i < N_CHAINS; ++i) {
if (DST_CHAINS[i] == THIS_CHAIN_ID) continue;
if (cache.lens[i] > 0) {
chainId = DST_CHAINS[i];
superformIds = _toDynamicUint256Array(cache.dstVaults[i], cache.lens[i]);
amounts = _toDynamicUint256Array(cache.sharesPerVault[i], cache.lens[i]);
uint256 totalDebtReduction;
// reduce vault debt
for (uint256 j = 0; j < superformIds.length;) {
vaults[superformIds[j]].totalDebt =
_sub0(vaults[superformIds[j]].totalDebt, cache.assetsPerVault[i][j]).toUint128();
totalDebtReduction += cache.assetsPerVault[i][j];
unchecked {
++j;
}
}
// Withdraw from multiple vaults asynchronously(crosschain)
_liquidateSingleXChainMultiVault(
chainId,
superformIds,
amounts,
config.controller,
config.sXmV,
totalDebtReduction,
_toDynamicUint256Array(cache.assetsPerVault[i], cache.lens[i])
);
break;
}
}
}
}
// If its multichain
else {
// If its single vault
if (!cache.isMultiVault) {
uint256 chainsLen;
for (uint256 i = 0; i < cache.lens.length; i++) {
if (cache.lens[i] > 0) chainsLen++;
}
uint8[][] memory ambIds = new uint8[][](chainsLen);
uint64[] memory dstChainIds = new uint64[](chainsLen);
SingleVaultSFData[] memory singleVaultDatas = new SingleVaultSFData[](chainsLen);
uint256 lastChainsIndex;
for (uint256 i = 0; i < N_CHAINS; i++) {
if (cache.lens[i] > 0) {
dstChainIds[lastChainsIndex] = DST_CHAINS[i];
++lastChainsIndex;
}
}
uint256[] memory totalDebtReductions = new uint256[](chainsLen);
for (uint256 i = 0; i < N_CHAINS; i++) {
totalDebtReductions[i] = cache.assetsPerVault[i][0];
singleVaultDatas[i] = SingleVaultSFData({
superformId: cache.dstVaults[i][0],
amount: cache.sharesPerVault[i][0],
outputAmount: config.mXsV.outputAmounts[i],
maxSlippage: config.mXsV.maxSlippages[i],
liqRequest: config.mXsV.liqRequests[i],
permit2data: "",
hasDstSwap: config.mXsV.hasDstSwaps[i],
retain4626: false,
receiverAddress: config.controller,
receiverAddressSP: address(0),
extraFormData: ""
});
ambIds[i] = config.mXsV.ambIds[i];
vaults[cache.dstVaults[i][0]].totalDebt =
_sub0(vaults[cache.dstVaults[i][0]].totalDebt, cache.assetsPerVault[i][0]).toUint128();
}
_liquidateMultiDstSingleVault(
ambIds, dstChainIds, singleVaultDatas, config.mXsV.value, totalDebtReductions
);
}
// If its multi-vault
else {
// Cache the number of chains we will withdraw from
uint256 chainsLen;
for (uint256 i = 0; i < cache.lens.length; i++) {
if (cache.lens[i] > 0) chainsLen++;
}
uint8[][] memory ambIds = new uint8[][](chainsLen);
// Cacche destination chains
uint64[] memory dstChainIds = new uint64[](chainsLen);
// Cache multivault calls for each chain
MultiVaultSFData[] memory multiVaultDatas = new MultiVaultSFData[](chainsLen);
uint256 lastChainsIndex;
for (uint256 i = 0; i < N_CHAINS; i++) {
if (cache.lens[i] > 0) {
dstChainIds[lastChainsIndex] = DST_CHAINS[i];
++lastChainsIndex;
}
}
uint256[] memory totalDebtReductions = new uint256[](chainsLen);
uint256[][] memory debtReductionsPerVault = new uint256[][](chainsLen);
for (uint256 i = 0; i < N_CHAINS; i++) {
bool[] memory emptyBoolArray = _getEmptyBoolArray(cache.lens[i]);
uint256[] memory superformIds = _toDynamicUint256Array(cache.dstVaults[i], cache.lens[i]);
multiVaultDatas[i] = MultiVaultSFData({
superformIds: superformIds,
amounts: _toDynamicUint256Array(cache.sharesPerVault[i], cache.lens[i]),
outputAmounts: config.mXmV.outputAmounts[i],
maxSlippages: config.mXmV.maxSlippages[i],
liqRequests: config.mXmV.liqRequests[i],
permit2data: "",
hasDstSwaps: config.mXmV.hasDstSwaps[i],
retain4626s: emptyBoolArray,
receiverAddress: config.controller,
receiverAddressSP: address(0),
extraFormData: ""
});
ambIds[i] = config.mXmV.ambIds[i];
debtReductionsPerVault[i] = _toDynamicUint256Array(cache.sharesPerVault[i], cache.lens[i]);
for (uint256 j = 0; j < superformIds.length;) {
vaults[superformIds[j]].totalDebt =
_sub0(vaults[superformIds[j]].totalDebt, cache.assetsPerVault[i][j]).toUint128();
totalDebtReductions[i] += cache.assetsPerVault[i][j];
unchecked {
++j;
}
}
}
// Withdraw from multiple vaults and chains asynchronously
_liquidateMultiDstMultiVault(
ambIds,
dstChainIds,
multiVaultDatas,
config.mXmV.value,
totalDebtReductions,
debtReductionsPerVault
);
}
}
}
// Optimistically deduct all assets to withdraw from the total
_totalIdle = cache.totalIdle.toUint128();
_totalIdle -= cache.totalClaimableWithdraw.toUint128();
_totalDebt = cache.totalDebt.toUint128();
emit ProcessRedeemRequest(config.controller, config.shares);
// Burn all shares from this contract(they already have been transferred)
_burn(address(this), config.shares);
// Fulfill request with instant withdrawals only
_fulfillRedeemRequest(cache.sharesFulfilled, cache.totalClaimableWithdraw, config.controller);
}
ClaimableRequest → Completed
/// @dev Hook that is called when processing a redeem request and make it claimable.
/// @dev It assumes user transferred its shares to the contract when requesting a redeem
function _fulfillRedeemRequest(
uint256 sharesFulfilled,
uint256 assetsWithdrawn,
address controller
)
internal
virtual
{
_pendingRedeemRequest[controller] = _pendingRedeemRequest[controller].sub(sharesFulfilled);
_claimableRedeemRequest[controller].assets += assetsWithdrawn;
_claimableRedeemRequest[controller].shares += sharesFulfilled;
}
Cross-Chain State Management
Investment State Tracking
// Pending investment tracking
mapping(uint256 => uint256) public pendingXChainInvests;
uint256 public totalpendingXChainInvests;
// Receiver contract tracking
mapping(bytes32 => address) public receivers;
Withdrawal State Tracking
// Queue tracking
EnumerableSetLib.Bytes32Set internal _requestsQueue;
uint256 public totalPendingXChainDivests;
// Lock states
mapping(address => bool) public redeemLocked;
mapping(address => uint256) internal _depositLockCheckPoint;
Direct Investment Flow
State Updates
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;
}
Security Considerations
State Locks
// Reentrancy protection
modifier nonReentrant() {
require(_locked != 2, "ReentrancyGuard: reentrant call");
_locked = 2;
_;
_locked = 1;
}
// Emergency shutdown
modifier noEmergencyShutdown() {
if (emergencyShutdown) {
revert VaultShutdown();
}
_;
}
State Validations
/// @dev Reverts if deposited shares are locked
/// @param controller shares controller
function _checkSharesLocked(address controller) private view {
if (block.timestamp < _depositLockCheckPoint[controller] + sharesLockTime) revert SharesLocked();
}
Critical Invariants
Total Assets = Idle + Debt + Pending Investments
Share Price Watermark ≤ Current Share Price
Request Queue Ordering Must Be Maintained
Receiver Contracts Must Be Unique Per Request
State Recovery
/// @notice Settles a cross-chain liquidation by processing received assets
/// @dev Pulls assets from the receiver contract and fulfills the settlement in the vault.
/// Only callable by addresses with RELAYER_ROLE. The key for lookup is generated based on
/// whether it's a single vault (superformId) or multiple vaults (array of superformIds).
/// @param key identifier of the receiver contract
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);
}
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)
);
}
Last updated