VaultHub
Central registry and lifecycle manager for StakingVaults connected to the Lido protocol. Handles vault connection, minting/burning stETH against vault collateral, rebalancing, fee settlement, and bad debt management.
What is VaultHub?​
VaultHub is the coordinator between individual StakingVaults and the Lido protocol:
- Connection management: vaults connect permissionlessly (with OperatorGrid limits) and can disconnect voluntarily or be disconnected by governance
- Minting/burning: vault owners mint stETH shares against their vault's total value as collateral
- Rebalancing: when vaults become unhealthy, anyone can trigger forced rebalancing using available vault funds
- Fee settlement: Lido fees accrue on vaults and can be settled permissionlessly
- Bad debt handling: governance can socialize bad debt to other vaults or internalize it as protocol loss
- Connect deposit lock: VaultHub enforces the 1 ETH connect deposit as minimal reserve
- Beacon chain deposits auto-pause: deposits are automatically paused when vaults have outstanding obligations
Inherits​
Constants​
| Constant | Value | Description |
|---|---|---|
CONNECT_DEPOSIT | 1 ether | ETH locked on connection, returned on disconnect |
REPORT_FRESHNESS_DELTA | 2 days | Maximum age for a report to be considered fresh |
MIN_BEACON_DEPOSIT | 1 ether | Threshold for beacon chain deposits pause on unsettled fees |
PDG_ACTIVATION_DEPOSIT | 31 ether | ETH required per validator activation after PDG predeposit |
DISCONNECT_NOT_INITIATED | type(uint48).max | Special value indicating vault is connected (not pending disconnect) |
TOTAL_BASIS_POINTS | 10000 | Basis points denominator |
Immutable variables​
| Variable | Description |
|---|---|
LIDO | stETH contract for minting/burning external shares |
LIDO_LOCATOR | Protocol locator for resolving contract addresses |
CONSENSUS_CONTRACT | HashConsensus contract for ref slot tracking |
MAX_RELATIVE_SHARE_LIMIT_BP | Maximum share limit relative to Lido TVL (basis points) |
Roles​
| Role | Description |
|---|---|
VAULT_MASTER_ROLE | Can force disconnect vaults from the hub |
REDEMPTION_MASTER_ROLE | Can set liability shares targets for Lido Core redemptions |
VALIDATOR_EXIT_ROLE | Can trigger forced validator exits for unhealthy vaults |
BAD_DEBT_MASTER_ROLE | Can socialize or internalize bad debt from vaults |
Storage​
ERC-7201 Namespaced Storage​
struct Storage {
mapping(address vault => VaultRecord) records;
mapping(address vault => VaultConnection) connections;
address[] vaults; // 1-based array, index 0 reserved
RefSlotCache.Uint104WithCache badDebtToInternalize;
}
Structs​
VaultConnection​
Connection parameters for a vault:
struct VaultConnection {
address owner; // Vault owner address
uint96 shareLimit; // Maximum stETH shares mintable
uint96 vaultIndex; // Index in vaults array (1-based, 0 = not connected)
uint48 disconnectInitiatedTs; // Timestamp when disconnect started (max = connected)
uint16 reserveRatioBP; // Reserve ratio (e.g., 30% = 3000)
uint16 forcedRebalanceThresholdBP; // Health threshold for forced rebalance
uint16 infraFeeBP; // Infrastructure fee (basis points)
uint16 liquidityFeeBP; // Liquidity fee (basis points)
uint16 reservationFeeBP; // Reservation fee (basis points)
bool beaconChainDepositsPauseIntent; // Owner's intent to pause beacon deposits
}
VaultRecord​
Accounting record for a vault:
struct VaultRecord {
Report report; // Latest oracle report
uint96 maxLiabilityShares; // Peak liability shares in current period
uint96 liabilityShares; // Current liability shares
DoubleRefSlotCache.Int104WithCache[2] inOutDelta; // Cumulative deposits - withdrawals
uint128 minimalReserve; // min(CONNECT_DEPOSIT, slashingReserve)
uint128 redemptionShares; // Shares marked for Lido Core redemption
uint128 cumulativeLidoFees; // Total accrued Lido fees
uint128 settledLidoFees; // Total settled Lido fees
}
Report​
Oracle report snapshot:
struct Report {
uint104 totalValue; // Total vault value (ETH)
int104 inOutDelta; // inOutDelta at report time
uint48 timestamp; // Report timestamp
}
Obligations mechanism​
Vaults have obligations that must be covered before withdrawals:
- Health restoration: Shares to burn/rebalance to restore health ratio
- Redemptions: Shares marked as
redemptionSharesfor Lido Core - Fee settlement: Accrued but unsettled Lido fees (≥1 ETH triggers deposit pause)
The obligations() view returns (sharesToBurn, feesToSettle). If sharesToBurn is type(uint256).max, the vault has bad debt.
Beacon chain deposits auto-pause​
Deposits are automatically paused when:
- Vault is unhealthy (health shortfall > 0)
- Vault has redemption shares to cover
- Unsettled Lido fees ≥
MIN_BEACON_DEPOSIT(1 ETH)
Once obligations are cleared and owner hasn't set manual pause intent, deposits resume automatically.
Manual pause intent​
Vault owners can explicitly pause or resume beacon chain deposits via pauseBeaconChainDeposits() and resumeBeaconChainDeposits() (typically through Dashboard). This toggles beaconChainDepositsPauseIntent:
- If pause intent is set, deposits stay paused regardless of obligations.
- If pause intent is cleared, deposits still remain paused until obligations are covered.
View methods​
vaultsCount()​
function vaultsCount() external view returns (uint256)
Returns the number of vaults connected to the hub.
vaultByIndex(uint256 _index)​
function vaultByIndex(uint256 _index) external view returns (address)
Returns vault address by 1-based index. Indexes are not stable across transactions.
vaultConnection(address _vault)​
function vaultConnection(address _vault) external view returns (VaultConnection memory)
Returns connection parameters for a vault. Returns empty struct if not connected.
vaultRecord(address _vault)​
function vaultRecord(address _vault) external view returns (VaultRecord memory)
Returns accounting record for a vault. Returns empty struct if not connected.
isVaultConnected(address _vault)​
function isVaultConnected(address _vault) external view returns (bool)
Returns true if vault is connected (or pending disconnect).
isPendingDisconnect(address _vault)​
function isPendingDisconnect(address _vault) external view returns (bool)
Returns true if vault disconnect has been initiated and awaiting completion.
totalValue(address _vault)​
function totalValue(address _vault) external view returns (uint256)
Returns current total value of the vault (report value + inOutDelta changes).
liabilityShares(address _vault)​
function liabilityShares(address _vault) external view returns (uint256)
Returns current liability shares (minted stETH) of the vault.
locked(address _vault)​
function locked(address _vault) external view returns (uint256)
Returns amount of ETH locked on the vault based on current liability and reserve ratio.
maxLockableValue(address _vault)​
function maxLockableValue(address _vault) external view returns (uint256)
Returns maximum ETH that can be locked given current total value.
totalMintingCapacityShares(address _vault, int256 _deltaValue)​
function totalMintingCapacityShares(address _vault, int256 _deltaValue) external view returns (uint256)
Returns total shares that can be minted, accounting for reserve ratio, minimal reserve, and operator grid limits. _deltaValue allows simulating value changes.
withdrawableValue(address _vault)​
function withdrawableValue(address _vault) external view returns (uint256)
Returns ETH instantly withdrawable from the vault (accounts for locked value, redemptions, and unsettled fees).
latestReport(address _vault)​
function latestReport(address _vault) external view returns (Report memory)
Returns the latest oracle report for the vault.
isReportFresh(address _vault)​
function isReportFresh(address _vault) external view returns (bool)
Returns true if the vault's report is considered fresh (within REPORT_FRESHNESS_DELTA of latest oracle report).
isVaultHealthy(address _vault)​
function isVaultHealthy(address _vault) external view returns (bool)
Returns true if vault's total value meets the forced rebalance threshold.
healthShortfallShares(address _vault)​
function healthShortfallShares(address _vault) external view returns (uint256)
Returns shares needed to restore vault health. Returns type(uint256).max if bad debt (impossible to fix via rebalance).
obligationsShortfallValue(address _vault)​
function obligationsShortfallValue(address _vault) external view returns (uint256)
Returns ETH shortfall needed to cover all obligations.
obligations(address _vault)​
function obligations(address _vault) external view returns (uint256 sharesToBurn, uint256 feesToSettle)
Returns the vault's current obligations: shares to burn/rebalance and fees to settle.
settleableLidoFeesValue(address _vault)​
function settleableLidoFeesValue(address _vault) external view returns (uint256)
Returns Lido fees that can currently be settled (limited by available withdrawable funds).
badDebtToInternalize()​
function badDebtToInternalize() external view returns (uint256)
Returns bad debt shares pending internalization as protocol loss.
badDebtToInternalizeForLastRefSlot()​
function badDebtToInternalizeForLastRefSlot() external view returns (uint256)
Returns bad debt shares that were pending at the last reference slot (for oracle accounting).
Methods​
initialize(address _admin)​
function initialize(address _admin) external initializer
Initializes the VaultHub with admin address.
connectVault(address _vault)​
function connectVault(address _vault) external whenResumed
Connects a vault to the hub permissionlessly. Vault must:
- Be deployed by a valid factory
- Have
msg.senderas current owner - Have
VaultHubas pending owner - Not be ossified
- Have PDG as depositor
- Have staged balance matching pending activations × 31 ETH
- Have at least
CONNECT_DEPOSIT(1 ETH) available balance
Connection parameters are fetched from OperatorGrid based on the vault's tier.
voluntaryDisconnect(address _vault)​
function voluntaryDisconnect(address _vault) external whenResumed
Initiates voluntary disconnect. Requires:
msg.senderis vault owner- Fresh report
- Zero liability shares
- Full fee settlement (if funds available)
disconnect(address _vault)​
function disconnect(address _vault) external onlyRole(VAULT_MASTER_ROLE)
Governance-initiated disconnect. Same requirements as voluntary disconnect but allows partial fee settlement.
updateConnection(...)​
function updateConnection(
address _vault,
uint256 _shareLimit,
uint256 _reserveRatioBP,
uint256 _forcedRebalanceThresholdBP,
uint256 _infraFeeBP,
uint256 _liquidityFeeBP,
uint256 _reservationFeeBP
) external
Updates vault connection parameters. Only callable by OperatorGrid. Requires fresh report and validates new parameters don't breach minting capacity.
fund(address _vault)​
function fund(address _vault) external payable whenResumed
Funds the vault with ETH. Only callable by vault owner.
withdraw(address _vault, address _recipient, uint256 _ether)​
function withdraw(address _vault, address _recipient, uint256 _ether) external whenResumed
Withdraws ETH from vault. Only callable by vault owner. Requires fresh report and respects withdrawable limits.
mintShares(address _vault, address _recipient, uint256 _amountOfShares)​
function mintShares(address _vault, address _recipient, uint256 _amountOfShares) external whenResumed
Mints stETH shares backed by vault collateral. Only callable by vault owner. Requires fresh report.
burnShares(address _vault, uint256 _amountOfShares)​
function burnShares(address _vault, uint256 _amountOfShares) public whenResumed
Burns stETH shares from VaultHub balance to reduce liability. Only callable by vault owner.
transferAndBurnShares(address _vault, uint256 _amountOfShares)​
function transferAndBurnShares(address _vault, uint256 _amountOfShares) external
Transfers shares from msg.sender to VaultHub and burns them. For EOA vault owners.
rebalance(address _vault, uint256 _shares)​
function rebalance(address _vault, uint256 _shares) external whenResumed
Voluntary rebalance by vault owner. Withdraws ETH and burns corresponding shares.
forceRebalance(address _vault)​
function forceRebalance(address _vault) external
Permissionless forced rebalance for unhealthy vaults. Uses all available balance to cover obligations.
settleLidoFees(address _vault)​
function settleLidoFees(address _vault) external
Permissionless fee settlement. Sends unsettled fees to treasury.
setLiabilitySharesTarget(address _vault, uint256 _liabilitySharesTarget)​
function setLiabilitySharesTarget(address _vault, uint256 _liabilitySharesTarget) external onlyRole(REDEMPTION_MASTER_ROLE)
Sets target liability, marking excess as redemption shares. Used for Lido Core redemptions.
transferVaultOwnership(address _vault, address _newOwner)​
function transferVaultOwnership(address _vault, address _newOwner) external
Transfers vault ownership within VaultHub without disconnecting.
pauseBeaconChainDeposits(address _vault)​
function pauseBeaconChainDeposits(address _vault) external
Owner sets intent to pause beacon chain deposits.
resumeBeaconChainDeposits(address _vault)​
function resumeBeaconChainDeposits(address _vault) external
Owner clears pause intent. Deposits may remain paused if obligations exist.
requestValidatorExit(address _vault, bytes calldata _pubkeys)​
function requestValidatorExit(address _vault, bytes calldata _pubkeys) external
Emits exit request events for node operator. Only callable by vault owner.
triggerValidatorWithdrawals(...)​
function triggerValidatorWithdrawals(
address _vault,
bytes calldata _pubkeys,
uint64[] calldata _amountsInGwei,
address _refundRecipient
) external payable
Triggers EIP-7002 validator withdrawals. Partial withdrawals require fresh report and sufficient amount to cover obligations shortfall.
forceValidatorExit(address _vault, bytes calldata _pubkeys, address _refundRecipient)​
function forceValidatorExit(
address _vault,
bytes calldata _pubkeys,
address _refundRecipient
) external payable onlyRole(VALIDATOR_EXIT_ROLE)
Forces full validator exits for vaults with obligations shortfall.
socializeBadDebt(address _badDebtVault, address _vaultAcceptor, uint256 _maxSharesToSocialize)​
function socializeBadDebt(
address _badDebtVault,
address _vaultAcceptor,
uint256 _maxSharesToSocialize
) external onlyRole(BAD_DEBT_MASTER_ROLE) returns (uint256)
Transfers bad debt to another vault of the same node operator. Requires fresh reports for both vaults.
internalizeBadDebt(address _badDebtVault, uint256 _maxSharesToInternalize)​
function internalizeBadDebt(
address _badDebtVault,
uint256 _maxSharesToInternalize
) external onlyRole(BAD_DEBT_MASTER_ROLE) returns (uint256)
Internalizes bad debt as protocol loss. Requires fresh report.
decreaseInternalizedBadDebt(uint256 _amountOfShares)​
function decreaseInternalizedBadDebt(uint256 _amountOfShares) external
Called by Accounting contract to clear internalized bad debt after settlement.
applyVaultReport(...)​
function applyVaultReport(
address _vault,
uint256 _reportTimestamp,
uint256 _reportTotalValue,
int256 _reportInOutDelta,
uint256 _reportCumulativeLidoFees,
uint256 _reportLiabilityShares,
uint256 _reportMaxLiabilityShares,
uint256 _reportSlashingReserve
) external whenResumed
Applies oracle report to vault. Only callable by LazyOracle. Updates vault record and may complete pending disconnect.
proveUnknownValidatorToPDG(address _vault, IPredepositGuarantee.ValidatorWitness calldata _witness)​
function proveUnknownValidatorToPDG(
address _vault,
IPredepositGuarantee.ValidatorWitness calldata _witness
) external
Proves unknown validators to PDG. Only callable by vault owner.
collectERC20FromVault(address _vault, address _token, address _recipient, uint256 _amount)​
function collectERC20FromVault(
address _vault,
address _token,
address _recipient,
uint256 _amount
) external
Recovers ERC-20 tokens from vault. Only callable by vault owner.
Events​
event VaultConnected(
address indexed vault,
uint256 shareLimit,
uint256 reserveRatioBP,
uint256 forcedRebalanceThresholdBP,
uint256 infraFeeBP,
uint256 liquidityFeeBP,
uint256 reservationFeeBP
);
event VaultConnectionUpdated(
address indexed vault,
address indexed nodeOperator,
uint256 shareLimit,
uint256 reserveRatioBP,
uint256 forcedRebalanceThresholdBP
);
event VaultFeesUpdated(
address indexed vault,
uint256 preInfraFeeBP,
uint256 preLiquidityFeeBP,
uint256 preReservationFeeBP,
uint256 infraFeeBP,
uint256 liquidityFeeBP,
uint256 reservationFeeBP
);
event VaultDisconnectInitiated(address indexed vault);
event VaultDisconnectCompleted(address indexed vault);
event VaultDisconnectAborted(address indexed vault, uint256 slashingReserve);
event VaultReportApplied(
address indexed vault,
uint256 reportTimestamp,
uint256 reportTotalValue,
int256 reportInOutDelta,
uint256 reportCumulativeLidoFees,
uint256 reportLiabilityShares,
uint256 reportMaxLiabilityShares,
uint256 reportSlashingReserve
);
event MintedSharesOnVault(address indexed vault, uint256 amountOfShares, uint256 lockedAmount);
event BurnedSharesOnVault(address indexed vault, uint256 amountOfShares);
event VaultRebalanced(address indexed vault, uint256 sharesBurned, uint256 etherWithdrawn);
event VaultInOutDeltaUpdated(address indexed vault, int256 inOutDelta);
event ForcedValidatorExitTriggered(address indexed vault, bytes pubkeys, address refundRecipient);
event VaultOwnershipTransferred(address indexed vault, address indexed newOwner, address indexed oldOwner);
event LidoFeesSettled(address indexed vault, uint256 transferred, uint256 cumulativeLidoFees, uint256 settledLidoFees);
event VaultRedemptionSharesUpdated(address indexed vault, uint256 redemptionShares);
event BeaconChainDepositsPauseIntentSet(address indexed vault, bool pauseIntent);
event BadDebtSocialized(address indexed vaultDonor, address indexed vaultAcceptor, uint256 badDebtShares);
event BadDebtWrittenOffToBeInternalized(address indexed vault, uint256 badDebtShares);