Overview
ETH Balance
0 ETH
More Info
ContractCreator
Multichain Info
N/A
Latest 25 from a total of 482 transactions
| Transaction Hash |
Method
|
Block
|
From
|
To
|
Amount
|
||||
|---|---|---|---|---|---|---|---|---|---|
| Burn Shares | 1666811 | 17 days ago | IN | 0 ETH | 0.00041012 | ||||
| Prove Unknown Va... | 1645316 | 20 days ago | IN | 0 ETH | 0.00060154 | ||||
| Unguaranteed Dep... | 1640055 | 21 days ago | IN | 0 ETH | 0.00024967 | ||||
| Fund | 1612111 | 25 days ago | IN | 0.02 ETH | 0.00012542 | ||||
| Fund | 1612085 | 25 days ago | IN | 0.02 ETH | 0.00009252 | ||||
| Fund | 1611984 | 25 days ago | IN | 0.02 ETH | 0.00010764 | ||||
| Trigger Validato... | 1599973 | 27 days ago | IN | 1 wei | 0.00024381 | ||||
| Trigger Validato... | 1599109 | 27 days ago | IN | 0.1 ETH | 0.00038746 | ||||
| Burn Wst ETH | 1593450 | 28 days ago | IN | 0 ETH | 0.00027304 | ||||
| Prove Unknown Va... | 1592880 | 28 days ago | IN | 0 ETH | 0.00040918 | ||||
| Burn St ETH | 1592831 | 28 days ago | IN | 0 ETH | 0.00023295 | ||||
| Burn Shares | 1592609 | 28 days ago | IN | 0 ETH | 0.00022314 | ||||
| Fund | 1592587 | 28 days ago | IN | 0.02 ETH | 0.0001036 | ||||
| Fund | 1592572 | 28 days ago | IN | 0.02 ETH | 0.00009772 | ||||
| Unguaranteed Dep... | 1592327 | 28 days ago | IN | 0 ETH | 0.00026371 | ||||
| Fund | 1592322 | 28 days ago | IN | 2,048 ETH | 0.00010783 | ||||
| Prove Unknown Va... | 1575076 | 31 days ago | IN | 0 ETH | 0.00039555 | ||||
| Grant Role | 1573585 | 31 days ago | IN | 0 ETH | 0.00015051 | ||||
| Prove Unknown Va... | 1573455 | 31 days ago | IN | 0 ETH | 0.00041039 | ||||
| Mint Wst ETH | 1573413 | 31 days ago | IN | 0 ETH | 0.00035679 | ||||
| Mint Shares | 1573405 | 31 days ago | IN | 0 ETH | 0.00064456 | ||||
| Mint Shares | 1573386 | 31 days ago | IN | 0 ETH | 0.00023521 | ||||
| Mint St ETH | 1573375 | 31 days ago | IN | 0 ETH | 0.00029447 | ||||
| Withdraw | 1573358 | 31 days ago | IN | 0 ETH | 0.00022886 | ||||
| Withdraw | 1573353 | 31 days ago | IN | 0 ETH | 0.0002243 |
Latest 25 internal transactions (View All)
Advanced mode:
| Parent Transaction Hash | Method | Block |
From
|
To
|
Amount
|
||
|---|---|---|---|---|---|---|---|
| Burn Shares | 1666811 | 17 days ago | 0 ETH | ||||
| Transfer* | 1666811 | 17 days ago | 0 ETH | ||||
| Burn Shares | 1666811 | 17 days ago | 0 ETH | ||||
| Prove Unknown Va... | 1645316 | 20 days ago | 0 ETH | ||||
| Prove Unknown Va... | 1645316 | 20 days ago | 0 ETH | ||||
| Transfer* | 1640055 | 21 days ago | 36 ETH | ||||
| Withdrawal Crede... | 1640055 | 21 days ago | 0 ETH | ||||
| Transfer | 1640055 | 21 days ago | 36 ETH | ||||
| Transfer | 1640055 | 21 days ago | 36 ETH | ||||
| Withdraw | 1640055 | 21 days ago | 0 ETH | ||||
| Latest Report | 1640055 | 21 days ago | 0 ETH | ||||
| Withdrawable Val... | 1640055 | 21 days ago | 0 ETH | ||||
| DEPOSIT_CONTRACT | 1640055 | 21 days ago | 0 ETH | ||||
| Unguaranteed Dep... | 1640055 | 21 days ago | 0 ETH | ||||
| Fund | 1612111 | 25 days ago | 0.02 ETH | ||||
| Fund | 1612111 | 25 days ago | 0.02 ETH | ||||
| Fund | 1612085 | 25 days ago | 0.02 ETH | ||||
| Fund | 1612085 | 25 days ago | 0.02 ETH | ||||
| Fund | 1611984 | 25 days ago | 0.02 ETH | ||||
| Fund | 1611984 | 25 days ago | 0.02 ETH | ||||
| Trigger Validato... | 1599973 | 27 days ago | 1 wei | ||||
| Trigger Validato... | 1599973 | 27 days ago | 1 wei | ||||
| Trigger Validato... | 1599109 | 27 days ago | 0.1 ETH | ||||
| Trigger Validato... | 1599109 | 27 days ago | 0.1 ETH | ||||
| Burn Shares | 1593450 | 28 days ago | 0 ETH |
Loading...
Loading
Loading...
Loading
Minimal Proxy Contract for 0x7ca203e3b7341341a4a83086780137eb283a9338
Contract Name:
Dashboard
Compiler Version
v0.8.25+commit.b61c2a91
Optimization Enabled:
Yes with 200 runs
Other Settings:
cancun EvmVersion
Contract Source Code (Solidity Standard Json-Input format)
// SPDX-FileCopyrightText: 2025 Lido <[email protected]> // SPDX-License-Identifier: GPL-3.0 // See contracts/COMPILERS.md pragma solidity 0.8.25; import {SafeERC20} from "@openzeppelin/contracts-v5.2/token/ERC20/utils/SafeERC20.sol"; import {IERC20} from "@openzeppelin/contracts-v5.2/token/ERC20/IERC20.sol"; import {RecoverTokens} from "../lib/RecoverTokens.sol"; import {ILido as IStETH} from "contracts/common/interfaces/ILido.sol"; import {IDepositContract} from "contracts/common/interfaces/IDepositContract.sol"; import {IStakingVault} from "../interfaces/IStakingVault.sol"; import {IPredepositGuarantee} from "../interfaces/IPredepositGuarantee.sol"; import {NodeOperatorFee} from "./NodeOperatorFee.sol"; import {VaultHub} from "../VaultHub.sol"; interface IWstETH is IERC20 { function wrap(uint256 _stETHAmount) external returns (uint256); function unwrap(uint256 _wstETHAmount) external returns (uint256); } /** * @title Dashboard * @notice This contract is a UX-layer for StakingVault and meant to be used as its owner. * This contract improves the vault UX by bundling all functions from the StakingVault and VaultHub * in this single contract. It provides administrative functions for managing the StakingVault, * including funding, withdrawing, minting, burning, and rebalancing operations. */ contract Dashboard is NodeOperatorFee { /// @dev 0xb694d4d19c77484e8f232470d9bf7e10450638db998b577a833d46df71fb6d97 bytes32 public constant COLLECT_VAULT_ERC20_ROLE = keccak256("vaults.Dashboard.CollectVaultERC20"); /** * @notice The stETH token contract */ IStETH public immutable STETH; /** * @notice The wstETH token contract */ IWstETH public immutable WSTETH; /** * @notice Slot for the fund-on-receive flag * keccak256("vaults.Dashboard.fundOnReceive") */ bytes32 public constant FUND_ON_RECEIVE_FLAG_SLOT = 0x7408b7b034fda7051615c19182918ecb91d753231cffd86f81a45d996d63e038; /** * @notice The PDG policy modes. * "STRICT": deposits require the full PDG process. * "ALLOW_PROVE": allows the node operator to prove unknown validators to PDG. * "ALLOW_DEPOSIT_AND_PROVE": allows the node operator to perform unguaranteed deposits * (bypassing the predeposit requirement) and proving unknown validators. */ enum PDGPolicy { STRICT, ALLOW_PROVE, ALLOW_DEPOSIT_AND_PROVE } /** * @notice Current active PDG policy set by `DEFAULT_ADMIN_ROLE`. */ PDGPolicy public pdgPolicy = PDGPolicy.STRICT; /** * @notice Constructor sets the stETH, and WSTETH token addresses, * and passes the address of the vault hub up the inheritance chain. * @param _stETH Address of the stETH token contract. * @param _wstETH Address of the wstETH token contract. * @param _vaultHub Address of the vault hub contract. * @param _lidoLocator Address of the Lido locator contract. */ constructor( address _stETH, address _wstETH, address _vaultHub, address _lidoLocator ) NodeOperatorFee(_vaultHub, _lidoLocator) { _requireNotZero(_stETH); _requireNotZero(_wstETH); // stETH and wstETH are cached as immutable to save gas for main operations STETH = IStETH(_stETH); WSTETH = IWstETH(_wstETH); } /** * @notice Calls the parent's initializer and approves the max allowance for WSTETH for gas savings * @param _defaultAdmin The address of the default admin * @param _nodeOperatorManager The address of the node operator manager * @param _nodeOperatorFeeRecipient The address of the node operator fee recipient * @param _nodeOperatorFeeBP The node operator fee in basis points * @param _confirmExpiry The confirmation expiry time in seconds */ function initialize( address _defaultAdmin, address _nodeOperatorManager, address _nodeOperatorFeeRecipient, uint256 _nodeOperatorFeeBP, uint256 _confirmExpiry ) external { super._initialize( _defaultAdmin, _nodeOperatorManager, _nodeOperatorFeeRecipient, _nodeOperatorFeeBP, _confirmExpiry ); // reduces gas cost for `mintWsteth` // invariant: dashboard does not hold stETH on its balance STETH.approve(address(WSTETH), type(uint256).max); } // ==================== View Functions ==================== /** * @notice Returns the vault connection data for the staking vault. * @return VaultConnection struct containing vault data */ function vaultConnection() public view returns (VaultHub.VaultConnection memory) { return VAULT_HUB.vaultConnection(address(_stakingVault())); } /** * @notice Returns the number of stETH shares minted */ function liabilityShares() public view returns (uint256) { return VAULT_HUB.liabilityShares(address(_stakingVault())); } /** * @notice Returns the total value of the vault in ether. */ function totalValue() external view returns (uint256) { return VAULT_HUB.totalValue(address(_stakingVault())); } /** * @notice Returns the locked amount of ether for the vault */ function locked() external view returns (uint256) { return VAULT_HUB.locked(address(_stakingVault())); } /** * @notice Returns the amount of shares to burn to restore vault healthiness or to cover redemptions and the * amount of outstanding Lido fees * @return sharesToBurn amount of shares to burn or to rebalance * @return feesToSettle amount of Lido fees to be settled */ function obligations() external view returns (uint256 sharesToBurn, uint256 feesToSettle) { (sharesToBurn, feesToSettle) = VAULT_HUB.obligations(address(_stakingVault())); } /** * @notice Returns the amount of shares to rebalance to restore vault healthiness or to cover redemptions * @dev returns UINT256_MAX if it's impossible to make the vault healthy using rebalance */ function healthShortfallShares() external view returns (uint256) { return VAULT_HUB.healthShortfallShares(address(_stakingVault())); } /** * @notice Returns the amount of ether required to cover obligations shortfall of the vault * @dev returns UINT256_MAX if it's impossible to cover obligations shortfall * @dev NB: obligationsShortfallValue includes healthShortfallShares converted to ether and any unsettled Lido fees * in case they are greater than the minimum beacon deposit */ function obligationsShortfallValue() external view returns (uint256) { return VAULT_HUB.obligationsShortfallValue(address(_stakingVault())); } /** * @notice Returns the amount of ether that is locked on the vault only as a reserve. * @dev There is no way to mint stETH for it (it includes connection deposit and slashing reserve) */ function minimalReserve() public view returns (uint256) { return VAULT_HUB.vaultRecord(address(_stakingVault())).minimalReserve; } /** * @notice Returns the max total lockable amount of ether for the vault (excluding the Lido and node operator fees) */ function maxLockableValue() external view returns (uint256) { uint256 maxLockableValue_ = VAULT_HUB.maxLockableValue(address(_stakingVault())); uint256 nodeOperatorFee = accruedFee(); return maxLockableValue_ > nodeOperatorFee ? maxLockableValue_ - nodeOperatorFee : 0; } /** * @notice Returns the overall capacity for stETH shares that can be minted by the vault */ function totalMintingCapacityShares() external view returns (uint256) { return _totalMintingCapacityShares(-int256(accruedFee())); } /** * @notice Returns the remaining capacity for stETH shares that can be minted * by the vault if additional ether is funded * @param _etherToFund the amount of ether to be funded, can be zero * @return the number of shares that can be minted using additional ether */ function remainingMintingCapacityShares(uint256 _etherToFund) public view returns (uint256) { int256 deltaValue = int256(_etherToFund) - int256(accruedFee()); uint256 vaultTotalMintingCapacityShares = _totalMintingCapacityShares(deltaValue); uint256 vaultLiabilityShares = liabilityShares(); if (vaultTotalMintingCapacityShares <= vaultLiabilityShares) return 0; return vaultTotalMintingCapacityShares - vaultLiabilityShares; } /** * @notice Returns the amount of ether that can be instantly withdrawn from the staking vault. * @dev This is the amount of ether that is not locked in the StakingVault and not reserved for fees and obligations. */ function withdrawableValue() public view returns (uint256) { uint256 withdrawable = VAULT_HUB.withdrawableValue(address(_stakingVault())); uint256 nodeOperatorFee = accruedFee(); return withdrawable > nodeOperatorFee ? withdrawable - nodeOperatorFee : 0; } // ==================== Vault Management Functions ==================== /** * @dev Automatically funds the staking vault with ether */ receive() external payable { if (_shouldFundOnReceive()) _fund(msg.value); } /** * @notice Transfers the ownership of the underlying StakingVault from this contract to a new owner * without disconnecting it from the hub * @param _newOwner Address of the new owner. * @return bool True if the ownership transfer was executed, false if pending for confirmation */ function transferVaultOwnership(address _newOwner) external returns (bool) { return _transferVaultOwnership(_newOwner); } /** * @notice Disconnects the underlying StakingVault from the hub and passing its ownership to Dashboard. * After receiving the final report, one can call reconnectToVaultHub() to reconnect to the hub * or abandonDashboard() to transfer the ownership to a new owner. */ function voluntaryDisconnect() external { disburseFee(); _voluntaryDisconnect(); } /** * @notice Accepts the ownership over the disconnected StakingVault transferred from VaultHub * and immediately passes it to a new pending owner. This new owner will have to accept the ownership * on the StakingVault contract. * Resets the settled growth to 0 to encourage correction before reconnection. * @param _newOwner The address to transfer the StakingVault ownership to. */ function abandonDashboard(address _newOwner) external { if (VAULT_HUB.isVaultConnected(address(_stakingVault()))) revert ConnectedToVaultHub(); if (_newOwner == address(this)) revert DashboardNotAllowed(); if (settledGrowth != 0) _setSettledGrowth(0); _acceptOwnership(); _transferOwnership(_newOwner); } /** * @notice Accepts the ownership over the StakingVault and connects to VaultHub. Can be called to reconnect * to the hub after voluntaryDisconnect() * @param _currentSettledGrowth The current settled growth value to verify against the stored one */ function reconnectToVaultHub(uint256 _currentSettledGrowth) external { _acceptOwnership(); connectToVaultHub(_currentSettledGrowth); } /** * @notice Connects to VaultHub, transferring underlying StakingVault ownership to VaultHub. * @param _currentSettledGrowth The current settled growth value to verify against the stored one */ function connectToVaultHub(uint256 _currentSettledGrowth) public payable { if (settledGrowth != int256(_currentSettledGrowth)) { revert SettledGrowthMismatch(); } if (msg.value > 0) _stakingVault().fund{value: msg.value}(); _transferOwnership(address(VAULT_HUB)); VAULT_HUB.connectVault(address(_stakingVault())); } /** * @notice Changes the tier of the vault and connects to VaultHub * @param _tierId The tier to change to * @param _requestedShareLimit The requested share limit * @param _currentSettledGrowth The current settled growth value to verify against the stored one */ function connectAndAcceptTier(uint256 _tierId, uint256 _requestedShareLimit, uint256 _currentSettledGrowth) external payable { connectToVaultHub(_currentSettledGrowth); if (!_changeTier(_tierId, _requestedShareLimit)) { revert TierChangeNotConfirmed(); } } /** * @notice Funds the staking vault with ether */ function fund() external payable { _fund(msg.value); } /** * @notice Withdraws ether from the staking vault to a recipient * @param _recipient Address of the recipient * @param _ether Amount of ether to withdraw */ function withdraw(address _recipient, uint256 _ether) external { uint256 withdrawableEther = withdrawableValue(); if (_ether > withdrawableEther) { revert ExceedsWithdrawable(_ether, withdrawableEther); } _withdraw(_recipient, _ether); } /** * @notice Mints stETH shares backed by the vault to the recipient. * @param _recipient Address of the recipient * @param _amountOfShares Amount of stETH shares to mint */ function mintShares(address _recipient, uint256 _amountOfShares) external payable fundable { _mintSharesWithinMintingCapacity(_recipient, _amountOfShares); } /** * @notice Mints stETH tokens backed by the vault to the recipient. * !NB: this will revert with `ZeroArgument()` if the amount of stETH is less than 1 share * @param _recipient Address of the recipient * @param _amountOfStETH Amount of stETH to mint */ function mintStETH(address _recipient, uint256 _amountOfStETH) external payable fundable { _mintSharesWithinMintingCapacity(_recipient, _getSharesByPooledEth(_amountOfStETH)); } /** * @notice Mints wstETH tokens backed by the vault to a recipient. * @param _recipient Address of the recipient * @param _amountOfWstETH Amount of tokens to mint */ function mintWstETH(address _recipient, uint256 _amountOfWstETH) external payable fundable { _mintSharesWithinMintingCapacity(address(this), _amountOfWstETH); uint256 mintedStETH = STETH.getPooledEthBySharesRoundUp(_amountOfWstETH); uint256 wrappedWstETH = WSTETH.wrap(mintedStETH); SafeERC20.safeTransfer(WSTETH, _recipient, wrappedWstETH); } /** * @notice Burns stETH shares from the sender backed by the vault. * Expects corresponding amount of stETH approved to this contract. * @param _amountOfShares Amount of stETH shares to burn */ function burnShares(uint256 _amountOfShares) external { STETH.transferSharesFrom(msg.sender, address(VAULT_HUB), _amountOfShares); _burnShares(_amountOfShares); } /** * @notice Burns stETH tokens from the sender backed by the vault. Expects stETH amount approved to this contract. * !NB: this will revert with `ZeroArgument()` if the amount of stETH is less than 1 share * @param _amountOfStETH Amount of stETH tokens to burn */ function burnStETH(uint256 _amountOfStETH) external { _burnStETH(_amountOfStETH); } /** * @notice Burns wstETH tokens from the sender backed by the vault. Expects wstETH amount approved to this contract. * @dev !NB: this will revert with `ZeroArgument()` on 1 wei of wstETH due to rounding inside wstETH unwrap method * @param _amountOfWstETH Amount of wstETH tokens to burn */ function burnWstETH(uint256 _amountOfWstETH) external { _burnWstETH(_amountOfWstETH); } /** * @notice Rebalances the vault's position by transferring ether corresponding to the passed `_shares` * number to Lido Core and writing it off from the vault's liability. * @param _shares amount of shares to rebalance */ function rebalanceVaultWithShares(uint256 _shares) external { _rebalanceVault(_shares); } /** * @notice Rebalances the vault by transferring ether and writing off the respective shares amount fro the vault's * liability * @param _ether amount of ether to rebalance * @dev the amount of ether transferred can differ a bit because of the rounding */ function rebalanceVaultWithEther(uint256 _ether) external payable fundable { _rebalanceVault(_getSharesByPooledEth(_ether)); } /** * @notice Changes the PDG policy. PDGPolicy regulates the possibility of deposits without PredepositGuarantee * @param _pdgPolicy new PDG policy */ function setPDGPolicy(PDGPolicy _pdgPolicy) external onlyRoleMemberOrAdmin(DEFAULT_ADMIN_ROLE) { if (_pdgPolicy == pdgPolicy) revert PDGPolicyAlreadyActive(); pdgPolicy = _pdgPolicy; emit PDGPolicyEnacted(_pdgPolicy); } /** * @notice Withdraws ether from vault and deposits directly to provided validators bypassing the default PDG process, * allowing validators to be proven post-factum via `proveUnknownValidatorsToPDG` * clearing them for future deposits via `PDG.topUpValidators` * @param _deposits array of IStakingVault.Deposit structs containing deposit data * @return totalAmount total amount of ether deposited to beacon chain * @dev requires the PDG policy set to `ALLOW_DEPOSIT_AND_PROVE` * @dev requires the caller to have the `NODE_OPERATOR_UNGUARANTEED_DEPOSIT_ROLE` * @dev Warning! vulnerable to deposit frontrunning and requires putting trust on the node operator */ function unguaranteedDepositToBeaconChain( IStakingVault.Deposit[] calldata _deposits ) external returns (uint256 totalAmount) { if (pdgPolicy != PDGPolicy.ALLOW_DEPOSIT_AND_PROVE) revert ForbiddenByPDGPolicy(); IStakingVault stakingVault_ = _stakingVault(); IDepositContract depositContract = stakingVault_.DEPOSIT_CONTRACT(); for (uint256 i = 0; i < _deposits.length; i++) { totalAmount += _deposits[i].amount; } uint256 withdrawableEther = withdrawableValue(); if (totalAmount > withdrawableEther) { revert ExceedsWithdrawable(totalAmount, withdrawableEther); } _disableFundOnReceive(); _withdrawForUnguaranteedDepositToBeaconChain(totalAmount); // Instead of relying on auto-reset at the end of the transaction, // re-enable fund-on-receive manually to restore the default receive() behavior in the same transaction _enableFundOnReceive(); _addFeeExemption(totalAmount); bytes memory withdrawalCredentials = bytes.concat(stakingVault_.withdrawalCredentials()); IStakingVault.Deposit calldata deposit; for (uint256 i = 0; i < _deposits.length; i++) { deposit = _deposits[i]; depositContract.deposit{value: deposit.amount}( deposit.pubkey, withdrawalCredentials, deposit.signature, deposit.depositDataRoot ); } emit UnguaranteedDeposits(address(stakingVault_), _deposits.length, totalAmount); } /** * @notice Proves validators with correct vault WC if they are unknown to PDG * @param _witnesses array of IPredepositGuarantee.ValidatorWitness structs containing proof data for validators * @dev requires the PDG policy set to `ALLOW_PROVE` or `ALLOW_DEPOSIT_AND_PROVE` * @dev requires the caller to have the `NODE_OPERATOR_PROVE_UNKNOWN_VALIDATOR_ROLE` */ function proveUnknownValidatorsToPDG(IPredepositGuarantee.ValidatorWitness[] calldata _witnesses) external { if (pdgPolicy == PDGPolicy.STRICT) revert ForbiddenByPDGPolicy(); _proveUnknownValidatorsToPDG(_witnesses); } /** * @notice Recovers ERC20 tokens or ether from the dashboard contract to the recipient * @param _token Address of the token to recover or 0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee for ether (EIP-7528) * @param _recipient Address of the recovery recipient * @param _amount Amount of tokens or ether to recover */ function recoverERC20( address _token, address _recipient, uint256 _amount ) external onlyRoleMemberOrAdmin(DEFAULT_ADMIN_ROLE) { _requireNotZero(_token); _requireNotZero(_recipient); _requireNotZero(_amount); if (_token == RecoverTokens.ETH) { RecoverTokens._recoverEth(_recipient, _amount); } else { RecoverTokens._recoverERC20(_token, _recipient, _amount); } } /** * @notice Collects ERC20 tokens from vault contract balance to the recipient * @param _token Address of the token to collect * @param _recipient Address of the recipient * @param _amount Amount of tokens to collect * @dev will revert on EIP-7528 ETH address with EthCollectionNotAllowed() or on zero arguments with ZeroArgument() */ function collectERC20FromVault( address _token, address _recipient, uint256 _amount ) external onlyRoleMemberOrAdmin(COLLECT_VAULT_ERC20_ROLE) { VAULT_HUB.collectERC20FromVault(address(_stakingVault()), _token, _recipient, _amount); } /** * @notice Pauses beacon chain deposits on the StakingVault. */ function pauseBeaconChainDeposits() external { _pauseBeaconChainDeposits(); } /** * @notice Resumes beacon chain deposits on the StakingVault. */ function resumeBeaconChainDeposits() external { _resumeBeaconChainDeposits(); } /** * @notice Signals to node operators that specific validators should exit from the beacon chain. It DOES NOT * directly trigger the exit - node operators must monitor for request events and handle the exits. * @param _pubkeys Concatenated validator public keys (48 bytes each). * @dev Emits `ValidatorExitRequested` event for each validator public key through the `StakingVault`. * This is a voluntary exit request - node operators can choose whether to act on it or not. */ function requestValidatorExit(bytes calldata _pubkeys) external { _requestValidatorExit(_pubkeys); } /** * @notice Initiates a withdrawal from validator(s) on the beacon chain using EIP-7002 triggerable withdrawals * Both partial withdrawals (disabled for if vault is unhealthy) and full validator exits are supported. * @param _pubkeys Concatenated validator public keys (48 bytes each). * @param _amountsInGwei Withdrawal amounts in Gwei for each validator key. Must match _pubkeys length. * Set amount to 0 for a full validator exit. For partial withdrawals, amounts may be trimmed to keep * MIN_ACTIVATION_BALANCE on the validator to avoid deactivation. * @param _refundRecipient Address to receive any fee refunds * @dev A withdrawal fee must be paid via msg.value. * You can use `StakingVault.calculateValidatorWithdrawalFee()` to calculate the approximate fee amount but * it's accurate only for the current block. The fee may change when the tx is included, so it's recommended * to send some surplus. The exact amount required will be paid and the excess will be refunded to the * `_refundRecipient` address. The fee required can grow exponentially, so limit msg.value wisely to avoid * overspending. */ function triggerValidatorWithdrawals( bytes calldata _pubkeys, uint64[] calldata _amountsInGwei, address _refundRecipient ) external payable { _triggerValidatorWithdrawals(_pubkeys, _amountsInGwei, _refundRecipient); } /** * @notice Requests a change of tier on the OperatorGrid. * @param _tierId The tier to change to. * @param _requestedShareLimit The requested share limit. * @return bool True if the tier change was executed, false if pending for confirmation. * @dev Tier change confirmation logic: * - Both vault owner (via this function) AND node operator (via OperatorGrid) confirmations are always required * - First call returns false (pending), second call with both confirmations completes the tier change * - Confirmations expire after the configured period (default: 1 day) */ function changeTier(uint256 _tierId, uint256 _requestedShareLimit) external returns (bool) { return _changeTier(_tierId, _requestedShareLimit); } /** * @notice Requests a sync of tier on the OperatorGrid. * @return bool True if the tier sync was executed, false if pending for confirmation. * @dev Tier sync confirmation logic: * - Both vault owner (via this function) AND node operator (via OperatorGrid) confirmations are required * - First call returns false (pending), second call with both confirmations completes the operation * - Confirmations expire after the configured period (default: 1 day) */ function syncTier() external returns (bool) { return _syncTier(); } /** * @notice Requests a change of share limit on the OperatorGrid. * @param _requestedShareLimit The requested share limit. * @return bool True if the share limit change was executed, false if pending for confirmation. * @dev Share limit update confirmation logic: * - Both vault owner (via this function) AND node operator (via OperatorGrid) confirmations required * - First call returns false (pending), second call with node operator confirmation completes the operation * - Confirmations expire after the configured period (default: 1 day) */ function updateShareLimit(uint256 _requestedShareLimit) external returns (bool) { return _updateVaultShareLimit(_requestedShareLimit); } // ==================== Internal Functions ==================== /** * @dev Modifier to fund the staking vault if msg.value > 0 */ modifier fundable() { if (msg.value > 0) { _fund(msg.value); } _; } /** * @notice Mints shares within the mintable capacity, * and reverts if the resulting backing is greater than the mintable capacity. * @param _recipient The address of the recipient. * @param _amountOfShares The amount of shares to mint. */ function _mintSharesWithinMintingCapacity(address _recipient, uint256 _amountOfShares) internal { uint256 remainingShares = remainingMintingCapacityShares(0); if (_amountOfShares > remainingShares) revert ExceedsMintingCapacity(_amountOfShares, remainingShares); _mintShares(_recipient, _amountOfShares); } /** * @dev Burns stETH tokens from the sender backed by the vault * @param _amountOfStETH Amount of tokens to burn */ function _burnStETH(uint256 _amountOfStETH) internal { uint256 _amountOfShares = _getSharesByPooledEth(_amountOfStETH); STETH.transferSharesFrom(msg.sender, address(VAULT_HUB), _amountOfShares); _burnShares(_amountOfShares); } /** * @dev Burns wstETH tokens from the sender backed by the vault * @param _amountOfWstETH Amount of tokens to burn */ function _burnWstETH(uint256 _amountOfWstETH) internal { SafeERC20.safeTransferFrom(WSTETH, msg.sender, address(this), _amountOfWstETH); uint256 unwrappedStETH = WSTETH.unwrap(_amountOfWstETH); uint256 unwrappedShares = _getSharesByPooledEth(unwrappedStETH); STETH.transferShares(address(VAULT_HUB), unwrappedShares); _burnShares(unwrappedShares); } /// @notice Calculates the total number of shares that is possible to mint on the vault /// @dev the delta value is the amount of ether to add or subtract from the total value of the vault function _totalMintingCapacityShares(int256 _deltaValue) internal view returns (uint256) { return VAULT_HUB.totalMintingCapacityShares(address(_stakingVault()), _deltaValue); } /// @notice Converts the given amount of stETH to shares function _getSharesByPooledEth(uint256 _amountOfStETH) internal view returns (uint256) { return STETH.getSharesByPooledEth(_amountOfStETH); } // @dev The logic is inverted, 0 means fund-on-receive is enabled, // so that fund-on-receive is enabled by default function _shouldFundOnReceive() internal view returns (bool shouldFund) { assembly { shouldFund := iszero(tload(FUND_ON_RECEIVE_FLAG_SLOT)) } } function _enableFundOnReceive() internal { assembly { tstore(FUND_ON_RECEIVE_FLAG_SLOT, 0) } } function _disableFundOnReceive() internal { assembly { tstore(FUND_ON_RECEIVE_FLAG_SLOT, 1) } } /** * @dev Withdraws ether from vault to this contract for unguaranteed deposit to validators * Requires the caller to have the `NODE_OPERATOR_UNGUARANTEED_DEPOSIT_ROLE`. */ function _withdrawForUnguaranteedDepositToBeaconChain( uint256 _ether ) internal onlyRoleMemberOrAdmin(NODE_OPERATOR_UNGUARANTEED_DEPOSIT_ROLE) { VAULT_HUB.withdraw(address(_stakingVault()), address(this), _ether); } /** * @dev Proves validators unknown to PDG that have correct vault WC * Requires the caller to have the `NODE_OPERATOR_PROVE_UNKNOWN_VALIDATOR_ROLE`. */ function _proveUnknownValidatorsToPDG( IPredepositGuarantee.ValidatorWitness[] calldata _witnesses ) internal onlyRoleMemberOrAdmin(NODE_OPERATOR_PROVE_UNKNOWN_VALIDATOR_ROLE) { for (uint256 i = 0; i < _witnesses.length; i++) { VAULT_HUB.proveUnknownValidatorToPDG(address(_stakingVault()), _witnesses[i]); } } // ==================== Events ==================== /** * @notice Emitted when ether was withdrawn from the staking vault and deposited to validators directly bypassing PDG * @param stakingVault the address of owned staking vault * @param deposits the number of deposits * @param totalAmount the total amount of ether deposited to beacon chain */ event UnguaranteedDeposits(address indexed stakingVault, uint256 deposits, uint256 totalAmount); /** * @notice Emitted when the PDG policy is updated. */ event PDGPolicyEnacted(PDGPolicy pdgPolicy); // ==================== Errors ==================== /** * @notice Emitted when the withdrawable amount of ether is exceeded * @param amount The amount of ether that was attempted to be withdrawn * @param withdrawableValue The amount of withdrawable ether available */ error ExceedsWithdrawable(uint256 amount, uint256 withdrawableValue); /** * @notice Error thrown when minting capacity is exceeded */ error ExceedsMintingCapacity(uint256 requestedShares, uint256 remainingShares); /** * @notice Error when the StakingVault is still connected to the VaultHub. */ error ConnectedToVaultHub(); /** * @notice Error thrown when attempting to connect to VaultHub without confirmed tier change */ error TierChangeNotConfirmed(); /** * @notice Error when attempting to abandon the Dashboard contract itself. */ error DashboardNotAllowed(); /** * @notice Error when attempting to set the same PDG policy that is already active. */ error PDGPolicyAlreadyActive(); /** * @notice Error when attempting to perform an operation that is not allowed * by the current active PDG policy. */ error ForbiddenByPDGPolicy(); }
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (token/ERC20/IERC20.sol)
pragma solidity ^0.8.0;
/**
* @dev Interface of the ERC20 standard as defined in the EIP.
*/
interface IERC20 {
/**
* @dev Returns the amount of tokens in existence.
*/
function totalSupply() external view returns (uint256);
/**
* @dev Returns the amount of tokens owned by `account`.
*/
function balanceOf(address account) external view returns (uint256);
/**
* @dev Moves `amount` tokens from the caller's account to `recipient`.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transfer(address recipient, uint256 amount) external returns (bool);
/**
* @dev Returns the remaining number of tokens that `spender` will be
* allowed to spend on behalf of `owner` through {transferFrom}. This is
* zero by default.
*
* This value changes when {approve} or {transferFrom} are called.
*/
function allowance(address owner, address spender) external view returns (uint256);
/**
* @dev Sets `amount` as the allowance of `spender` over the caller's tokens.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* IMPORTANT: Beware that changing an allowance with this method brings the risk
* that someone may use both the old and the new allowance by unfortunate
* transaction ordering. One possible solution to mitigate this race
* condition is to first reduce the spender's allowance to 0 and set the
* desired value afterwards:
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
*
* Emits an {Approval} event.
*/
function approve(address spender, uint256 amount) external returns (bool);
/**
* @dev Moves `amount` tokens from `sender` to `recipient` using the
* allowance mechanism. `amount` is then deducted from the caller's
* allowance.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transferFrom(
address sender,
address recipient,
uint256 amount
) external returns (bool);
/**
* @dev Emitted when `value` tokens are moved from one account (`from`) to
* another (`to`).
*
* Note that `value` may be zero.
*/
event Transfer(address indexed from, address indexed to, uint256 value);
/**
* @dev Emitted when the allowance of a `spender` for an `owner` is set by
* a call to {approve}. `value` is the new allowance.
*/
event Approval(address indexed owner, address indexed spender, uint256 value);
}// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (access/AccessControl.sol)
pragma solidity ^0.8.20;
import {IAccessControl} from "./IAccessControl.sol";
import {Context} from "../utils/Context.sol";
import {ERC165} from "../utils/introspection/ERC165.sol";
/**
* @dev Contract module that allows children to implement role-based access
* control mechanisms. This is a lightweight version that doesn't allow enumerating role
* members except through off-chain means by accessing the contract event logs. Some
* applications may benefit from on-chain enumerability, for those cases see
* {AccessControlEnumerable}.
*
* Roles are referred to by their `bytes32` identifier. These should be exposed
* in the external API and be unique. The best way to achieve this is by
* using `public constant` hash digests:
*
* ```solidity
* bytes32 public constant MY_ROLE = keccak256("MY_ROLE");
* ```
*
* Roles can be used to represent a set of permissions. To restrict access to a
* function call, use {hasRole}:
*
* ```solidity
* function foo() public {
* require(hasRole(MY_ROLE, msg.sender));
* ...
* }
* ```
*
* Roles can be granted and revoked dynamically via the {grantRole} and
* {revokeRole} functions. Each role has an associated admin role, and only
* accounts that have a role's admin role can call {grantRole} and {revokeRole}.
*
* By default, the admin role for all roles is `DEFAULT_ADMIN_ROLE`, which means
* that only accounts with this role will be able to grant or revoke other
* roles. More complex role relationships can be created by using
* {_setRoleAdmin}.
*
* WARNING: The `DEFAULT_ADMIN_ROLE` is also its own admin: it has permission to
* grant and revoke this role. Extra precautions should be taken to secure
* accounts that have been granted it. We recommend using {AccessControlDefaultAdminRules}
* to enforce additional security measures for this role.
*/
abstract contract AccessControl is Context, IAccessControl, ERC165 {
struct RoleData {
mapping(address account => bool) hasRole;
bytes32 adminRole;
}
mapping(bytes32 role => RoleData) private _roles;
bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00;
/**
* @dev Modifier that checks that an account has a specific role. Reverts
* with an {AccessControlUnauthorizedAccount} error including the required role.
*/
modifier onlyRole(bytes32 role) {
_checkRole(role);
_;
}
/**
* @dev See {IERC165-supportsInterface}.
*/
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
return interfaceId == type(IAccessControl).interfaceId || super.supportsInterface(interfaceId);
}
/**
* @dev Returns `true` if `account` has been granted `role`.
*/
function hasRole(bytes32 role, address account) public view virtual returns (bool) {
return _roles[role].hasRole[account];
}
/**
* @dev Reverts with an {AccessControlUnauthorizedAccount} error if `_msgSender()`
* is missing `role`. Overriding this function changes the behavior of the {onlyRole} modifier.
*/
function _checkRole(bytes32 role) internal view virtual {
_checkRole(role, _msgSender());
}
/**
* @dev Reverts with an {AccessControlUnauthorizedAccount} error if `account`
* is missing `role`.
*/
function _checkRole(bytes32 role, address account) internal view virtual {
if (!hasRole(role, account)) {
revert AccessControlUnauthorizedAccount(account, role);
}
}
/**
* @dev Returns the admin role that controls `role`. See {grantRole} and
* {revokeRole}.
*
* To change a role's admin, use {_setRoleAdmin}.
*/
function getRoleAdmin(bytes32 role) public view virtual returns (bytes32) {
return _roles[role].adminRole;
}
/**
* @dev Grants `role` to `account`.
*
* If `account` had not been already granted `role`, emits a {RoleGranted}
* event.
*
* Requirements:
*
* - the caller must have ``role``'s admin role.
*
* May emit a {RoleGranted} event.
*/
function grantRole(bytes32 role, address account) public virtual onlyRole(getRoleAdmin(role)) {
_grantRole(role, account);
}
/**
* @dev Revokes `role` from `account`.
*
* If `account` had been granted `role`, emits a {RoleRevoked} event.
*
* Requirements:
*
* - the caller must have ``role``'s admin role.
*
* May emit a {RoleRevoked} event.
*/
function revokeRole(bytes32 role, address account) public virtual onlyRole(getRoleAdmin(role)) {
_revokeRole(role, account);
}
/**
* @dev Revokes `role` from the calling account.
*
* Roles are often managed via {grantRole} and {revokeRole}: this function's
* purpose is to provide a mechanism for accounts to lose their privileges
* if they are compromised (such as when a trusted device is misplaced).
*
* If the calling account had been revoked `role`, emits a {RoleRevoked}
* event.
*
* Requirements:
*
* - the caller must be `callerConfirmation`.
*
* May emit a {RoleRevoked} event.
*/
function renounceRole(bytes32 role, address callerConfirmation) public virtual {
if (callerConfirmation != _msgSender()) {
revert AccessControlBadConfirmation();
}
_revokeRole(role, callerConfirmation);
}
/**
* @dev Sets `adminRole` as ``role``'s admin role.
*
* Emits a {RoleAdminChanged} event.
*/
function _setRoleAdmin(bytes32 role, bytes32 adminRole) internal virtual {
bytes32 previousAdminRole = getRoleAdmin(role);
_roles[role].adminRole = adminRole;
emit RoleAdminChanged(role, previousAdminRole, adminRole);
}
/**
* @dev Attempts to grant `role` to `account` and returns a boolean indicating if `role` was granted.
*
* Internal function without access restriction.
*
* May emit a {RoleGranted} event.
*/
function _grantRole(bytes32 role, address account) internal virtual returns (bool) {
if (!hasRole(role, account)) {
_roles[role].hasRole[account] = true;
emit RoleGranted(role, account, _msgSender());
return true;
} else {
return false;
}
}
/**
* @dev Attempts to revoke `role` to `account` and returns a boolean indicating if `role` was revoked.
*
* Internal function without access restriction.
*
* May emit a {RoleRevoked} event.
*/
function _revokeRole(bytes32 role, address account) internal virtual returns (bool) {
if (hasRole(role, account)) {
_roles[role].hasRole[account] = false;
emit RoleRevoked(role, account, _msgSender());
return true;
} else {
return false;
}
}
}// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (access/extensions/AccessControlEnumerable.sol)
pragma solidity ^0.8.20;
import {IAccessControlEnumerable} from "./IAccessControlEnumerable.sol";
import {AccessControl} from "../AccessControl.sol";
import {EnumerableSet} from "../../utils/structs/EnumerableSet.sol";
/**
* @dev Extension of {AccessControl} that allows enumerating the members of each role.
*/
abstract contract AccessControlEnumerable is IAccessControlEnumerable, AccessControl {
using EnumerableSet for EnumerableSet.AddressSet;
mapping(bytes32 role => EnumerableSet.AddressSet) private _roleMembers;
/**
* @dev See {IERC165-supportsInterface}.
*/
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
return interfaceId == type(IAccessControlEnumerable).interfaceId || super.supportsInterface(interfaceId);
}
/**
* @dev Returns one of the accounts that have `role`. `index` must be a
* value between 0 and {getRoleMemberCount}, non-inclusive.
*
* Role bearers are not sorted in any particular way, and their ordering may
* change at any point.
*
* WARNING: When using {getRoleMember} and {getRoleMemberCount}, make sure
* you perform all queries on the same block. See the following
* https://forum.openzeppelin.com/t/iterating-over-elements-on-enumerableset-in-openzeppelin-contracts/2296[forum post]
* for more information.
*/
function getRoleMember(bytes32 role, uint256 index) public view virtual returns (address) {
return _roleMembers[role].at(index);
}
/**
* @dev Returns the number of accounts that have `role`. Can be used
* together with {getRoleMember} to enumerate all bearers of a role.
*/
function getRoleMemberCount(bytes32 role) public view virtual returns (uint256) {
return _roleMembers[role].length();
}
/**
* @dev Return all accounts that have `role`
*
* WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed
* to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that
* this function has an unbounded cost, and using it as part of a state-changing function may render the function
* uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.
*/
function getRoleMembers(bytes32 role) public view virtual returns (address[] memory) {
return _roleMembers[role].values();
}
/**
* @dev Overload {AccessControl-_grantRole} to track enumerable memberships
*/
function _grantRole(bytes32 role, address account) internal virtual override returns (bool) {
bool granted = super._grantRole(role, account);
if (granted) {
_roleMembers[role].add(account);
}
return granted;
}
/**
* @dev Overload {AccessControl-_revokeRole} to track enumerable memberships
*/
function _revokeRole(bytes32 role, address account) internal virtual override returns (bool) {
bool revoked = super._revokeRole(role, account);
if (revoked) {
_roleMembers[role].remove(account);
}
return revoked;
}
}// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (access/extensions/IAccessControlEnumerable.sol)
pragma solidity ^0.8.20;
import {IAccessControl} from "../IAccessControl.sol";
/**
* @dev External interface of AccessControlEnumerable declared to support ERC-165 detection.
*/
interface IAccessControlEnumerable is IAccessControl {
/**
* @dev Returns one of the accounts that have `role`. `index` must be a
* value between 0 and {getRoleMemberCount}, non-inclusive.
*
* Role bearers are not sorted in any particular way, and their ordering may
* change at any point.
*
* WARNING: When using {getRoleMember} and {getRoleMemberCount}, make sure
* you perform all queries on the same block. See the following
* https://forum.openzeppelin.com/t/iterating-over-elements-on-enumerableset-in-openzeppelin-contracts/2296[forum post]
* for more information.
*/
function getRoleMember(bytes32 role, uint256 index) external view returns (address);
/**
* @dev Returns the number of accounts that have `role`. Can be used
* together with {getRoleMember} to enumerate all bearers of a role.
*/
function getRoleMemberCount(bytes32 role) external view returns (uint256);
}// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (access/IAccessControl.sol)
pragma solidity ^0.8.20;
/**
* @dev External interface of AccessControl declared to support ERC-165 detection.
*/
interface IAccessControl {
/**
* @dev The `account` is missing a role.
*/
error AccessControlUnauthorizedAccount(address account, bytes32 neededRole);
/**
* @dev The caller of a function is not the expected one.
*
* NOTE: Don't confuse with {AccessControlUnauthorizedAccount}.
*/
error AccessControlBadConfirmation();
/**
* @dev Emitted when `newAdminRole` is set as ``role``'s admin role, replacing `previousAdminRole`
*
* `DEFAULT_ADMIN_ROLE` is the starting admin for all roles, despite
* {RoleAdminChanged} not being emitted signaling this.
*/
event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole);
/**
* @dev Emitted when `account` is granted `role`.
*
* `sender` is the account that originated the contract call. This account bears the admin role (for the granted role).
* Expected in cases where the role was granted using the internal {AccessControl-_grantRole}.
*/
event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender);
/**
* @dev Emitted when `account` is revoked `role`.
*
* `sender` is the account that originated the contract call:
* - if using `revokeRole`, it is the admin role bearer
* - if using `renounceRole`, it is the role bearer (i.e. `account`)
*/
event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender);
/**
* @dev Returns `true` if `account` has been granted `role`.
*/
function hasRole(bytes32 role, address account) external view returns (bool);
/**
* @dev Returns the admin role that controls `role`. See {grantRole} and
* {revokeRole}.
*
* To change a role's admin, use {AccessControl-_setRoleAdmin}.
*/
function getRoleAdmin(bytes32 role) external view returns (bytes32);
/**
* @dev Grants `role` to `account`.
*
* If `account` had not been already granted `role`, emits a {RoleGranted}
* event.
*
* Requirements:
*
* - the caller must have ``role``'s admin role.
*/
function grantRole(bytes32 role, address account) external;
/**
* @dev Revokes `role` from `account`.
*
* If `account` had been granted `role`, emits a {RoleRevoked} event.
*
* Requirements:
*
* - the caller must have ``role``'s admin role.
*/
function revokeRole(bytes32 role, address account) external;
/**
* @dev Revokes `role` from the calling account.
*
* Roles are often managed via {grantRole} and {revokeRole}: this function's
* purpose is to provide a mechanism for accounts to lose their privileges
* if they are compromised (such as when a trusted device is misplaced).
*
* If the calling account had been granted `role`, emits a {RoleRevoked}
* event.
*
* Requirements:
*
* - the caller must be `callerConfirmation`.
*/
function renounceRole(bytes32 role, address callerConfirmation) external;
}// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (interfaces/IERC1363.sol)
pragma solidity ^0.8.20;
import {IERC20} from "./IERC20.sol";
import {IERC165} from "./IERC165.sol";
/**
* @title IERC1363
* @dev Interface of the ERC-1363 standard as defined in the https://eips.ethereum.org/EIPS/eip-1363[ERC-1363].
*
* Defines an extension interface for ERC-20 tokens that supports executing code on a recipient contract
* after `transfer` or `transferFrom`, or code on a spender contract after `approve`, in a single transaction.
*/
interface IERC1363 is IERC20, IERC165 {
/*
* Note: the ERC-165 identifier for this interface is 0xb0202a11.
* 0xb0202a11 ===
* bytes4(keccak256('transferAndCall(address,uint256)')) ^
* bytes4(keccak256('transferAndCall(address,uint256,bytes)')) ^
* bytes4(keccak256('transferFromAndCall(address,address,uint256)')) ^
* bytes4(keccak256('transferFromAndCall(address,address,uint256,bytes)')) ^
* bytes4(keccak256('approveAndCall(address,uint256)')) ^
* bytes4(keccak256('approveAndCall(address,uint256,bytes)'))
*/
/**
* @dev Moves a `value` amount of tokens from the caller's account to `to`
* and then calls {IERC1363Receiver-onTransferReceived} on `to`.
* @param to The address which you want to transfer to.
* @param value The amount of tokens to be transferred.
* @return A boolean value indicating whether the operation succeeded unless throwing.
*/
function transferAndCall(address to, uint256 value) external returns (bool);
/**
* @dev Moves a `value` amount of tokens from the caller's account to `to`
* and then calls {IERC1363Receiver-onTransferReceived} on `to`.
* @param to The address which you want to transfer to.
* @param value The amount of tokens to be transferred.
* @param data Additional data with no specified format, sent in call to `to`.
* @return A boolean value indicating whether the operation succeeded unless throwing.
*/
function transferAndCall(address to, uint256 value, bytes calldata data) external returns (bool);
/**
* @dev Moves a `value` amount of tokens from `from` to `to` using the allowance mechanism
* and then calls {IERC1363Receiver-onTransferReceived} on `to`.
* @param from The address which you want to send tokens from.
* @param to The address which you want to transfer to.
* @param value The amount of tokens to be transferred.
* @return A boolean value indicating whether the operation succeeded unless throwing.
*/
function transferFromAndCall(address from, address to, uint256 value) external returns (bool);
/**
* @dev Moves a `value` amount of tokens from `from` to `to` using the allowance mechanism
* and then calls {IERC1363Receiver-onTransferReceived} on `to`.
* @param from The address which you want to send tokens from.
* @param to The address which you want to transfer to.
* @param value The amount of tokens to be transferred.
* @param data Additional data with no specified format, sent in call to `to`.
* @return A boolean value indicating whether the operation succeeded unless throwing.
*/
function transferFromAndCall(address from, address to, uint256 value, bytes calldata data) external returns (bool);
/**
* @dev Sets a `value` amount of tokens as the allowance of `spender` over the
* caller's tokens and then calls {IERC1363Spender-onApprovalReceived} on `spender`.
* @param spender The address which will spend the funds.
* @param value The amount of tokens to be spent.
* @return A boolean value indicating whether the operation succeeded unless throwing.
*/
function approveAndCall(address spender, uint256 value) external returns (bool);
/**
* @dev Sets a `value` amount of tokens as the allowance of `spender` over the
* caller's tokens and then calls {IERC1363Spender-onApprovalReceived} on `spender`.
* @param spender The address which will spend the funds.
* @param value The amount of tokens to be spent.
* @param data Additional data with no specified format, sent in call to `spender`.
* @return A boolean value indicating whether the operation succeeded unless throwing.
*/
function approveAndCall(address spender, uint256 value, bytes calldata data) external returns (bool);
}// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (interfaces/IERC165.sol)
pragma solidity ^0.8.20;
import {IERC165} from "../utils/introspection/IERC165.sol";// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (interfaces/IERC20.sol)
pragma solidity ^0.8.20;
import {IERC20} from "../token/ERC20/IERC20.sol";// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.2.0) (proxy/Clones.sol)
pragma solidity ^0.8.20;
import {Create2} from "../utils/Create2.sol";
import {Errors} from "../utils/Errors.sol";
/**
* @dev https://eips.ethereum.org/EIPS/eip-1167[ERC-1167] is a standard for
* deploying minimal proxy contracts, also known as "clones".
*
* > To simply and cheaply clone contract functionality in an immutable way, this standard specifies
* > a minimal bytecode implementation that delegates all calls to a known, fixed address.
*
* The library includes functions to deploy a proxy using either `create` (traditional deployment) or `create2`
* (salted deterministic deployment). It also includes functions to predict the addresses of clones deployed using the
* deterministic method.
*/
library Clones {
error CloneArgumentsTooLong();
/**
* @dev Deploys and returns the address of a clone that mimics the behaviour of `implementation`.
*
* This function uses the create opcode, which should never revert.
*/
function clone(address implementation) internal returns (address instance) {
return clone(implementation, 0);
}
/**
* @dev Same as {xref-Clones-clone-address-}[clone], but with a `value` parameter to send native currency
* to the new contract.
*
* NOTE: Using a non-zero value at creation will require the contract using this function (e.g. a factory)
* to always have enough balance for new deployments. Consider exposing this function under a payable method.
*/
function clone(address implementation, uint256 value) internal returns (address instance) {
if (address(this).balance < value) {
revert Errors.InsufficientBalance(address(this).balance, value);
}
assembly ("memory-safe") {
// Cleans the upper 96 bits of the `implementation` word, then packs the first 3 bytes
// of the `implementation` address with the bytecode before the address.
mstore(0x00, or(shr(0xe8, shl(0x60, implementation)), 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000))
// Packs the remaining 17 bytes of `implementation` with the bytecode after the address.
mstore(0x20, or(shl(0x78, implementation), 0x5af43d82803e903d91602b57fd5bf3))
instance := create(value, 0x09, 0x37)
}
if (instance == address(0)) {
revert Errors.FailedDeployment();
}
}
/**
* @dev Deploys and returns the address of a clone that mimics the behaviour of `implementation`.
*
* This function uses the create2 opcode and a `salt` to deterministically deploy
* the clone. Using the same `implementation` and `salt` multiple times will revert, since
* the clones cannot be deployed twice at the same address.
*/
function cloneDeterministic(address implementation, bytes32 salt) internal returns (address instance) {
return cloneDeterministic(implementation, salt, 0);
}
/**
* @dev Same as {xref-Clones-cloneDeterministic-address-bytes32-}[cloneDeterministic], but with
* a `value` parameter to send native currency to the new contract.
*
* NOTE: Using a non-zero value at creation will require the contract using this function (e.g. a factory)
* to always have enough balance for new deployments. Consider exposing this function under a payable method.
*/
function cloneDeterministic(
address implementation,
bytes32 salt,
uint256 value
) internal returns (address instance) {
if (address(this).balance < value) {
revert Errors.InsufficientBalance(address(this).balance, value);
}
assembly ("memory-safe") {
// Cleans the upper 96 bits of the `implementation` word, then packs the first 3 bytes
// of the `implementation` address with the bytecode before the address.
mstore(0x00, or(shr(0xe8, shl(0x60, implementation)), 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000))
// Packs the remaining 17 bytes of `implementation` with the bytecode after the address.
mstore(0x20, or(shl(0x78, implementation), 0x5af43d82803e903d91602b57fd5bf3))
instance := create2(value, 0x09, 0x37, salt)
}
if (instance == address(0)) {
revert Errors.FailedDeployment();
}
}
/**
* @dev Computes the address of a clone deployed using {Clones-cloneDeterministic}.
*/
function predictDeterministicAddress(
address implementation,
bytes32 salt,
address deployer
) internal pure returns (address predicted) {
assembly ("memory-safe") {
let ptr := mload(0x40)
mstore(add(ptr, 0x38), deployer)
mstore(add(ptr, 0x24), 0x5af43d82803e903d91602b57fd5bf3ff)
mstore(add(ptr, 0x14), implementation)
mstore(ptr, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73)
mstore(add(ptr, 0x58), salt)
mstore(add(ptr, 0x78), keccak256(add(ptr, 0x0c), 0x37))
predicted := and(keccak256(add(ptr, 0x43), 0x55), 0xffffffffffffffffffffffffffffffffffffffff)
}
}
/**
* @dev Computes the address of a clone deployed using {Clones-cloneDeterministic}.
*/
function predictDeterministicAddress(
address implementation,
bytes32 salt
) internal view returns (address predicted) {
return predictDeterministicAddress(implementation, salt, address(this));
}
/**
* @dev Deploys and returns the address of a clone that mimics the behavior of `implementation` with custom
* immutable arguments. These are provided through `args` and cannot be changed after deployment. To
* access the arguments within the implementation, use {fetchCloneArgs}.
*
* This function uses the create opcode, which should never revert.
*/
function cloneWithImmutableArgs(address implementation, bytes memory args) internal returns (address instance) {
return cloneWithImmutableArgs(implementation, args, 0);
}
/**
* @dev Same as {xref-Clones-cloneWithImmutableArgs-address-bytes-}[cloneWithImmutableArgs], but with a `value`
* parameter to send native currency to the new contract.
*
* NOTE: Using a non-zero value at creation will require the contract using this function (e.g. a factory)
* to always have enough balance for new deployments. Consider exposing this function under a payable method.
*/
function cloneWithImmutableArgs(
address implementation,
bytes memory args,
uint256 value
) internal returns (address instance) {
if (address(this).balance < value) {
revert Errors.InsufficientBalance(address(this).balance, value);
}
bytes memory bytecode = _cloneCodeWithImmutableArgs(implementation, args);
assembly ("memory-safe") {
instance := create(value, add(bytecode, 0x20), mload(bytecode))
}
if (instance == address(0)) {
revert Errors.FailedDeployment();
}
}
/**
* @dev Deploys and returns the address of a clone that mimics the behaviour of `implementation` with custom
* immutable arguments. These are provided through `args` and cannot be changed after deployment. To
* access the arguments within the implementation, use {fetchCloneArgs}.
*
* This function uses the create2 opcode and a `salt` to deterministically deploy the clone. Using the same
* `implementation`, `args` and `salt` multiple times will revert, since the clones cannot be deployed twice
* at the same address.
*/
function cloneDeterministicWithImmutableArgs(
address implementation,
bytes memory args,
bytes32 salt
) internal returns (address instance) {
return cloneDeterministicWithImmutableArgs(implementation, args, salt, 0);
}
/**
* @dev Same as {xref-Clones-cloneDeterministicWithImmutableArgs-address-bytes-bytes32-}[cloneDeterministicWithImmutableArgs],
* but with a `value` parameter to send native currency to the new contract.
*
* NOTE: Using a non-zero value at creation will require the contract using this function (e.g. a factory)
* to always have enough balance for new deployments. Consider exposing this function under a payable method.
*/
function cloneDeterministicWithImmutableArgs(
address implementation,
bytes memory args,
bytes32 salt,
uint256 value
) internal returns (address instance) {
bytes memory bytecode = _cloneCodeWithImmutableArgs(implementation, args);
return Create2.deploy(value, salt, bytecode);
}
/**
* @dev Computes the address of a clone deployed using {Clones-cloneDeterministicWithImmutableArgs}.
*/
function predictDeterministicAddressWithImmutableArgs(
address implementation,
bytes memory args,
bytes32 salt,
address deployer
) internal pure returns (address predicted) {
bytes memory bytecode = _cloneCodeWithImmutableArgs(implementation, args);
return Create2.computeAddress(salt, keccak256(bytecode), deployer);
}
/**
* @dev Computes the address of a clone deployed using {Clones-cloneDeterministicWithImmutableArgs}.
*/
function predictDeterministicAddressWithImmutableArgs(
address implementation,
bytes memory args,
bytes32 salt
) internal view returns (address predicted) {
return predictDeterministicAddressWithImmutableArgs(implementation, args, salt, address(this));
}
/**
* @dev Get the immutable args attached to a clone.
*
* - If `instance` is a clone that was deployed using `clone` or `cloneDeterministic`, this
* function will return an empty array.
* - If `instance` is a clone that was deployed using `cloneWithImmutableArgs` or
* `cloneDeterministicWithImmutableArgs`, this function will return the args array used at
* creation.
* - If `instance` is NOT a clone deployed using this library, the behavior is undefined. This
* function should only be used to check addresses that are known to be clones.
*/
function fetchCloneArgs(address instance) internal view returns (bytes memory) {
bytes memory result = new bytes(instance.code.length - 45); // revert if length is too short
assembly ("memory-safe") {
extcodecopy(instance, add(result, 32), 45, mload(result))
}
return result;
}
/**
* @dev Helper that prepares the initcode of the proxy with immutable args.
*
* An assembly variant of this function requires copying the `args` array, which can be efficiently done using
* `mcopy`. Unfortunately, that opcode is not available before cancun. A pure solidity implementation using
* abi.encodePacked is more expensive but also more portable and easier to review.
*
* NOTE: https://eips.ethereum.org/EIPS/eip-170[EIP-170] limits the length of the contract code to 24576 bytes.
* With the proxy code taking 45 bytes, that limits the length of the immutable args to 24531 bytes.
*/
function _cloneCodeWithImmutableArgs(
address implementation,
bytes memory args
) private pure returns (bytes memory) {
if (args.length > 24531) revert CloneArgumentsTooLong();
return
abi.encodePacked(
hex"61",
uint16(args.length + 45),
hex"3d81600a3d39f3363d3d373d3d3d363d73",
implementation,
hex"5af43d82803e903d91602b57fd5bf3",
args
);
}
}// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/IERC20.sol)
pragma solidity ^0.8.20;
/**
* @dev Interface of the ERC-20 standard as defined in the ERC.
*/
interface IERC20 {
/**
* @dev Emitted when `value` tokens are moved from one account (`from`) to
* another (`to`).
*
* Note that `value` may be zero.
*/
event Transfer(address indexed from, address indexed to, uint256 value);
/**
* @dev Emitted when the allowance of a `spender` for an `owner` is set by
* a call to {approve}. `value` is the new allowance.
*/
event Approval(address indexed owner, address indexed spender, uint256 value);
/**
* @dev Returns the value of tokens in existence.
*/
function totalSupply() external view returns (uint256);
/**
* @dev Returns the value of tokens owned by `account`.
*/
function balanceOf(address account) external view returns (uint256);
/**
* @dev Moves a `value` amount of tokens from the caller's account to `to`.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transfer(address to, uint256 value) external returns (bool);
/**
* @dev Returns the remaining number of tokens that `spender` will be
* allowed to spend on behalf of `owner` through {transferFrom}. This is
* zero by default.
*
* This value changes when {approve} or {transferFrom} are called.
*/
function allowance(address owner, address spender) external view returns (uint256);
/**
* @dev Sets a `value` amount of tokens as the allowance of `spender` over the
* caller's tokens.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* IMPORTANT: Beware that changing an allowance with this method brings the risk
* that someone may use both the old and the new allowance by unfortunate
* transaction ordering. One possible solution to mitigate this race
* condition is to first reduce the spender's allowance to 0 and set the
* desired value afterwards:
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
*
* Emits an {Approval} event.
*/
function approve(address spender, uint256 value) external returns (bool);
/**
* @dev Moves a `value` amount of tokens from `from` to `to` using the
* allowance mechanism. `value` is then deducted from the caller's
* allowance.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transferFrom(address from, address to, uint256 value) external returns (bool);
}// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.2.0) (token/ERC20/utils/SafeERC20.sol)
pragma solidity ^0.8.20;
import {IERC20} from "../IERC20.sol";
import {IERC1363} from "../../../interfaces/IERC1363.sol";
/**
* @title SafeERC20
* @dev Wrappers around ERC-20 operations that throw on failure (when the token
* contract returns false). Tokens that return no value (and instead revert or
* throw on failure) are also supported, non-reverting calls are assumed to be
* successful.
* To use this library you can add a `using SafeERC20 for IERC20;` statement to your contract,
* which allows you to call the safe operations as `token.safeTransfer(...)`, etc.
*/
library SafeERC20 {
/**
* @dev An operation with an ERC-20 token failed.
*/
error SafeERC20FailedOperation(address token);
/**
* @dev Indicates a failed `decreaseAllowance` request.
*/
error SafeERC20FailedDecreaseAllowance(address spender, uint256 currentAllowance, uint256 requestedDecrease);
/**
* @dev Transfer `value` amount of `token` from the calling contract to `to`. If `token` returns no value,
* non-reverting calls are assumed to be successful.
*/
function safeTransfer(IERC20 token, address to, uint256 value) internal {
_callOptionalReturn(token, abi.encodeCall(token.transfer, (to, value)));
}
/**
* @dev Transfer `value` amount of `token` from `from` to `to`, spending the approval given by `from` to the
* calling contract. If `token` returns no value, non-reverting calls are assumed to be successful.
*/
function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal {
_callOptionalReturn(token, abi.encodeCall(token.transferFrom, (from, to, value)));
}
/**
* @dev Increase the calling contract's allowance toward `spender` by `value`. If `token` returns no value,
* non-reverting calls are assumed to be successful.
*
* IMPORTANT: If the token implements ERC-7674 (ERC-20 with temporary allowance), and if the "client"
* smart contract uses ERC-7674 to set temporary allowances, then the "client" smart contract should avoid using
* this function. Performing a {safeIncreaseAllowance} or {safeDecreaseAllowance} operation on a token contract
* that has a non-zero temporary allowance (for that particular owner-spender) will result in unexpected behavior.
*/
function safeIncreaseAllowance(IERC20 token, address spender, uint256 value) internal {
uint256 oldAllowance = token.allowance(address(this), spender);
forceApprove(token, spender, oldAllowance + value);
}
/**
* @dev Decrease the calling contract's allowance toward `spender` by `requestedDecrease`. If `token` returns no
* value, non-reverting calls are assumed to be successful.
*
* IMPORTANT: If the token implements ERC-7674 (ERC-20 with temporary allowance), and if the "client"
* smart contract uses ERC-7674 to set temporary allowances, then the "client" smart contract should avoid using
* this function. Performing a {safeIncreaseAllowance} or {safeDecreaseAllowance} operation on a token contract
* that has a non-zero temporary allowance (for that particular owner-spender) will result in unexpected behavior.
*/
function safeDecreaseAllowance(IERC20 token, address spender, uint256 requestedDecrease) internal {
unchecked {
uint256 currentAllowance = token.allowance(address(this), spender);
if (currentAllowance < requestedDecrease) {
revert SafeERC20FailedDecreaseAllowance(spender, currentAllowance, requestedDecrease);
}
forceApprove(token, spender, currentAllowance - requestedDecrease);
}
}
/**
* @dev Set the calling contract's allowance toward `spender` to `value`. If `token` returns no value,
* non-reverting calls are assumed to be successful. Meant to be used with tokens that require the approval
* to be set to zero before setting it to a non-zero value, such as USDT.
*
* NOTE: If the token implements ERC-7674, this function will not modify any temporary allowance. This function
* only sets the "standard" allowance. Any temporary allowance will remain active, in addition to the value being
* set here.
*/
function forceApprove(IERC20 token, address spender, uint256 value) internal {
bytes memory approvalCall = abi.encodeCall(token.approve, (spender, value));
if (!_callOptionalReturnBool(token, approvalCall)) {
_callOptionalReturn(token, abi.encodeCall(token.approve, (spender, 0)));
_callOptionalReturn(token, approvalCall);
}
}
/**
* @dev Performs an {ERC1363} transferAndCall, with a fallback to the simple {ERC20} transfer if the target has no
* code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when
* targeting contracts.
*
* Reverts if the returned value is other than `true`.
*/
function transferAndCallRelaxed(IERC1363 token, address to, uint256 value, bytes memory data) internal {
if (to.code.length == 0) {
safeTransfer(token, to, value);
} else if (!token.transferAndCall(to, value, data)) {
revert SafeERC20FailedOperation(address(token));
}
}
/**
* @dev Performs an {ERC1363} transferFromAndCall, with a fallback to the simple {ERC20} transferFrom if the target
* has no code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when
* targeting contracts.
*
* Reverts if the returned value is other than `true`.
*/
function transferFromAndCallRelaxed(
IERC1363 token,
address from,
address to,
uint256 value,
bytes memory data
) internal {
if (to.code.length == 0) {
safeTransferFrom(token, from, to, value);
} else if (!token.transferFromAndCall(from, to, value, data)) {
revert SafeERC20FailedOperation(address(token));
}
}
/**
* @dev Performs an {ERC1363} approveAndCall, with a fallback to the simple {ERC20} approve if the target has no
* code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when
* targeting contracts.
*
* NOTE: When the recipient address (`to`) has no code (i.e. is an EOA), this function behaves as {forceApprove}.
* Opposedly, when the recipient address (`to`) has code, this function only attempts to call {ERC1363-approveAndCall}
* once without retrying, and relies on the returned value to be true.
*
* Reverts if the returned value is other than `true`.
*/
function approveAndCallRelaxed(IERC1363 token, address to, uint256 value, bytes memory data) internal {
if (to.code.length == 0) {
forceApprove(token, to, value);
} else if (!token.approveAndCall(to, value, data)) {
revert SafeERC20FailedOperation(address(token));
}
}
/**
* @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement
* on the return value: the return value is optional (but if data is returned, it must not be false).
* @param token The token targeted by the call.
* @param data The call data (encoded using abi.encode or one of its variants).
*
* This is a variant of {_callOptionalReturnBool} that reverts if call fails to meet the requirements.
*/
function _callOptionalReturn(IERC20 token, bytes memory data) private {
uint256 returnSize;
uint256 returnValue;
assembly ("memory-safe") {
let success := call(gas(), token, 0, add(data, 0x20), mload(data), 0, 0x20)
// bubble errors
if iszero(success) {
let ptr := mload(0x40)
returndatacopy(ptr, 0, returndatasize())
revert(ptr, returndatasize())
}
returnSize := returndatasize()
returnValue := mload(0)
}
if (returnSize == 0 ? address(token).code.length == 0 : returnValue != 1) {
revert SafeERC20FailedOperation(address(token));
}
}
/**
* @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement
* on the return value: the return value is optional (but if data is returned, it must not be false).
* @param token The token targeted by the call.
* @param data The call data (encoded using abi.encode or one of its variants).
*
* This is a variant of {_callOptionalReturn} that silently catches all reverts and returns a bool instead.
*/
function _callOptionalReturnBool(IERC20 token, bytes memory data) private returns (bool) {
bool success;
uint256 returnSize;
uint256 returnValue;
assembly ("memory-safe") {
success := call(gas(), token, 0, add(data, 0x20), mload(data), 0, 0x20)
returnSize := returndatasize()
returnValue := mload(0)
}
return success && (returnSize == 0 ? address(token).code.length > 0 : returnValue == 1);
}
}// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.1) (utils/Context.sol)
pragma solidity ^0.8.20;
/**
* @dev Provides information about the current execution context, including the
* sender of the transaction and its data. While these are generally available
* via msg.sender and msg.data, they should not be accessed in such a direct
* manner, since when dealing with meta-transactions the account sending and
* paying for execution may not be the actual sender (as far as an application
* is concerned).
*
* This contract is only required for intermediate, library-like contracts.
*/
abstract contract Context {
function _msgSender() internal view virtual returns (address) {
return msg.sender;
}
function _msgData() internal view virtual returns (bytes calldata) {
return msg.data;
}
function _contextSuffixLength() internal view virtual returns (uint256) {
return 0;
}
}// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (utils/Create2.sol)
pragma solidity ^0.8.20;
import {Errors} from "./Errors.sol";
/**
* @dev Helper to make usage of the `CREATE2` EVM opcode easier and safer.
* `CREATE2` can be used to compute in advance the address where a smart
* contract will be deployed, which allows for interesting new mechanisms known
* as 'counterfactual interactions'.
*
* See the https://eips.ethereum.org/EIPS/eip-1014#motivation[EIP] for more
* information.
*/
library Create2 {
/**
* @dev There's no code to deploy.
*/
error Create2EmptyBytecode();
/**
* @dev Deploys a contract using `CREATE2`. The address where the contract
* will be deployed can be known in advance via {computeAddress}.
*
* The bytecode for a contract can be obtained from Solidity with
* `type(contractName).creationCode`.
*
* Requirements:
*
* - `bytecode` must not be empty.
* - `salt` must have not been used for `bytecode` already.
* - the factory must have a balance of at least `amount`.
* - if `amount` is non-zero, `bytecode` must have a `payable` constructor.
*/
function deploy(uint256 amount, bytes32 salt, bytes memory bytecode) internal returns (address addr) {
if (address(this).balance < amount) {
revert Errors.InsufficientBalance(address(this).balance, amount);
}
if (bytecode.length == 0) {
revert Create2EmptyBytecode();
}
assembly ("memory-safe") {
addr := create2(amount, add(bytecode, 0x20), mload(bytecode), salt)
// if no address was created, and returndata is not empty, bubble revert
if and(iszero(addr), not(iszero(returndatasize()))) {
let p := mload(0x40)
returndatacopy(p, 0, returndatasize())
revert(p, returndatasize())
}
}
if (addr == address(0)) {
revert Errors.FailedDeployment();
}
}
/**
* @dev Returns the address where a contract will be stored if deployed via {deploy}. Any change in the
* `bytecodeHash` or `salt` will result in a new destination address.
*/
function computeAddress(bytes32 salt, bytes32 bytecodeHash) internal view returns (address) {
return computeAddress(salt, bytecodeHash, address(this));
}
/**
* @dev Returns the address where a contract will be stored if deployed via {deploy} from a contract located at
* `deployer`. If `deployer` is this contract's address, returns the same value as {computeAddress}.
*/
function computeAddress(bytes32 salt, bytes32 bytecodeHash, address deployer) internal pure returns (address addr) {
assembly ("memory-safe") {
let ptr := mload(0x40) // Get free memory pointer
// | | ↓ ptr ... ↓ ptr + 0x0B (start) ... ↓ ptr + 0x20 ... ↓ ptr + 0x40 ... |
// |-------------------|---------------------------------------------------------------------------|
// | bytecodeHash | CCCCCCCCCCCCC...CC |
// | salt | BBBBBBBBBBBBB...BB |
// | deployer | 000000...0000AAAAAAAAAAAAAAAAAAA...AA |
// | 0xFF | FF |
// |-------------------|---------------------------------------------------------------------------|
// | memory | 000000...00FFAAAAAAAAAAAAAAAAAAA...AABBBBBBBBBBBBB...BBCCCCCCCCCCCCC...CC |
// | keccak(start, 85) | ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑ |
mstore(add(ptr, 0x40), bytecodeHash)
mstore(add(ptr, 0x20), salt)
mstore(ptr, deployer) // Right-aligned with 12 preceding garbage bytes
let start := add(ptr, 0x0b) // The hashed data starts at the final garbage byte which we will set to 0xff
mstore8(start, 0xff)
addr := and(keccak256(start, 85), 0xffffffffffffffffffffffffffffffffffffffff)
}
}
}// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (utils/cryptography/Hashes.sol)
pragma solidity ^0.8.20;
/**
* @dev Library of standard hash functions.
*
* _Available since v5.1._
*/
library Hashes {
/**
* @dev Commutative Keccak256 hash of a sorted pair of bytes32. Frequently used when working with merkle proofs.
*
* NOTE: Equivalent to the `standardNodeHash` in our https://github.com/OpenZeppelin/merkle-tree[JavaScript library].
*/
function commutativeKeccak256(bytes32 a, bytes32 b) internal pure returns (bytes32) {
return a < b ? _efficientKeccak256(a, b) : _efficientKeccak256(b, a);
}
/**
* @dev Implementation of keccak256(abi.encode(a, b)) that doesn't allocate or expand memory.
*/
function _efficientKeccak256(bytes32 a, bytes32 b) private pure returns (bytes32 value) {
assembly ("memory-safe") {
mstore(0x00, a)
mstore(0x20, b)
value := keccak256(0x00, 0x40)
}
}
}// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (utils/cryptography/MerkleProof.sol)
// This file was procedurally generated from scripts/generate/templates/MerkleProof.js.
pragma solidity ^0.8.20;
import {Hashes} from "./Hashes.sol";
/**
* @dev These functions deal with verification of Merkle Tree proofs.
*
* The tree and the proofs can be generated using our
* https://github.com/OpenZeppelin/merkle-tree[JavaScript library].
* You will find a quickstart guide in the readme.
*
* WARNING: You should avoid using leaf values that are 64 bytes long prior to
* hashing, or use a hash function other than keccak256 for hashing leaves.
* This is because the concatenation of a sorted pair of internal nodes in
* the Merkle tree could be reinterpreted as a leaf value.
* OpenZeppelin's JavaScript library generates Merkle trees that are safe
* against this attack out of the box.
*
* IMPORTANT: Consider memory side-effects when using custom hashing functions
* that access memory in an unsafe way.
*
* NOTE: This library supports proof verification for merkle trees built using
* custom _commutative_ hashing functions (i.e. `H(a, b) == H(b, a)`). Proving
* leaf inclusion in trees built using non-commutative hashing functions requires
* additional logic that is not supported by this library.
*/
library MerkleProof {
/**
*@dev The multiproof provided is not valid.
*/
error MerkleProofInvalidMultiproof();
/**
* @dev Returns true if a `leaf` can be proved to be a part of a Merkle tree
* defined by `root`. For this, a `proof` must be provided, containing
* sibling hashes on the branch from the leaf to the root of the tree. Each
* pair of leaves and each pair of pre-images are assumed to be sorted.
*
* This version handles proofs in memory with the default hashing function.
*/
function verify(bytes32[] memory proof, bytes32 root, bytes32 leaf) internal pure returns (bool) {
return processProof(proof, leaf) == root;
}
/**
* @dev Returns the rebuilt hash obtained by traversing a Merkle tree up
* from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt
* hash matches the root of the tree. When processing the proof, the pairs
* of leaves & pre-images are assumed to be sorted.
*
* This version handles proofs in memory with the default hashing function.
*/
function processProof(bytes32[] memory proof, bytes32 leaf) internal pure returns (bytes32) {
bytes32 computedHash = leaf;
for (uint256 i = 0; i < proof.length; i++) {
computedHash = Hashes.commutativeKeccak256(computedHash, proof[i]);
}
return computedHash;
}
/**
* @dev Returns true if a `leaf` can be proved to be a part of a Merkle tree
* defined by `root`. For this, a `proof` must be provided, containing
* sibling hashes on the branch from the leaf to the root of the tree. Each
* pair of leaves and each pair of pre-images are assumed to be sorted.
*
* This version handles proofs in memory with a custom hashing function.
*/
function verify(
bytes32[] memory proof,
bytes32 root,
bytes32 leaf,
function(bytes32, bytes32) view returns (bytes32) hasher
) internal view returns (bool) {
return processProof(proof, leaf, hasher) == root;
}
/**
* @dev Returns the rebuilt hash obtained by traversing a Merkle tree up
* from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt
* hash matches the root of the tree. When processing the proof, the pairs
* of leaves & pre-images are assumed to be sorted.
*
* This version handles proofs in memory with a custom hashing function.
*/
function processProof(
bytes32[] memory proof,
bytes32 leaf,
function(bytes32, bytes32) view returns (bytes32) hasher
) internal view returns (bytes32) {
bytes32 computedHash = leaf;
for (uint256 i = 0; i < proof.length; i++) {
computedHash = hasher(computedHash, proof[i]);
}
return computedHash;
}
/**
* @dev Returns true if a `leaf` can be proved to be a part of a Merkle tree
* defined by `root`. For this, a `proof` must be provided, containing
* sibling hashes on the branch from the leaf to the root of the tree. Each
* pair of leaves and each pair of pre-images are assumed to be sorted.
*
* This version handles proofs in calldata with the default hashing function.
*/
function verifyCalldata(bytes32[] calldata proof, bytes32 root, bytes32 leaf) internal pure returns (bool) {
return processProofCalldata(proof, leaf) == root;
}
/**
* @dev Returns the rebuilt hash obtained by traversing a Merkle tree up
* from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt
* hash matches the root of the tree. When processing the proof, the pairs
* of leaves & pre-images are assumed to be sorted.
*
* This version handles proofs in calldata with the default hashing function.
*/
function processProofCalldata(bytes32[] calldata proof, bytes32 leaf) internal pure returns (bytes32) {
bytes32 computedHash = leaf;
for (uint256 i = 0; i < proof.length; i++) {
computedHash = Hashes.commutativeKeccak256(computedHash, proof[i]);
}
return computedHash;
}
/**
* @dev Returns true if a `leaf` can be proved to be a part of a Merkle tree
* defined by `root`. For this, a `proof` must be provided, containing
* sibling hashes on the branch from the leaf to the root of the tree. Each
* pair of leaves and each pair of pre-images are assumed to be sorted.
*
* This version handles proofs in calldata with a custom hashing function.
*/
function verifyCalldata(
bytes32[] calldata proof,
bytes32 root,
bytes32 leaf,
function(bytes32, bytes32) view returns (bytes32) hasher
) internal view returns (bool) {
return processProofCalldata(proof, leaf, hasher) == root;
}
/**
* @dev Returns the rebuilt hash obtained by traversing a Merkle tree up
* from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt
* hash matches the root of the tree. When processing the proof, the pairs
* of leaves & pre-images are assumed to be sorted.
*
* This version handles proofs in calldata with a custom hashing function.
*/
function processProofCalldata(
bytes32[] calldata proof,
bytes32 leaf,
function(bytes32, bytes32) view returns (bytes32) hasher
) internal view returns (bytes32) {
bytes32 computedHash = leaf;
for (uint256 i = 0; i < proof.length; i++) {
computedHash = hasher(computedHash, proof[i]);
}
return computedHash;
}
/**
* @dev Returns true if the `leaves` can be simultaneously proven to be a part of a Merkle tree defined by
* `root`, according to `proof` and `proofFlags` as described in {processMultiProof}.
*
* This version handles multiproofs in memory with the default hashing function.
*
* CAUTION: Not all Merkle trees admit multiproofs. See {processMultiProof} for details.
*
* NOTE: Consider the case where `root == proof[0] && leaves.length == 0` as it will return `true`.
* The `leaves` must be validated independently. See {processMultiProof}.
*/
function multiProofVerify(
bytes32[] memory proof,
bool[] memory proofFlags,
bytes32 root,
bytes32[] memory leaves
) internal pure returns (bool) {
return processMultiProof(proof, proofFlags, leaves) == root;
}
/**
* @dev Returns the root of a tree reconstructed from `leaves` and sibling nodes in `proof`. The reconstruction
* proceeds by incrementally reconstructing all inner nodes by combining a leaf/inner node with either another
* leaf/inner node or a proof sibling node, depending on whether each `proofFlags` item is true or false
* respectively.
*
* This version handles multiproofs in memory with the default hashing function.
*
* CAUTION: Not all Merkle trees admit multiproofs. To use multiproofs, it is sufficient to ensure that: 1) the tree
* is complete (but not necessarily perfect), 2) the leaves to be proven are in the opposite order they are in the
* tree (i.e., as seen from right to left starting at the deepest layer and continuing at the next layer).
*
* NOTE: The _empty set_ (i.e. the case where `proof.length == 1 && leaves.length == 0`) is considered a no-op,
* and therefore a valid multiproof (i.e. it returns `proof[0]`). Consider disallowing this case if you're not
* validating the leaves elsewhere.
*/
function processMultiProof(
bytes32[] memory proof,
bool[] memory proofFlags,
bytes32[] memory leaves
) internal pure returns (bytes32 merkleRoot) {
// This function rebuilds the root hash by traversing the tree up from the leaves. The root is rebuilt by
// consuming and producing values on a queue. The queue starts with the `leaves` array, then goes onto the
// `hashes` array. At the end of the process, the last hash in the `hashes` array should contain the root of
// the Merkle tree.
uint256 leavesLen = leaves.length;
uint256 proofFlagsLen = proofFlags.length;
// Check proof validity.
if (leavesLen + proof.length != proofFlagsLen + 1) {
revert MerkleProofInvalidMultiproof();
}
// The xxxPos values are "pointers" to the next value to consume in each array. All accesses are done using
// `xxx[xxxPos++]`, which return the current value and increment the pointer, thus mimicking a queue's "pop".
bytes32[] memory hashes = new bytes32[](proofFlagsLen);
uint256 leafPos = 0;
uint256 hashPos = 0;
uint256 proofPos = 0;
// At each step, we compute the next hash using two values:
// - a value from the "main queue". If not all leaves have been consumed, we get the next leaf, otherwise we
// get the next hash.
// - depending on the flag, either another value from the "main queue" (merging branches) or an element from the
// `proof` array.
for (uint256 i = 0; i < proofFlagsLen; i++) {
bytes32 a = leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++];
bytes32 b = proofFlags[i]
? (leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++])
: proof[proofPos++];
hashes[i] = Hashes.commutativeKeccak256(a, b);
}
if (proofFlagsLen > 0) {
if (proofPos != proof.length) {
revert MerkleProofInvalidMultiproof();
}
unchecked {
return hashes[proofFlagsLen - 1];
}
} else if (leavesLen > 0) {
return leaves[0];
} else {
return proof[0];
}
}
/**
* @dev Returns true if the `leaves` can be simultaneously proven to be a part of a Merkle tree defined by
* `root`, according to `proof` and `proofFlags` as described in {processMultiProof}.
*
* This version handles multiproofs in memory with a custom hashing function.
*
* CAUTION: Not all Merkle trees admit multiproofs. See {processMultiProof} for details.
*
* NOTE: Consider the case where `root == proof[0] && leaves.length == 0` as it will return `true`.
* The `leaves` must be validated independently. See {processMultiProof}.
*/
function multiProofVerify(
bytes32[] memory proof,
bool[] memory proofFlags,
bytes32 root,
bytes32[] memory leaves,
function(bytes32, bytes32) view returns (bytes32) hasher
) internal view returns (bool) {
return processMultiProof(proof, proofFlags, leaves, hasher) == root;
}
/**
* @dev Returns the root of a tree reconstructed from `leaves` and sibling nodes in `proof`. The reconstruction
* proceeds by incrementally reconstructing all inner nodes by combining a leaf/inner node with either another
* leaf/inner node or a proof sibling node, depending on whether each `proofFlags` item is true or false
* respectively.
*
* This version handles multiproofs in memory with a custom hashing function.
*
* CAUTION: Not all Merkle trees admit multiproofs. To use multiproofs, it is sufficient to ensure that: 1) the tree
* is complete (but not necessarily perfect), 2) the leaves to be proven are in the opposite order they are in the
* tree (i.e., as seen from right to left starting at the deepest layer and continuing at the next layer).
*
* NOTE: The _empty set_ (i.e. the case where `proof.length == 1 && leaves.length == 0`) is considered a no-op,
* and therefore a valid multiproof (i.e. it returns `proof[0]`). Consider disallowing this case if you're not
* validating the leaves elsewhere.
*/
function processMultiProof(
bytes32[] memory proof,
bool[] memory proofFlags,
bytes32[] memory leaves,
function(bytes32, bytes32) view returns (bytes32) hasher
) internal view returns (bytes32 merkleRoot) {
// This function rebuilds the root hash by traversing the tree up from the leaves. The root is rebuilt by
// consuming and producing values on a queue. The queue starts with the `leaves` array, then goes onto the
// `hashes` array. At the end of the process, the last hash in the `hashes` array should contain the root of
// the Merkle tree.
uint256 leavesLen = leaves.length;
uint256 proofFlagsLen = proofFlags.length;
// Check proof validity.
if (leavesLen + proof.length != proofFlagsLen + 1) {
revert MerkleProofInvalidMultiproof();
}
// The xxxPos values are "pointers" to the next value to consume in each array. All accesses are done using
// `xxx[xxxPos++]`, which return the current value and increment the pointer, thus mimicking a queue's "pop".
bytes32[] memory hashes = new bytes32[](proofFlagsLen);
uint256 leafPos = 0;
uint256 hashPos = 0;
uint256 proofPos = 0;
// At each step, we compute the next hash using two values:
// - a value from the "main queue". If not all leaves have been consumed, we get the next leaf, otherwise we
// get the next hash.
// - depending on the flag, either another value from the "main queue" (merging branches) or an element from the
// `proof` array.
for (uint256 i = 0; i < proofFlagsLen; i++) {
bytes32 a = leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++];
bytes32 b = proofFlags[i]
? (leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++])
: proof[proofPos++];
hashes[i] = hasher(a, b);
}
if (proofFlagsLen > 0) {
if (proofPos != proof.length) {
revert MerkleProofInvalidMultiproof();
}
unchecked {
return hashes[proofFlagsLen - 1];
}
} else if (leavesLen > 0) {
return leaves[0];
} else {
return proof[0];
}
}
/**
* @dev Returns true if the `leaves` can be simultaneously proven to be a part of a Merkle tree defined by
* `root`, according to `proof` and `proofFlags` as described in {processMultiProof}.
*
* This version handles multiproofs in calldata with the default hashing function.
*
* CAUTION: Not all Merkle trees admit multiproofs. See {processMultiProof} for details.
*
* NOTE: Consider the case where `root == proof[0] && leaves.length == 0` as it will return `true`.
* The `leaves` must be validated independently. See {processMultiProofCalldata}.
*/
function multiProofVerifyCalldata(
bytes32[] calldata proof,
bool[] calldata proofFlags,
bytes32 root,
bytes32[] memory leaves
) internal pure returns (bool) {
return processMultiProofCalldata(proof, proofFlags, leaves) == root;
}
/**
* @dev Returns the root of a tree reconstructed from `leaves` and sibling nodes in `proof`. The reconstruction
* proceeds by incrementally reconstructing all inner nodes by combining a leaf/inner node with either another
* leaf/inner node or a proof sibling node, depending on whether each `proofFlags` item is true or false
* respectively.
*
* This version handles multiproofs in calldata with the default hashing function.
*
* CAUTION: Not all Merkle trees admit multiproofs. To use multiproofs, it is sufficient to ensure that: 1) the tree
* is complete (but not necessarily perfect), 2) the leaves to be proven are in the opposite order they are in the
* tree (i.e., as seen from right to left starting at the deepest layer and continuing at the next layer).
*
* NOTE: The _empty set_ (i.e. the case where `proof.length == 1 && leaves.length == 0`) is considered a no-op,
* and therefore a valid multiproof (i.e. it returns `proof[0]`). Consider disallowing this case if you're not
* validating the leaves elsewhere.
*/
function processMultiProofCalldata(
bytes32[] calldata proof,
bool[] calldata proofFlags,
bytes32[] memory leaves
) internal pure returns (bytes32 merkleRoot) {
// This function rebuilds the root hash by traversing the tree up from the leaves. The root is rebuilt by
// consuming and producing values on a queue. The queue starts with the `leaves` array, then goes onto the
// `hashes` array. At the end of the process, the last hash in the `hashes` array should contain the root of
// the Merkle tree.
uint256 leavesLen = leaves.length;
uint256 proofFlagsLen = proofFlags.length;
// Check proof validity.
if (leavesLen + proof.length != proofFlagsLen + 1) {
revert MerkleProofInvalidMultiproof();
}
// The xxxPos values are "pointers" to the next value to consume in each array. All accesses are done using
// `xxx[xxxPos++]`, which return the current value and increment the pointer, thus mimicking a queue's "pop".
bytes32[] memory hashes = new bytes32[](proofFlagsLen);
uint256 leafPos = 0;
uint256 hashPos = 0;
uint256 proofPos = 0;
// At each step, we compute the next hash using two values:
// - a value from the "main queue". If not all leaves have been consumed, we get the next leaf, otherwise we
// get the next hash.
// - depending on the flag, either another value from the "main queue" (merging branches) or an element from the
// `proof` array.
for (uint256 i = 0; i < proofFlagsLen; i++) {
bytes32 a = leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++];
bytes32 b = proofFlags[i]
? (leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++])
: proof[proofPos++];
hashes[i] = Hashes.commutativeKeccak256(a, b);
}
if (proofFlagsLen > 0) {
if (proofPos != proof.length) {
revert MerkleProofInvalidMultiproof();
}
unchecked {
return hashes[proofFlagsLen - 1];
}
} else if (leavesLen > 0) {
return leaves[0];
} else {
return proof[0];
}
}
/**
* @dev Returns true if the `leaves` can be simultaneously proven to be a part of a Merkle tree defined by
* `root`, according to `proof` and `proofFlags` as described in {processMultiProof}.
*
* This version handles multiproofs in calldata with a custom hashing function.
*
* CAUTION: Not all Merkle trees admit multiproofs. See {processMultiProof} for details.
*
* NOTE: Consider the case where `root == proof[0] && leaves.length == 0` as it will return `true`.
* The `leaves` must be validated independently. See {processMultiProofCalldata}.
*/
function multiProofVerifyCalldata(
bytes32[] calldata proof,
bool[] calldata proofFlags,
bytes32 root,
bytes32[] memory leaves,
function(bytes32, bytes32) view returns (bytes32) hasher
) internal view returns (bool) {
return processMultiProofCalldata(proof, proofFlags, leaves, hasher) == root;
}
/**
* @dev Returns the root of a tree reconstructed from `leaves` and sibling nodes in `proof`. The reconstruction
* proceeds by incrementally reconstructing all inner nodes by combining a leaf/inner node with either another
* leaf/inner node or a proof sibling node, depending on whether each `proofFlags` item is true or false
* respectively.
*
* This version handles multiproofs in calldata with a custom hashing function.
*
* CAUTION: Not all Merkle trees admit multiproofs. To use multiproofs, it is sufficient to ensure that: 1) the tree
* is complete (but not necessarily perfect), 2) the leaves to be proven are in the opposite order they are in the
* tree (i.e., as seen from right to left starting at the deepest layer and continuing at the next layer).
*
* NOTE: The _empty set_ (i.e. the case where `proof.length == 1 && leaves.length == 0`) is considered a no-op,
* and therefore a valid multiproof (i.e. it returns `proof[0]`). Consider disallowing this case if you're not
* validating the leaves elsewhere.
*/
function processMultiProofCalldata(
bytes32[] calldata proof,
bool[] calldata proofFlags,
bytes32[] memory leaves,
function(bytes32, bytes32) view returns (bytes32) hasher
) internal view returns (bytes32 merkleRoot) {
// This function rebuilds the root hash by traversing the tree up from the leaves. The root is rebuilt by
// consuming and producing values on a queue. The queue starts with the `leaves` array, then goes onto the
// `hashes` array. At the end of the process, the last hash in the `hashes` array should contain the root of
// the Merkle tree.
uint256 leavesLen = leaves.length;
uint256 proofFlagsLen = proofFlags.length;
// Check proof validity.
if (leavesLen + proof.length != proofFlagsLen + 1) {
revert MerkleProofInvalidMultiproof();
}
// The xxxPos values are "pointers" to the next value to consume in each array. All accesses are done using
// `xxx[xxxPos++]`, which return the current value and increment the pointer, thus mimicking a queue's "pop".
bytes32[] memory hashes = new bytes32[](proofFlagsLen);
uint256 leafPos = 0;
uint256 hashPos = 0;
uint256 proofPos = 0;
// At each step, we compute the next hash using two values:
// - a value from the "main queue". If not all leaves have been consumed, we get the next leaf, otherwise we
// get the next hash.
// - depending on the flag, either another value from the "main queue" (merging branches) or an element from the
// `proof` array.
for (uint256 i = 0; i < proofFlagsLen; i++) {
bytes32 a = leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++];
bytes32 b = proofFlags[i]
? (leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++])
: proof[proofPos++];
hashes[i] = hasher(a, b);
}
if (proofFlagsLen > 0) {
if (proofPos != proof.length) {
revert MerkleProofInvalidMultiproof();
}
unchecked {
return hashes[proofFlagsLen - 1];
}
} else if (leavesLen > 0) {
return leaves[0];
} else {
return proof[0];
}
}
}// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (utils/Errors.sol)
pragma solidity ^0.8.20;
/**
* @dev Collection of common custom errors used in multiple contracts
*
* IMPORTANT: Backwards compatibility is not guaranteed in future versions of the library.
* It is recommended to avoid relying on the error API for critical functionality.
*
* _Available since v5.1._
*/
library Errors {
/**
* @dev The ETH balance of the account is not enough to perform the operation.
*/
error InsufficientBalance(uint256 balance, uint256 needed);
/**
* @dev A call to an address target failed. The target may have reverted.
*/
error FailedCall();
/**
* @dev The deployment failed.
*/
error FailedDeployment();
/**
* @dev A necessary precompile is missing.
*/
error MissingPrecompile(address);
}// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (utils/introspection/ERC165.sol)
pragma solidity ^0.8.20;
import {IERC165} from "./IERC165.sol";
/**
* @dev Implementation of the {IERC165} interface.
*
* Contracts that want to implement ERC-165 should inherit from this contract and override {supportsInterface} to check
* for the additional interface id that will be supported. For example:
*
* ```solidity
* function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
* return interfaceId == type(MyInterface).interfaceId || super.supportsInterface(interfaceId);
* }
* ```
*/
abstract contract ERC165 is IERC165 {
/**
* @dev See {IERC165-supportsInterface}.
*/
function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) {
return interfaceId == type(IERC165).interfaceId;
}
}// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (utils/introspection/IERC165.sol)
pragma solidity ^0.8.20;
/**
* @dev Interface of the ERC-165 standard, as defined in the
* https://eips.ethereum.org/EIPS/eip-165[ERC].
*
* Implementers can declare support of contract interfaces, which can then be
* queried by others ({ERC165Checker}).
*
* For an implementation, see {ERC165}.
*/
interface IERC165 {
/**
* @dev Returns true if this contract implements the interface defined by
* `interfaceId`. See the corresponding
* https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[ERC section]
* to learn more about how these ids are created.
*
* This function call must use less than 30 000 gas.
*/
function supportsInterface(bytes4 interfaceId) external view returns (bool);
}// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (utils/math/SafeCast.sol)
// This file was procedurally generated from scripts/generate/templates/SafeCast.js.
pragma solidity ^0.8.20;
/**
* @dev Wrappers over Solidity's uintXX/intXX/bool casting operators with added overflow
* checks.
*
* Downcasting from uint256/int256 in Solidity does not revert on overflow. This can
* easily result in undesired exploitation or bugs, since developers usually
* assume that overflows raise errors. `SafeCast` restores this intuition by
* reverting the transaction when such an operation overflows.
*
* Using this library instead of the unchecked operations eliminates an entire
* class of bugs, so it's recommended to use it always.
*/
library SafeCast {
/**
* @dev Value doesn't fit in an uint of `bits` size.
*/
error SafeCastOverflowedUintDowncast(uint8 bits, uint256 value);
/**
* @dev An int value doesn't fit in an uint of `bits` size.
*/
error SafeCastOverflowedIntToUint(int256 value);
/**
* @dev Value doesn't fit in an int of `bits` size.
*/
error SafeCastOverflowedIntDowncast(uint8 bits, int256 value);
/**
* @dev An uint value doesn't fit in an int of `bits` size.
*/
error SafeCastOverflowedUintToInt(uint256 value);
/**
* @dev Returns the downcasted uint248 from uint256, reverting on
* overflow (when the input is greater than largest uint248).
*
* Counterpart to Solidity's `uint248` operator.
*
* Requirements:
*
* - input must fit into 248 bits
*/
function toUint248(uint256 value) internal pure returns (uint248) {
if (value > type(uint248).max) {
revert SafeCastOverflowedUintDowncast(248, value);
}
return uint248(value);
}
/**
* @dev Returns the downcasted uint240 from uint256, reverting on
* overflow (when the input is greater than largest uint240).
*
* Counterpart to Solidity's `uint240` operator.
*
* Requirements:
*
* - input must fit into 240 bits
*/
function toUint240(uint256 value) internal pure returns (uint240) {
if (value > type(uint240).max) {
revert SafeCastOverflowedUintDowncast(240, value);
}
return uint240(value);
}
/**
* @dev Returns the downcasted uint232 from uint256, reverting on
* overflow (when the input is greater than largest uint232).
*
* Counterpart to Solidity's `uint232` operator.
*
* Requirements:
*
* - input must fit into 232 bits
*/
function toUint232(uint256 value) internal pure returns (uint232) {
if (value > type(uint232).max) {
revert SafeCastOverflowedUintDowncast(232, value);
}
return uint232(value);
}
/**
* @dev Returns the downcasted uint224 from uint256, reverting on
* overflow (when the input is greater than largest uint224).
*
* Counterpart to Solidity's `uint224` operator.
*
* Requirements:
*
* - input must fit into 224 bits
*/
function toUint224(uint256 value) internal pure returns (uint224) {
if (value > type(uint224).max) {
revert SafeCastOverflowedUintDowncast(224, value);
}
return uint224(value);
}
/**
* @dev Returns the downcasted uint216 from uint256, reverting on
* overflow (when the input is greater than largest uint216).
*
* Counterpart to Solidity's `uint216` operator.
*
* Requirements:
*
* - input must fit into 216 bits
*/
function toUint216(uint256 value) internal pure returns (uint216) {
if (value > type(uint216).max) {
revert SafeCastOverflowedUintDowncast(216, value);
}
return uint216(value);
}
/**
* @dev Returns the downcasted uint208 from uint256, reverting on
* overflow (when the input is greater than largest uint208).
*
* Counterpart to Solidity's `uint208` operator.
*
* Requirements:
*
* - input must fit into 208 bits
*/
function toUint208(uint256 value) internal pure returns (uint208) {
if (value > type(uint208).max) {
revert SafeCastOverflowedUintDowncast(208, value);
}
return uint208(value);
}
/**
* @dev Returns the downcasted uint200 from uint256, reverting on
* overflow (when the input is greater than largest uint200).
*
* Counterpart to Solidity's `uint200` operator.
*
* Requirements:
*
* - input must fit into 200 bits
*/
function toUint200(uint256 value) internal pure returns (uint200) {
if (value > type(uint200).max) {
revert SafeCastOverflowedUintDowncast(200, value);
}
return uint200(value);
}
/**
* @dev Returns the downcasted uint192 from uint256, reverting on
* overflow (when the input is greater than largest uint192).
*
* Counterpart to Solidity's `uint192` operator.
*
* Requirements:
*
* - input must fit into 192 bits
*/
function toUint192(uint256 value) internal pure returns (uint192) {
if (value > type(uint192).max) {
revert SafeCastOverflowedUintDowncast(192, value);
}
return uint192(value);
}
/**
* @dev Returns the downcasted uint184 from uint256, reverting on
* overflow (when the input is greater than largest uint184).
*
* Counterpart to Solidity's `uint184` operator.
*
* Requirements:
*
* - input must fit into 184 bits
*/
function toUint184(uint256 value) internal pure returns (uint184) {
if (value > type(uint184).max) {
revert SafeCastOverflowedUintDowncast(184, value);
}
return uint184(value);
}
/**
* @dev Returns the downcasted uint176 from uint256, reverting on
* overflow (when the input is greater than largest uint176).
*
* Counterpart to Solidity's `uint176` operator.
*
* Requirements:
*
* - input must fit into 176 bits
*/
function toUint176(uint256 value) internal pure returns (uint176) {
if (value > type(uint176).max) {
revert SafeCastOverflowedUintDowncast(176, value);
}
return uint176(value);
}
/**
* @dev Returns the downcasted uint168 from uint256, reverting on
* overflow (when the input is greater than largest uint168).
*
* Counterpart to Solidity's `uint168` operator.
*
* Requirements:
*
* - input must fit into 168 bits
*/
function toUint168(uint256 value) internal pure returns (uint168) {
if (value > type(uint168).max) {
revert SafeCastOverflowedUintDowncast(168, value);
}
return uint168(value);
}
/**
* @dev Returns the downcasted uint160 from uint256, reverting on
* overflow (when the input is greater than largest uint160).
*
* Counterpart to Solidity's `uint160` operator.
*
* Requirements:
*
* - input must fit into 160 bits
*/
function toUint160(uint256 value) internal pure returns (uint160) {
if (value > type(uint160).max) {
revert SafeCastOverflowedUintDowncast(160, value);
}
return uint160(value);
}
/**
* @dev Returns the downcasted uint152 from uint256, reverting on
* overflow (when the input is greater than largest uint152).
*
* Counterpart to Solidity's `uint152` operator.
*
* Requirements:
*
* - input must fit into 152 bits
*/
function toUint152(uint256 value) internal pure returns (uint152) {
if (value > type(uint152).max) {
revert SafeCastOverflowedUintDowncast(152, value);
}
return uint152(value);
}
/**
* @dev Returns the downcasted uint144 from uint256, reverting on
* overflow (when the input is greater than largest uint144).
*
* Counterpart to Solidity's `uint144` operator.
*
* Requirements:
*
* - input must fit into 144 bits
*/
function toUint144(uint256 value) internal pure returns (uint144) {
if (value > type(uint144).max) {
revert SafeCastOverflowedUintDowncast(144, value);
}
return uint144(value);
}
/**
* @dev Returns the downcasted uint136 from uint256, reverting on
* overflow (when the input is greater than largest uint136).
*
* Counterpart to Solidity's `uint136` operator.
*
* Requirements:
*
* - input must fit into 136 bits
*/
function toUint136(uint256 value) internal pure returns (uint136) {
if (value > type(uint136).max) {
revert SafeCastOverflowedUintDowncast(136, value);
}
return uint136(value);
}
/**
* @dev Returns the downcasted uint128 from uint256, reverting on
* overflow (when the input is greater than largest uint128).
*
* Counterpart to Solidity's `uint128` operator.
*
* Requirements:
*
* - input must fit into 128 bits
*/
function toUint128(uint256 value) internal pure returns (uint128) {
if (value > type(uint128).max) {
revert SafeCastOverflowedUintDowncast(128, value);
}
return uint128(value);
}
/**
* @dev Returns the downcasted uint120 from uint256, reverting on
* overflow (when the input is greater than largest uint120).
*
* Counterpart to Solidity's `uint120` operator.
*
* Requirements:
*
* - input must fit into 120 bits
*/
function toUint120(uint256 value) internal pure returns (uint120) {
if (value > type(uint120).max) {
revert SafeCastOverflowedUintDowncast(120, value);
}
return uint120(value);
}
/**
* @dev Returns the downcasted uint112 from uint256, reverting on
* overflow (when the input is greater than largest uint112).
*
* Counterpart to Solidity's `uint112` operator.
*
* Requirements:
*
* - input must fit into 112 bits
*/
function toUint112(uint256 value) internal pure returns (uint112) {
if (value > type(uint112).max) {
revert SafeCastOverflowedUintDowncast(112, value);
}
return uint112(value);
}
/**
* @dev Returns the downcasted uint104 from uint256, reverting on
* overflow (when the input is greater than largest uint104).
*
* Counterpart to Solidity's `uint104` operator.
*
* Requirements:
*
* - input must fit into 104 bits
*/
function toUint104(uint256 value) internal pure returns (uint104) {
if (value > type(uint104).max) {
revert SafeCastOverflowedUintDowncast(104, value);
}
return uint104(value);
}
/**
* @dev Returns the downcasted uint96 from uint256, reverting on
* overflow (when the input is greater than largest uint96).
*
* Counterpart to Solidity's `uint96` operator.
*
* Requirements:
*
* - input must fit into 96 bits
*/
function toUint96(uint256 value) internal pure returns (uint96) {
if (value > type(uint96).max) {
revert SafeCastOverflowedUintDowncast(96, value);
}
return uint96(value);
}
/**
* @dev Returns the downcasted uint88 from uint256, reverting on
* overflow (when the input is greater than largest uint88).
*
* Counterpart to Solidity's `uint88` operator.
*
* Requirements:
*
* - input must fit into 88 bits
*/
function toUint88(uint256 value) internal pure returns (uint88) {
if (value > type(uint88).max) {
revert SafeCastOverflowedUintDowncast(88, value);
}
return uint88(value);
}
/**
* @dev Returns the downcasted uint80 from uint256, reverting on
* overflow (when the input is greater than largest uint80).
*
* Counterpart to Solidity's `uint80` operator.
*
* Requirements:
*
* - input must fit into 80 bits
*/
function toUint80(uint256 value) internal pure returns (uint80) {
if (value > type(uint80).max) {
revert SafeCastOverflowedUintDowncast(80, value);
}
return uint80(value);
}
/**
* @dev Returns the downcasted uint72 from uint256, reverting on
* overflow (when the input is greater than largest uint72).
*
* Counterpart to Solidity's `uint72` operator.
*
* Requirements:
*
* - input must fit into 72 bits
*/
function toUint72(uint256 value) internal pure returns (uint72) {
if (value > type(uint72).max) {
revert SafeCastOverflowedUintDowncast(72, value);
}
return uint72(value);
}
/**
* @dev Returns the downcasted uint64 from uint256, reverting on
* overflow (when the input is greater than largest uint64).
*
* Counterpart to Solidity's `uint64` operator.
*
* Requirements:
*
* - input must fit into 64 bits
*/
function toUint64(uint256 value) internal pure returns (uint64) {
if (value > type(uint64).max) {
revert SafeCastOverflowedUintDowncast(64, value);
}
return uint64(value);
}
/**
* @dev Returns the downcasted uint56 from uint256, reverting on
* overflow (when the input is greater than largest uint56).
*
* Counterpart to Solidity's `uint56` operator.
*
* Requirements:
*
* - input must fit into 56 bits
*/
function toUint56(uint256 value) internal pure returns (uint56) {
if (value > type(uint56).max) {
revert SafeCastOverflowedUintDowncast(56, value);
}
return uint56(value);
}
/**
* @dev Returns the downcasted uint48 from uint256, reverting on
* overflow (when the input is greater than largest uint48).
*
* Counterpart to Solidity's `uint48` operator.
*
* Requirements:
*
* - input must fit into 48 bits
*/
function toUint48(uint256 value) internal pure returns (uint48) {
if (value > type(uint48).max) {
revert SafeCastOverflowedUintDowncast(48, value);
}
return uint48(value);
}
/**
* @dev Returns the downcasted uint40 from uint256, reverting on
* overflow (when the input is greater than largest uint40).
*
* Counterpart to Solidity's `uint40` operator.
*
* Requirements:
*
* - input must fit into 40 bits
*/
function toUint40(uint256 value) internal pure returns (uint40) {
if (value > type(uint40).max) {
revert SafeCastOverflowedUintDowncast(40, value);
}
return uint40(value);
}
/**
* @dev Returns the downcasted uint32 from uint256, reverting on
* overflow (when the input is greater than largest uint32).
*
* Counterpart to Solidity's `uint32` operator.
*
* Requirements:
*
* - input must fit into 32 bits
*/
function toUint32(uint256 value) internal pure returns (uint32) {
if (value > type(uint32).max) {
revert SafeCastOverflowedUintDowncast(32, value);
}
return uint32(value);
}
/**
* @dev Returns the downcasted uint24 from uint256, reverting on
* overflow (when the input is greater than largest uint24).
*
* Counterpart to Solidity's `uint24` operator.
*
* Requirements:
*
* - input must fit into 24 bits
*/
function toUint24(uint256 value) internal pure returns (uint24) {
if (value > type(uint24).max) {
revert SafeCastOverflowedUintDowncast(24, value);
}
return uint24(value);
}
/**
* @dev Returns the downcasted uint16 from uint256, reverting on
* overflow (when the input is greater than largest uint16).
*
* Counterpart to Solidity's `uint16` operator.
*
* Requirements:
*
* - input must fit into 16 bits
*/
function toUint16(uint256 value) internal pure returns (uint16) {
if (value > type(uint16).max) {
revert SafeCastOverflowedUintDowncast(16, value);
}
return uint16(value);
}
/**
* @dev Returns the downcasted uint8 from uint256, reverting on
* overflow (when the input is greater than largest uint8).
*
* Counterpart to Solidity's `uint8` operator.
*
* Requirements:
*
* - input must fit into 8 bits
*/
function toUint8(uint256 value) internal pure returns (uint8) {
if (value > type(uint8).max) {
revert SafeCastOverflowedUintDowncast(8, value);
}
return uint8(value);
}
/**
* @dev Converts a signed int256 into an unsigned uint256.
*
* Requirements:
*
* - input must be greater than or equal to 0.
*/
function toUint256(int256 value) internal pure returns (uint256) {
if (value < 0) {
revert SafeCastOverflowedIntToUint(value);
}
return uint256(value);
}
/**
* @dev Returns the downcasted int248 from int256, reverting on
* overflow (when the input is less than smallest int248 or
* greater than largest int248).
*
* Counterpart to Solidity's `int248` operator.
*
* Requirements:
*
* - input must fit into 248 bits
*/
function toInt248(int256 value) internal pure returns (int248 downcasted) {
downcasted = int248(value);
if (downcasted != value) {
revert SafeCastOverflowedIntDowncast(248, value);
}
}
/**
* @dev Returns the downcasted int240 from int256, reverting on
* overflow (when the input is less than smallest int240 or
* greater than largest int240).
*
* Counterpart to Solidity's `int240` operator.
*
* Requirements:
*
* - input must fit into 240 bits
*/
function toInt240(int256 value) internal pure returns (int240 downcasted) {
downcasted = int240(value);
if (downcasted != value) {
revert SafeCastOverflowedIntDowncast(240, value);
}
}
/**
* @dev Returns the downcasted int232 from int256, reverting on
* overflow (when the input is less than smallest int232 or
* greater than largest int232).
*
* Counterpart to Solidity's `int232` operator.
*
* Requirements:
*
* - input must fit into 232 bits
*/
function toInt232(int256 value) internal pure returns (int232 downcasted) {
downcasted = int232(value);
if (downcasted != value) {
revert SafeCastOverflowedIntDowncast(232, value);
}
}
/**
* @dev Returns the downcasted int224 from int256, reverting on
* overflow (when the input is less than smallest int224 or
* greater than largest int224).
*
* Counterpart to Solidity's `int224` operator.
*
* Requirements:
*
* - input must fit into 224 bits
*/
function toInt224(int256 value) internal pure returns (int224 downcasted) {
downcasted = int224(value);
if (downcasted != value) {
revert SafeCastOverflowedIntDowncast(224, value);
}
}
/**
* @dev Returns the downcasted int216 from int256, reverting on
* overflow (when the input is less than smallest int216 or
* greater than largest int216).
*
* Counterpart to Solidity's `int216` operator.
*
* Requirements:
*
* - input must fit into 216 bits
*/
function toInt216(int256 value) internal pure returns (int216 downcasted) {
downcasted = int216(value);
if (downcasted != value) {
revert SafeCastOverflowedIntDowncast(216, value);
}
}
/**
* @dev Returns the downcasted int208 from int256, reverting on
* overflow (when the input is less than smallest int208 or
* greater than largest int208).
*
* Counterpart to Solidity's `int208` operator.
*
* Requirements:
*
* - input must fit into 208 bits
*/
function toInt208(int256 value) internal pure returns (int208 downcasted) {
downcasted = int208(value);
if (downcasted != value) {
revert SafeCastOverflowedIntDowncast(208, value);
}
}
/**
* @dev Returns the downcasted int200 from int256, reverting on
* overflow (when the input is less than smallest int200 or
* greater than largest int200).
*
* Counterpart to Solidity's `int200` operator.
*
* Requirements:
*
* - input must fit into 200 bits
*/
function toInt200(int256 value) internal pure returns (int200 downcasted) {
downcasted = int200(value);
if (downcasted != value) {
revert SafeCastOverflowedIntDowncast(200, value);
}
}
/**
* @dev Returns the downcasted int192 from int256, reverting on
* overflow (when the input is less than smallest int192 or
* greater than largest int192).
*
* Counterpart to Solidity's `int192` operator.
*
* Requirements:
*
* - input must fit into 192 bits
*/
function toInt192(int256 value) internal pure returns (int192 downcasted) {
downcasted = int192(value);
if (downcasted != value) {
revert SafeCastOverflowedIntDowncast(192, value);
}
}
/**
* @dev Returns the downcasted int184 from int256, reverting on
* overflow (when the input is less than smallest int184 or
* greater than largest int184).
*
* Counterpart to Solidity's `int184` operator.
*
* Requirements:
*
* - input must fit into 184 bits
*/
function toInt184(int256 value) internal pure returns (int184 downcasted) {
downcasted = int184(value);
if (downcasted != value) {
revert SafeCastOverflowedIntDowncast(184, value);
}
}
/**
* @dev Returns the downcasted int176 from int256, reverting on
* overflow (when the input is less than smallest int176 or
* greater than largest int176).
*
* Counterpart to Solidity's `int176` operator.
*
* Requirements:
*
* - input must fit into 176 bits
*/
function toInt176(int256 value) internal pure returns (int176 downcasted) {
downcasted = int176(value);
if (downcasted != value) {
revert SafeCastOverflowedIntDowncast(176, value);
}
}
/**
* @dev Returns the downcasted int168 from int256, reverting on
* overflow (when the input is less than smallest int168 or
* greater than largest int168).
*
* Counterpart to Solidity's `int168` operator.
*
* Requirements:
*
* - input must fit into 168 bits
*/
function toInt168(int256 value) internal pure returns (int168 downcasted) {
downcasted = int168(value);
if (downcasted != value) {
revert SafeCastOverflowedIntDowncast(168, value);
}
}
/**
* @dev Returns the downcasted int160 from int256, reverting on
* overflow (when the input is less than smallest int160 or
* greater than largest int160).
*
* Counterpart to Solidity's `int160` operator.
*
* Requirements:
*
* - input must fit into 160 bits
*/
function toInt160(int256 value) internal pure returns (int160 downcasted) {
downcasted = int160(value);
if (downcasted != value) {
revert SafeCastOverflowedIntDowncast(160, value);
}
}
/**
* @dev Returns the downcasted int152 from int256, reverting on
* overflow (when the input is less than smallest int152 or
* greater than largest int152).
*
* Counterpart to Solidity's `int152` operator.
*
* Requirements:
*
* - input must fit into 152 bits
*/
function toInt152(int256 value) internal pure returns (int152 downcasted) {
downcasted = int152(value);
if (downcasted != value) {
revert SafeCastOverflowedIntDowncast(152, value);
}
}
/**
* @dev Returns the downcasted int144 from int256, reverting on
* overflow (when the input is less than smallest int144 or
* greater than largest int144).
*
* Counterpart to Solidity's `int144` operator.
*
* Requirements:
*
* - input must fit into 144 bits
*/
function toInt144(int256 value) internal pure returns (int144 downcasted) {
downcasted = int144(value);
if (downcasted != value) {
revert SafeCastOverflowedIntDowncast(144, value);
}
}
/**
* @dev Returns the downcasted int136 from int256, reverting on
* overflow (when the input is less than smallest int136 or
* greater than largest int136).
*
* Counterpart to Solidity's `int136` operator.
*
* Requirements:
*
* - input must fit into 136 bits
*/
function toInt136(int256 value) internal pure returns (int136 downcasted) {
downcasted = int136(value);
if (downcasted != value) {
revert SafeCastOverflowedIntDowncast(136, value);
}
}
/**
* @dev Returns the downcasted int128 from int256, reverting on
* overflow (when the input is less than smallest int128 or
* greater than largest int128).
*
* Counterpart to Solidity's `int128` operator.
*
* Requirements:
*
* - input must fit into 128 bits
*/
function toInt128(int256 value) internal pure returns (int128 downcasted) {
downcasted = int128(value);
if (downcasted != value) {
revert SafeCastOverflowedIntDowncast(128, value);
}
}
/**
* @dev Returns the downcasted int120 from int256, reverting on
* overflow (when the input is less than smallest int120 or
* greater than largest int120).
*
* Counterpart to Solidity's `int120` operator.
*
* Requirements:
*
* - input must fit into 120 bits
*/
function toInt120(int256 value) internal pure returns (int120 downcasted) {
downcasted = int120(value);
if (downcasted != value) {
revert SafeCastOverflowedIntDowncast(120, value);
}
}
/**
* @dev Returns the downcasted int112 from int256, reverting on
* overflow (when the input is less than smallest int112 or
* greater than largest int112).
*
* Counterpart to Solidity's `int112` operator.
*
* Requirements:
*
* - input must fit into 112 bits
*/
function toInt112(int256 value) internal pure returns (int112 downcasted) {
downcasted = int112(value);
if (downcasted != value) {
revert SafeCastOverflowedIntDowncast(112, value);
}
}
/**
* @dev Returns the downcasted int104 from int256, reverting on
* overflow (when the input is less than smallest int104 or
* greater than largest int104).
*
* Counterpart to Solidity's `int104` operator.
*
* Requirements:
*
* - input must fit into 104 bits
*/
function toInt104(int256 value) internal pure returns (int104 downcasted) {
downcasted = int104(value);
if (downcasted != value) {
revert SafeCastOverflowedIntDowncast(104, value);
}
}
/**
* @dev Returns the downcasted int96 from int256, reverting on
* overflow (when the input is less than smallest int96 or
* greater than largest int96).
*
* Counterpart to Solidity's `int96` operator.
*
* Requirements:
*
* - input must fit into 96 bits
*/
function toInt96(int256 value) internal pure returns (int96 downcasted) {
downcasted = int96(value);
if (downcasted != value) {
revert SafeCastOverflowedIntDowncast(96, value);
}
}
/**
* @dev Returns the downcasted int88 from int256, reverting on
* overflow (when the input is less than smallest int88 or
* greater than largest int88).
*
* Counterpart to Solidity's `int88` operator.
*
* Requirements:
*
* - input must fit into 88 bits
*/
function toInt88(int256 value) internal pure returns (int88 downcasted) {
downcasted = int88(value);
if (downcasted != value) {
revert SafeCastOverflowedIntDowncast(88, value);
}
}
/**
* @dev Returns the downcasted int80 from int256, reverting on
* overflow (when the input is less than smallest int80 or
* greater than largest int80).
*
* Counterpart to Solidity's `int80` operator.
*
* Requirements:
*
* - input must fit into 80 bits
*/
function toInt80(int256 value) internal pure returns (int80 downcasted) {
downcasted = int80(value);
if (downcasted != value) {
revert SafeCastOverflowedIntDowncast(80, value);
}
}
/**
* @dev Returns the downcasted int72 from int256, reverting on
* overflow (when the input is less than smallest int72 or
* greater than largest int72).
*
* Counterpart to Solidity's `int72` operator.
*
* Requirements:
*
* - input must fit into 72 bits
*/
function toInt72(int256 value) internal pure returns (int72 downcasted) {
downcasted = int72(value);
if (downcasted != value) {
revert SafeCastOverflowedIntDowncast(72, value);
}
}
/**
* @dev Returns the downcasted int64 from int256, reverting on
* overflow (when the input is less than smallest int64 or
* greater than largest int64).
*
* Counterpart to Solidity's `int64` operator.
*
* Requirements:
*
* - input must fit into 64 bits
*/
function toInt64(int256 value) internal pure returns (int64 downcasted) {
downcasted = int64(value);
if (downcasted != value) {
revert SafeCastOverflowedIntDowncast(64, value);
}
}
/**
* @dev Returns the downcasted int56 from int256, reverting on
* overflow (when the input is less than smallest int56 or
* greater than largest int56).
*
* Counterpart to Solidity's `int56` operator.
*
* Requirements:
*
* - input must fit into 56 bits
*/
function toInt56(int256 value) internal pure returns (int56 downcasted) {
downcasted = int56(value);
if (downcasted != value) {
revert SafeCastOverflowedIntDowncast(56, value);
}
}
/**
* @dev Returns the downcasted int48 from int256, reverting on
* overflow (when the input is less than smallest int48 or
* greater than largest int48).
*
* Counterpart to Solidity's `int48` operator.
*
* Requirements:
*
* - input must fit into 48 bits
*/
function toInt48(int256 value) internal pure returns (int48 downcasted) {
downcasted = int48(value);
if (downcasted != value) {
revert SafeCastOverflowedIntDowncast(48, value);
}
}
/**
* @dev Returns the downcasted int40 from int256, reverting on
* overflow (when the input is less than smallest int40 or
* greater than largest int40).
*
* Counterpart to Solidity's `int40` operator.
*
* Requirements:
*
* - input must fit into 40 bits
*/
function toInt40(int256 value) internal pure returns (int40 downcasted) {
downcasted = int40(value);
if (downcasted != value) {
revert SafeCastOverflowedIntDowncast(40, value);
}
}
/**
* @dev Returns the downcasted int32 from int256, reverting on
* overflow (when the input is less than smallest int32 or
* greater than largest int32).
*
* Counterpart to Solidity's `int32` operator.
*
* Requirements:
*
* - input must fit into 32 bits
*/
function toInt32(int256 value) internal pure returns (int32 downcasted) {
downcasted = int32(value);
if (downcasted != value) {
revert SafeCastOverflowedIntDowncast(32, value);
}
}
/**
* @dev Returns the downcasted int24 from int256, reverting on
* overflow (when the input is less than smallest int24 or
* greater than largest int24).
*
* Counterpart to Solidity's `int24` operator.
*
* Requirements:
*
* - input must fit into 24 bits
*/
function toInt24(int256 value) internal pure returns (int24 downcasted) {
downcasted = int24(value);
if (downcasted != value) {
revert SafeCastOverflowedIntDowncast(24, value);
}
}
/**
* @dev Returns the downcasted int16 from int256, reverting on
* overflow (when the input is less than smallest int16 or
* greater than largest int16).
*
* Counterpart to Solidity's `int16` operator.
*
* Requirements:
*
* - input must fit into 16 bits
*/
function toInt16(int256 value) internal pure returns (int16 downcasted) {
downcasted = int16(value);
if (downcasted != value) {
revert SafeCastOverflowedIntDowncast(16, value);
}
}
/**
* @dev Returns the downcasted int8 from int256, reverting on
* overflow (when the input is less than smallest int8 or
* greater than largest int8).
*
* Counterpart to Solidity's `int8` operator.
*
* Requirements:
*
* - input must fit into 8 bits
*/
function toInt8(int256 value) internal pure returns (int8 downcasted) {
downcasted = int8(value);
if (downcasted != value) {
revert SafeCastOverflowedIntDowncast(8, value);
}
}
/**
* @dev Converts an unsigned uint256 into a signed int256.
*
* Requirements:
*
* - input must be less than or equal to maxInt256.
*/
function toInt256(uint256 value) internal pure returns (int256) {
// Note: Unsafe cast below is okay because `type(int256).max` is guaranteed to be positive
if (value > uint256(type(int256).max)) {
revert SafeCastOverflowedUintToInt(value);
}
return int256(value);
}
/**
* @dev Cast a boolean (false or true) to a uint256 (0 or 1) with no jump.
*/
function toUint(bool b) internal pure returns (uint256 u) {
assembly ("memory-safe") {
u := iszero(iszero(b))
}
}
}// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (utils/structs/EnumerableSet.sol)
// This file was procedurally generated from scripts/generate/templates/EnumerableSet.js.
pragma solidity ^0.8.20;
/**
* @dev Library for managing
* https://en.wikipedia.org/wiki/Set_(abstract_data_type)[sets] of primitive
* types.
*
* Sets have the following properties:
*
* - Elements are added, removed, and checked for existence in constant time
* (O(1)).
* - Elements are enumerated in O(n). No guarantees are made on the ordering.
*
* ```solidity
* contract Example {
* // Add the library methods
* using EnumerableSet for EnumerableSet.AddressSet;
*
* // Declare a set state variable
* EnumerableSet.AddressSet private mySet;
* }
* ```
*
* As of v3.3.0, sets of type `bytes32` (`Bytes32Set`), `address` (`AddressSet`)
* and `uint256` (`UintSet`) are supported.
*
* [WARNING]
* ====
* Trying to delete such a structure from storage will likely result in data corruption, rendering the structure
* unusable.
* See https://github.com/ethereum/solidity/pull/11843[ethereum/solidity#11843] for more info.
*
* In order to clean an EnumerableSet, you can either remove all elements one by one or create a fresh instance using an
* array of EnumerableSet.
* ====
*/
library EnumerableSet {
// To implement this library for multiple types with as little code
// repetition as possible, we write it in terms of a generic Set type with
// bytes32 values.
// The Set implementation uses private functions, and user-facing
// implementations (such as AddressSet) are just wrappers around the
// underlying Set.
// This means that we can only create new EnumerableSets for types that fit
// in bytes32.
struct Set {
// Storage of set values
bytes32[] _values;
// Position is the index of the value in the `values` array plus 1.
// Position 0 is used to mean a value is not in the set.
mapping(bytes32 value => uint256) _positions;
}
/**
* @dev Add a value to a set. O(1).
*
* Returns true if the value was added to the set, that is if it was not
* already present.
*/
function _add(Set storage set, bytes32 value) private returns (bool) {
if (!_contains(set, value)) {
set._values.push(value);
// The value is stored at length-1, but we add 1 to all indexes
// and use 0 as a sentinel value
set._positions[value] = set._values.length;
return true;
} else {
return false;
}
}
/**
* @dev Removes a value from a set. O(1).
*
* Returns true if the value was removed from the set, that is if it was
* present.
*/
function _remove(Set storage set, bytes32 value) private returns (bool) {
// We cache the value's position to prevent multiple reads from the same storage slot
uint256 position = set._positions[value];
if (position != 0) {
// Equivalent to contains(set, value)
// To delete an element from the _values array in O(1), we swap the element to delete with the last one in
// the array, and then remove the last element (sometimes called as 'swap and pop').
// This modifies the order of the array, as noted in {at}.
uint256 valueIndex = position - 1;
uint256 lastIndex = set._values.length - 1;
if (valueIndex != lastIndex) {
bytes32 lastValue = set._values[lastIndex];
// Move the lastValue to the index where the value to delete is
set._values[valueIndex] = lastValue;
// Update the tracked position of the lastValue (that was just moved)
set._positions[lastValue] = position;
}
// Delete the slot where the moved value was stored
set._values.pop();
// Delete the tracked position for the deleted slot
delete set._positions[value];
return true;
} else {
return false;
}
}
/**
* @dev Returns true if the value is in the set. O(1).
*/
function _contains(Set storage set, bytes32 value) private view returns (bool) {
return set._positions[value] != 0;
}
/**
* @dev Returns the number of values on the set. O(1).
*/
function _length(Set storage set) private view returns (uint256) {
return set._values.length;
}
/**
* @dev Returns the value stored at position `index` in the set. O(1).
*
* Note that there are no guarantees on the ordering of values inside the
* array, and it may change when more values are added or removed.
*
* Requirements:
*
* - `index` must be strictly less than {length}.
*/
function _at(Set storage set, uint256 index) private view returns (bytes32) {
return set._values[index];
}
/**
* @dev Return the entire set in an array
*
* WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed
* to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that
* this function has an unbounded cost, and using it as part of a state-changing function may render the function
* uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.
*/
function _values(Set storage set) private view returns (bytes32[] memory) {
return set._values;
}
// Bytes32Set
struct Bytes32Set {
Set _inner;
}
/**
* @dev Add a value to a set. O(1).
*
* Returns true if the value was added to the set, that is if it was not
* already present.
*/
function add(Bytes32Set storage set, bytes32 value) internal returns (bool) {
return _add(set._inner, value);
}
/**
* @dev Removes a value from a set. O(1).
*
* Returns true if the value was removed from the set, that is if it was
* present.
*/
function remove(Bytes32Set storage set, bytes32 value) internal returns (bool) {
return _remove(set._inner, value);
}
/**
* @dev Returns true if the value is in the set. O(1).
*/
function contains(Bytes32Set storage set, bytes32 value) internal view returns (bool) {
return _contains(set._inner, value);
}
/**
* @dev Returns the number of values in the set. O(1).
*/
function length(Bytes32Set storage set) internal view returns (uint256) {
return _length(set._inner);
}
/**
* @dev Returns the value stored at position `index` in the set. O(1).
*
* Note that there are no guarantees on the ordering of values inside the
* array, and it may change when more values are added or removed.
*
* Requirements:
*
* - `index` must be strictly less than {length}.
*/
function at(Bytes32Set storage set, uint256 index) internal view returns (bytes32) {
return _at(set._inner, index);
}
/**
* @dev Return the entire set in an array
*
* WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed
* to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that
* this function has an unbounded cost, and using it as part of a state-changing function may render the function
* uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.
*/
function values(Bytes32Set storage set) internal view returns (bytes32[] memory) {
bytes32[] memory store = _values(set._inner);
bytes32[] memory result;
assembly ("memory-safe") {
result := store
}
return result;
}
// AddressSet
struct AddressSet {
Set _inner;
}
/**
* @dev Add a value to a set. O(1).
*
* Returns true if the value was added to the set, that is if it was not
* already present.
*/
function add(AddressSet storage set, address value) internal returns (bool) {
return _add(set._inner, bytes32(uint256(uint160(value))));
}
/**
* @dev Removes a value from a set. O(1).
*
* Returns true if the value was removed from the set, that is if it was
* present.
*/
function remove(AddressSet storage set, address value) internal returns (bool) {
return _remove(set._inner, bytes32(uint256(uint160(value))));
}
/**
* @dev Returns true if the value is in the set. O(1).
*/
function contains(AddressSet storage set, address value) internal view returns (bool) {
return _contains(set._inner, bytes32(uint256(uint160(value))));
}
/**
* @dev Returns the number of values in the set. O(1).
*/
function length(AddressSet storage set) internal view returns (uint256) {
return _length(set._inner);
}
/**
* @dev Returns the value stored at position `index` in the set. O(1).
*
* Note that there are no guarantees on the ordering of values inside the
* array, and it may change when more values are added or removed.
*
* Requirements:
*
* - `index` must be strictly less than {length}.
*/
function at(AddressSet storage set, uint256 index) internal view returns (address) {
return address(uint160(uint256(_at(set._inner, index))));
}
/**
* @dev Return the entire set in an array
*
* WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed
* to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that
* this function has an unbounded cost, and using it as part of a state-changing function may render the function
* uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.
*/
function values(AddressSet storage set) internal view returns (address[] memory) {
bytes32[] memory store = _values(set._inner);
address[] memory result;
assembly ("memory-safe") {
result := store
}
return result;
}
// UintSet
struct UintSet {
Set _inner;
}
/**
* @dev Add a value to a set. O(1).
*
* Returns true if the value was added to the set, that is if it was not
* already present.
*/
function add(UintSet storage set, uint256 value) internal returns (bool) {
return _add(set._inner, bytes32(value));
}
/**
* @dev Removes a value from a set. O(1).
*
* Returns true if the value was removed from the set, that is if it was
* present.
*/
function remove(UintSet storage set, uint256 value) internal returns (bool) {
return _remove(set._inner, bytes32(value));
}
/**
* @dev Returns true if the value is in the set. O(1).
*/
function contains(UintSet storage set, uint256 value) internal view returns (bool) {
return _contains(set._inner, bytes32(value));
}
/**
* @dev Returns the number of values in the set. O(1).
*/
function length(UintSet storage set) internal view returns (uint256) {
return _length(set._inner);
}
/**
* @dev Returns the value stored at position `index` in the set. O(1).
*
* Note that there are no guarantees on the ordering of values inside the
* array, and it may change when more values are added or removed.
*
* Requirements:
*
* - `index` must be strictly less than {length}.
*/
function at(UintSet storage set, uint256 index) internal view returns (uint256) {
return uint256(_at(set._inner, index));
}
/**
* @dev Return the entire set in an array
*
* WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed
* to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that
* this function has an unbounded cost, and using it as part of a state-changing function may render the function
* uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.
*/
function values(UintSet storage set) internal view returns (uint256[] memory) {
bytes32[] memory store = _values(set._inner);
uint256[] memory result;
assembly ("memory-safe") {
result := store
}
return result;
}
}// SPDX-FileCopyrightText: 2025 Lido <[email protected]> // SPDX-License-Identifier: GPL-3.0 // See contracts/COMPILERS.md pragma solidity 0.8.25; import {AccessControlEnumerable} from "@openzeppelin/contracts-v5.2/access/extensions/AccessControlEnumerable.sol"; import {Confirmations} from "./Confirmations.sol"; /** * @title AccessControlConfirmable * @author Lido * @notice An extension of AccessControlEnumerable that allows executing functions by mutual confirmation. * @dev This contract extends Confirmations and AccessControlEnumerable and adds a confirmation mechanism. */ abstract contract AccessControlConfirmable is AccessControlEnumerable, Confirmations { constructor() { __Confirmations_init(); } function _isValidConfirmer(bytes32 _role) internal view override returns (bool) { return hasRole(_role, msg.sender); } }
// SPDX-FileCopyrightText: 2025 Lido <[email protected]> // SPDX-License-Identifier: GPL-3.0 // See contracts/COMPILERS.md pragma solidity 0.8.25; import {Confirmations} from "./Confirmations.sol"; /** * @title Confirmable2Addresses * @author Lido * @notice An extension of Confirmations that allows executing functions by mutual confirmation. * @dev In this implementation, roles are treated as addresses. */ abstract contract Confirmable2Addresses is Confirmations { function _collectAndCheckConfirmations(bytes calldata _calldata, address _role1, address _role2) internal returns (bool) { bytes32[] memory roles = new bytes32[](2); roles[0] = bytes32(uint256(uint160(_role1))); roles[1] = bytes32(uint256(uint160(_role2))); return _collectAndCheckConfirmations(_calldata, roles); } function _isValidConfirmer(bytes32 _roleAsAddress) internal view override returns (bool) { return _roleAsAddress == bytes32(uint256(uint160(msg.sender))); } }
// SPDX-FileCopyrightText: 2025 Lido <[email protected]> // SPDX-License-Identifier: GPL-3.0 // See contracts/COMPILERS.md pragma solidity 0.8.25; /** * @title Confirmations * @author Lido * @notice A contract that allows executing functions by mutual confirmation. */ abstract contract Confirmations { /** * @notice Tracks confirmations * @custom:storage-location erc7201:Lido.Utils.Confirmations * @dev We cannot set confirmExpiry to 0 because this means that all confirmations have to be in the same block, * which can never be guaranteed. And, more importantly, if the `_setConfirmExpiry` requires confirmation, * the confirmation expiry will be tricky to change. * This is why confirmExpiry is private, set to a default value of 1 days and cannot be set to 0. * * Storage layout: * - callData: msg.data of the call (selector + arguments) * - role: role that confirmed the action * - expiryTimestamp: timestamp of the confirmation * * - confirmExpiry: confirmation expiry period in seconds */ struct ConfirmationStorage { mapping(bytes callData => mapping(bytes32 role => uint256 expiryTimestamp)) confirmations; uint256 confirmExpiry; } /** * @notice Storage offset slot for ERC-7201 namespace * The storage namespace is used to prevent upgrade collisions * keccak256(abi.encode(uint256(keccak256("Lido.Utils.Confirmations")) - 1)) & ~bytes32(uint256(0xff)) */ bytes32 private constant CONFIRMATIONS_STORAGE_LOCATION = 0xe4ca011a1344eb515c922209bf867930fc05bf79f4b0e3bb4ec9938eedd47700; /** * @notice Minimal confirmation expiry in seconds. */ uint256 public constant MIN_CONFIRM_EXPIRY = 1 hours; /** * @notice Maximal confirmation expiry in seconds. */ uint256 public constant MAX_CONFIRM_EXPIRY = 30 days; function __Confirmations_init() internal { _setConfirmExpiry(1 days); } /** * @notice Returns the confirmation expiry. * @return The confirmation expiry in seconds. */ function getConfirmExpiry() public view returns (uint256) { return _getConfirmationsStorage().confirmExpiry; } /** * @notice Returns the confirmation expiry for a given call data and confirmer. * @param _callData The call data of the function. * @param _role The role of the confirmer. * @return The confirmation expiration timestamp or 0 if there was no confirmation from this role to this _callData */ function confirmation(bytes memory _callData, bytes32 _role) external view returns (uint256) { return _getConfirmationsStorage().confirmations[_callData][_role]; } /** * @dev Processes a confirmation from the current caller and checks if all required confirmations are present. * Confirmation, in this context, is a call to the same function with the same arguments. * This is a one-off operation that either: * - Collects the current caller's confirmation and returns false if not enough confirmations * - Or clears all confirmations and returns true if all required confirmations are present * * The confirmation process works as follows: * 1. When a role member calls the function: * - Their confirmation is counted immediately * - If not enough confirmations exist, their confirmation is recorded * - If they're not a member of any of the specified roles, the call reverts * * 2. Confirmation counting: * - Counts the current caller's confirmations if they're a member of any of the specified roles * - Counts existing confirmations that are not expired, i.e. expiry is not exceeded * * 3. Confirmation Management: * - If all members of the specified roles have confirmed: * a. Clears all confirmations for this call * b. Returns true to indicate that the function can be executed * - If not enough confirmations: * a. Stores the current confirmations * b. Returns false to indicate that the function cannot be executed yet * - Thus, if the caller has all the roles, returns true immediately * * 4. Gas Optimization: * - Confirmations are stored in a deferred manner using a memory array * - Confirmation storage writes only occur if the function cannot be executed immediately * - This prevents unnecessary storage writes when all confirmations are present, * because the confirmations are cleared anyway after the function is executed, * - i.e. this optimization is beneficial for the deciding caller and * saves 1 storage write for each role the deciding caller has * * @param _calldata msg.data of the call (selector + arguments) * @param _roles Array of role identifiers that must confirm the call in order to execute it * @return bool True if all required confirmations are present and the function can be executed, false otherwise * * @notice Confirmations past their expiry are not counted and must be recast * @notice Only members of the specified roles can submit confirmations * @notice The order of confirmations does not matter * */ function _collectAndCheckConfirmations(bytes calldata _calldata, bytes32[] memory _roles) internal returns (bool) { if (_roles.length == 0) revert ZeroConfirmingRoles(); uint256 numberOfRoles = _roles.length; uint256 numberOfConfirms = 0; bool[] memory deferredConfirms = new bool[](numberOfRoles); bool isRoleMember = false; ConfirmationStorage storage $ = _getConfirmationsStorage(); uint256 expiryTimestamp = block.timestamp + $.confirmExpiry; for (uint256 i = 0; i < numberOfRoles; ++i) { bytes32 role = _roles[i]; if (_isValidConfirmer(role)) { isRoleMember = true; numberOfConfirms++; deferredConfirms[i] = true; emit RoleMemberConfirmed(msg.sender, role, block.timestamp, expiryTimestamp, msg.data); } else if ($.confirmations[_calldata][role] >= block.timestamp) { numberOfConfirms++; } } if (!isRoleMember) revert SenderNotMember(); if (numberOfConfirms == numberOfRoles) { for (uint256 i = 0; i < numberOfRoles; ++i) { bytes32 role = _roles[i]; delete $.confirmations[_calldata][role]; } return true; } else { for (uint256 i = 0; i < numberOfRoles; ++i) { if (deferredConfirms[i]) { bytes32 role = _roles[i]; $.confirmations[_calldata][role] = expiryTimestamp; } } return false; } } /** * @notice Checks if the caller is a valid confirmer * @param _role The role to check * @return bool True if the caller is a valid confirmer */ function _isValidConfirmer(bytes32 _role) internal view virtual returns (bool); /** * @dev Sets the confirmation expiry. * Confirmation expiry is a period during which the confirmation is counted. Once expired, * the confirmation no longer counts and must be recasted for the confirmation to go through. * @dev Does not retroactively apply to existing confirmations. * @param _newConfirmExpiry The new confirmation expiry in seconds. */ function _setConfirmExpiry(uint256 _newConfirmExpiry) internal { _validateConfirmExpiry(_newConfirmExpiry); ConfirmationStorage storage $ = _getConfirmationsStorage(); uint256 oldConfirmExpiry = $.confirmExpiry; $.confirmExpiry = _newConfirmExpiry; emit ConfirmExpirySet(msg.sender, oldConfirmExpiry, _newConfirmExpiry); } function _validateConfirmExpiry(uint256 _newConfirmExpiry) internal pure { if (_newConfirmExpiry < MIN_CONFIRM_EXPIRY || _newConfirmExpiry > MAX_CONFIRM_EXPIRY) revert ConfirmExpiryOutOfBounds(); } function _getConfirmationsStorage() private pure returns (ConfirmationStorage storage $) { assembly { $.slot := CONFIRMATIONS_STORAGE_LOCATION } } /** * @dev Emitted when the confirmation expiry is set. * @param sender msg.sender of the call * @param oldConfirmExpiry The old confirmation expiry. * @param newConfirmExpiry The new confirmation expiry. */ event ConfirmExpirySet(address indexed sender, uint256 oldConfirmExpiry, uint256 newConfirmExpiry); /** * @dev Emitted when a role member confirms. * @param member The address of the confirming member. * @param roleOrAddress The role or address of the confirming member. * @param confirmTimestamp The timestamp of the confirmation. * @param expiryTimestamp The timestamp when this confirmation expires. * @param data The msg.data of the confirmation (selector + arguments). */ event RoleMemberConfirmed(address indexed member, bytes32 indexed roleOrAddress, uint256 confirmTimestamp, uint256 expiryTimestamp, bytes data); /** * @dev Thrown when attempting to set confirmation expiry out of bounds. */ error ConfirmExpiryOutOfBounds(); /** * @dev Thrown when a caller without a required role attempts to confirm. */ error SenderNotMember(); /** * @dev Thrown when the roles array is empty. */ error ZeroConfirmingRoles(); }
// SPDX-FileCopyrightText: 2025 Lido <[email protected]> // SPDX-License-Identifier: GPL-3.0 // See contracts/COMPILERS.md pragma solidity 0.8.25; import {AccessControlEnumerableUpgradeable} from "contracts/openzeppelin/5.2/upgradeable/access/extensions/AccessControlEnumerableUpgradeable.sol"; import {PausableUntil} from "contracts/common/utils/PausableUntil.sol"; /** * @title PausableUntilWithRoles * @notice a `PausableUntil` implementation using OpenZeppelin's `AccessControlEnumerableUpgradeable` * @dev the inheriting contract must use `whenNotPaused` modifier from `PausableUntil` to block some functions on pause */ abstract contract PausableUntilWithRoles is PausableUntil, AccessControlEnumerableUpgradeable { /// @notice role that allows to pause the contract /// @dev 0x8d0e4ae4847b49935b55c99f9c3ce025c87e7c4604c35b7ae56929bd32fa5a78 bytes32 public constant PAUSE_ROLE = keccak256("PausableUntilWithRoles.PauseRole"); /// @notice role that allows to resume the contract /// @dev 0xa79a6aede309e0d48bf2ef0f71355c06ad317956d4c0da2deb0dc47cc34f826c bytes32 public constant RESUME_ROLE = keccak256("PausableUntilWithRoles.ResumeRole"); /** * @notice Resume the contract * @dev Reverts if contracts is not paused * @dev Reverts if sender has no `RESUME_ROLE` */ function resume() external onlyRole(RESUME_ROLE) { _resume(); } /** * @notice Pause the contract for a specified period * @param _duration pause duration in seconds (use `PAUSE_INFINITELY` for unlimited) * @dev Reverts if contract is already paused * @dev Reverts if sender has no `PAUSE_ROLE` * @dev Reverts if zero duration is passed */ function pauseFor(uint256 _duration) external onlyRole(PAUSE_ROLE) { _pauseFor(_duration); } /** * @notice Pause the contract until a specified timestamp * @param _pauseUntilInclusive the last second to pause until inclusive * @dev Reverts if the timestamp is in the past * @dev Reverts if sender has no `PAUSE_ROLE` * @dev Reverts if contract is already paused */ function pauseUntil(uint256 _pauseUntilInclusive) external onlyRole(PAUSE_ROLE) { _pauseUntil(_pauseUntilInclusive); } }
// SPDX-FileCopyrightText: 2025 Lido <[email protected]> // SPDX-License-Identifier: GPL-3.0 // See contracts/COMPILERS.md pragma solidity 0.8.25; import {VaultHub} from "../VaultHub.sol"; import {LazyOracle} from "../LazyOracle.sol"; import {Permissions} from "./Permissions.sol"; import {SafeCast} from "@openzeppelin/contracts-v5.2/utils/math/SafeCast.sol"; /** * @title NodeOperatorFee * @author Lido * @notice A contract that manages the node operator fee. */ contract NodeOperatorFee is Permissions { using SafeCast for uint256; using SafeCast for int256; /** * @notice Total basis points; 1bp = 0.01%, 100_00bp = 100.00%. */ uint256 internal constant TOTAL_BASIS_POINTS = 100_00; /** * @notice Parent role representing the node operator of the underlying StakingVault. * The members may not include the node operator address recorded in the underlying StakingVault * but it is assumed that the members of this role act in the interest of that node operator. * * @dev 0x59783a4ae82167eefad593739a5430c1d9e896a16c35f1e5285ddd0c0980885c */ bytes32 public constant NODE_OPERATOR_MANAGER_ROLE = keccak256("vaults.NodeOperatorFee.NodeOperatorManagerRole"); /** * @notice Node operator's sub-role for fee exemptions. * Managed by `NODE_OPERATOR_MANAGER_ROLE`. * * @dev 0xcceeef0309e9a678ed7f11f20499aeb00a9a4b0d50e53daa428f8591debc583a */ bytes32 public constant NODE_OPERATOR_FEE_EXEMPT_ROLE = keccak256("vaults.NodeOperatorFee.FeeExemptRole"); /** * @notice Node operator's sub-role for unguaranteed deposit * Managed by `NODE_OPERATOR_MANAGER_ROLE`. * * @dev 0x5c17b14b08ace6dda14c9642528ae92de2a73d59eacb65c71f39f309a5611063 */ bytes32 public constant NODE_OPERATOR_UNGUARANTEED_DEPOSIT_ROLE = keccak256("vaults.NodeOperatorFee.UnguaranteedDepositRole"); /** * @notice Node operator's sub-role for proving unknown validators. * Managed by `NODE_OPERATOR_MANAGER_ROLE`. * * @dev 0x7b564705f4e61596c4a9469b6884980f89e475befabdb849d69719f0791628be */ bytes32 public constant NODE_OPERATOR_PROVE_UNKNOWN_VALIDATOR_ROLE = keccak256("vaults.NodeOperatorFee.ProveUnknownValidatorsRole"); /** * @notice If the accrued fee exceeds this BP of the total value, it is considered abnormally high. * An abnormally high fee can only be disbursed by `DEFAULT_ADMIN_ROLE`. * This threshold is to prevent accidental overpayment due to outdated settled growth. * * Why 1% threshold? * * - Assume a very generous annual staking APR of ~5% (3% CL + 2% EL). * - A very high node operator fee rate of 10% translates to a 0.5% annual fee. * - Thus, a 1% fee threshold would therefore be reached in 2 years. * - Meaning: as long as the operator disburses fees at least once every 2 years, * the threshold will never be hit. * * Since these assumptions are highly conservative, in practice the operator * would need to disburse even less frequently before approaching the threshold. */ uint256 constant internal ABNORMALLY_HIGH_FEE_THRESHOLD_BP = 1_00; // ==================== Packed Storage Slot 1 ==================== /** * @notice Address that receives node operator fee disbursements. * This address is set by the node operator manager and receives disbursed fees. */ address public feeRecipient; /** * @notice Node operator fee rate in basis points (1 bp = 0.01%). * Cannot exceed 100.00% (10000 basis points). */ uint16 public feeRate; // ==================== Packed Storage Slot 2 ==================== /** * @notice Growth of the vault not subject to fees. * * Growth is the difference between inOutDelta and totalValue, * i.e. the component of totalValue that has not been directly funded to the underlying StakingVault via `fund()`: * inOutDelta + growth = totalValue * * Settled growth is the portion of the total growth that: * - has already been charged by the node operator, * - or is not subject to fee (exempted) such as unguaranteed/side deposits, consolidations. */ int128 public settledGrowth; /** * @notice Timestamp of the most recent settled growth correction. * This timestamp is used to prevent retroactive fees after a fee rate change. * The timestamp ensures that all fee exemptions and corrections are fully reported before changing the fee rate. * Regular fee disbursements do not update this timestamp. */ uint64 public latestCorrectionTimestamp; /** * @notice Passes the address of the vault hub up the inheritance chain. * @param _vaultHub The address of the vault hub. * @param _lidoLocator The address of the Lido locator. */ constructor(address _vaultHub, address _lidoLocator) Permissions(_vaultHub, _lidoLocator) {} /** * @dev Calls the parent's initializer, sets the node operator fee, assigns the node operator manager role, * and makes the node operator manager the admin for the node operator roles. * @param _defaultAdmin The address of the default admin * @param _nodeOperatorManager The address of the node operator manager * @param _feeRecipient The node operator fee recipient address * @param _feeRate The node operator fee rate * @param _confirmExpiry The confirmation expiry time in seconds */ function _initialize( address _defaultAdmin, address _nodeOperatorManager, address _feeRecipient, uint256 _feeRate, uint256 _confirmExpiry ) internal { _requireNotZero(_nodeOperatorManager); super._initialize(_defaultAdmin, _confirmExpiry); _setFeeRate(_feeRate); _setFeeRecipient(_feeRecipient); _grantRole(NODE_OPERATOR_MANAGER_ROLE, _nodeOperatorManager); _setRoleAdmin(NODE_OPERATOR_MANAGER_ROLE, NODE_OPERATOR_MANAGER_ROLE); _setRoleAdmin(NODE_OPERATOR_FEE_EXEMPT_ROLE, NODE_OPERATOR_MANAGER_ROLE); _setRoleAdmin(NODE_OPERATOR_UNGUARANTEED_DEPOSIT_ROLE, NODE_OPERATOR_MANAGER_ROLE); _setRoleAdmin(NODE_OPERATOR_PROVE_UNKNOWN_VALIDATOR_ROLE, NODE_OPERATOR_MANAGER_ROLE); } /** * @notice The roles that must confirm critical parameter changes in the contract. * @return roles is an array of roles that form the confirming roles. */ function confirmingRoles() public pure override returns (bytes32[] memory roles) { roles = new bytes32[](2); roles[0] = DEFAULT_ADMIN_ROLE; roles[1] = NODE_OPERATOR_MANAGER_ROLE; } /** * @notice The latest vault report for the underlying StakingVault. * @return report The latest report containing totalValue, inOutDelta, and timestamp */ function latestReport() public view returns (VaultHub.Report memory) { return VAULT_HUB.latestReport(address(_stakingVault())); } /** * @notice Calculates the current node operator fee amount in ETH. * * Fee calculation steps: * 1. Retrieve latest vault report (totalValue, inOutDelta) * 2. Calculate current growth: totalValue - inOutDelta * 3. Determine unsettled growth: currentGrowth - settledGrowth * 4. Apply fee rate: unsettledGrowth × feeRate / 10000 * * @return fee The amount of ETH accrued as fee */ function accruedFee() public view returns (uint256 fee) { (fee,, ) = _calculateFee(); } /** * @notice Disburses node operator fees permissionlessly. * Can be called by anyone as long as fee is not abnormally high. * * Fee disbursement steps: * 1. Calculate current vault growth from latest report * 2. Determine fee amount on unsettled growth * 3. Update settled growth to current growth (marking fees as paid) * 4. Withdraws fee amount from vault to node operator recipient */ function disburseFee() public { (uint256 fee, int128 growth, uint256 abnormallyHighFeeThreshold) = _calculateFee(); if (fee > abnormallyHighFeeThreshold) revert AbnormallyHighFee(); _disburseFee(fee, growth); } /** * @notice Disburses an abnormally high fee as `DEFAULT_ADMIN_ROLE`. * Before calling this function, the caller must ensure that the high fee is expected, * and the settled growth (used as baseline for fee) is set correctly. */ function disburseAbnormallyHighFee() external onlyRoleMemberOrAdmin(DEFAULT_ADMIN_ROLE) { (uint256 fee, int128 growth,) = _calculateFee(); _disburseFee(fee, growth); } /** * @notice Updates the node operator's fee rate with dual confirmation. * @param _newFeeRate The new fee rate in basis points (max 10000 = 100%) * @return bool True if fee rate was updated, false if still awaiting confirmations */ function setFeeRate(uint256 _newFeeRate) external returns (bool) { // The report must be fresh so that the total value of the vault is up to date // and all the node operator fees are paid out fairly up to the moment of the latest fresh report if (!VAULT_HUB.isReportFresh(address(_stakingVault()))) revert ReportStale(); // Latest fee exemption must be earlier than the latest fresh report timestamp if (latestCorrectionTimestamp >= _lazyOracle().latestReportTimestamp()) revert CorrectionAfterReport(); // If the vault is quarantined, the total value is reduced and may not reflect the exemption if (_lazyOracle().vaultQuarantine(address(_stakingVault())).isActive) revert VaultQuarantined(); // store the caller's confirmation; only proceed if the required number of confirmations is met. if (!_collectAndCheckConfirmations(msg.data, confirmingRoles())) return false; // Disburse any outstanding fees at the current rate before changing it disburseFee(); _setFeeRate(_newFeeRate); return true; } /** * @notice Manually corrects the settled growth value with dual confirmation. * Used to correct fee calculation. * * @param _newSettledGrowth The corrected settled growth value * @param _expectedSettledGrowth The expected current settled growth * @return bool True if correction was applied, false if awaiting confirmations */ function correctSettledGrowth(int256 _newSettledGrowth, int256 _expectedSettledGrowth) public returns (bool) { if (settledGrowth != _expectedSettledGrowth) revert UnexpectedSettledGrowth(); if (!_collectAndCheckConfirmations(msg.data, confirmingRoles())) return false; _correctSettledGrowth(_newSettledGrowth); return true; } /** * @notice Adds a fee exemption to exclude this value from node operator fee base. * The exemption works by increasing the settled growth, * effectively treating the exempted amount as if fees were already paid on it. * * @param _exemptedAmount Amount in ETH to exempt from fee calculations */ function addFeeExemption(uint256 _exemptedAmount) external onlyRoleMemberOrAdmin(NODE_OPERATOR_FEE_EXEMPT_ROLE) { _addFeeExemption(_exemptedAmount); } /** * @notice Sets the confirmation expiry period with dual confirmation. * @param _newConfirmExpiry The new confirmation expiry period in seconds * @return bool True if expiry was updated, false if awaiting confirmations */ function setConfirmExpiry(uint256 _newConfirmExpiry) external returns (bool) { _validateConfirmExpiry(_newConfirmExpiry); if (!_collectAndCheckConfirmations(msg.data, confirmingRoles())) return false; _setConfirmExpiry(_newConfirmExpiry); return true; } /** * @notice Sets the address that receives node operator fee disbursements. * @param _newFeeRecipient The new recipient address for fee payments */ function setFeeRecipient(address _newFeeRecipient) external onlyRoleMemberOrAdmin(NODE_OPERATOR_MANAGER_ROLE) { _setFeeRecipient(_newFeeRecipient); } // ==================== Internal Functions ==================== function _lazyOracle() internal view returns (LazyOracle) { return LazyOracle(LIDO_LOCATOR.lazyOracle()); } function _disburseFee(uint256 fee, int128 growth) internal { // it's important not to revert here so as not to block disconnect if (fee == 0) return; _setSettledGrowth(growth); VAULT_HUB.withdraw(address(_stakingVault()), feeRecipient, fee); emit FeeDisbursed(msg.sender, fee); } function _setSettledGrowth(int256 _newSettledGrowth) internal { int128 oldSettledGrowth = settledGrowth; if (oldSettledGrowth == _newSettledGrowth) revert SameSettledGrowth(); int128 newSettledGrowth = _newSettledGrowth.toInt128(); settledGrowth = newSettledGrowth; emit SettledGrowthSet(oldSettledGrowth, newSettledGrowth); } /** * @dev Set a new settled growth and updates the timestamp. * Should be used to correct settled growth for total value change that might not have been reported yet */ function _correctSettledGrowth(int256 _newSettledGrowth) internal { _setSettledGrowth(_newSettledGrowth); latestCorrectionTimestamp = uint64(block.timestamp); emit CorrectionTimestampUpdated(block.timestamp); } /** * @dev Increases settled growth for total value increases not subject to fee, * which is why it updates the timestamp to ensure that the exemption comes before * the total value report during the fee rate change, which guarantees that the exemption is reported * @dev fee exemption can only be positive */ function _addFeeExemption(uint256 _amount) internal { if (_amount > type(uint104).max) revert UnexpectedFeeExemptionAmount(); _correctSettledGrowth(settledGrowth + int256(_amount)); } function _calculateFee() internal view returns (uint256 fee, int128 growth, uint256 abnormallyHighFeeThreshold) { VaultHub.Report memory report = latestReport(); growth = int128(uint128(report.totalValue)) - int128(report.inOutDelta); int256 unsettledGrowth = growth - settledGrowth; if (unsettledGrowth > 0) { fee = (uint256(unsettledGrowth) * feeRate) / TOTAL_BASIS_POINTS; } abnormallyHighFeeThreshold = (report.totalValue * ABNORMALLY_HIGH_FEE_THRESHOLD_BP) / TOTAL_BASIS_POINTS; } function _setFeeRate(uint256 _newFeeRate) internal { if (_newFeeRate > TOTAL_BASIS_POINTS) revert FeeValueExceed100Percent(); uint256 oldFeeRate = feeRate; uint256 newFeeRate = _newFeeRate; feeRate = uint16(newFeeRate); emit FeeRateSet(msg.sender, oldFeeRate, newFeeRate); } function _setFeeRecipient(address _newFeeRecipient) internal { _requireNotZero(_newFeeRecipient); if (_newFeeRecipient == feeRecipient) revert SameRecipient(); address oldFeeRecipient = feeRecipient; feeRecipient = _newFeeRecipient; emit FeeRecipientSet(msg.sender, oldFeeRecipient, _newFeeRecipient); } // ==================== Events ==================== /** * @dev Emitted when the node operator fee is set. * @param sender the address of the sender * @param oldFeeRate The old node operator fee rate. * @param newFeeRate The new node operator fee rate. */ event FeeRateSet(address indexed sender, uint256 oldFeeRate, uint256 newFeeRate); /** * @dev Emitted when the node operator fee is disbursed. * @param sender the address of the sender * @param fee the amount of disbursed fee. */ event FeeDisbursed(address indexed sender, uint256 fee); /** * @dev Emitted when the node operator fee recipient is set. * @param sender the address of the sender who set the recipient * @param oldFeeRecipient the old node operator fee recipient * @param newFeeRecipient the new node operator fee recipient */ event FeeRecipientSet(address indexed sender, address oldFeeRecipient, address newFeeRecipient); /** * @dev Emitted when the settled growth is set. * @param oldSettledGrowth the old settled growth * @param newSettledGrowth the new settled growth */ event SettledGrowthSet(int128 oldSettledGrowth, int128 newSettledGrowth); /** * @dev Emitted when the settled growth is corrected. * @param timestamp new correction timestamp */ event CorrectionTimestampUpdated(uint256 timestamp); // ==================== Errors ==================== /** * @dev Error emitted when the combined feeBPs exceed 100%. */ error FeeValueExceed100Percent(); /** * @dev Error emitted when trying to disburse an abnormally high fee. */ error AbnormallyHighFee(); /** * @dev Error emitted when trying to set same value for recipient */ error SameRecipient(); /** * @dev Error emitted when trying to set same value for settled growth */ error SameSettledGrowth(); /** * @dev Error emitted when the settled growth does not match the expected value during connection. */ error SettledGrowthMismatch(); /** * @dev Error emitted when the report is stale. */ error ReportStale(); /** * @dev Error emitted when the correction is made after the report. */ error CorrectionAfterReport(); /** * @dev Error emitted when the settled growth does not match the expected value. */ error UnexpectedSettledGrowth(); /** * @dev Error emitted when the fee exemption amount does not match the expected value */ error UnexpectedFeeExemptionAmount(); /** * @dev Error emitted when the vault is quarantined. */ error VaultQuarantined(); }
// SPDX-License-Identifier: GPL-3.0 // SPDX-FileCopyrightText: 2025 Lido <[email protected]> // See contracts/COMPILERS.md pragma solidity 0.8.25; import {Clones} from "@openzeppelin/contracts-v5.2/proxy/Clones.sol"; import {AccessControlConfirmable} from "contracts/0.8.25/utils/AccessControlConfirmable.sol"; import {ILidoLocator} from "contracts/common/interfaces/ILidoLocator.sol"; import {IStakingVault} from "../interfaces/IStakingVault.sol"; import {OperatorGrid} from "../OperatorGrid.sol"; import {VaultHub} from "../VaultHub.sol"; /** * @title Permissions * @author Lido * @notice Provides granular permissions for StakingVault operations. */ abstract contract Permissions is AccessControlConfirmable { /** * @notice Struct containing an account and a role for granting/revoking roles. */ struct RoleAssignment { address account; bytes32 role; } /** * @notice Permission for funding the StakingVault. */ /// @dev 0x933b7d5c112a4d05b489cea0b2ced98acb27d3d0fc9827c92cdacb2d6c5559c2 bytes32 public constant FUND_ROLE = keccak256("vaults.Permissions.Fund"); /** * @notice Permission for withdrawing funds from the StakingVault. */ /// @dev 0x355caf1c2580ed8185acb5ea3573b71f85186b41bdf69e3eb8f1fcd122a562df bytes32 public constant WITHDRAW_ROLE = keccak256("vaults.Permissions.Withdraw"); /** * @notice Permission for minting stETH shares backed by the StakingVault. */ /// @dev 0xe996ac9b332538bb1fa3cd6743aa47011623cdb94bd964a494ee9d371e4a27d3 bytes32 public constant MINT_ROLE = keccak256("vaults.Permissions.Mint"); /** * @notice Permission for burning stETH shares backed by the StakingVault. */ /// @dev 0x689f0a569be0c9b6cd2c11c81cb0add722272abdae6b649fdb1e05f1d9bb8a2f bytes32 public constant BURN_ROLE = keccak256("vaults.Permissions.Burn"); /** * @notice Permission for rebalancing the StakingVault. */ /// @dev 0x3f82ecf462ddac43fc17ba11472c35f18b7760b4f5a5fc50b9625f9b5a22cf62 bytes32 public constant REBALANCE_ROLE = keccak256("vaults.Permissions.Rebalance"); /** * @notice Permission for pausing beacon chain deposits on the StakingVault. */ /// @dev 0xa90c7030a27f389f9fc8ed21a0556f40c88130cc14a80db936bed68261819b2c bytes32 public constant PAUSE_BEACON_CHAIN_DEPOSITS_ROLE = keccak256("vaults.Permissions.PauseDeposits"); /** * @notice Permission for resuming beacon chain deposits on the StakingVault. */ /// @dev 0x59d005e32db662b94335d6bedfeb453fd2202b9f0cc7a6ed498d9098171744b0 bytes32 public constant RESUME_BEACON_CHAIN_DEPOSITS_ROLE = keccak256("vaults.Permissions.ResumeDeposits"); /** * @notice Permission for requesting validator exit from the StakingVault. */ /// @dev 0x32d0d6546e21c13ff633616141dc9daad87d248d1d37c56bf493d06d627ecb7b bytes32 public constant REQUEST_VALIDATOR_EXIT_ROLE = keccak256("vaults.Permissions.RequestValidatorExit"); /** * @notice Permission for triggering validator withdrawal from the StakingVault using EIP-7002 triggerable exit. */ /// @dev 0xea19d3b23bd90fdd52445ad672f2b6fb1fef7230d49c6a827c1cd288d02994d5 bytes32 public constant TRIGGER_VALIDATOR_WITHDRAWAL_ROLE = keccak256("vaults.Permissions.TriggerValidatorWithdrawal"); /** * @notice Permission for voluntary disconnecting the StakingVault. */ /// @dev 0x9586321ac05f110e4b4a0a42aba899709345af0ca78910e8832ddfd71fed2bf4 bytes32 public constant VOLUNTARY_DISCONNECT_ROLE = keccak256("vaults.Permissions.VoluntaryDisconnect"); /** * @dev Permission for vault configuration operations on the OperatorGrid (tier changes, tier sync, share limit updates). */ /// @dev 0x25482e7dc9e29f6da5bd70b6d19d17bbf44021da51ba0664a9f430c94a09c674 bytes32 public constant VAULT_CONFIGURATION_ROLE = keccak256("vaults.Permissions.VaultConfiguration"); VaultHub public immutable VAULT_HUB; ILidoLocator public immutable LIDO_LOCATOR; /** * @notice Indicates whether the contract has been initialized */ bool public initialized; constructor(address _vaultHub, address _lidoLocator) { _requireNotZero(_vaultHub); _requireNotZero(_lidoLocator); initialized = true; // @dev vaultHub is cached as immutable to save gas for main operations VAULT_HUB = VaultHub(payable(_vaultHub)); LIDO_LOCATOR = ILidoLocator(_lidoLocator); } /** * @notice Modifier to prevent reinitialization of the contract. * @dev Extracted to modifier to avoid Slither warning. */ modifier initializer() { if (initialized) revert AlreadyInitialized(); initialized = true; _; emit Initialized(); } /** * @dev Sets the ACL default admin and confirmation expiry time. * @param _defaultAdmin The address of the default admin * @param _confirmExpiry The confirmation expiry time in seconds */ function _initialize(address _defaultAdmin, uint256 _confirmExpiry) internal initializer { _requireNotZero(_defaultAdmin); _grantRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); _validateConfirmExpiry(_confirmExpiry); _setConfirmExpiry(_confirmExpiry); } /** * @notice Returns the address of the underlying StakingVault. * @return The address of the StakingVault. */ function stakingVault() external view returns (IStakingVault) { return _stakingVault(); } // ==================== Role Management Functions ==================== /** * @notice Mass-grants multiple roles to multiple accounts. * @param _assignments An array of role assignments. * @dev Performs the role admin checks internally. * @dev If an account is already a member of a role, doesn't revert, emits no events. */ function grantRoles(RoleAssignment[] calldata _assignments) external { _requireNotZero(_assignments.length); for (uint256 i = 0; i < _assignments.length; i++) { grantRole(_assignments[i].role, _assignments[i].account); } } /** * @notice Mass-revokes multiple roles from multiple accounts. * @param _assignments An array of role assignments. * @dev Performs the role admin checks internally. * @dev If an account is not a member of a role, doesn't revert, emits no events. */ function revokeRoles(RoleAssignment[] calldata _assignments) external { _requireNotZero(_assignments.length); for (uint256 i = 0; i < _assignments.length; i++) { revokeRole(_assignments[i].role, _assignments[i].account); } } /** * @dev Returns an array of roles that need to confirm the calls that require confirmations * @return The roles that need to confirm the call. */ function confirmingRoles() public pure virtual returns (bytes32[] memory); /** * @dev A custom modifier that checks if the caller has a role or the admin role for a given role. * @param _role The role to check. */ modifier onlyRoleMemberOrAdmin(bytes32 _role) { if (!(hasRole(_role, msg.sender) || hasRole(getRoleAdmin(_role), msg.sender))) { revert AccessControlUnauthorizedAccount(msg.sender, _role); } _; } /** * @dev Checks the FUND_ROLE and funds the StakingVault. * @param _ether The amount of ether to fund the StakingVault with. */ function _fund(uint256 _ether) internal onlyRoleMemberOrAdmin(FUND_ROLE) { VAULT_HUB.fund{value: _ether}(address(_stakingVault())); } /** * @dev Checks the WITHDRAW_ROLE and withdraws funds from the StakingVault. * @param _recipient The address to withdraw the funds to. * @param _ether The amount of ether to withdraw from the StakingVault. */ function _withdraw(address _recipient, uint256 _ether) internal virtual onlyRoleMemberOrAdmin(WITHDRAW_ROLE) { VAULT_HUB.withdraw(address(_stakingVault()), _recipient, _ether); } /** * @dev Checks the MINT_ROLE and mints shares backed by the StakingVault. * @param _recipient The address to mint the shares to. * @param _shares The amount of shares to mint. */ function _mintShares(address _recipient, uint256 _shares) internal onlyRoleMemberOrAdmin(MINT_ROLE) { VAULT_HUB.mintShares(address(_stakingVault()), _recipient, _shares); } /** * @dev Checks the BURN_ROLE and burns shares backed by the StakingVault. * @param _shares The amount of shares to burn. */ function _burnShares(uint256 _shares) internal onlyRoleMemberOrAdmin(BURN_ROLE) { VAULT_HUB.burnShares(address(_stakingVault()), _shares); } /** * @dev Checks the REBALANCE_ROLE and rebalances the StakingVault. * @param _shares The amount of shares to rebalance the StakingVault with. */ function _rebalanceVault(uint256 _shares) internal onlyRoleMemberOrAdmin(REBALANCE_ROLE) { VAULT_HUB.rebalance(address(_stakingVault()), _shares); } /** * @dev Checks the PAUSE_BEACON_CHAIN_DEPOSITS_ROLE and pauses beacon chain deposits on the StakingVault. */ function _pauseBeaconChainDeposits() internal onlyRoleMemberOrAdmin(PAUSE_BEACON_CHAIN_DEPOSITS_ROLE) { VAULT_HUB.pauseBeaconChainDeposits(address(_stakingVault())); } /** * @dev Checks the RESUME_BEACON_CHAIN_DEPOSITS_ROLE and resumes beacon chain deposits on the StakingVault. */ function _resumeBeaconChainDeposits() internal onlyRoleMemberOrAdmin(RESUME_BEACON_CHAIN_DEPOSITS_ROLE) { VAULT_HUB.resumeBeaconChainDeposits(address(_stakingVault())); } /** * @dev Checks the REQUEST_VALIDATOR_EXIT_ROLE and requests validator exit on the StakingVault. */ function _requestValidatorExit( bytes calldata _pubkeys ) internal onlyRoleMemberOrAdmin(REQUEST_VALIDATOR_EXIT_ROLE) { VAULT_HUB.requestValidatorExit(address(_stakingVault()), _pubkeys); } /** * @dev Checks the TRIGGER_VALIDATOR_WITHDRAWAL_ROLE and triggers validator withdrawal on the StakingVault * using EIP-7002 triggerable exit. */ function _triggerValidatorWithdrawals( bytes calldata _pubkeys, uint64[] calldata _amountsInGwei, address _refundRecipient ) internal onlyRoleMemberOrAdmin(TRIGGER_VALIDATOR_WITHDRAWAL_ROLE) { VAULT_HUB.triggerValidatorWithdrawals{value: msg.value}( address(_stakingVault()), _pubkeys, _amountsInGwei, _refundRecipient ); } /** * @dev Checks the VOLUNTARY_DISCONNECT_ROLE and voluntarily disconnects the StakingVault. */ function _voluntaryDisconnect() internal onlyRoleMemberOrAdmin(VOLUNTARY_DISCONNECT_ROLE) { VAULT_HUB.voluntaryDisconnect(address(_stakingVault())); } /** * @dev Checks the DEFAULT_ADMIN_ROLE and transfers the StakingVault ownership. * @param _newOwner The address to transfer the ownership to. */ function _transferOwnership(address _newOwner) internal onlyRole(DEFAULT_ADMIN_ROLE) { _stakingVault().transferOwnership(_newOwner); } /** * @dev Checks the DEFAULT_ADMIN_ROLE and accepts the StakingVault ownership. */ function _acceptOwnership() internal onlyRole(DEFAULT_ADMIN_ROLE) { _stakingVault().acceptOwnership(); } /** * @dev Checks the confirming roles and transfer the ownership of the vault without disconnecting it from the hub * @param _newOwner The address to set the owner to. */ function _transferVaultOwnership(address _newOwner) internal returns (bool) { if (!_collectAndCheckConfirmations(msg.data, confirmingRoles())) return false; VAULT_HUB.transferVaultOwnership(address(_stakingVault()), _newOwner); return true; } /** * @dev Checks the VAULT_CONFIGURATION_ROLE and requests a change of the tier on the OperatorGrid. * @param _tierId The tier to change to. * @param _requestedShareLimit The requested share limit. * @return bool Whether the tier change was executed. */ function _changeTier( uint256 _tierId, uint256 _requestedShareLimit ) internal onlyRoleMemberOrAdmin(VAULT_CONFIGURATION_ROLE) returns (bool) { return _operatorGrid().changeTier(address(_stakingVault()), _tierId, _requestedShareLimit); } /** * @dev Checks the VAULT_CONFIGURATION_ROLE and requests a sync of the tier on the OperatorGrid. * @return bool Whether the tier sync was executed. */ function _syncTier() internal onlyRoleMemberOrAdmin(VAULT_CONFIGURATION_ROLE) returns (bool) { return _operatorGrid().syncTier(address(_stakingVault())); } /** * @dev Checks the VAULT_CONFIGURATION_ROLE and updates the share limit on the OperatorGrid. * @param _requestedShareLimit The requested share limit. * @return bool Whether the share limit update was executed. */ function _updateVaultShareLimit(uint256 _requestedShareLimit) internal onlyRoleMemberOrAdmin(VAULT_CONFIGURATION_ROLE) returns (bool) { return _operatorGrid().updateVaultShareLimit(address(_stakingVault()), _requestedShareLimit); } /** * @dev Loads the address of the underlying StakingVault. * @return addr The address of the StakingVault. */ function _stakingVault() internal view returns (IStakingVault) { bytes memory args = Clones.fetchCloneArgs(address(this)); address stakingVaultAddress; assembly { stakingVaultAddress := mload(add(args, 32)) } return IStakingVault(stakingVaultAddress); } function _operatorGrid() internal view returns (OperatorGrid) { return OperatorGrid(LIDO_LOCATOR.operatorGrid()); } function _requireNotZero(uint256 _value) internal pure { if (_value == 0) revert ZeroArgument(); } function _requireNotZero(address _address) internal pure { if (_address == address(0)) revert ZeroAddress(); } /** * @notice Emitted when the contract is initialized */ event Initialized(); /** * @notice Error when the contract is already initialized. */ error AlreadyInitialized(); /** * @notice Error thrown for when a given value cannot be zero */ error ZeroArgument(); /** * @notice Error thrown for when a given address cannot be zero */ error ZeroAddress(); }
// SPDX-FileCopyrightText: 2025 Lido <[email protected]> // SPDX-License-Identifier: GPL-3.0 // See contracts/COMPILERS.md // solhint-disable-next-line lido/fixed-compiler-version pragma solidity >=0.8.0; /** * @title IPinnedBeaconProxy * @author Lido * @notice Interface for the `PinnedBeaconProxy` contract */ interface IPinnedBeaconProxy { function isOssified() external view returns (bool); }
// SPDX-FileCopyrightText: 2025 Lido <[email protected]> // SPDX-License-Identifier: GPL-3.0 // See contracts/COMPILERS.md // solhint-disable-next-line lido/fixed-compiler-version pragma solidity >=0.8.0; import {IStakingVault} from "./IStakingVault.sol"; /** * @title IPredepositGuarantee * @author Lido * @notice Interface for the `PredepositGuarantee` contract */ interface IPredepositGuarantee { /** * @notice represents validator stages in PDG flow * @param NONE - initial stage * @param PREDEPOSITED - PREDEPOSIT_AMOUNT is deposited to this validator by the vault * @param PROVEN - validator is proven to be valid and can be used to deposit to beacon chain * @param ACTIVATED - validator is proven and the ACTIVATION_DEPOSIT_AMOUNT is deposited to this validator * @param COMPENSATED - disproven validator has its PREDEPOSIT_AMOUNT ether compensated to staking vault owner and validator cannot be used in PDG anymore */ enum ValidatorStage { NONE, PREDEPOSITED, PROVEN, ACTIVATED, COMPENSATED } /** * @notice represents status of the validator in PDG * @param stage represents validator stage in PDG flow * @param stakingVault pins validator to specific StakingVault * @param nodeOperator pins validator to specific NO */ struct ValidatorStatus { ValidatorStage stage; IStakingVault stakingVault; address nodeOperator; } /** * @notice user input for validator proof verification * @custom:proof array of merkle proofs from parent(pubkey,wc) node to Beacon block root * @custom:pubkey of validator to prove * @custom:validatorIndex of validator in CL state tree * @custom:childBlockTimestamp of EL block that has parent block beacon root in BEACON_ROOTS contract * @custom:slot of the beacon block for which the proof is generated * @custom:proposerIndex of the beacon block for which the proof is generated */ struct ValidatorWitness { bytes32[] proof; bytes pubkey; uint256 validatorIndex; uint64 childBlockTimestamp; uint64 slot; uint64 proposerIndex; } function pendingActivations(IStakingVault _vault) external view returns (uint256); function validatorStatus(bytes calldata _pubkey) external view returns (ValidatorStatus memory); function proveUnknownValidator(ValidatorWitness calldata _witness, IStakingVault _stakingVault) external; }
// SPDX-FileCopyrightText: 2025 Lido <[email protected]> // SPDX-License-Identifier: GPL-3.0 // See contracts/COMPILERS.md // solhint-disable-next-line lido/fixed-compiler-version pragma solidity >=0.8.0; import {IDepositContract} from "contracts/common/interfaces/IDepositContract.sol"; /** * @title IStakingVault * @author Lido * @notice Interface for the `StakingVault` contract */ interface IStakingVault { /** * @notice validator deposit from the `StakingVault` to the beacon chain * @dev withdrawal credentials are provided by the vault * @custom:pubkey The validator's BLS public key (48 bytes) * @custom:signature BLS signature of the deposit data (96 bytes) * @custom:amount Amount of ETH to deposit in wei (must be a multiple of 1 Gwei and minimum of 1 ETH) * @custom:depositDataRoot The root hash of the deposit data per ETH beacon spec */ struct Deposit { bytes pubkey; bytes signature; uint256 amount; bytes32 depositDataRoot; } function DEPOSIT_CONTRACT() external view returns (IDepositContract); function initialize(address _owner, address _nodeOperator, address _depositor) external; function version() external pure returns (uint64); function getInitializedVersion() external view returns (uint64); function withdrawalCredentials() external view returns (bytes32); function owner() external view returns (address); function pendingOwner() external view returns (address); function acceptOwnership() external; function transferOwnership(address _newOwner) external; function nodeOperator() external view returns (address); function depositor() external view returns (address); function calculateValidatorWithdrawalFee(uint256 _keysCount) external view returns (uint256); function fund() external payable; function withdraw(address _recipient, uint256 _ether) external; function beaconChainDepositsPaused() external view returns (bool); function pauseBeaconChainDeposits() external; function resumeBeaconChainDeposits() external; function depositToBeaconChain(Deposit calldata _deposits) external; function requestValidatorExit(bytes calldata _pubkeys) external; function triggerValidatorWithdrawals(bytes calldata _pubkeys, uint64[] calldata _amountsInGwei, address _refundRecipient) external payable; function ejectValidators(bytes calldata _pubkeys, address _refundRecipient) external payable; function setDepositor(address _depositor) external; function ossify() external; function collectERC20(address _token, address _recipient, uint256 _amount) external; function availableBalance() external view returns (uint256); function stagedBalance() external view returns (uint256); function stage(uint256 _ether) external; function unstage(uint256 _ether) external; function depositFromStaged(Deposit calldata _deposit, uint256 _additionalAmount) external; }
// SPDX-FileCopyrightText: 2025 Lido <[email protected]> // SPDX-License-Identifier: GPL-3.0 // See contracts/COMPILERS.md pragma solidity 0.8.25; interface IVaultFactory { function deployedVaults(address _vault) external view returns (bool); }
// SPDX-FileCopyrightText: 2025 Lido <[email protected]> // SPDX-License-Identifier: GPL-3.0 // See contracts/COMPILERS.md pragma solidity 0.8.25; import {MerkleProof} from "@openzeppelin/contracts-v5.2/utils/cryptography/MerkleProof.sol"; import {AccessControlEnumerableUpgradeable} from "contracts/openzeppelin/5.2/upgradeable/access/extensions/AccessControlEnumerableUpgradeable.sol"; import {ILazyOracle} from "contracts/common/interfaces/ILazyOracle.sol"; import {ILidoLocator} from "contracts/common/interfaces/ILidoLocator.sol"; import {ILido} from "contracts/common/interfaces/ILido.sol"; import {VaultHub} from "./VaultHub.sol"; import {OperatorGrid} from "./OperatorGrid.sol"; import {IStakingVault} from "./interfaces/IStakingVault.sol"; import {IPredepositGuarantee} from "./interfaces/IPredepositGuarantee.sol"; import {DoubleRefSlotCache, DOUBLE_CACHE_LENGTH} from "./lib/RefSlotCache.sol"; contract LazyOracle is ILazyOracle, AccessControlEnumerableUpgradeable { using DoubleRefSlotCache for DoubleRefSlotCache.Int104WithCache[DOUBLE_CACHE_LENGTH]; enum QuarantineState { NO_QUARANTINE, // No active quarantine QUARANTINE_ACTIVE, // Quarantine active, not expired QUARANTINE_EXPIRED // Quarantine period has passed } /// @custom:storage-location erc7201:Lido.Vaults.LazyOracle struct Storage { /// @notice root of the vaults data tree bytes32 vaultsDataTreeRoot; /// @notice CID of the vaults data tree string vaultsDataReportCid; /// @notice timestamp of the vaults data uint64 vaultsDataTimestamp; /// @notice refSlot of the vaults data uint48 vaultsDataRefSlot; /// @notice total value increase quarantine period uint64 quarantinePeriod; /// @notice max reward ratio for refSlot-observed total value, basis points uint16 maxRewardRatioBP; /// @notice max Lido fee rate per second, in wei uint64 maxLidoFeeRatePerSecond; // 64 bit is enough for up to 18 ETH/s /// @notice deposit quarantines for each vault mapping(address vault => Quarantine) vaultQuarantines; } /* A quarantine is a timelock applied to any sudden jump in a vault's reported total value that cannot be immediately confirmed on-chain (via the inOutDelta difference). If the reported total value exceeds the expected routine EL/CL rewards, the excess is pushed into a quarantine buffer for a predefined cooldown period. Only after this delay is the quarantined value released into VaultHub's total value. Normal top-ups — where the vault owner funds the contract directly using the `fund()` function — do not go through quarantine, as they can be verified on-chain via the inOutDelta value. These direct fundings are reflected immediately. In contrast, consolidations or deposits that bypass the vault's balance must sit in quarantine. Example flow: Time 0: Total Value = 100 ETH ┌────────────────────────────────────┐ │ 100 ETH Active │ └────────────────────────────────────┘ Time 1: Sudden jump of +50 ETH → start quarantine for 50 ETH ┌────────────────────────────────────┐ │ 100 ETH Active │ │ 50 ETH Quarantined │ └────────────────────────────────────┘ Time 2: Another jump of +70 ETH → wait for current quarantine to expire ┌────────────────────────────────────┐ │ 100 ETH Active │ │ 50 ETH Quarantined │ │ 70 ETH Quarantine Queue │ └────────────────────────────────────┘ Time 3: First quarantine expires → add 50 ETH to active value, start new quarantine for 70 ETH ┌────────────────────────────────────┐ │ 150 ETH Active │ │ 70 ETH Quarantined │ └────────────────────────────────────┘ Time 4: Second quarantine expires → add 70 ETH to active value ┌────────────────────────────────────┐ │ 220 ETH Active │ └────────────────────────────────────┘ */ struct Quarantine { uint128 pendingTotalValueIncrease; uint64 startTimestamp; } struct QuarantineInfo { bool isActive; uint256 pendingTotalValueIncrease; uint256 startTimestamp; uint256 endTimestamp; } struct VaultInfo { address vault; uint256 aggregatedBalance; // includes availableBalance and stagedBalance int256 inOutDelta; bytes32 withdrawalCredentials; uint256 liabilityShares; uint256 maxLiabilityShares; uint256 mintableStETH; uint96 shareLimit; uint16 reserveRatioBP; uint16 forcedRebalanceThresholdBP; uint16 infraFeeBP; uint16 liquidityFeeBP; uint16 reservationFeeBP; bool pendingDisconnect; } // keccak256(abi.encode(uint256(keccak256("Lido.Vaults.LazyOracle")) - 1)) & ~bytes32(uint256(0xff)) bytes32 private constant LAZY_ORACLE_STORAGE_LOCATION = 0x73a2a247d4b1b6fe056fe90935e9bd3694e896bafdd08f046c2afe6ec2db2100; /// @dev 0x7baf7f4a9784fa74c97162de631a3eb567edeb85878cb6965945310f2c512c63 bytes32 public constant UPDATE_SANITY_PARAMS_ROLE = keccak256("vaults.LazyOracle.UpdateSanityParams"); ILidoLocator public immutable LIDO_LOCATOR; /// @dev basis points base uint256 private constant TOTAL_BASIS_POINTS = 100_00; uint256 private constant MAX_SANE_TOTAL_VALUE = type(uint96).max; uint256 public constant MAX_QUARANTINE_PERIOD = 30 days; /// @dev max value for reward ratio - it's about 650% uint256 public constant MAX_REWARD_RATIO = type(uint16).max; uint256 public constant MAX_LIDO_FEE_RATE_PER_SECOND = 10 ether; constructor(address _lidoLocator) { LIDO_LOCATOR = ILidoLocator(payable(_lidoLocator)); _disableInitializers(); } /// @notice Initializes the contract /// @param _admin Address of the admin /// @param _quarantinePeriod the quarantine period, seconds /// @param _maxRewardRatioBP the max reward ratio, basis points /// @param _maxLidoFeeRatePerSecond the max Lido fee rate per second function initialize( address _admin, uint256 _quarantinePeriod, uint256 _maxRewardRatioBP, uint256 _maxLidoFeeRatePerSecond ) external initializer { if (_admin == address(0)) revert AdminCannotBeZero(); _grantRole(DEFAULT_ADMIN_ROLE, _admin); _updateSanityParams(_quarantinePeriod, _maxRewardRatioBP, _maxLidoFeeRatePerSecond); } /// @notice returns the latest report data /// @return timestamp of the report /// @return refSlot of the report /// @return treeRoot merkle root of the report /// @return reportCid IPFS CID for the report JSON file function latestReportData() external view returns ( uint256 timestamp, uint256 refSlot, bytes32 treeRoot, string memory reportCid ) { Storage storage $ = _storage(); return ($.vaultsDataTimestamp, $.vaultsDataRefSlot, $.vaultsDataTreeRoot, $.vaultsDataReportCid); } /// @notice returns the latest report timestamp function latestReportTimestamp() external view returns (uint256) { return _storage().vaultsDataTimestamp; } /// @notice returns the quarantine period function quarantinePeriod() external view returns (uint256) { return _storage().quarantinePeriod; } /// @notice returns the max reward ratio for refSlot total value, basis points function maxRewardRatioBP() external view returns (uint256) { return _storage().maxRewardRatioBP; } /// @notice returns the max Lido fee rate per second, in ether function maxLidoFeeRatePerSecond() external view returns (uint256) { return _storage().maxLidoFeeRatePerSecond; } /// @notice returns the quarantine info for the vault /// @param _vault the address of the vault /// @dev returns zeroed structure if there is no active quarantine function vaultQuarantine(address _vault) external view returns (QuarantineInfo memory) { Quarantine storage q = _storage().vaultQuarantines[_vault]; if (q.pendingTotalValueIncrease == 0) { return QuarantineInfo(false, 0, 0, 0); } return QuarantineInfo({ isActive: true, pendingTotalValueIncrease: q.pendingTotalValueIncrease, startTimestamp: q.startTimestamp, endTimestamp: q.startTimestamp + _storage().quarantinePeriod }); } /// @notice returns the number of vaults connected to the VaultHub /// @return the number of vaults connected to the VaultHub function vaultsCount() external view returns (uint256) { return _vaultHub().vaultsCount(); } /// @notice returns batch of vaults info /// @param _offset in the vaults list [0, vaultsCount) /// @param _limit maximum number of vaults to return /// @return batch of vaults info function batchVaultsInfo(uint256 _offset, uint256 _limit) external view returns (VaultInfo[] memory) { VaultHub vaultHub = _vaultHub(); uint256 vaultCount = vaultHub.vaultsCount(); uint256 batchSize; if (_offset > vaultCount) { batchSize = 0; } else { batchSize = _offset + _limit > vaultCount ? vaultCount - _offset : _limit; } VaultInfo[] memory batch = new VaultInfo[](batchSize); for (uint256 i = 0; i < batchSize; i++) { address vaultAddress = vaultHub.vaultByIndex(_offset + i + 1); batch[i] = _vaultInfo(vaultAddress, vaultHub); } return batch; } /// @notice returns the vault data info /// @param _vault the address of the vault /// @return the vault data info function vaultInfo(address _vault) external view returns (VaultInfo memory) { return _vaultInfo(_vault, _vaultHub()); } /** * @notice batch method to mass check the validator stages in PredepositGuarantee contract * @param _pubkeys the array of validator's pubkeys to check */ function batchValidatorStages( bytes[] calldata _pubkeys ) external view returns (IPredepositGuarantee.ValidatorStage[] memory batch) { batch = new IPredepositGuarantee.ValidatorStage[](_pubkeys.length); for (uint256 i = 0; i < _pubkeys.length; i++) { batch[i] = predepositGuarantee().validatorStatus(_pubkeys[i]).stage; } } /// @notice update the sanity parameters /// @param _quarantinePeriod the quarantine period /// @param _maxRewardRatioBP the max EL CL rewards /// @param _maxLidoFeeRatePerSecond the max Lido fee rate per second function updateSanityParams( uint256 _quarantinePeriod, uint256 _maxRewardRatioBP, uint256 _maxLidoFeeRatePerSecond ) external onlyRole(UPDATE_SANITY_PARAMS_ROLE) { _updateSanityParams(_quarantinePeriod, _maxRewardRatioBP, _maxLidoFeeRatePerSecond); } /// @notice Store the report root and its meta information /// @param _vaultsDataTimestamp the timestamp of the report /// @param _vaultsDataRefSlot the refSlot of the report /// @param _vaultsDataTreeRoot the root of the report /// @param _vaultsDataReportCid the CID of the report function updateReportData( uint256 _vaultsDataTimestamp, uint256 _vaultsDataRefSlot, bytes32 _vaultsDataTreeRoot, string memory _vaultsDataReportCid ) external override(ILazyOracle) { if (msg.sender != LIDO_LOCATOR.accountingOracle()) revert NotAuthorized(); Storage storage $ = _storage(); $.vaultsDataTimestamp = uint64(_vaultsDataTimestamp); $.vaultsDataRefSlot = uint48(_vaultsDataRefSlot); $.vaultsDataTreeRoot = _vaultsDataTreeRoot; $.vaultsDataReportCid = _vaultsDataReportCid; emit VaultsReportDataUpdated( _vaultsDataTimestamp, _vaultsDataRefSlot, _vaultsDataTreeRoot, _vaultsDataReportCid ); } /// @notice Permissionless update of the vault data /// @param _vault the address of the vault /// @param _totalValue the total value of the vault /// @param _cumulativeLidoFees the cumulative Lido fees accrued on the vault (nominated in ether) /// @param _liabilityShares the liabilityShares value of the vault (on the vaultsDataRefSlot) /// @param _maxLiabilityShares the maxLiabilityShares value of the vault (on the vaultsDataRefSlot) /// @param _proof the proof of the reported data function updateVaultData( address _vault, uint256 _totalValue, uint256 _cumulativeLidoFees, uint256 _liabilityShares, uint256 _maxLiabilityShares, uint256 _slashingReserve, bytes32[] calldata _proof ) external { bytes32 leaf = keccak256( bytes.concat( keccak256( abi.encode( _vault, _totalValue, _cumulativeLidoFees, _liabilityShares, _maxLiabilityShares, _slashingReserve ) ) ) ); if (!MerkleProof.verify(_proof, _storage().vaultsDataTreeRoot, leaf)) revert InvalidProof(); uint256 vaultsDataTimestamp = _storage().vaultsDataTimestamp; (uint256 checkedTotalValue, int256 inOutDelta) = _handleSanityChecks( _vault, _totalValue, _storage().vaultsDataRefSlot, vaultsDataTimestamp, _cumulativeLidoFees, _liabilityShares, _maxLiabilityShares ); _vaultHub().applyVaultReport( _vault, vaultsDataTimestamp, checkedTotalValue, inOutDelta, _cumulativeLidoFees, _liabilityShares, _maxLiabilityShares, _slashingReserve ); } /// @notice removes the quarantine for the vault /// @param _vault the address of the vault function removeVaultQuarantine(address _vault) external { if (msg.sender != LIDO_LOCATOR.vaultHub()) revert NotAuthorized(); mapping(address => Quarantine) storage quarantines = _storage().vaultQuarantines; if (quarantines[_vault].pendingTotalValueIncrease > 0) { emit QuarantineRemoved(_vault); } delete quarantines[_vault]; } function _vaultInfo(address _vault, VaultHub _vh) internal view returns (VaultInfo memory) { IStakingVault vault = IStakingVault(_vault); VaultHub.VaultConnection memory connection = _vh.vaultConnection(_vault); VaultHub.VaultRecord memory record = _vh.vaultRecord(_vault); return VaultInfo( _vault, vault.availableBalance() + vault.stagedBalance(), record.inOutDelta.currentValue(), vault.withdrawalCredentials(), record.liabilityShares, record.maxLiabilityShares, _mintableStETH(_vault, _vh), connection.shareLimit, connection.reserveRatioBP, connection.forcedRebalanceThresholdBP, connection.infraFeeBP, connection.liquidityFeeBP, connection.reservationFeeBP, _vh.isPendingDisconnect(_vault) ); } /// @notice handle sanity checks for the vault lazy report data /// @param _vault the address of the vault /// @param _totalValue the total value of the vault in refSlot /// @param _reportRefSlot the refSlot of the report /// @param _reportTimestamp the timestamp of the report /// @param _cumulativeLidoFees the cumulative Lido fees accrued on the vault (nominated in ether) /// @param _liabilityShares the liabilityShares value of the vault (on the _reportRefSlot) /// @param _maxLiabilityShares the maxLiabilityShares value of the vault (on the _reportRefSlot) /// @return totalValueWithoutQuarantine the smoothed total value of the vault after sanity checks /// @return inOutDeltaOnRefSlot the inOutDelta in the refSlot function _handleSanityChecks( address _vault, uint256 _totalValue, uint256 _reportRefSlot, uint256 _reportTimestamp, uint256 _cumulativeLidoFees, uint256 _liabilityShares, uint256 _maxLiabilityShares ) internal returns (uint256 totalValueWithoutQuarantine, int256 inOutDeltaOnRefSlot) { VaultHub vaultHub = _vaultHub(); VaultHub.VaultRecord memory record = vaultHub.vaultRecord(_vault); uint48 previousReportTs = record.report.timestamp; // 0. Check if the report is already fresh enough if (uint48(_reportTimestamp) <= previousReportTs) { revert VaultReportIsFreshEnough(); } // 1. Calculate inOutDelta in the refSlot int256 currentInOutDelta = record.inOutDelta.currentValue(); inOutDeltaOnRefSlot = record.inOutDelta.getValueForRefSlot(uint48(_reportRefSlot)); // 2. Sanity check for total value increase totalValueWithoutQuarantine = _processTotalValue( _vault, _totalValue, inOutDeltaOnRefSlot, record, _reportTimestamp); // 3. Sanity check for dynamic total value underflow if (int256(totalValueWithoutQuarantine) + currentInOutDelta - inOutDeltaOnRefSlot < 0) { revert UnderflowInTotalValueCalculation(); } // 4. Sanity check for cumulative Lido fees uint256 previousCumulativeLidoFees = record.cumulativeLidoFees; if (previousCumulativeLidoFees > _cumulativeLidoFees) { revert CumulativeLidoFeesTooLow(_cumulativeLidoFees, previousCumulativeLidoFees); } uint256 maxLidoFees = (_reportTimestamp - previousReportTs) * uint256(_storage().maxLidoFeeRatePerSecond); if (_cumulativeLidoFees - previousCumulativeLidoFees > maxLidoFees) { revert CumulativeLidoFeesTooLarge(_cumulativeLidoFees - previousCumulativeLidoFees, maxLidoFees); } // 5. _maxLiabilityShares is greater or equal than _liabilityShares and current `maxLiabilityShares` if (_maxLiabilityShares < _liabilityShares || _maxLiabilityShares < record.maxLiabilityShares) { revert InvalidMaxLiabilityShares(); } } /* Quarantine State Diagram States: • NO_QUARANTINE: No active quarantine, all value is immediately available • QUARANTINE_ACTIVE: Total value increase is quarantined, waiting for expiration • QUARANTINE_EXPIRED: Quarantine period passed, quarantined value can be released ┌─────────────────┐ ┌──────────────────┐ │ NO_QUARANTINE │ reported > threshold │QUARANTINE_ACTIVE │ │ ├─────────────────────────────►│ │ │ quarantined=0 │ │ quarantined>0 │ │ startTime=0 │◄─────────────────────────────┤ startTime>0 │ │ | │ time<expiration | └─────────────────┘ reported ≤ threshold └───┬──────────────┘ ▲ (early release) │ ▲ │ │ │ increase > quarantined + rewards │ time ≥ │ │ (release old, start new) │ quarantine period │ │ │ ▼ │ │ ┌─────────────┴────────┐ │ reported ≤ threshold OR │ QUARANTINE_EXPIRED │ │ increase ≤ quarantined + rewards │ │ │ │ quarantined>0 │ │ │ startTime>0 │ └──────────────────────────────────────┤ time>=expiration │ └──────────────────────┘ Legend: • threshold = onchainTotalValue * (100% + maxRewardRatio) • increase = reportedTotalValue - onchainTotalValue • quarantined - total value increase that is currently quarantined • rewards - expected EL/CL rewards based on maxRewardRatio • time = block.timestamp • expiration = quarantine.startTimestamp + quarantinePeriod */ function _processTotalValue( address _vault, uint256 _reportedTotalValue, int256 _inOutDeltaOnRefSlot, VaultHub.VaultRecord memory record, uint256 _reportTimestamp ) internal returns (uint256 totalValueWithoutQuarantine) { if (_reportedTotalValue > MAX_SANE_TOTAL_VALUE) { revert TotalValueTooLarge(); } // Calculate base values for quarantine logic ------------------------- // -------------------------------------------------------------------- // 0. Read storage values Storage storage $ = _storage(); Quarantine storage quarantine = $.vaultQuarantines[_vault]; uint256 quarantinedValue = quarantine.pendingTotalValueIncrease; // 1. Onchain total value on refSlot, it does not include CL difference and EL rewards for the period uint256 onchainTotalValueOnRefSlot = uint256(int256(uint256(record.report.totalValue)) + _inOutDeltaOnRefSlot - record.report.inOutDelta); // 2. Some percentage of funds that haven’t passed through the vault’s balance is allowed for handling EL and CL rewards. // NB: allowed amount of rewards is not scaled by time here, because: // - if we set a small per-day percentage, honest vaults receiving unexpectedly high MEV would get quarantined; // - if we set a large per-day percentage, a vault that hasn’t reported for a long time could bypass quarantine; // As a result, we would need to impose very tiny limits for non-quarantine percentage — which would complicate the logic // without bringing meaningful improvements. uint256 quarantineThreshold = onchainTotalValueOnRefSlot * (TOTAL_BASIS_POINTS + $.maxRewardRatioBP) / TOTAL_BASIS_POINTS; // 3. Determine current quarantine state QuarantineState currentState = _determineQuarantineState(quarantine, quarantinedValue, _reportTimestamp); // Execute logic based on current state and conditions ---------------- // -------------------------------------------------------------------- if (currentState == QuarantineState.NO_QUARANTINE) { if (_reportedTotalValue <= quarantineThreshold) { // Transition: NO_QUARANTINE → NO_QUARANTINE (no change needed) return _reportedTotalValue; } else { // Transition: NO_QUARANTINE → QUARANTINE_ACTIVE (start new quarantine) _startNewQuarantine( _vault, quarantine, _reportedTotalValue - onchainTotalValueOnRefSlot, _reportTimestamp ); return onchainTotalValueOnRefSlot; } } else if (currentState == QuarantineState.QUARANTINE_ACTIVE) { if (_reportedTotalValue <= quarantineThreshold) { // Transition: QUARANTINE_ACTIVE → NO_QUARANTINE (release quarantine early) delete $.vaultQuarantines[_vault]; emit QuarantineReleased(_vault, 0); return _reportedTotalValue; } else { // Transition: QUARANTINE_ACTIVE → QUARANTINE_ACTIVE (maintain quarantine) return onchainTotalValueOnRefSlot; } } else { // QuarantineState.QUARANTINE_EXPIRED uint256 totalValueIncrease = _reportedTotalValue > onchainTotalValueOnRefSlot ? _reportedTotalValue - onchainTotalValueOnRefSlot : 0; uint256 quarantineThresholdWithRewards = quarantineThreshold + quarantinedValue * (TOTAL_BASIS_POINTS + $.maxRewardRatioBP) / TOTAL_BASIS_POINTS; if (_reportedTotalValue <= quarantineThresholdWithRewards) { // Transition: QUARANTINE_EXPIRED → NO_QUARANTINE (release and accept all) delete $.vaultQuarantines[_vault]; emit QuarantineReleased(_vault, _reportedTotalValue <= quarantineThreshold ? 0 : totalValueIncrease); return _reportedTotalValue; } else { // Transition: QUARANTINE_EXPIRED → QUARANTINE_ACTIVE (release old, start new) emit QuarantineReleased(_vault, quarantinedValue); _startNewQuarantine(_vault, quarantine, totalValueIncrease - quarantinedValue, _reportTimestamp); return onchainTotalValueOnRefSlot + quarantinedValue; } } } function _determineQuarantineState( Quarantine storage _quarantine, uint256 _quarantinedValue, uint256 _vaultsDataTimestamp ) internal view returns (QuarantineState) { if (_quarantinedValue == 0) { return QuarantineState.NO_QUARANTINE; } bool isQuarantineExpired = (_vaultsDataTimestamp - _quarantine.startTimestamp) >= _storage().quarantinePeriod; return isQuarantineExpired ? QuarantineState.QUARANTINE_EXPIRED : QuarantineState.QUARANTINE_ACTIVE; } function _startNewQuarantine( address _vault, Quarantine storage _quarantine, uint256 _amountToQuarantine, uint256 _currentTimestamp ) internal { _quarantine.pendingTotalValueIncrease = uint128(_amountToQuarantine); _quarantine.startTimestamp = uint64(_currentTimestamp); emit QuarantineActivated(_vault, _amountToQuarantine); } function _updateSanityParams(uint256 _quarantinePeriod, uint256 _maxRewardRatioBP, uint256 _maxLidoFeeRatePerSecond) internal { if (_quarantinePeriod > MAX_QUARANTINE_PERIOD) revert QuarantinePeriodTooLarge(_quarantinePeriod, MAX_QUARANTINE_PERIOD); if (_maxRewardRatioBP > MAX_REWARD_RATIO) revert MaxRewardRatioTooLarge(_maxRewardRatioBP, MAX_REWARD_RATIO); if (_maxLidoFeeRatePerSecond > MAX_LIDO_FEE_RATE_PER_SECOND) revert MaxLidoFeeRatePerSecondTooLarge(_maxLidoFeeRatePerSecond, MAX_LIDO_FEE_RATE_PER_SECOND); Storage storage $ = _storage(); $.quarantinePeriod = uint64(_quarantinePeriod); $.maxRewardRatioBP = uint16(_maxRewardRatioBP); $.maxLidoFeeRatePerSecond = uint64(_maxLidoFeeRatePerSecond); emit SanityParamsUpdated(_quarantinePeriod, _maxRewardRatioBP, _maxLidoFeeRatePerSecond); } function _mintableStETH(address _vault, VaultHub _vh) internal view returns (uint256) { uint256 mintableShares = _vh.totalMintingCapacityShares(_vault, 0 /* zero eth delta */); return _getPooledEthBySharesRoundUp(mintableShares); } function _storage() internal pure returns (Storage storage $) { assembly { $.slot := LAZY_ORACLE_STORAGE_LOCATION } } function predepositGuarantee() internal view returns (IPredepositGuarantee) { return IPredepositGuarantee(LIDO_LOCATOR.predepositGuarantee()); } function _vaultHub() internal view returns (VaultHub) { return VaultHub(payable(LIDO_LOCATOR.vaultHub())); } function _operatorGrid() internal view returns (OperatorGrid) { return OperatorGrid(LIDO_LOCATOR.operatorGrid()); } function _getPooledEthBySharesRoundUp(uint256 _shares) internal view returns (uint256) { return ILido(LIDO_LOCATOR.lido()).getPooledEthBySharesRoundUp(_shares); } event VaultsReportDataUpdated(uint256 indexed timestamp, uint256 indexed refSlot, bytes32 indexed root, string cid); event QuarantineActivated(address indexed vault, uint256 delta); event QuarantineReleased(address indexed vault, uint256 delta); event QuarantineRemoved(address indexed vault); event SanityParamsUpdated(uint256 quarantinePeriod, uint256 maxRewardRatioBP, uint256 maxLidoFeeRatePerSecond); error AdminCannotBeZero(); error NotAuthorized(); error InvalidProof(); error UnderflowInTotalValueCalculation(); error TotalValueTooLarge(); error VaultReportIsFreshEnough(); error CumulativeLidoFeesTooLow(uint256 reportingFees, uint256 previousFees); error CumulativeLidoFeesTooLarge(uint256 feeIncrease, uint256 maxFeeIncrease); error QuarantinePeriodTooLarge(uint256 quarantinePeriod, uint256 maxQuarantinePeriod); error MaxRewardRatioTooLarge(uint256 rewardRatio, uint256 maxRewardRatio); error MaxLidoFeeRatePerSecondTooLarge(uint256 feeRate, uint256 maxFeeRate); error InvalidMaxLiabilityShares(); }
// SPDX-FileCopyrightText: 2025 Lido <[email protected]> // SPDX-License-Identifier: GPL-3.0 // See contracts/COMPILERS.md pragma solidity 0.8.25; import {SafeERC20} from "@openzeppelin/contracts-v5.2/token/ERC20/utils/SafeERC20.sol"; import {IERC20} from "@openzeppelin/contracts-v5.2/token/ERC20/IERC20.sol"; library RecoverTokens { /** * @notice ETH address convention per EIP-7528 */ address internal constant ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; /** * @notice Emitted when the ERC20 `token` or ether is recovered (i.e. transferred) * @param to The address of the recovery recipient * @param assetAddress The address of the recovered ERC20 token (0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee for ether) * @param amount The amount of the token recovered */ event AssetsRecovered(address indexed to, address indexed assetAddress, uint256 amount); /** * @notice Error thrown when recovery of ETH fails on transfer to recipient * @param recipient Address of the recovery recipient * @param amount Amount of ETH attempted to recover */ error EthTransferFailed(address recipient, uint256 amount); function _recoverEth( address _recipient, uint256 _amount ) internal { (bool success,) = payable(_recipient).call{value: _amount}(""); if (!success) revert EthTransferFailed(_recipient, _amount); emit AssetsRecovered(_recipient, ETH, _amount); } function _recoverERC20( address _token, address _recipient, uint256 _amount ) internal { SafeERC20.safeTransfer(IERC20(_token), _recipient, _amount); emit AssetsRecovered(_recipient, _token, _amount); } }
// SPDX-FileCopyrightText: 2025 Lido <[email protected]> // SPDX-License-Identifier: GPL-3.0 // See contracts/COMPILERS.md // solhint-disable one-contract-per-file pragma solidity 0.8.25; import {IHashConsensus} from "contracts/common/interfaces/IHashConsensus.sol"; uint256 constant DOUBLE_CACHE_LENGTH = 2; // wrap external call in function to save bytecode function _getCurrentRefSlot(IHashConsensus _consensus) view returns (uint256) { (uint256 refSlot, ) = _consensus.getCurrentFrame(); return refSlot; } library RefSlotCache { struct Uint104WithCache { uint104 value; uint104 valueOnRefSlot; uint48 refSlot; } /// @notice Increases the value and caches the previous value for the current refSlot /// @param _storage The storage slot to update /// @param _consensus The consensus contract to get the current refSlot /// @param _increment increment the value by this amount /// @return the updated struct to be saved in storage function withValueIncrease( Uint104WithCache storage _storage, IHashConsensus _consensus, uint104 _increment ) internal view returns (Uint104WithCache memory) { uint256 refSlot = _getCurrentRefSlot(_consensus); Uint104WithCache memory newCache = _storage; if (newCache.refSlot != uint48(refSlot)) { newCache.valueOnRefSlot = _storage.value; newCache.refSlot = uint48(refSlot); } newCache.value += _increment; return newCache; } /// @notice Returns the value for the current refSlot /// @param _storage the storage pointer for the cached value /// @param _consensus the consensus contract to get the current refSlot /// @return the cached value if it's changed since the last refSlot, the current value otherwise function getValueForLastRefSlot( Uint104WithCache storage _storage, IHashConsensus _consensus ) internal view returns (uint104) { uint256 refSlot = _getCurrentRefSlot(_consensus); if (uint48(refSlot) != _storage.refSlot) { return _storage.value; } else { return _storage.valueOnRefSlot; } } } library DoubleRefSlotCache { struct Int104WithCache { int104 value; int104 valueOnRefSlot; uint48 refSlot; } /// @notice Initializes the cache with the given value /// @param _value the value to initialize the cache with /// @return the initialized cache function initializeInt104DoubleCache( int104 _value ) internal pure returns (Int104WithCache[DOUBLE_CACHE_LENGTH] memory) { return [ Int104WithCache({ value: _value, valueOnRefSlot: 0, refSlot: 0 // first cache slot is active by default (as >= used in _activeCacheIndex) }), Int104WithCache(0, 0, 0) ]; } /// @notice Increases the value and caches the previous value for the current refSlot /// @param _storage The storage slot to update /// @param _consensus The consensus contract to get the current refSlot /// @param _increment increment the value by this amount /// @return the updated struct to be saved in storage function withValueIncrease( Int104WithCache[DOUBLE_CACHE_LENGTH] storage _storage, IHashConsensus _consensus, int104 _increment ) internal view returns (Int104WithCache[DOUBLE_CACHE_LENGTH] memory) { uint256 refSlot = _getCurrentRefSlot(_consensus); Int104WithCache[DOUBLE_CACHE_LENGTH] memory newCache = _storage; uint256 activeCacheIndex = _activeCacheIndex(newCache); if (newCache[activeCacheIndex].refSlot != uint48(refSlot)) { uint256 previousCacheIndex = activeCacheIndex; activeCacheIndex = 1 - activeCacheIndex; newCache[activeCacheIndex].value = newCache[previousCacheIndex].value; newCache[activeCacheIndex].valueOnRefSlot = newCache[previousCacheIndex].value; newCache[activeCacheIndex].refSlot = uint48(refSlot); } newCache[activeCacheIndex].value += _increment; return newCache; } /// @notice Returns the current value of the cache /// @param _cache the storage pointer for the array of cached values /// @return the current value of the cache function currentValue(Int104WithCache[DOUBLE_CACHE_LENGTH] memory _cache) internal pure returns (int104) { return _cache[_activeCacheIndex(_cache)].value; } /// @notice Returns the value for the refSlot /// @param _cache the storage pointer for the cached value /// @param _refSlot the refSlot to get the value for /// @return the cached value if it's changed since the last refSlot, the current value otherwise /// @dev reverts if the cache was overwritten after target refSlot function getValueForRefSlot( Int104WithCache[DOUBLE_CACHE_LENGTH] memory _cache, uint48 _refSlot ) internal pure returns (int104) { uint256 activeCacheIndex = _activeCacheIndex(_cache); // 1. refSlot is more than activeRefSlot if (_refSlot > _cache[activeCacheIndex].refSlot) { return _cache[activeCacheIndex].value; } uint256 previousCacheIndex = 1 - activeCacheIndex; // 2. refSlot is in (prevRefSlot, activeRefSlot] if (_refSlot > _cache[previousCacheIndex].refSlot) { return _cache[activeCacheIndex].valueOnRefSlot; } // 3. refSlot is equal to prevRefSlot if (_refSlot == _cache[previousCacheIndex].refSlot) { return _cache[previousCacheIndex].valueOnRefSlot; } // 4. refSlot is less than prevRefSlot revert InOutDeltaCacheIsOverwritten(); } /// @dev There is a limitation on the refSlot value: it must be less than 2^48. /// If it exceeds this limit, the refSlot will be truncated to 48 bits. /// _activeCacheIndex may work incorrectly if one refSlot value is truncated and the other is not, /// because the non-truncated value will always be greater than the truncated one, /// causing incorrect activeIndex determination. However, 2^48 is a very large number, /// so if block time will be 1 second, it will take 8_925_512 years to reach this limit. function _activeCacheIndex(Int104WithCache[DOUBLE_CACHE_LENGTH] memory _cache) private pure returns (uint256) { return _cache[0].refSlot >= _cache[1].refSlot ? 0 : 1; } error InOutDeltaCacheIsOverwritten(); }
// SPDX-FileCopyrightText: 2025 Lido <[email protected]> // SPDX-License-Identifier: GPL-3.0 // See contracts/COMPILERS.md pragma solidity 0.8.25; import {AccessControlEnumerableUpgradeable} from "contracts/openzeppelin/5.2/upgradeable/access/extensions/AccessControlEnumerableUpgradeable.sol"; import {SafeCast} from "@openzeppelin/contracts-v5.2/utils/math/SafeCast.sol"; import {Math256} from "contracts/common/lib/Math256.sol"; import {ILidoLocator} from "contracts/common/interfaces/ILidoLocator.sol"; import {Confirmable2Addresses} from "../utils/Confirmable2Addresses.sol"; import {IStakingVault} from "./interfaces/IStakingVault.sol"; import {VaultHub} from "./VaultHub.sol"; struct TierParams { uint256 shareLimit; uint256 reserveRatioBP; uint256 forcedRebalanceThresholdBP; uint256 infraFeeBP; uint256 liquidityFeeBP; uint256 reservationFeeBP; } /** * @title OperatorGrid * @author loga4 * @notice * OperatorGrid is a contract that manages mint parameters for vaults when they are connected to the VaultHub. * These parameters include: * - shareLimit: maximum amount of shares that can be minted * - reserveRatioBP: reserve ratio in basis points * - forcedRebalanceThresholdBP: forced rebalance threshold in basis points * - infraFeeBP: infra fee in basis points * - liquidityFeeBP: liquidity fee in basis points * - reservationFeeBP: reservation fee in basis points * * These parameters are determined by the Tier in which the Vault is registered. * */ contract OperatorGrid is AccessControlEnumerableUpgradeable, Confirmable2Addresses { /* Key concepts: 1. Default Registration: - All Vaults initially have default tier (DEFAULT_TIER_ID = 0) - The default tier has no group DEFAULT_TIER_ID = 0 ┌──────────────────────┐ │ Tier 1 │ │ tierShareLimit = z │ │ Vault_1 ... Vault_m │ └──────────────────────┘ 2. Tier Change Process: - To predefine vaults tier or modify the existing vault's connection parameters to VaultHub, a tier change must be requested - Both vault owner and node operator must confirm the change (doesn't matter who confirms first) - The confirmation has an expiry time (default 1 hour) 3. Tier Reset: - When a vault is disconnected from VaultHub, its tier is automatically reset to the default tier (DEFAULT_TIER_ID) 4. Tier Capacity: - Tiers are not limited by the number of vaults - Tiers are limited by the sum of vaults' liability shares - Administrative operations (like bad debt socialization) can bypass tier/group limits ┌──────────────────────────────────────────────────────┐ │ Group 1 = operator 1 │ │ ┌────────────────────────────────────────────────┐ │ │ │ groupShareLimit = 1kk │ │ │ └────────────────────────────────────────────────┘ │ │ ┌──────────────────────┐ ┌──────────────────────┐ │ │ │ Tier 1 │ │ Tier 2 │ │ │ │ tierShareLimit = x │ │ tierShareLimit = y │ │ │ │ Vault_2 ... Vault_k │ │ │ │ │ └──────────────────────┘ └──────────────────────┘ │ └──────────────────────────────────────────────────────┘ 5. Jail Mechanism: - A vault can be "jailed" as a penalty mechanism for misbehavior or violations - When a vault is in jail, it cannot mint new stETH shares (normal minting operations are blocked) - Vaults can be jailed/unjailed by addresses with appropriate governance roles - Administrative operations (like bad debt socialization) can bypass jail restrictions */ /// @dev 0xa495a3428837724c7f7648cda02eb83c9c4c778c8688d6f254c7f3f80c154d55 bytes32 public constant REGISTRY_ROLE = keccak256("vaults.OperatorsGrid.Registry"); /// @notice Lido Locator contract ILidoLocator public immutable LIDO_LOCATOR; uint256 public constant DEFAULT_TIER_ID = 0; // Special address to denote that default tier is not linked to any real operator address public constant DEFAULT_TIER_OPERATOR = address(uint160(type(uint160).max)); /// @dev basis points base uint256 internal constant TOTAL_BASIS_POINTS = 100_00; /// @dev max value for fees in basis points - it's about 650% uint256 internal constant MAX_FEE_BP = type(uint16).max; /// @dev max value for reserve ratio in basis points - 9999 uint256 internal constant MAX_RESERVE_RATIO_BP = 99_99; // ----------------------------- // STRUCTS // ----------------------------- struct Group { address operator; uint96 shareLimit; uint96 liabilityShares; uint256[] tierIds; } struct Tier { address operator; uint96 shareLimit; uint96 liabilityShares; uint16 reserveRatioBP; uint16 forcedRebalanceThresholdBP; uint16 infraFeeBP; uint16 liquidityFeeBP; uint16 reservationFeeBP; } /** * @notice ERC-7201 storage namespace for the OperatorGrid * @dev ERC-7201 namespace is used to prevent upgrade collisions * @custom:storage-location erc7201:Lido.Vaults.OperatorGrid * @custom:tiers Tiers * @custom:vaultTier Vault tier * @custom:groups Groups * @custom:nodeOperators Node operators * @custom:isVaultInJail if true, vault is in jail and can't mint stETH */ struct ERC7201Storage { Tier[] tiers; mapping(address vault => uint256 tierId) vaultTier; mapping(address nodeOperator => Group) groups; address[] nodeOperators; mapping(address vault => bool isInJail) isVaultInJail; } /** * @notice Storage offset slot for ERC-7201 namespace * The storage namespace is used to prevent upgrade collisions * keccak256(abi.encode(uint256(keccak256("Lido.Vaults.OperatorGrid")) - 1)) & ~bytes32(uint256(0xff)) */ bytes32 private constant OPERATOR_GRID_STORAGE_LOCATION = 0x6b64617c951381e2c1eff2be939fe368ab6d76b7d335df2e47ba2309eba1c700; /// @notice Initializes the contract with a LidoLocator /// @param _locator LidoLocator contract constructor(ILidoLocator _locator) { LIDO_LOCATOR = _locator; _disableInitializers(); } /// @notice Initializes the contract with an admin /// @param _admin Address of the admin /// @param _defaultTierParams Default tier params for the default tier function initialize(address _admin, TierParams calldata _defaultTierParams) external initializer { if (_admin == address(0)) revert ZeroArgument("_admin"); __AccessControlEnumerable_init(); __Confirmations_init(); _grantRole(DEFAULT_ADMIN_ROLE, _admin); _validateParams( DEFAULT_TIER_ID, _defaultTierParams.reserveRatioBP, _defaultTierParams.forcedRebalanceThresholdBP, _defaultTierParams.infraFeeBP, _defaultTierParams.liquidityFeeBP, _defaultTierParams.reservationFeeBP ); ERC7201Storage storage $ = _getStorage(); //create default tier with default share limit $.tiers.push( Tier({ operator: DEFAULT_TIER_OPERATOR, shareLimit: uint96(_defaultTierParams.shareLimit), reserveRatioBP: uint16(_defaultTierParams.reserveRatioBP), forcedRebalanceThresholdBP: uint16(_defaultTierParams.forcedRebalanceThresholdBP), infraFeeBP: uint16(_defaultTierParams.infraFeeBP), liquidityFeeBP: uint16(_defaultTierParams.liquidityFeeBP), reservationFeeBP: uint16(_defaultTierParams.reservationFeeBP), liabilityShares: 0 }) ); } /// @notice Sets the confirmation expiry period /// @param _newConfirmExpiry The new confirmation expiry period in seconds function setConfirmExpiry(uint256 _newConfirmExpiry) external onlyRole(REGISTRY_ROLE) { _validateConfirmExpiry(_newConfirmExpiry); _setConfirmExpiry(_newConfirmExpiry); } /// @notice Registers a new group /// @param _nodeOperator address of the node operator /// @param _shareLimit Maximum share limit for the group function registerGroup(address _nodeOperator, uint256 _shareLimit) external onlyRole(REGISTRY_ROLE) { if (_nodeOperator == address(0)) revert ZeroArgument("_nodeOperator"); ERC7201Storage storage $ = _getStorage(); if ($.groups[_nodeOperator].operator != address(0)) revert GroupExists(); $.groups[_nodeOperator] = Group({ operator: _nodeOperator, shareLimit: SafeCast.toUint96(_shareLimit), liabilityShares: 0, tierIds: new uint256[](0) }); $.nodeOperators.push(_nodeOperator); emit GroupAdded(_nodeOperator, _shareLimit); } /// @notice Updates the share limit of a group /// @param _nodeOperator address of the node operator /// @param _shareLimit New share limit value function updateGroupShareLimit(address _nodeOperator, uint256 _shareLimit) external onlyRole(REGISTRY_ROLE) { if (_nodeOperator == address(0)) revert ZeroArgument("_nodeOperator"); ERC7201Storage storage $ = _getStorage(); Group storage group_ = $.groups[_nodeOperator]; if (group_.operator == address(0)) revert GroupNotExists(); group_.shareLimit = SafeCast.toUint96(_shareLimit); emit GroupShareLimitUpdated(_nodeOperator, _shareLimit); } /// @notice Returns a group by node operator address /// @param _nodeOperator address of the node operator /// @return Group function group(address _nodeOperator) external view returns (Group memory) { return _getStorage().groups[_nodeOperator]; } /// @notice Returns a node operator address by index /// @param _index index of the node operator /// @return Node operator address function nodeOperatorAddress(uint256 _index) external view returns (address) { ERC7201Storage storage $ = _getStorage(); if (_index >= $.nodeOperators.length) revert NodeOperatorNotExists(); return $.nodeOperators[_index]; } /// @notice Returns a node operator count /// @return Node operator count function nodeOperatorCount() external view returns (uint256) { return _getStorage().nodeOperators.length; } /// @notice Registers a new tier /// @param _nodeOperator address of the node operator /// @param _tiers array of tiers to register function registerTiers( address _nodeOperator, TierParams[] calldata _tiers ) external onlyRole(REGISTRY_ROLE) { if (_nodeOperator == address(0)) revert ZeroArgument("_nodeOperator"); ERC7201Storage storage $ = _getStorage(); Group storage group_ = $.groups[_nodeOperator]; if (group_.operator == address(0)) revert GroupNotExists(); uint256 tierId = $.tiers.length; uint256 length = _tiers.length; for (uint256 i = 0; i < length; i++) { _validateParams( tierId, _tiers[i].reserveRatioBP, _tiers[i].forcedRebalanceThresholdBP, _tiers[i].infraFeeBP, _tiers[i].liquidityFeeBP, _tiers[i].reservationFeeBP ); Tier memory tier_ = Tier({ operator: _nodeOperator, shareLimit: uint96(_tiers[i].shareLimit), reserveRatioBP: uint16(_tiers[i].reserveRatioBP), forcedRebalanceThresholdBP: uint16(_tiers[i].forcedRebalanceThresholdBP), infraFeeBP: uint16(_tiers[i].infraFeeBP), liquidityFeeBP: uint16(_tiers[i].liquidityFeeBP), reservationFeeBP: uint16(_tiers[i].reservationFeeBP), liabilityShares: 0 }); $.tiers.push(tier_); group_.tierIds.push(tierId); emit TierAdded( _nodeOperator, tierId, uint96(tier_.shareLimit), uint16(tier_.reserveRatioBP), uint16(tier_.forcedRebalanceThresholdBP), uint16(tier_.infraFeeBP), uint16(tier_.liquidityFeeBP), uint16(tier_.reservationFeeBP) ); tierId++; } } /// @notice Returns a tier by ID /// @param _tierId id of the tier /// @return Tier function tier(uint256 _tierId) external view returns (Tier memory) { ERC7201Storage storage $ = _getStorage(); if (_tierId >= $.tiers.length) revert TierNotExists(); return $.tiers[_tierId]; } /// @notice Returns a tiers count /// @return Tiers count function tiersCount() external view returns (uint256) { return _getStorage().tiers.length; } /// @notice Alters multiple tiers /// @dev We do not enforce to update old vaults with the new tier params, only new ones. /// @param _tierIds array of tier ids to alter /// @param _tierParams array of new tier params function alterTiers( uint256[] calldata _tierIds, TierParams[] calldata _tierParams ) external onlyRole(REGISTRY_ROLE) { if (_tierIds.length != _tierParams.length) revert ArrayLengthMismatch(); ERC7201Storage storage $ = _getStorage(); uint256 length = _tierIds.length; uint256 tiersLength = $.tiers.length; for (uint256 i = 0; i < length; i++) { if (_tierIds[i] >= tiersLength) revert TierNotExists(); _validateParams( _tierIds[i], _tierParams[i].reserveRatioBP, _tierParams[i].forcedRebalanceThresholdBP, _tierParams[i].infraFeeBP, _tierParams[i].liquidityFeeBP, _tierParams[i].reservationFeeBP ); Tier storage tier_ = $.tiers[_tierIds[i]]; tier_.shareLimit = uint96(_tierParams[i].shareLimit); tier_.reserveRatioBP = uint16(_tierParams[i].reserveRatioBP); tier_.forcedRebalanceThresholdBP = uint16(_tierParams[i].forcedRebalanceThresholdBP); tier_.infraFeeBP = uint16(_tierParams[i].infraFeeBP); tier_.liquidityFeeBP = uint16(_tierParams[i].liquidityFeeBP); tier_.reservationFeeBP = uint16(_tierParams[i].reservationFeeBP); emit TierUpdated( _tierIds[i], tier_.shareLimit, tier_.reserveRatioBP, tier_.forcedRebalanceThresholdBP, tier_.infraFeeBP, tier_.liquidityFeeBP, tier_.reservationFeeBP ); } } /* Legend: V = Vault1.liabilityShares LS = liabilityShares Scheme1 - transfer Vault from default tier to Tier2 ┌──────────────────────────────┐ │ Group 1 │ │ │ ┌────────────────────┐ │ ┌─────────┐ ┌───────────┐ │ │ Tier 1 (default) │ confirm │ │ Tier 2 │ │ Tier 3 │ │ │ LS: -V │ ─────> │ │ LS:+V │ │ │ │ └────────────────────┘ │ └─────────┘ └───────────┘ │ │ │ │ Group1.liabilityShares: +V │ └──────────────────────────────┘ After confirmation: - Tier 1.liabilityShares = -V - Tier 2.liabilityShares = +V - Group1.liabilityShares = +V -------------------------------------------------------------------------- Scheme2 - transfer Vault from Tier2 to Tier3, no need to change group minted shares ┌────────────────────────────────┐ ┌────────────────────────────────┐ │ Group 1 │ │ Group 2 │ │ │ │ │ │ ┌───────────┐ ┌───────────┐ │ │ ┌───────────┐ │ │ │ Tier 2 │ │ Tier 3 │ │ │ │ Tier 4 │ │ │ │ LS:-V │ │ LS:+V │ │ │ │ │ │ │ └───────────┘ └───────────┘ │ │ └───────────┘ │ │ operator1 │ │ operator2 │ └────────────────────────────────┘ └────────────────────────────────┘ After confirmation: - Tier 2.liabilityShares = -V - Tier 3.liabilityShares = +V NB: Cannot change from Tier2 to Tier1, because Tier1 has no group NB: Cannot change from Tier2 to Tier4, because Tier4 has different operator. */ /// @notice Vault tier change with multi-role confirmation /// @param _vault address of the vault /// @param _requestedTierId id of the tier /// @param _requestedShareLimit share limit to set /// @return bool Whether the tier change was executed. /// @dev Requires vault to be connected to VaultHub to finalize tier change. /// @dev Both vault owner (via Dashboard) and node operator confirmations are required. function changeTier( address _vault, uint256 _requestedTierId, uint256 _requestedShareLimit ) external returns (bool) { if (_vault == address(0)) revert ZeroArgument("_vault"); ERC7201Storage storage $ = _getStorage(); if (_requestedTierId >= $.tiers.length) revert TierNotExists(); if (_requestedTierId == DEFAULT_TIER_ID) revert CannotChangeToDefaultTier(); VaultHub vaultHub = _vaultHub(); bool isVaultConnected = vaultHub.isVaultConnected(_vault); address vaultOwner = isVaultConnected ? vaultHub.vaultConnection(_vault).owner : IStakingVault(_vault).owner(); address nodeOperator = IStakingVault(_vault).nodeOperator(); uint256 vaultTierId = $.vaultTier[_vault]; if (vaultTierId == _requestedTierId) revert TierAlreadySet(); Tier storage requestedTier = $.tiers[_requestedTierId]; if (nodeOperator != requestedTier.operator) revert TierNotInOperatorGroup(); if (_requestedShareLimit > requestedTier.shareLimit) { revert RequestedShareLimitTooHigh(_requestedShareLimit, requestedTier.shareLimit); } // store the caller's confirmation; only proceed if the required number of confirmations is met. if (!_collectAndCheckConfirmations(msg.data, vaultOwner, nodeOperator)) return false; uint256 vaultLiabilityShares = vaultHub.liabilityShares(_vault); // check if tier limit is exceeded if (requestedTier.liabilityShares + vaultLiabilityShares > requestedTier.shareLimit) revert TierLimitExceeded(); // if the vault was in the default tier: // - that mean that the vault has no group, so we decrease only the minted shares of the default tier // - but need to check requested group limit exceeded if (vaultTierId == DEFAULT_TIER_ID) { Group storage requestedGroup = $.groups[nodeOperator]; if (requestedGroup.liabilityShares + vaultLiabilityShares > requestedGroup.shareLimit) { revert GroupLimitExceeded(); } requestedGroup.liabilityShares += uint96(vaultLiabilityShares); } Tier storage currentTier = $.tiers[vaultTierId]; currentTier.liabilityShares -= uint96(vaultLiabilityShares); requestedTier.liabilityShares += uint96(vaultLiabilityShares); $.vaultTier[_vault] = _requestedTierId; vaultHub.updateConnection( _vault, _requestedShareLimit, requestedTier.reserveRatioBP, requestedTier.forcedRebalanceThresholdBP, requestedTier.infraFeeBP, requestedTier.liquidityFeeBP, requestedTier.reservationFeeBP ); emit TierChanged(_vault, _requestedTierId, _requestedShareLimit); return true; } /// @notice Syncs vault tier with current tier params /// @param _vault address of the vault /// @return bool Whether the sync was executed. /// @dev Requires vault to be connected to VaultHub. /// @dev Both vault owner (via Dashboard) and node operator confirmations are required. function syncTier(address _vault) external returns (bool) { (VaultHub vaultHub, VaultHub.VaultConnection memory vaultConnection, address vaultOwner, address nodeOperator, uint256 vaultTierId) = _getVaultContextForConnectedVault(_vault); Tier storage tier_ = _getStorage().tiers[vaultTierId]; if ( vaultConnection.reserveRatioBP == tier_.reserveRatioBP && vaultConnection.forcedRebalanceThresholdBP == tier_.forcedRebalanceThresholdBP && vaultConnection.infraFeeBP == tier_.infraFeeBP && vaultConnection.liquidityFeeBP == tier_.liquidityFeeBP && vaultConnection.reservationFeeBP == tier_.reservationFeeBP ) { revert VaultAlreadySyncedWithTier(); } // store the caller's confirmation; only proceed if the required number of confirmations is met. if (!_collectAndCheckConfirmations(msg.data, vaultOwner, nodeOperator)) return false; vaultHub.updateConnection( _vault, vaultConnection.shareLimit, tier_.reserveRatioBP, tier_.forcedRebalanceThresholdBP, tier_.infraFeeBP, tier_.liquidityFeeBP, tier_.reservationFeeBP ); return true; } /// @notice Update vault share limit /// @param _vault address of the vault /// @param _requestedShareLimit share limit to set /// @return bool Whether the update was executed. /// @dev Requires vault to be connected to VaultHub. /// @dev Both vault owner (via Dashboard) and node operator confirmations are required. function updateVaultShareLimit(address _vault, uint256 _requestedShareLimit) external returns (bool) { (VaultHub vaultHub, VaultHub.VaultConnection memory vaultConnection, address vaultOwner, address nodeOperator, uint256 vaultTierId) = _getVaultContextForConnectedVault(_vault); uint256 tierShareLimit = _getStorage().tiers[vaultTierId].shareLimit; if (_requestedShareLimit > tierShareLimit) revert RequestedShareLimitTooHigh(_requestedShareLimit, tierShareLimit); if (_requestedShareLimit == vaultConnection.shareLimit) revert ShareLimitAlreadySet(); // store the caller's confirmation; only proceed if the required number of confirmations is met. if (!_collectAndCheckConfirmations(msg.data, vaultOwner, nodeOperator)) return false; vaultHub.updateConnection( _vault, _requestedShareLimit, vaultConnection.reserveRatioBP, vaultConnection.forcedRebalanceThresholdBP, vaultConnection.infraFeeBP, vaultConnection.liquidityFeeBP, vaultConnection.reservationFeeBP ); return true; } /// @notice Reset vault's tier to default /// @param _vault address of the vault /// @dev Requires vault's liabilityShares to be zero before resetting the tier function resetVaultTier(address _vault) external { if (msg.sender != LIDO_LOCATOR.vaultHub()) revert NotAuthorized("resetVaultTier", msg.sender); ERC7201Storage storage $ = _getStorage(); if ($.vaultTier[_vault] != DEFAULT_TIER_ID) { $.vaultTier[_vault] = DEFAULT_TIER_ID; emit TierChanged(_vault, DEFAULT_TIER_ID, $.tiers[DEFAULT_TIER_ID].shareLimit); } } /// @notice updates fees for the vault /// @param _vault vault address /// @param _infraFeeBP new infra fee in basis points /// @param _liquidityFeeBP new liquidity fee in basis points /// @param _reservationFeeBP new reservation fee in basis points function updateVaultFees( address _vault, uint256 _infraFeeBP, uint256 _liquidityFeeBP, uint256 _reservationFeeBP ) external onlyRole(REGISTRY_ROLE) { if (_vault == address(0)) revert ZeroArgument("_vault"); _requireLessOrEqToBP(_infraFeeBP, MAX_FEE_BP); _requireLessOrEqToBP(_liquidityFeeBP, MAX_FEE_BP); _requireLessOrEqToBP(_reservationFeeBP, MAX_FEE_BP); VaultHub vaultHub = _vaultHub(); if (!vaultHub.isVaultConnected(_vault)) revert VaultNotConnected(); VaultHub.VaultConnection memory vaultConnection = vaultHub.vaultConnection(_vault); vaultHub.updateConnection( _vault, vaultConnection.shareLimit, vaultConnection.reserveRatioBP, vaultConnection.forcedRebalanceThresholdBP, _infraFeeBP, _liquidityFeeBP, _reservationFeeBP ); } // ----------------------------- // MINT / BURN // ----------------------------- /// @notice Mint shares limit check /// @param _vault address of the vault /// @param _amount amount of shares will be minted /// @param _overrideLimits true if group and tier limits should not be checked function onMintedShares( address _vault, uint256 _amount, bool _overrideLimits ) external { if (msg.sender != LIDO_LOCATOR.vaultHub()) revert NotAuthorized("onMintedShares", msg.sender); ERC7201Storage storage $ = _getStorage(); if (!_overrideLimits && $.isVaultInJail[_vault]) revert VaultInJail(); uint256 tierId = $.vaultTier[_vault]; Tier storage tier_ = $.tiers[tierId]; uint96 tierLiabilityShares = tier_.liabilityShares; if (!_overrideLimits && tierLiabilityShares + _amount > tier_.shareLimit) { revert TierLimitExceeded(); } tier_.liabilityShares = tierLiabilityShares + uint96(_amount); if (tierId != DEFAULT_TIER_ID) { Group storage group_ = $.groups[tier_.operator]; uint96 groupMintedShares = group_.liabilityShares; if (!_overrideLimits && groupMintedShares + _amount > group_.shareLimit) { revert GroupLimitExceeded(); } group_.liabilityShares = groupMintedShares + uint96(_amount); } } /// @notice Burn shares limit check /// @param _vault address of the vault /// @param _amount amount of shares to burn function onBurnedShares( address _vault, uint256 _amount ) external { if (msg.sender != LIDO_LOCATOR.vaultHub()) revert NotAuthorized("burnShares", msg.sender); ERC7201Storage storage $ = _getStorage(); uint256 tierId = $.vaultTier[_vault]; Tier storage tier_ = $.tiers[tierId]; // we skip the check for minted shared underflow, because it's done in the VaultHub.burnShares() tier_.liabilityShares -= uint96(_amount); if (tierId != DEFAULT_TIER_ID) { Group storage group_ = $.groups[tier_.operator]; group_.liabilityShares -= uint96(_amount); } } /// @notice Updates if the vault is in jail /// @param _vault vault address /// @param _isInJail true if the vault is in jail, false otherwise function setVaultJailStatus(address _vault, bool _isInJail) external onlyRole(REGISTRY_ROLE) { if (_vault == address(0)) revert ZeroArgument("_vault"); ERC7201Storage storage $ = _getStorage(); if ($.isVaultInJail[_vault] == _isInJail) revert VaultInJailAlreadySet(); $.isVaultInJail[_vault] = _isInJail; emit VaultJailStatusUpdated(_vault, _isInJail); } /// @notice Get vault's tier limits /// @param _vault address of the vault /// @return nodeOperator node operator of the vault /// @return tierId tier id of the vault /// @return shareLimit share limit of the vault /// @return reserveRatioBP reserve ratio of the vault /// @return forcedRebalanceThresholdBP forced rebalance threshold of the vault /// @return infraFeeBP infra fee of the vault /// @return liquidityFeeBP liquidity fee of the vault /// @return reservationFeeBP reservation fee of the vault function vaultTierInfo(address _vault) external view returns ( address nodeOperator, uint256 tierId, uint256 shareLimit, uint256 reserveRatioBP, uint256 forcedRebalanceThresholdBP, uint256 infraFeeBP, uint256 liquidityFeeBP, uint256 reservationFeeBP ) { ERC7201Storage storage $ = _getStorage(); tierId = $.vaultTier[_vault]; Tier memory t = $.tiers[tierId]; nodeOperator = t.operator; shareLimit = t.shareLimit; reserveRatioBP = t.reserveRatioBP; forcedRebalanceThresholdBP = t.forcedRebalanceThresholdBP; infraFeeBP = t.infraFeeBP; liquidityFeeBP = t.liquidityFeeBP; reservationFeeBP = t.reservationFeeBP; } /// @notice Returns the effective share limit of a vault according to the OperatorGrid and vault share limits /// @param _vault address of the vault /// @return shareLimit effective share limit of the vault function effectiveShareLimit(address _vault) public view returns (uint256) { VaultHub vaultHub = _vaultHub(); uint256 shareLimit = vaultHub.vaultConnection(_vault).shareLimit; uint256 liabilityShares = vaultHub.liabilityShares(_vault); uint256 gridShareLimit = _gridRemainingShareLimit(_vault) + liabilityShares; return Math256.min(gridShareLimit, shareLimit); } /// @notice Returns true if the vault is in jail /// @param _vault address of the vault /// @return true if the vault is in jail function isVaultInJail(address _vault) external view returns (bool) { return _getStorage().isVaultInJail[_vault]; } /// @notice Returns the remaining share limit in a given tier and group /// @param _vault address of the vault /// @return remaining share limit /// @dev remaining share limit inherits the limits of the vault tier and group, /// and accounts liabilities of other vaults belonging to the same tier and group function _gridRemainingShareLimit(address _vault) internal view returns (uint256) { ERC7201Storage storage $ = _getStorage(); uint256 tierId = $.vaultTier[_vault]; Tier storage t = $.tiers[tierId]; uint256 tierLimit = t.shareLimit; uint256 tierRemaining = tierLimit > t.liabilityShares ? tierLimit - t.liabilityShares : 0; if (tierId == DEFAULT_TIER_ID) return tierRemaining; Group storage g = $.groups[t.operator]; uint256 groupLimit = g.shareLimit; uint256 groupRemaining = groupLimit > g.liabilityShares ? groupLimit - g.liabilityShares : 0; return Math256.min(tierRemaining, groupRemaining); } /// @notice Validates tier parameters /// @param _reserveRatioBP Reserve ratio /// @param _forcedRebalanceThresholdBP Forced rebalance threshold /// @param _infraFeeBP Infra fee /// @param _liquidityFeeBP Liquidity fee /// @param _reservationFeeBP Reservation fee function _validateParams( uint256 _tierId, uint256 _reserveRatioBP, uint256 _forcedRebalanceThresholdBP, uint256 _infraFeeBP, uint256 _liquidityFeeBP, uint256 _reservationFeeBP ) internal pure { if (_reserveRatioBP == 0) revert ZeroArgument("_reserveRatioBP"); if (_reserveRatioBP > MAX_RESERVE_RATIO_BP) revert ReserveRatioTooHigh(_tierId, _reserveRatioBP, MAX_RESERVE_RATIO_BP); if (_forcedRebalanceThresholdBP == 0) revert ZeroArgument("_forcedRebalanceThresholdBP"); if (_forcedRebalanceThresholdBP > _reserveRatioBP) revert ForcedRebalanceThresholdTooHigh(_tierId, _forcedRebalanceThresholdBP, _reserveRatioBP); if (_infraFeeBP > MAX_FEE_BP) revert InfraFeeTooHigh(_tierId, _infraFeeBP, MAX_FEE_BP); if (_liquidityFeeBP > MAX_FEE_BP) revert LiquidityFeeTooHigh(_tierId, _liquidityFeeBP, MAX_FEE_BP); if (_reservationFeeBP > MAX_FEE_BP) revert ReservationFeeTooHigh(_tierId, _reservationFeeBP, MAX_FEE_BP); } function _vaultHub() internal view returns (VaultHub) { return VaultHub(payable(LIDO_LOCATOR.vaultHub())); } function _getStorage() private pure returns (ERC7201Storage storage $) { assembly { $.slot := OPERATOR_GRID_STORAGE_LOCATION } } function _getVaultContextForConnectedVault(address _vault) internal view returns ( VaultHub vaultHub, VaultHub.VaultConnection memory vaultConnection, address vaultOwner, address nodeOperator, uint256 vaultTierId ) { if (_vault == address(0)) revert ZeroArgument("_vault"); vaultHub = _vaultHub(); if (!vaultHub.isVaultConnected(_vault)) revert VaultNotConnected(); vaultConnection = vaultHub.vaultConnection(_vault); vaultOwner = vaultConnection.owner; nodeOperator = IStakingVault(_vault).nodeOperator(); vaultTierId = _getStorage().vaultTier[_vault]; } function _requireLessOrEqToBP(uint256 _valueBP, uint256 _maxValueBP) internal pure { if (_valueBP > _maxValueBP) revert InvalidBasisPoints(_valueBP, _maxValueBP); } // ----------------------------- // EVENTS // ----------------------------- event GroupAdded(address indexed nodeOperator, uint256 shareLimit); event GroupShareLimitUpdated(address indexed nodeOperator, uint256 shareLimit); event TierAdded( address indexed nodeOperator, uint256 indexed tierId, uint256 shareLimit, uint256 reserveRatioBP, uint256 forcedRebalanceThresholdBP, uint256 infraFeeBP, uint256 liquidityFeeBP, uint256 reservationFeeBP ); event TierChanged(address indexed vault, uint256 indexed tierId, uint256 shareLimit); event TierUpdated( uint256 indexed tierId, uint256 shareLimit, uint256 reserveRatioBP, uint256 forcedRebalanceThresholdBP, uint256 infraFeeBP, uint256 liquidityFeeBP, uint256 reservationFeeBP ); event VaultJailStatusUpdated(address indexed vault, bool isInJail); // ----------------------------- // ERRORS // ----------------------------- error NotAuthorized(string operation, address sender); error ZeroArgument(string argument); error GroupExists(); error GroupNotExists(); error GroupLimitExceeded(); error NodeOperatorNotExists(); error TierLimitExceeded(); error VaultInJailAlreadySet(); error VaultInJail(); error TierNotExists(); error TierAlreadySet(); error TierNotInOperatorGroup(); error CannotChangeToDefaultTier(); error ReserveRatioTooHigh(uint256 tierId, uint256 reserveRatioBP, uint256 maxReserveRatioBP); error ForcedRebalanceThresholdTooHigh(uint256 tierId, uint256 forcedRebalanceThresholdBP, uint256 reserveRatioBP); error InfraFeeTooHigh(uint256 tierId, uint256 infraFeeBP, uint256 maxInfraFeeBP); error LiquidityFeeTooHigh(uint256 tierId, uint256 liquidityFeeBP, uint256 maxLiquidityFeeBP); error ReservationFeeTooHigh(uint256 tierId, uint256 reservationFeeBP, uint256 maxReservationFeeBP); error ArrayLengthMismatch(); error RequestedShareLimitTooHigh(uint256 requestedShareLimit, uint256 tierShareLimit); error VaultNotConnected(); error VaultAlreadySyncedWithTier(); error ShareLimitAlreadySet(); error InvalidBasisPoints(uint256 valueBP, uint256 maxValueBP); }
// SPDX-FileCopyrightText: 2025 Lido <[email protected]> // SPDX-License-Identifier: GPL-3.0 // See contracts/COMPILERS.md pragma solidity 0.8.25; import {SafeCast} from "@openzeppelin/contracts-v5.2/utils/math/SafeCast.sol"; import {Math256} from "contracts/common/lib/Math256.sol"; import {ILidoLocator} from "contracts/common/interfaces/ILidoLocator.sol"; import {ILido} from "contracts/common/interfaces/ILido.sol"; import {IHashConsensus} from "contracts/common/interfaces/IHashConsensus.sol"; import {IStakingVault} from "./interfaces/IStakingVault.sol"; import {IPredepositGuarantee} from "./interfaces/IPredepositGuarantee.sol"; import {IPinnedBeaconProxy} from "./interfaces/IPinnedBeaconProxy.sol"; import {IVaultFactory} from "./interfaces/IVaultFactory.sol"; import {OperatorGrid} from "./OperatorGrid.sol"; import {LazyOracle} from "./LazyOracle.sol"; import {PausableUntilWithRoles} from "../utils/PausableUntilWithRoles.sol"; import {RefSlotCache, DoubleRefSlotCache, DOUBLE_CACHE_LENGTH} from "./lib/RefSlotCache.sol"; /// @notice VaultHub is a contract that manages StakingVaults connected to the Lido protocol /// It allows to connect and disconnect vaults, mint and burn stETH using vaults as collateral /// Also, it facilitates the individual per-vault reports from the lazy oracle to the vaults and charges Lido fees /// @author folkyatina contract VaultHub is PausableUntilWithRoles { using RefSlotCache for RefSlotCache.Uint104WithCache; using DoubleRefSlotCache for DoubleRefSlotCache.Int104WithCache[DOUBLE_CACHE_LENGTH]; // ----------------------------- // STORAGE STRUCTS // ----------------------------- /// @custom:storage-location erc7201:Lido.Vaults.VaultHub struct Storage { /// @notice accounting records for each vault mapping(address vault => VaultRecord) records; /// @notice connection parameters for each vault mapping(address vault => VaultConnection) connections; /// @notice 1-based array of vaults connected to the hub. index 0 is reserved for not connected vaults address[] vaults; /// @notice amount of bad debt that was internalized from the vault to become the protocol loss RefSlotCache.Uint104WithCache badDebtToInternalize; } struct VaultConnection { // ### 1st slot /// @notice address of the vault owner address owner; /// @notice maximum number of stETH shares that can be minted by vault owner uint96 shareLimit; // ### 2nd slot /// @notice index of the vault in the list of vaults. Indexes are not guaranteed to be stable. /// @dev vaultIndex is always greater than 0 uint96 vaultIndex; /// @notice timestamp of the block when disconnection was initiated /// equal 0 if vault is disconnected and max(uint48) - for connected , uint48 disconnectInitiatedTs; /// @notice share of ether that is locked on the vault as an additional reserve /// e.g RR=30% means that for 1stETH minted 1/(1-0.3)=1.428571428571428571 ETH is locked on the vault uint16 reserveRatioBP; /// @notice if vault's reserve decreases to this threshold, it should be force rebalanced uint16 forcedRebalanceThresholdBP; /// @notice infra fee in basis points uint16 infraFeeBP; /// @notice liquidity fee in basis points uint16 liquidityFeeBP; /// @notice reservation fee in basis points uint16 reservationFeeBP; /// @notice if true, vault owner intends to pause the beacon chain deposits bool beaconChainDepositsPauseIntent; /// 24 bits gap } struct VaultRecord { // ### 1st slot /// @notice latest report for the vault Report report; // ### 2nd slot /// @notice max number of shares that was minted by the vault in current Oracle period /// (used to calculate the locked value on the vault) uint96 maxLiabilityShares; /// @notice liability shares of the vault uint96 liabilityShares; // ### 3rd and 4th slots /// @notice inOutDelta of the vault (all deposits - all withdrawals) DoubleRefSlotCache.Int104WithCache[DOUBLE_CACHE_LENGTH] inOutDelta; // ### 5th slot /// @notice the minimal value that the reserve part of the locked can be uint128 minimalReserve; /// @notice part of liability shares reserved to be burnt as Lido core redemptions uint128 redemptionShares; // ### 6th slot /// @notice cumulative value for Lido fees that accrued on the vault uint128 cumulativeLidoFees; /// @notice cumulative value for Lido fees that were settled on the vault uint128 settledLidoFees; } struct Report { /// @notice total value of the vault uint104 totalValue; /// @notice inOutDelta of the report int104 inOutDelta; /// @notice timestamp (in seconds) uint48 timestamp; } // ----------------------------- // CONSTANTS // ----------------------------- // some constants are immutables to save bytecode // keccak256(abi.encode(uint256(keccak256("Lido.Vaults.VaultHub")) - 1)) & ~bytes32(uint256(0xff)) bytes32 private constant STORAGE_LOCATION = 0x9eb73ffa4c77d08d5d1746cf5a5e50a47018b610ea5d728ea9bd9e399b76e200; /// @notice role that allows to disconnect vaults from the hub /// @dev 0x479bc4a51d27fbdc8e51b5b1ebd3dcd58bd229090980bff226f8930587e69ce3 bytes32 public immutable VAULT_MASTER_ROLE = keccak256("vaults.VaultHub.VaultMasterRole"); /// @notice role that allows to accrue Lido Core redemptions on the vault /// @dev 0x44f007e8cc2a08047a03d8d9c295057454942eb49ee3ced9c87e9b9406f21174 bytes32 public immutable REDEMPTION_MASTER_ROLE = keccak256("vaults.VaultHub.RedemptionMasterRole"); /// @notice role that allows to trigger validator exits under extreme conditions /// @dev 0x2159c5943234d9f3a7225b9a743ea06e4a0d0ba5ed82889e867759a8a9eb7883 bytes32 public immutable VALIDATOR_EXIT_ROLE = keccak256("vaults.VaultHub.ValidatorExitRole"); /// @notice role that allows to bail out vaults with bad debt /// @dev 0xa85bab4b576ca359fa6ae02ab8744b5c85c7e7ed4d7e0bca7b5b64580ac5d17d bytes32 public immutable BAD_DEBT_MASTER_ROLE = keccak256("vaults.VaultHub.BadDebtMasterRole"); /// @notice amount of ETH that is locked on the vault on connect and can be withdrawn on disconnect only uint256 public constant CONNECT_DEPOSIT = 1 ether; /// @notice The time delta for report freshness check uint256 public constant REPORT_FRESHNESS_DELTA = 2 days; /// @dev basis points base uint256 internal constant TOTAL_BASIS_POINTS = 100_00; /// @dev special value for `disconnectTimestamp` storage means the vault is not marked for disconnect uint48 internal constant DISCONNECT_NOT_INITIATED = type(uint48).max; /// @notice minimum amount of ether that is required for the beacon chain deposit /// @dev used as a threshold for the beacon chain deposits pause uint256 internal constant MIN_BEACON_DEPOSIT = 1 ether; /// @dev amount of ether required to activate a validator after PDG uint256 internal constant PDG_ACTIVATION_DEPOSIT = 31 ether; // ----------------------------- // IMMUTABLES // ----------------------------- /// @notice limit for a single vault share limit relative to Lido TVL in basis points uint256 public immutable MAX_RELATIVE_SHARE_LIMIT_BP; ILido public immutable LIDO; ILidoLocator public immutable LIDO_LOCATOR; /// @dev it's cached as immutable to save the gas, but it's add some rigidity to the contract structure /// and will require update of the VaultHub if HashConsensus changes IHashConsensus public immutable CONSENSUS_CONTRACT; /// @param _locator Lido Locator contract /// @param _lido Lido stETH contract /// @param _consensusContract Hash consensus contract /// @param _maxRelativeShareLimitBP Maximum share limit relative to TVL in basis points constructor(ILidoLocator _locator, ILido _lido, IHashConsensus _consensusContract, uint256 _maxRelativeShareLimitBP) { _requireNotZero(_maxRelativeShareLimitBP); if (_maxRelativeShareLimitBP > TOTAL_BASIS_POINTS) revert InvalidBasisPoints(_maxRelativeShareLimitBP, TOTAL_BASIS_POINTS); MAX_RELATIVE_SHARE_LIMIT_BP = _maxRelativeShareLimitBP; LIDO_LOCATOR = _locator; LIDO = _lido; CONSENSUS_CONTRACT = _consensusContract; _disableInitializers(); } /// @dev used to perform rebalance operations receive() external payable {} /// @notice initialize the vault hub /// @param _admin default admin address function initialize(address _admin) external initializer { _requireNotZero(_admin); __AccessControlEnumerable_init(); // the stone in the elevator. index 0 is reserved for not connected vaults _storage().vaults.push(address(0)); _grantRole(DEFAULT_ADMIN_ROLE, _admin); } /// @notice returns the number of vaults connected to the hub /// @dev since index 0 is reserved for not connected vaults, it's always 1 less than the vaults array length function vaultsCount() external view returns (uint256) { return _storage().vaults.length - 1; } /// @notice returns the vault address by its index /// @param _index index of the vault in the 1-based list of vaults. possible range [1, vaultsCount()] /// @dev Indexes are guaranteed to be stable only in one transaction. function vaultByIndex(uint256 _index) external view returns (address) { _requireNotZero(_index); return _storage().vaults[_index]; } /// @return connection parameters struct for the given vault /// @dev it returns empty struct if the vault is not connected to the hub /// @dev it may return connection even if it's pending to be disconnected function vaultConnection(address _vault) external view returns (VaultConnection memory) { return _vaultConnection(_vault); } /// @return the accounting record struct for the given vault /// @dev it returns empty struct if the vault is not connected to the hub function vaultRecord(address _vault) external view returns (VaultRecord memory) { return _vaultRecord(_vault); } /// @return true if the vault is connected to the hub or pending to be disconnected function isVaultConnected(address _vault) external view returns (bool) { return _vaultConnection(_vault).vaultIndex != 0; } /// @return true if vault is pending for disconnect, false if vault is connected or disconnected /// @dev disconnect can be performed by applying the report for the period when it was initiated function isPendingDisconnect(address _vault) external view returns (bool) { return _isPendingDisconnect(_vaultConnection(_vault)); } /// @return total value of the vault /// @dev returns 0 if the vault is not connected function totalValue(address _vault) external view returns (uint256) { return _totalValue(_vaultRecord(_vault)); } /// @return liability shares of the vault /// @dev returns 0 if the vault is not connected function liabilityShares(address _vault) external view returns (uint256) { return _vaultRecord(_vault).liabilityShares; } /// @return locked amount of ether for the vault /// @dev returns 0 if the vault is not connected function locked(address _vault) external view returns (uint256) { return _locked(_vaultConnection(_vault), _vaultRecord(_vault)); } /// @return the amount of ether that can be locked in the vault given the current total value /// @dev returns 0 if the vault is not connected function maxLockableValue(address _vault) external view returns (uint256) { return _maxLockableValue(_vaultRecord(_vault)); } /// @notice Calculates the total number of shares that is possible to mint on the vault /// @param _vault The address of the vault /// @param _deltaValue The delta value to apply to the total value of the vault (may be negative) /// @return the number of shares that can be minted /// @dev returns 0 if the vault is not connected function totalMintingCapacityShares(address _vault, int256 _deltaValue) external view returns (uint256) { return _totalMintingCapacityShares(_vault, _deltaValue); } /// @return the amount of ether that can be instantly withdrawn from the staking vault /// @dev returns 0 if the vault is not connected or disconnect pending function withdrawableValue(address _vault) external view returns (uint256) { VaultConnection storage connection = _vaultConnection(_vault); if (_isPendingDisconnect(connection)) return 0; return _withdrawableValue(_vault, connection, _vaultRecord(_vault)); } /// @return latest report for the vault /// @dev returns empty struct if the vault is not connected function latestReport(address _vault) external view returns (Report memory) { return _vaultRecord(_vault).report; } /// @return true if the report for the vault is fresh, false otherwise /// @dev returns false if the vault is not connected function isReportFresh(address _vault) external view returns (bool) { return _isReportFresh(_vaultRecord(_vault)); } /// @notice checks if the vault is healthy by comparing its total value after applying forced rebalance threshold /// against current liability shares /// @param _vault vault address /// @return true if vault is healthy, false otherwise /// @dev returns true if the vault is not connected function isVaultHealthy(address _vault) external view returns (bool) { return _isVaultHealthy(_vaultConnection(_vault), _vaultRecord(_vault)); } /// @notice calculate shares amount to make the vault healthy using rebalance /// @param _vault vault address /// @return shares amount or UINT256_MAX if it's impossible to make the vault healthy using rebalance /// @dev returns 0 if the vault is not connected function healthShortfallShares(address _vault) external view returns (uint256) { return _healthShortfallShares(_vaultConnection(_vault), _vaultRecord(_vault)); } /// @notice calculate ether amount required to cover obligations shortfall of the vault /// @param _vault vault address /// @return ether amount or UINT256_MAX if it's impossible to cover obligations shortfall /// @dev returns 0 if the vault is not connected function obligationsShortfallValue(address _vault) external view returns (uint256) { VaultConnection storage connection = _vaultConnection(_vault); if (connection.vaultIndex == 0) return 0; return _obligationsShortfallValue(_vault, connection, _vaultRecord(_vault)); } /// @notice returns the vault's current obligations toward the protocol /// /// Obligations are amounts the vault must cover, in the following priority: /// 1) Maintain healthiness - burn/rebalance liability shares until the health ratio is restored /// 2) Cover redemptions - burn/rebalance part of the liability shares marked as `redemptionShares` /// 3) Pay Lido fees - settle accrued but unsettled fees /// /// Effects: /// - Withdrawals from the vault are limited by the amount required to cover the obligations /// - Beacon chain deposits are auto-paused while the vault is unhealthy, has redemptions to cover, or has /// unsettled fees ≥ `MIN_BEACON_DEPOSIT` (1 ETH) /// /// How to settle: /// - Anyone can: /// - Rebalance shares permissionlessly when there are funds via `forceRebalance` (restores health / covers redemptions) /// - Settle fees permissionlessly when there are funds via `settleLidoFees` /// - The owner (or a trusted role) can trigger validator exits / withdrawals to source ETH when needed /// /// @param _vault vault address /// @return sharesToBurn amount of shares to burn / rebalance /// @return feesToSettle amount of Lido fees to settle /// @dev if the vault has bad debt (i.e. not fixable by rebalance), returns `type(uint256).max` for `sharesToBurn` /// @dev returns (0, 0) if the vault is not connected function obligations(address _vault) external view returns (uint256 sharesToBurn, uint256 feesToSettle) { VaultConnection storage connection = _vaultConnection(_vault); VaultRecord storage record = _vaultRecord(_vault); return ( _obligationsShares(connection, record), _unsettledLidoFeesValue(record) ); } /// @return the amount of Lido fees that currently can be settled. Even if vault's balance is sufficient to cover /// the fees, some amount may be blocked for redemptions, or locked ether /// @dev returns 0 if the vault is not connected function settleableLidoFeesValue(address _vault) external view returns (uint256) { VaultRecord storage record = _vaultRecord(_vault); return _settleableLidoFeesValue(_vault, _vaultConnection(_vault), record, _unsettledLidoFeesValue(record)); } /// @notice amount of bad debt to be internalized to become the protocol loss /// @return the number of shares to internalize as bad debt during the oracle report /// @dev the value is lagging increases that was done after the current refSlot to the next one function badDebtToInternalize() external view returns (uint256) { return _storage().badDebtToInternalize.getValueForLastRefSlot(CONSENSUS_CONTRACT); } /// @notice connects a vault to the hub in permissionless way, get limits from the Operator Grid /// @param _vault vault address /// @dev vault should have transferred ownership to the VaultHub contract function connectVault(address _vault) external whenResumed { _requireNotZero(_vault); if (!IVaultFactory(LIDO_LOCATOR.vaultFactory()).deployedVaults(_vault)) revert VaultNotFactoryDeployed(_vault); IStakingVault vault_ = IStakingVault(_vault); _requireSender(vault_.owner()); if (vault_.pendingOwner() != address(this)) revert VaultHubNotPendingOwner(_vault); if (IPinnedBeaconProxy(address(vault_)).isOssified()) revert VaultOssified(_vault); if (vault_.depositor() != address(_predepositGuarantee())) revert PDGNotDepositor(_vault); // we need vault to match staged balance with pendingActivations if (vault_.stagedBalance() != _predepositGuarantee().pendingActivations(vault_) * PDG_ACTIVATION_DEPOSIT) { revert InsufficientStagedBalance(_vault); } ( , // nodeOperatorInTier , // tierId uint256 shareLimit, uint256 reserveRatioBP, uint256 forcedRebalanceThresholdBP, uint256 infraFeeBP, uint256 liquidityFeeBP, uint256 reservationFeeBP ) = _operatorGrid().vaultTierInfo(_vault); _connectVault(_vault, shareLimit, reserveRatioBP, forcedRebalanceThresholdBP, infraFeeBP, liquidityFeeBP, reservationFeeBP ); IStakingVault(_vault).acceptOwnership(); emit VaultConnected({ vault: _vault, shareLimit: shareLimit, reserveRatioBP: reserveRatioBP, forcedRebalanceThresholdBP: forcedRebalanceThresholdBP, infraFeeBP: infraFeeBP, liquidityFeeBP: liquidityFeeBP, reservationFeeBP: reservationFeeBP }); } /// @notice updates a redemption shares on the vault /// @param _vault The address of the vault /// @param _liabilitySharesTarget maximum amount of liabilityShares that will be preserved, the rest will be /// marked as redemptionShares. If value is greater than liabilityShares, redemptionShares are set to 0 /// @dev NB: Mechanism to be triggered when Lido Core TVL <= stVaults TVL function setLiabilitySharesTarget(address _vault, uint256 _liabilitySharesTarget) external onlyRole(REDEMPTION_MASTER_ROLE) { VaultConnection storage connection = _checkConnection(_vault); VaultRecord storage record = _vaultRecord(_vault); uint256 liabilityShares_ = record.liabilityShares; uint256 redemptionShares = liabilityShares_ > _liabilitySharesTarget ? liabilityShares_ - _liabilitySharesTarget : 0; record.redemptionShares = uint128(redemptionShares); _updateBeaconChainDepositsPause(_vault, record, connection); emit VaultRedemptionSharesUpdated(_vault, record.redemptionShares); } /// @notice updates the vault's connection parameters /// @dev Reverts if the vault is not healthy as of latest report /// @param _vault vault address /// @param _shareLimit new share limit /// @param _reserveRatioBP new reserve ratio /// @param _forcedRebalanceThresholdBP new forced rebalance threshold /// @param _infraFeeBP new infra fee /// @param _liquidityFeeBP new liquidity fee /// @param _reservationFeeBP new reservation fee /// @dev requires the fresh report function updateConnection( address _vault, uint256 _shareLimit, uint256 _reserveRatioBP, uint256 _forcedRebalanceThresholdBP, uint256 _infraFeeBP, uint256 _liquidityFeeBP, uint256 _reservationFeeBP ) external { _requireSender(address(_operatorGrid())); _requireSaneShareLimit(_shareLimit); VaultConnection storage connection = _checkConnection(_vault); VaultRecord storage record = _vaultRecord(_vault); _requireFreshReport(_vault, record); uint256 totalValue_ = _totalValue(record); uint256 liabilityShares_ = record.liabilityShares; if (_isThresholdBreached(totalValue_, liabilityShares_, _reserveRatioBP)) { revert VaultMintingCapacityExceeded(_vault, totalValue_, liabilityShares_, _reserveRatioBP); } // special event for the Oracle to track fee calculation emit VaultFeesUpdated({ vault: _vault, preInfraFeeBP: connection.infraFeeBP, preLiquidityFeeBP: connection.liquidityFeeBP, preReservationFeeBP: connection.reservationFeeBP, infraFeeBP: _infraFeeBP, liquidityFeeBP: _liquidityFeeBP, reservationFeeBP: _reservationFeeBP }); connection.shareLimit = uint96(_shareLimit); connection.reserveRatioBP = uint16(_reserveRatioBP); connection.forcedRebalanceThresholdBP = uint16(_forcedRebalanceThresholdBP); connection.infraFeeBP = uint16(_infraFeeBP); connection.liquidityFeeBP = uint16(_liquidityFeeBP); connection.reservationFeeBP = uint16(_reservationFeeBP); emit VaultConnectionUpdated({ vault: _vault, nodeOperator: _nodeOperator(_vault), shareLimit: _shareLimit, reserveRatioBP: _reserveRatioBP, forcedRebalanceThresholdBP: _forcedRebalanceThresholdBP }); } /// @notice disconnect a vault from the hub /// @param _vault vault address /// @dev msg.sender must have VAULT_MASTER_ROLE /// @dev vault's `liabilityShares` should be zero /// @dev requires the fresh report (see _initiateDisconnection) function disconnect(address _vault) external onlyRole(VAULT_MASTER_ROLE) { _initiateDisconnection(_vault, _checkConnection(_vault), _vaultRecord(_vault), false); emit VaultDisconnectInitiated(_vault); } /// @notice update of the vault data by the lazy oracle report /// @param _vault the address of the vault /// @param _reportTimestamp the timestamp of the report (last 32 bits of it) /// @param _reportTotalValue the total value of the vault /// @param _reportInOutDelta the inOutDelta of the vault /// @param _reportCumulativeLidoFees the cumulative Lido fees of the vault /// @param _reportLiabilityShares the liabilityShares of the vault on refSlot /// @param _reportMaxLiabilityShares the maxLiabilityShares of the vault on refSlot /// @param _reportSlashingReserve the slashingReserve of the vault /// @dev NB: LazyOracle sanity checks already verify that the fee can only increase function applyVaultReport( address _vault, uint256 _reportTimestamp, uint256 _reportTotalValue, int256 _reportInOutDelta, uint256 _reportCumulativeLidoFees, uint256 _reportLiabilityShares, uint256 _reportMaxLiabilityShares, uint256 _reportSlashingReserve ) external whenResumed { _requireSender(address(_lazyOracle())); VaultConnection storage connection = _vaultConnection(_vault); _requireConnected(connection, _vault); VaultRecord storage record = _vaultRecord(_vault); if (connection.disconnectInitiatedTs <= _reportTimestamp) { if (_reportSlashingReserve == 0 && record.liabilityShares == 0) { // liabilityShares can increase if badDebt was socialized to this vault IStakingVault(_vault).transferOwnership(connection.owner); _deleteVault(_vault, connection); emit VaultDisconnectCompleted(_vault); return; } else { // we abort the disconnect process as there is a slashing conflict yet to be resolved connection.disconnectInitiatedTs = DISCONNECT_NOT_INITIATED; emit VaultDisconnectAborted(_vault, _reportSlashingReserve); } } _applyVaultReport( record, _reportTimestamp, _reportTotalValue, _reportInOutDelta, _reportCumulativeLidoFees, _reportLiabilityShares, _reportMaxLiabilityShares, _reportSlashingReserve ); emit VaultReportApplied({ vault: _vault, reportTimestamp: _reportTimestamp, reportTotalValue: _reportTotalValue, reportInOutDelta: _reportInOutDelta, reportCumulativeLidoFees: _reportCumulativeLidoFees, reportLiabilityShares: _reportLiabilityShares, reportMaxLiabilityShares: _reportMaxLiabilityShares, reportSlashingReserve: _reportSlashingReserve }); _updateBeaconChainDepositsPause(_vault, record, connection); } /// @notice Transfer the bad debt from the donor vault to the acceptor vault /// @param _badDebtVault address of the vault that has the bad debt /// @param _vaultAcceptor address of the vault that will accept the bad debt /// @param _maxSharesToSocialize maximum amount of shares to socialize /// @return number of shares that was socialized /// (it's limited by acceptor vault capacity and bad debt actual size) /// @dev msg.sender must have BAD_DEBT_MASTER_ROLE /// @dev requires the fresh report for both bad debt and acceptor vaults function socializeBadDebt( address _badDebtVault, address _vaultAcceptor, uint256 _maxSharesToSocialize ) external onlyRole(BAD_DEBT_MASTER_ROLE) returns (uint256) { _requireNotZero(_badDebtVault); _requireNotZero(_vaultAcceptor); _requireNotZero(_maxSharesToSocialize); if (_nodeOperator(_vaultAcceptor) != _nodeOperator(_badDebtVault)) { revert BadDebtSocializationNotAllowed(); } VaultConnection storage badDebtConnection = _vaultConnection(_badDebtVault); VaultRecord storage badDebtRecord = _vaultRecord(_badDebtVault); VaultConnection storage acceptorConnection = _vaultConnection(_vaultAcceptor); VaultRecord storage acceptorRecord = _vaultRecord(_vaultAcceptor); _requireConnected(badDebtConnection, _badDebtVault); _requireConnected(acceptorConnection, _vaultAcceptor); _requireFreshReport(_badDebtVault, badDebtRecord); _requireFreshReport(_vaultAcceptor, acceptorRecord); uint256 badDebtShares = _badDebtShares(badDebtRecord); uint256 badDebtToSocialize = Math256.min(badDebtShares, _maxSharesToSocialize); uint256 acceptorTotalValueShares = _getSharesByPooledEth(_totalValue(acceptorRecord)); uint256 acceptorLiabilityShares = acceptorRecord.liabilityShares; // it's possible to socialize up to bad debt: uint256 acceptorCapacity = acceptorTotalValueShares < acceptorLiabilityShares ? 0 : acceptorTotalValueShares - acceptorLiabilityShares; uint256 badDebtSharesToAccept = Math256.min(badDebtToSocialize, acceptorCapacity); if (badDebtSharesToAccept > 0) { _decreaseLiability(_badDebtVault, badDebtRecord, badDebtSharesToAccept); _increaseLiability({ _vault: _vaultAcceptor, _record: acceptorRecord, _amountOfShares: badDebtSharesToAccept, _reserveRatioBP: acceptorConnection.reserveRatioBP, // don't check any limits _lockableValueLimit: type(uint256).max, _shareLimit: type(uint256).max, _overrideOperatorLimits: true }); _updateBeaconChainDepositsPause(_vaultAcceptor, acceptorRecord, acceptorConnection); emit BadDebtSocialized(_badDebtVault, _vaultAcceptor, badDebtSharesToAccept); } return badDebtSharesToAccept; } /// @notice Internalize the bad debt to the protocol /// @param _badDebtVault address of the vault that has the bad debt /// @param _maxSharesToInternalize maximum amount of shares to internalize /// @return number of shares that was internalized (limited by actual size of the bad debt) /// @dev msg.sender must have BAD_DEBT_MASTER_ROLE /// @dev requires the fresh report function internalizeBadDebt( address _badDebtVault, uint256 _maxSharesToInternalize ) external onlyRole(BAD_DEBT_MASTER_ROLE) returns (uint256) { _requireNotZero(_badDebtVault); _requireNotZero(_maxSharesToInternalize); VaultConnection storage badDebtConnection = _vaultConnection(_badDebtVault); VaultRecord storage badDebtRecord = _vaultRecord(_badDebtVault); _requireConnected(badDebtConnection, _badDebtVault); _requireFreshReport(_badDebtVault, badDebtRecord); uint256 badDebtShares = _badDebtShares(badDebtRecord); uint256 badDebtToInternalize_ = Math256.min(badDebtShares, _maxSharesToInternalize); if (badDebtToInternalize_ > 0) { _decreaseLiability(_badDebtVault, badDebtRecord, badDebtToInternalize_); // store internalization in a separate counter that will be settled // by the Accounting Oracle during the report _storage().badDebtToInternalize = _storage().badDebtToInternalize.withValueIncrease({ _consensus: CONSENSUS_CONTRACT, _increment: SafeCast.toUint104(badDebtToInternalize_) }); emit BadDebtWrittenOffToBeInternalized(_badDebtVault, badDebtToInternalize_); } return badDebtToInternalize_; } /// @notice Reset the internalized bad debt to zero /// @dev msg.sender must be the accounting contract function decreaseInternalizedBadDebt(uint256 _amountOfShares) external { _requireSender(LIDO_LOCATOR.accounting()); // don't cache previous value, we don't need it for sure _storage().badDebtToInternalize.value -= uint104(_amountOfShares); } /// @notice transfer the ownership of the vault to a new owner without disconnecting it from the hub /// @param _vault vault address /// @param _newOwner new owner address /// @dev msg.sender should be vault's owner function transferVaultOwnership(address _vault, address _newOwner) external { _requireNotZero(_newOwner); VaultConnection storage connection = _checkConnection(_vault); address oldOwner = connection.owner; _requireSender(oldOwner); connection.owner = _newOwner; emit VaultOwnershipTransferred({ vault: _vault, newOwner: _newOwner, oldOwner: oldOwner }); } /// @notice disconnects a vault from the hub /// @param _vault vault address /// @dev msg.sender should be vault's owner /// @dev vault's `liabilityShares` should be zero /// @dev requires the fresh report (see _initiateDisconnection) function voluntaryDisconnect(address _vault) external whenResumed { VaultConnection storage connection = _checkConnectionAndOwner(_vault); _initiateDisconnection(_vault, connection, _vaultRecord(_vault), true); emit VaultDisconnectInitiated(_vault); } /// @notice funds the vault passing ether as msg.value /// @param _vault vault address /// @dev msg.sender should be vault's owner function fund(address _vault) external payable whenResumed { _requireNotZero(_vault); VaultConnection storage connection = _vaultConnection(_vault); _requireConnected(connection, _vault); _requireSender(connection.owner); _updateInOutDelta(_vault, _vaultRecord(_vault), int104(int256(msg.value))); IStakingVault(_vault).fund{value: msg.value}(); } /// @notice withdraws ether from the vault to the recipient address /// @param _vault vault address /// @param _recipient recipient address /// @param _ether amount of ether to withdraw /// @dev msg.sender should be vault's owner /// @dev requires the fresh report function withdraw(address _vault, address _recipient, uint256 _ether) external whenResumed { VaultConnection storage connection = _checkConnectionAndOwner(_vault); VaultRecord storage record = _vaultRecord(_vault); _requireFreshReport(_vault, record); uint256 withdrawable = _withdrawableValue(_vault, connection, record); if (_ether > withdrawable) { revert AmountExceedsWithdrawableValue(_vault, withdrawable, _ether); } _withdraw(_vault, record, _recipient, _ether); } /// @notice Rebalances StakingVault by withdrawing ether to VaultHub /// @param _vault vault address /// @param _shares amount of shares to rebalance /// @dev msg.sender should be vault's owner /// @dev requires the fresh report function rebalance(address _vault, uint256 _shares) external whenResumed { _requireNotZero(_shares); _checkConnectionAndOwner(_vault); VaultRecord storage record = _vaultRecord(_vault); _requireFreshReport(_vault, record); _rebalance(_vault, record, _shares); } /// @notice mint StETH shares backed by vault external balance to the receiver address /// @param _vault vault address /// @param _recipient address of the receiver /// @param _amountOfShares amount of stETH shares to mint /// @dev requires the fresh report function mintShares(address _vault, address _recipient, uint256 _amountOfShares) external whenResumed { _requireNotZero(_recipient); _requireNotZero(_amountOfShares); VaultConnection storage connection = _checkConnectionAndOwner(_vault); VaultRecord storage record = _vaultRecord(_vault); _requireFreshReport(_vault, record); _increaseLiability({ _vault: _vault, _record: record, _amountOfShares: _amountOfShares, _reserveRatioBP: connection.reserveRatioBP, _lockableValueLimit: _maxLockableValue(record), _shareLimit: connection.shareLimit, _overrideOperatorLimits: false }); LIDO.mintExternalShares(_recipient, _amountOfShares); emit MintedSharesOnVault(_vault, _amountOfShares, _locked(connection, record)); } /// @notice burn steth shares from the balance of the VaultHub contract /// @param _vault vault address /// @param _amountOfShares amount of shares to burn /// @dev msg.sender should be vault's owner /// @dev this function is designed to be used by the smart contract, for EOA see `transferAndBurnShares` function burnShares(address _vault, uint256 _amountOfShares) public whenResumed { _requireNotZero(_amountOfShares); _checkConnectionAndOwner(_vault); VaultRecord storage record = _vaultRecord(_vault); _decreaseLiability(_vault, record, _amountOfShares); LIDO.burnExternalShares(_amountOfShares); _updateBeaconChainDepositsPause(_vault, record, _vaultConnection(_vault)); emit BurnedSharesOnVault(_vault, _amountOfShares); } /// @notice separate burn function for EOA vault owners; requires vaultHub to be approved to transfer stETH /// @param _vault vault address /// @param _amountOfShares amount of shares to transfer and burn /// @dev msg.sender should be vault's owner function transferAndBurnShares(address _vault, uint256 _amountOfShares) external { LIDO.transferSharesFrom(msg.sender, address(this), _amountOfShares); burnShares(_vault, _amountOfShares); } /// @notice pauses beacon chain deposits for the vault /// @param _vault vault address /// @dev msg.sender should be vault's owner function pauseBeaconChainDeposits(address _vault) external { VaultConnection storage connection = _checkConnectionAndOwner(_vault); if (connection.beaconChainDepositsPauseIntent) revert PauseIntentAlreadySet(); connection.beaconChainDepositsPauseIntent = true; emit BeaconChainDepositsPauseIntentSet(_vault, true); _pauseBeaconChainDepositsIfNotAlready(IStakingVault(_vault)); } /// @notice resumes beacon chain deposits for the vault /// @param _vault vault address /// @dev msg.sender should be vault's owner /// @dev requires the fresh report /// @dev NB: if the vault has outstanding obligations, this call will clear the manual pause flag but deposits will /// remain paused until the obligations are covered. Once covered, deposits will resume automatically function resumeBeaconChainDeposits(address _vault) external { VaultConnection storage connection = _checkConnectionAndOwner(_vault); if (!connection.beaconChainDepositsPauseIntent) revert PauseIntentAlreadyUnset(); VaultRecord storage record = _vaultRecord(_vault); _requireFreshReport(_vault, record); connection.beaconChainDepositsPauseIntent = false; emit BeaconChainDepositsPauseIntentSet(_vault, false); _updateBeaconChainDepositsPause(_vault, record, connection); } /// @notice Emits a request event for the node operator to perform validator exit /// @param _vault vault address /// @param _pubkeys array of public keys of the validators to exit /// @dev msg.sender should be vault's owner function requestValidatorExit(address _vault, bytes calldata _pubkeys) external { _checkConnectionAndOwner(_vault); IStakingVault(_vault).requestValidatorExit(_pubkeys); } /// @notice Triggers validator withdrawals for the vault using EIP-7002 /// @param _vault vault address /// @param _pubkeys array of public keys of the validators to withdraw from /// @param _amountsInGwei array of amounts to withdraw from each validator (0 for full withdrawal) /// @param _refundRecipient address that will receive the refund for transaction costs /// @dev msg.sender should be vault's owner /// @dev requires the fresh report (in case of partial withdrawals) /// @dev A withdrawal fee must be paid via msg.value. /// `StakingVault.calculateValidatorWithdrawalFee()` can be used to calculate the approximate fee amount but /// it's accurate only for the current block. The fee may change when the tx is included, so it's recommended /// to send some surplus. The exact amount required will be paid and the excess will be refunded to the /// `_refundRecipient` address. The fee required can grow exponentially, so limit msg.value wisely to avoid /// overspending. function triggerValidatorWithdrawals( address _vault, bytes calldata _pubkeys, uint64[] calldata _amountsInGwei, address _refundRecipient ) external payable { VaultConnection storage connection = _checkConnectionAndOwner(_vault); VaultRecord storage record = _vaultRecord(_vault); uint256 minPartialAmountInGwei = type(uint256).max; for (uint256 i = 0; i < _amountsInGwei.length; i++) { if (_amountsInGwei[i] > 0 && _amountsInGwei[i] < minPartialAmountInGwei) { minPartialAmountInGwei = _amountsInGwei[i]; } } if (minPartialAmountInGwei < type(uint256).max) { _requireFreshReport(_vault, record); /// @dev NB: Disallow partial withdrawals when the vault has obligations shortfall in order to prevent the /// vault owner from clogging the consensus layer withdrawal queue by front-running and delaying the /// forceful validator exits required for rebalancing the vault. Partial withdrawals only allowed if /// the requested amount of withdrawals is enough to cover the uncovered obligations. uint256 obligationsShortfallAmount = _obligationsShortfallValue(_vault, connection, record); if (obligationsShortfallAmount > 0 && minPartialAmountInGwei * 1e9 < obligationsShortfallAmount) { revert PartialValidatorWithdrawalNotAllowed(); } } _triggerVaultValidatorWithdrawals(_vault, msg.value, _pubkeys, _amountsInGwei, _refundRecipient); } /// @notice Triggers validator full withdrawals for the vault using EIP-7002 if the vault has obligations shortfall /// @param _vault address of the vault to exit validators from /// @param _pubkeys array of public keys of the validators to exit /// @param _refundRecipient address that will receive the refund for transaction costs /// @dev In case the vault has obligations shortfall, trusted actor with the role can force its validators to /// exit the beacon chain. This returns the vault's deposited ETH back to vault's balance and allows to /// rebalance the vault /// @dev requires the fresh report /// @dev A withdrawal fee must be paid via msg.value. /// `StakingVault.calculateValidatorWithdrawalFee()` can be used to calculate the approximate fee amount but /// it's accurate only for the current block. The fee may change when the tx is included, so it's recommended /// to send some surplus. The exact amount required will be paid and the excess will be refunded to the /// `_refundRecipient` address. The fee required can grow exponentially, so limit msg.value wisely to avoid /// overspending. function forceValidatorExit( address _vault, bytes calldata _pubkeys, address _refundRecipient ) external payable onlyRole(VALIDATOR_EXIT_ROLE) { VaultConnection storage connection = _checkConnection(_vault); VaultRecord storage record = _vaultRecord(_vault); _requireFreshReport(_vault, record); uint256 obligationsShortfallAmount = _obligationsShortfallValue(_vault, connection, record); if (obligationsShortfallAmount == 0) revert ForcedValidatorExitNotAllowed(); uint64[] memory amountsInGwei = new uint64[](0); _triggerVaultValidatorWithdrawals(_vault, msg.value, _pubkeys, amountsInGwei, _refundRecipient); emit ForcedValidatorExitTriggered(_vault, _pubkeys, _refundRecipient); } /// @notice allows anyone to rebalance a vault with an obligations shortfall /// @param _vault vault address /// @dev uses all available ether in the vault to cover outstanding obligations and restore vault health; this /// operation does not settle Lido fees /// @dev requires the fresh report function forceRebalance(address _vault) external { VaultConnection storage connection = _checkConnection(_vault); VaultRecord storage record = _vaultRecord(_vault); _requireFreshReport(_vault, record); uint256 availableBalance = Math256.min(_availableBalance(_vault), _totalValue(record)); if (availableBalance == 0) revert NoFundsForForceRebalance(_vault); uint256 sharesToForceRebalance = Math256.min( _obligationsShares(connection, record), _getSharesByPooledEth(availableBalance) ); if (sharesToForceRebalance == 0) revert NoReasonForForceRebalance(_vault); _rebalance(_vault, record, sharesToForceRebalance); } /// @notice allows anyone to settle any outstanding Lido fees for a vault, sending them to the treasury /// @param _vault vault address /// @dev requires the fresh report function settleLidoFees(address _vault) external { VaultConnection storage connection = _checkConnection(_vault); VaultRecord storage record = _vaultRecord(_vault); _requireFreshReport(_vault, record); uint256 unsettledLidoFees = _unsettledLidoFeesValue(record); if (unsettledLidoFees == 0) revert NoUnsettledLidoFeesToSettle(_vault); uint256 valueToSettle = _settleableLidoFeesValue(_vault, connection, record, unsettledLidoFees); if (valueToSettle == 0) revert NoFundsToSettleLidoFees(_vault, unsettledLidoFees); _settleLidoFees(_vault, record, connection, valueToSettle); } /// @notice Proves that validators unknown to PDG have correct WC to participate in the vault /// @param _vault vault address /// @param _witness ValidatorWitness struct proving validator WC belonging to staking vault function proveUnknownValidatorToPDG( address _vault, IPredepositGuarantee.ValidatorWitness calldata _witness ) external { _checkConnectionAndOwner(_vault); _predepositGuarantee().proveUnknownValidator(_witness, IStakingVault(_vault)); } /// @notice collects ERC20 tokens from vault /// @param _vault vault address /// @param _token address of the ERC20 token to collect /// @param _recipient address to send collected tokens to /// @param _amount amount of tokens to collect /// @dev will revert with ZeroArgument() if _token, _recipient or _amount is zero /// @dev will revert with EthCollectionNotAllowed() if _token is ETH (via EIP-7528 address) function collectERC20FromVault( address _vault, address _token, address _recipient, uint256 _amount ) external { _checkConnectionAndOwner(_vault); IStakingVault(_vault).collectERC20(_token, _recipient, _amount); } function _connectVault( address _vault, uint256 _shareLimit, uint256 _reserveRatioBP, uint256 _forcedRebalanceThresholdBP, uint256 _infraFeeBP, uint256 _liquidityFeeBP, uint256 _reservationFeeBP ) internal { _requireSaneShareLimit(_shareLimit); VaultConnection memory connection = _vaultConnection(_vault); if (connection.vaultIndex != 0) revert AlreadyConnected(_vault, connection.vaultIndex); uint256 vaultBalance = _availableBalance(_vault); if (vaultBalance < CONNECT_DEPOSIT) revert VaultInsufficientBalance(_vault, vaultBalance, CONNECT_DEPOSIT); IStakingVault vault = IStakingVault(_vault); // Connecting a new vault with totalValue == balance VaultRecord memory record = VaultRecord({ report: Report({ totalValue: uint104(vaultBalance), inOutDelta: int104(int256(vaultBalance)), timestamp: uint48(block.timestamp) }), maxLiabilityShares: 0, liabilityShares: 0, inOutDelta: DoubleRefSlotCache.initializeInt104DoubleCache(int104(int256(vaultBalance))), minimalReserve: uint128(CONNECT_DEPOSIT), redemptionShares: 0, cumulativeLidoFees: 0, settledLidoFees: 0 }); connection = VaultConnection({ owner: vault.owner(), shareLimit: uint96(_shareLimit), vaultIndex: uint96(_storage().vaults.length), disconnectInitiatedTs: DISCONNECT_NOT_INITIATED, reserveRatioBP: uint16(_reserveRatioBP), forcedRebalanceThresholdBP: uint16(_forcedRebalanceThresholdBP), infraFeeBP: uint16(_infraFeeBP), liquidityFeeBP: uint16(_liquidityFeeBP), reservationFeeBP: uint16(_reservationFeeBP), beaconChainDepositsPauseIntent: vault.beaconChainDepositsPaused() }); _addVault(_vault, connection, record); } function _initiateDisconnection( address _vault, VaultConnection storage _connection, VaultRecord storage _record, bool _forceFullFeesSettlement ) internal { _requireFreshReport(_vault, _record); uint256 liabilityShares_ = _record.liabilityShares; if (liabilityShares_ > 0) revert NoLiabilitySharesShouldBeLeft(_vault, liabilityShares_); uint256 unsettledLidoFees = _unsettledLidoFeesValue(_record); if (unsettledLidoFees > 0) { uint256 balance = Math256.min(_availableBalance(_vault), _totalValue(_record)); if (_forceFullFeesSettlement) { if (balance < unsettledLidoFees) revert NoUnsettledLidoFeesShouldBeLeft(_vault, unsettledLidoFees); _settleLidoFees(_vault, _record, _connection, unsettledLidoFees); } else { uint256 withdrawable = Math256.min(balance, unsettledLidoFees); if (withdrawable > 0) { _settleLidoFees(_vault, _record, _connection, withdrawable); } } } _connection.disconnectInitiatedTs = uint48(block.timestamp); } function _applyVaultReport( VaultRecord storage _record, uint256 _reportTimestamp, uint256 _reportTotalValue, int256 _reportInOutDelta, uint256 _reportCumulativeLidoFees, uint256 _reportLiabilityShares, uint256 _reportMaxLiabilityShares, uint256 _reportSlashingReserve ) internal { _record.cumulativeLidoFees = uint128(_reportCumulativeLidoFees); _record.minimalReserve = uint128(Math256.max(CONNECT_DEPOSIT, _reportSlashingReserve)); // We want to prevent 1 tx looping here: // 1. bring ETH (TV+) // 2. mint stETH (locked+) // 3. burn stETH // 4. bring the last report (locked-) // 5. withdraw ETH(TV-) // current maxLiabilityShares will be greater than the report one // if any stETH is minted on funds added after the refslot // in that case we don't update it (preventing unlock) if (_record.maxLiabilityShares == _reportMaxLiabilityShares) { _record.maxLiabilityShares = uint96(Math256.max(_record.liabilityShares, _reportLiabilityShares)); } _record.report = Report({ totalValue: uint104(_reportTotalValue), inOutDelta: int104(_reportInOutDelta), timestamp: uint48(_reportTimestamp) }); } function _rebalance(address _vault, VaultRecord storage _record, uint256 _shares) internal { uint256 valueToRebalance = _getPooledEthBySharesRoundUp(_shares); _decreaseLiability(_vault, _record, _shares); _withdraw(_vault, _record, address(this), valueToRebalance); _rebalanceExternalEtherToInternal(valueToRebalance, _shares); _updateBeaconChainDepositsPause(_vault, _record, _vaultConnection(_vault)); emit VaultRebalanced(_vault, _shares, valueToRebalance); } function _withdraw(address _vault, VaultRecord storage _record, address _recipient, uint256 _amount) internal { uint256 totalValue_ = _totalValue(_record); if (_amount > totalValue_) { revert AmountExceedsTotalValue(_vault, totalValue_, _amount); } _updateInOutDelta(_vault, _record, -int104(int256(_amount))); _withdrawFromVault(_vault, _recipient, _amount); } /// @dev Increases liabilityShares of the vault and updates the locked amount function _increaseLiability( address _vault, VaultRecord storage _record, uint256 _amountOfShares, uint256 _reserveRatioBP, uint256 _lockableValueLimit, uint256 _shareLimit, bool _overrideOperatorLimits ) internal { uint256 sharesAfterMint = _record.liabilityShares + _amountOfShares; if (sharesAfterMint > _shareLimit) { revert ShareLimitExceeded(_vault, sharesAfterMint, _shareLimit); } // Calculate the minimum ETH that needs to be locked in the vault to maintain the reserve ratio uint256 etherToLock = _locked(sharesAfterMint, _record.minimalReserve, _reserveRatioBP); if (etherToLock > _lockableValueLimit) { revert InsufficientValue(_vault, etherToLock, _lockableValueLimit); } if (sharesAfterMint > _record.maxLiabilityShares) { _record.maxLiabilityShares = uint96(sharesAfterMint); } _record.liabilityShares = uint96(sharesAfterMint); _operatorGrid().onMintedShares(_vault, _amountOfShares, _overrideOperatorLimits); } function _decreaseLiability(address _vault, VaultRecord storage _record, uint256 _amountOfShares) internal { uint256 liabilityShares_ = _record.liabilityShares; if (liabilityShares_ < _amountOfShares) revert InsufficientSharesToBurn(_vault, liabilityShares_); _record.liabilityShares = uint96(liabilityShares_ - _amountOfShares); uint256 redemptionShares = _record.redemptionShares; if (_amountOfShares > 0 && redemptionShares > 0) { uint256 decreasedRedemptionShares = redemptionShares - Math256.min(redemptionShares, _amountOfShares); _record.redemptionShares = uint128(decreasedRedemptionShares); emit VaultRedemptionSharesUpdated(_vault, decreasedRedemptionShares); } _operatorGrid().onBurnedShares(_vault, _amountOfShares); } function _badDebtShares(VaultRecord storage _record) internal view returns (uint256) { uint256 liabilityShares_ = _record.liabilityShares; uint256 totalValueShares = _getSharesByPooledEth(_totalValue(_record)); if (totalValueShares > liabilityShares_) { return 0; } return liabilityShares_ - totalValueShares; } function _healthShortfallShares( VaultConnection storage _connection, VaultRecord storage _record ) internal view returns (uint256) { uint256 totalValue_ = _totalValue(_record); uint256 liabilityShares_ = _record.liabilityShares; bool isHealthy = !_isThresholdBreached( totalValue_, liabilityShares_, _connection.forcedRebalanceThresholdBP ); // Health vault do not need to rebalance if (isHealthy) { return 0; } uint256 reserveRatioBP = _connection.reserveRatioBP; uint256 maxMintableRatio = (TOTAL_BASIS_POINTS - reserveRatioBP); uint256 liability = _getPooledEthBySharesRoundUp(liabilityShares_); // Impossible to rebalance a vault with bad debt if (liability > totalValue_) { return type(uint256).max; } // Solve the equation for X: // L - liability, TV - totalValue // MR - maxMintableRatio, 100 - TOTAL_BASIS_POINTS, RR - reserveRatio // X - amount of shares that should be withdrawn (TV - X) and used to repay the debt (LS - X) // to reduce the L/TV ratio back to MR // (L - X) / (TV - X) = MR / 100 // (L - X) * 100 = (TV - X) * MR // L * 100 - X * 100 = TV * MR - X * MR // X * MR - X * 100 = TV * MR - L * 100 // X * (MR - 100) = TV * MR - L * 100 // X = (TV * MR - L * 100) / (MR - 100) // X = (L * 100 - TV * MR) / (100 - MR) // RR = 100 - MR // X = (L * 100 - TV * MR) / RR uint256 shortfallEth = (liability * TOTAL_BASIS_POINTS - totalValue_ * maxMintableRatio) / reserveRatioBP; // Add 10 extra shares to avoid dealing with rounding/precision issues uint256 shortfallShares = _getSharesByPooledEth(shortfallEth) + 10; return Math256.min(shortfallShares, liabilityShares_); } function _totalValue(VaultRecord storage _record) internal view returns (uint256) { Report memory report = _record.report; DoubleRefSlotCache.Int104WithCache[DOUBLE_CACHE_LENGTH] memory inOutDelta = _record.inOutDelta; return SafeCast.toUint256(int256(uint256(report.totalValue)) + inOutDelta.currentValue() - report.inOutDelta); } function _locked( VaultConnection storage _connection, VaultRecord storage _record ) internal view returns (uint256) { return _locked(_record.maxLiabilityShares, _record.minimalReserve, _connection.reserveRatioBP); } /// @param _liabilityShares amount of shares that the vault is minted /// @param _minimalReserve minimal amount of additional reserve to be locked /// @param _reserveRatioBP the reserve ratio of the vault /// @return the amount of collateral to be locked on the vault function _locked( uint256 _liabilityShares, uint256 _minimalReserve, uint256 _reserveRatioBP ) internal view returns (uint256) { uint256 liability = _getPooledEthBySharesRoundUp(_liabilityShares); // uint256 reserve = liability * TOTAL_BASIS_POINTS / (TOTAL_BASIS_POINTS - _reserveRatioBP) - liability; // simplified to: uint256 reserve = Math256.ceilDiv(liability * _reserveRatioBP, TOTAL_BASIS_POINTS - _reserveRatioBP); return liability + Math256.max(reserve, _minimalReserve); } function _isReportFresh(VaultRecord storage _record) internal view returns (bool) { uint256 latestReportTimestamp = _lazyOracle().latestReportTimestamp(); return // check if AccountingOracle brought fresh report uint48(latestReportTimestamp) <= _record.report.timestamp && // if Accounting Oracle stop bringing the report, last report is fresh during this time block.timestamp - latestReportTimestamp < REPORT_FRESHNESS_DELTA; } function _isVaultHealthy( VaultConnection storage _connection, VaultRecord storage _record ) internal view returns (bool) { return !_isThresholdBreached( _totalValue(_record), _record.liabilityShares, _connection.forcedRebalanceThresholdBP ); } /// @dev Returns true if the vault liability breached the given threshold (inverted) function _isThresholdBreached( uint256 _vaultTotalValue, uint256 _vaultLiabilityShares, uint256 _thresholdBP ) internal view returns (bool) { uint256 liability = _getPooledEthBySharesRoundUp(_vaultLiabilityShares); return liability > _vaultTotalValue * (TOTAL_BASIS_POINTS - _thresholdBP) / TOTAL_BASIS_POINTS; } /// @return the total amount of ether needed to fully cover all outstanding obligations of the vault, including: /// - shares to burn required to restore vault healthiness or cover redemptions /// - unsettled Lido fees (if above the minimum beacon deposit) function _obligationsAmount( VaultConnection storage _connection, VaultRecord storage _record ) internal view returns (uint256) { uint256 sharesToBurn = _obligationsShares(_connection, _record); if (sharesToBurn == type(uint256).max) return type(uint256).max; // no need to cover fees if they are less than the minimum beacon deposit uint256 unsettledLidoFees = _unsettledLidoFeesValue(_record); uint256 feesToSettle = unsettledLidoFees < MIN_BEACON_DEPOSIT ? 0 : unsettledLidoFees; return _getPooledEthBySharesRoundUp(sharesToBurn) + feesToSettle; } /// @return the ether shortfall required to fully cover all outstanding obligations amount of the vault function _obligationsShortfallValue( address _vault, VaultConnection storage _connection, VaultRecord storage _record ) internal view returns (uint256) { uint256 obligationsAmount_ = _obligationsAmount(_connection, _record); if (obligationsAmount_ == type(uint256).max) return type(uint256).max; uint256 balance = _availableBalance(_vault); return obligationsAmount_ > balance ? obligationsAmount_ - balance : 0; } function _addVault(address _vault, VaultConnection memory _connection, VaultRecord memory _record) internal { Storage storage $ = _storage(); $.vaults.push(_vault); $.connections[_vault] = _connection; $.records[_vault] = _record; } function _deleteVault(address _vault, VaultConnection storage _connection) internal { Storage storage $ = _storage(); uint96 vaultIndex = _connection.vaultIndex; address lastVault = $.vaults[$.vaults.length - 1]; $.connections[lastVault].vaultIndex = vaultIndex; $.vaults[vaultIndex] = lastVault; $.vaults.pop(); delete $.connections[_vault]; delete $.records[_vault]; _lazyOracle().removeVaultQuarantine(_vault); _operatorGrid().resetVaultTier(_vault); } function _checkConnectionAndOwner(address _vault) internal view returns (VaultConnection storage connection) { connection = _checkConnection(_vault); _requireSender(connection.owner); } function _isPendingDisconnect(VaultConnection storage _connection) internal view returns (bool) { uint256 disconnectionTs = _connection.disconnectInitiatedTs; return disconnectionTs != 0 // vault is disconnected && disconnectionTs != DISCONNECT_NOT_INITIATED; // vault in connected but not pending for disconnect } function _checkConnection(address _vault) internal view returns (VaultConnection storage) { _requireNotZero(_vault); VaultConnection storage connection = _vaultConnection(_vault); _requireConnected(connection, _vault); if (_isPendingDisconnect(connection)) revert VaultIsDisconnecting(_vault); return connection; } /// @dev Caches the inOutDelta of the latest refSlot and updates the value function _updateInOutDelta(address _vault, VaultRecord storage _record, int104 _increment) internal { DoubleRefSlotCache.Int104WithCache[DOUBLE_CACHE_LENGTH] memory inOutDelta = _record.inOutDelta.withValueIncrease({ _consensus: CONSENSUS_CONTRACT, _increment: _increment }); _record.inOutDelta = inOutDelta; emit VaultInOutDeltaUpdated(_vault, inOutDelta.currentValue()); } function _updateBeaconChainDepositsPause( address _vault, VaultRecord storage _record, VaultConnection storage _connection ) internal { IStakingVault vault_ = IStakingVault(_vault); uint256 obligationsAmount_ = _obligationsAmount(_connection, _record); if (obligationsAmount_ > 0) { _pauseBeaconChainDepositsIfNotAlready(vault_); } else if (!_connection.beaconChainDepositsPauseIntent) { _resumeBeaconChainDepositsIfNotAlready(vault_); } } function _settleLidoFees( address _vault, VaultRecord storage _record, VaultConnection storage _connection, uint256 _valueToSettle ) internal { uint256 settledLidoFees = _record.settledLidoFees + _valueToSettle; _record.settledLidoFees = uint128(settledLidoFees); _withdraw(_vault, _record, LIDO_LOCATOR.treasury(), _valueToSettle); _updateBeaconChainDepositsPause(_vault, _record, _connection); emit LidoFeesSettled({ vault: _vault, transferred: _valueToSettle, cumulativeLidoFees: _record.cumulativeLidoFees, settledLidoFees: settledLidoFees }); } /// @notice the amount of ether that can be withdrawn from the vault based on the available balance, /// locked value, vault redemption shares (does not include Lido fees) function _withdrawableValueFeesIncluded( address _vault, VaultConnection storage _connection, VaultRecord storage _record ) internal view returns (uint256) { uint256 availableBalance = Math256.min(_availableBalance(_vault), _totalValue(_record)); // We can't withdraw funds that can be used to cover redemptions uint256 redemptionValue = _getPooledEthBySharesRoundUp(_record.redemptionShares); if (redemptionValue > availableBalance) return 0; availableBalance -= redemptionValue; // We must account vaults locked value when calculating the withdrawable amount return Math256.min(availableBalance, _unlocked(_connection, _record)); } /// @notice the amount of lido fees that can be settled on the vault based on the withdrawable value function _settleableLidoFeesValue( address _vault, VaultConnection storage _connection, VaultRecord storage _record, uint256 _feesToSettle ) internal view returns (uint256) { return Math256.min(_withdrawableValueFeesIncluded(_vault, _connection, _record), _feesToSettle); } /// @notice the amount of ether that can be instantly withdrawn from the vault based on the available balance, /// locked value, vault redemption shares and unsettled Lido fees accrued on the vault function _withdrawableValue( address _vault, VaultConnection storage _connection, VaultRecord storage _record ) internal view returns (uint256) { uint256 withdrawable = _withdrawableValueFeesIncluded(_vault, _connection, _record); uint256 feesValue = _unsettledLidoFeesValue(_record); return withdrawable > feesValue ? withdrawable - feesValue : 0; } /// @notice Calculates the max lockable value of the vault /// @param _record The record of the vault /// @return the max lockable value of the vault function _maxLockableValue(VaultRecord storage _record) internal view returns (uint256) { uint256 totalValue_ = _totalValue(_record); uint256 unsettledLidoFees_ = _unsettledLidoFeesValue(_record); return totalValue_ > unsettledLidoFees_ ? totalValue_ - unsettledLidoFees_ : 0; } /// @notice Calculates the total number of shares that is possible to mint on the vault taking into account /// minimal reserve, reserve ratio and the operator grid share limit /// @param _vault The address of the vault /// @param _deltaValue The delta value to apply to the total value of the vault (may be negative) /// @return the number of shares that can be minted /// @dev returns 0 if the vault is not connected function _totalMintingCapacityShares(address _vault, int256 _deltaValue) internal view returns (uint256) { VaultRecord storage record = _vaultRecord(_vault); VaultConnection storage connection = _vaultConnection(_vault); uint256 maxLockableValue_ = _maxLockableValue(record); if (_deltaValue >= 0) { maxLockableValue_ += uint256(_deltaValue); } else { uint256 negDeltaValue = uint256(-_deltaValue); if (maxLockableValue_ < negDeltaValue) return 0; maxLockableValue_ -= negDeltaValue; } uint256 minimalReserve_ = record.minimalReserve; if (maxLockableValue_ <= minimalReserve_) return 0; uint256 reserve = Math256.ceilDiv(maxLockableValue_ * connection.reserveRatioBP, TOTAL_BASIS_POINTS); uint256 capacityShares = _getSharesByPooledEth(maxLockableValue_ - Math256.max(reserve, minimalReserve_)); return Math256.min(capacityShares, _operatorGrid().effectiveShareLimit(_vault)); } function _unlocked( VaultConnection storage _connection, VaultRecord storage _record ) internal view returns (uint256) { uint256 totalValue_ = _totalValue(_record); uint256 locked_ = _locked(_connection, _record); return totalValue_ > locked_ ? totalValue_ - locked_ : 0; } function _unsettledLidoFeesValue(VaultRecord storage _record) internal view returns (uint256) { return _record.cumulativeLidoFees - _record.settledLidoFees; } function _obligationsShares( VaultConnection storage _connection, VaultRecord storage _record ) internal view returns (uint256) { return Math256.max(_healthShortfallShares(_connection, _record), _record.redemptionShares); } function _storage() internal pure returns (Storage storage $) { assembly { $.slot := STORAGE_LOCATION } } function _vaultConnection(address _vault) internal view returns (VaultConnection storage) { return _storage().connections[_vault]; } function _vaultRecord(address _vault) internal view returns (VaultRecord storage) { return _storage().records[_vault]; } // ----------------------------- // EXTERNAL CALLS // ----------------------------- // All external calls that is used more than once is wrapped in internal function to save bytecode function _operatorGrid() internal view returns (OperatorGrid) { return OperatorGrid(LIDO_LOCATOR.operatorGrid()); } function _lazyOracle() internal view returns (LazyOracle) { return LazyOracle(LIDO_LOCATOR.lazyOracle()); } function _predepositGuarantee() internal view returns (IPredepositGuarantee) { return IPredepositGuarantee(LIDO_LOCATOR.predepositGuarantee()); } function _getSharesByPooledEth(uint256 _ether) internal view returns (uint256) { return LIDO.getSharesByPooledEth(_ether); } function _getPooledEthBySharesRoundUp(uint256 _shares) internal view returns (uint256) { return LIDO.getPooledEthBySharesRoundUp(_shares); } function _rebalanceExternalEtherToInternal(uint256 _ether, uint256 _amountOfShares) internal { LIDO.rebalanceExternalEtherToInternal{value: _ether}(_amountOfShares); } function _triggerVaultValidatorWithdrawals( address _vault, uint256 _value, bytes calldata _pubkeys, uint64[] memory _amountsInGwei, address _refundRecipient ) internal { IStakingVault(_vault).triggerValidatorWithdrawals{value: _value}(_pubkeys, _amountsInGwei, _refundRecipient); } function _withdrawFromVault(address _vault, address _recipient, uint256 _amount) internal { IStakingVault(_vault).withdraw(_recipient, _amount); } function _nodeOperator(address _vault) internal view returns (address) { return IStakingVault(_vault).nodeOperator(); } function _availableBalance(address _vault) internal view returns (uint256) { return IStakingVault(_vault).availableBalance(); } function _requireNotZero(uint256 _value) internal pure { if (_value == 0) revert ZeroArgument(); } function _requireNotZero(address _address) internal pure { if (_address == address(0)) revert ZeroAddress(); } function _requireSender(address _sender) internal view { if (msg.sender != _sender) revert NotAuthorized(); } function _requireSaneShareLimit(uint256 _shareLimit) internal view { uint256 maxSaneShareLimit = (LIDO.getTotalShares() * MAX_RELATIVE_SHARE_LIMIT_BP) / TOTAL_BASIS_POINTS; if (_shareLimit > maxSaneShareLimit) revert ShareLimitTooHigh(_shareLimit, maxSaneShareLimit); } function _requireConnected(VaultConnection storage _connection, address _vault) internal view { if (_connection.vaultIndex == 0) revert NotConnectedToHub(_vault); } function _requireFreshReport(address _vault, VaultRecord storage _record) internal view { if (!_isReportFresh(_record)) revert VaultReportStale(_vault); } function _isBeaconChainDepositsPaused(IStakingVault _vault) internal view returns (bool) { return _vault.beaconChainDepositsPaused(); } function _pauseBeaconChainDepositsIfNotAlready(IStakingVault _vault) internal { if (!_isBeaconChainDepositsPaused(_vault)) { _vault.pauseBeaconChainDeposits(); } } function _resumeBeaconChainDepositsIfNotAlready(IStakingVault _vault) internal { if (_isBeaconChainDepositsPaused(_vault)) { _vault.resumeBeaconChainDeposits(); } } // ----------------------------- // EVENTS // ----------------------------- /// @dev Warning! used by Accounting Oracle to calculate fees 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 ); /// @dev Warning! used by Accounting Oracle to calculate fees 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 ); /// @dev Warning! used by Accounting Oracle to calculate fees event MintedSharesOnVault(address indexed vault, uint256 amountOfShares, uint256 lockedAmount); /// @dev Warning! used by Accounting Oracle to calculate fees event BurnedSharesOnVault(address indexed vault, uint256 amountOfShares); /// @dev Warning! used by Accounting Oracle to calculate fees 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); /** * @notice Emitted when the vault ownership is changed * @param vault The address of the vault * @param newOwner The address of the new owner * @param oldOwner The address of the old owner */ 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); /// @dev Warning! used by Accounting Oracle to calculate fees event BadDebtSocialized(address indexed vaultDonor, address indexed vaultAcceptor, uint256 badDebtShares); /// @dev Warning! used by Accounting Oracle to calculate fees event BadDebtWrittenOffToBeInternalized(address indexed vault, uint256 badDebtShares); // ----------------------------- // ERRORS // ----------------------------- error PauseIntentAlreadySet(); error PauseIntentAlreadyUnset(); error AmountExceedsTotalValue(address vault, uint256 totalValue, uint256 withdrawAmount); error AmountExceedsWithdrawableValue(address vault, uint256 withdrawable, uint256 requested); error NoFundsForForceRebalance(address vault); error NoReasonForForceRebalance(address vault); error NoUnsettledLidoFeesToSettle(address vault); error NoFundsToSettleLidoFees(address vault, uint256 unsettledLidoFees); error VaultMintingCapacityExceeded( address vault, uint256 totalValue, uint256 liabilityShares, uint256 newRebalanceThresholdBP ); error InsufficientSharesToBurn(address vault, uint256 amount); error ShareLimitExceeded(address vault, uint256 expectedSharesAfterMint, uint256 shareLimit); error AlreadyConnected(address vault, uint256 index); error InsufficientStagedBalance(address vault); error NotConnectedToHub(address vault); error NotAuthorized(); error ZeroAddress(); error ZeroArgument(); error InvalidBasisPoints(uint256 valueBP, uint256 maxValueBP); error ShareLimitTooHigh(uint256 shareLimit, uint256 maxShareLimit); error InsufficientValue(address vault, uint256 etherToLock, uint256 maxLockableValue); error NoLiabilitySharesShouldBeLeft(address vault, uint256 liabilityShares); error NoUnsettledLidoFeesShouldBeLeft(address vault, uint256 unsettledLidoFees); error VaultOssified(address vault); error VaultInsufficientBalance(address vault, uint256 currentBalance, uint256 expectedBalance); error VaultReportStale(address vault); error PDGNotDepositor(address vault); error VaultHubNotPendingOwner(address vault); error VaultIsDisconnecting(address vault); error PartialValidatorWithdrawalNotAllowed(); error ForcedValidatorExitNotAllowed(); error BadDebtSocializationNotAllowed(); error VaultNotFactoryDeployed(address vault); }
// SPDX-FileCopyrightText: 2025 Lido <[email protected]> // SPDX-License-Identifier: GPL-3.0 // See contracts/COMPILERS.md // solhint-disable-next-line lido/fixed-compiler-version pragma solidity >=0.5.0; interface IDepositContract { function get_deposit_root() external view returns (bytes32 rootHash); function deposit( bytes calldata pubkey, // 48 bytes bytes calldata withdrawal_credentials, // 32 bytes bytes calldata signature, // 96 bytes bytes32 deposit_data_root ) external payable; }
// SPDX-FileCopyrightText: 2025 Lido <[email protected]> // SPDX-License-Identifier: GPL-3.0 // See contracts/COMPILERS.md // solhint-disable-next-line lido/fixed-compiler-version pragma solidity >=0.5.0; interface IHashConsensus { function getIsMember(address addr) external view returns (bool); function getCurrentFrame() external view returns ( uint256 refSlot, uint256 reportProcessingDeadlineSlot ); function getChainConfig() external view returns ( uint256 slotsPerEpoch, uint256 secondsPerSlot, uint256 genesisTime ); function getFrameConfig() external view returns (uint256 initialEpoch, uint256 epochsPerFrame); function getInitialRefSlot() external view returns (uint256); }
// SPDX-FileCopyrightText: 2025 Lido <[email protected]> // SPDX-License-Identifier: GPL-3.0 // See contracts/COMPILERS.md // solhint-disable-next-line lido/fixed-compiler-version pragma solidity >=0.5.0; /** * Interface to connect AccountingOracle with LazyOracle and force type consistency */ interface ILazyOracle { function updateReportData( uint256 _vaultsDataTimestamp, uint256 _vaultsDataRefSlot, bytes32 _vaultsDataTreeRoot, string memory _vaultsDataReportCid ) external; }
// SPDX-FileCopyrightText: 2025 Lido <[email protected]> // SPDX-License-Identifier: GPL-3.0 // See contracts/COMPILERS.md // solhint-disable-next-line lido/fixed-compiler-version pragma solidity >=0.8.0; import {IERC20} from "@openzeppelin/contracts-v4.4/token/ERC20/IERC20.sol"; import {IVersioned} from "contracts/common/interfaces/IVersioned.sol"; interface ILido is IERC20, IVersioned { function sharesOf(address) external view returns (uint256); function getSharesByPooledEth(uint256) external view returns (uint256); function getPooledEthByShares(uint256) external view returns (uint256); function getPooledEthBySharesRoundUp(uint256) external view returns (uint256); function transferSharesFrom(address, address, uint256) external returns (uint256); function transferShares(address, uint256) external returns (uint256); function rebalanceExternalEtherToInternal(uint256 _amountOfShares) external payable; function getTotalPooledEther() external view returns (uint256); function getExternalEther() external view returns (uint256); function getExternalShares() external view returns (uint256); function mintExternalShares(address, uint256) external; function burnExternalShares(uint256) external; function getTotalShares() external view returns (uint256); function getBeaconStat() external view returns (uint256 depositedValidators, uint256 beaconValidators, uint256 beaconBalance); function processClStateUpdate( uint256 _reportTimestamp, uint256 _preClValidators, uint256 _reportClValidators, uint256 _reportClBalance ) external; function collectRewardsAndProcessWithdrawals( uint256 _reportTimestamp, uint256 _reportClBalance, uint256 _adjustedPreCLBalance, uint256 _withdrawalsToWithdraw, uint256 _elRewardsToWithdraw, uint256 _lastWithdrawalRequestToFinalize, uint256 _simulatedShareRate, uint256 _etherToLockOnWithdrawalQueue ) external; function emitTokenRebase( uint256 _reportTimestamp, uint256 _timeElapsed, uint256 _preTotalShares, uint256 _preTotalEther, uint256 _postTotalShares, uint256 _postTotalEther, uint256 _postInternalShares, uint256 _postInternalEther, uint256 _sharesMintedAsFees ) external; function mintShares(address _recipient, uint256 _sharesAmount) external; function internalizeExternalBadDebt(uint256 _amountOfShares) external; }
// SPDX-FileCopyrightText: 2025 Lido <[email protected]> // SPDX-License-Identifier: GPL-3.0 // See contracts/COMPILERS.md // solhint-disable-next-line lido/fixed-compiler-version pragma solidity >=0.4.24 <0.9.0; interface ILidoLocator { function accountingOracle() external view returns(address); function depositSecurityModule() external view returns(address); function elRewardsVault() external view returns(address); function lido() external view returns(address); function oracleReportSanityChecker() external view returns(address); function burner() external view returns(address); function stakingRouter() external view returns(address); function treasury() external view returns(address); function validatorsExitBusOracle() external view returns(address); function withdrawalQueue() external view returns(address); function withdrawalVault() external view returns(address); function postTokenRebaseReceiver() external view returns(address); function oracleDaemonConfig() external view returns(address); function accounting() external view returns (address); function predepositGuarantee() external view returns (address); function wstETH() external view returns (address); function vaultHub() external view returns (address); function vaultFactory() external view returns (address); function lazyOracle() external view returns (address); function operatorGrid() external view returns (address); /// @notice Returns core Lido protocol component addresses in a single call /// @dev This function provides a gas-efficient way to fetch multiple component addresses in a single call function coreComponents() external view returns( address elRewardsVault, address oracleReportSanityChecker, address stakingRouter, address treasury, address withdrawalQueue, address withdrawalVault ); /// @notice Returns addresses of components involved in processing oracle reports in the Lido contract /// @dev This function provides a gas-efficient way to fetch multiple component addresses in a single call function oracleReportComponents() external view returns( address accountingOracle, address oracleReportSanityChecker, address burner, address withdrawalQueue, address postTokenRebaseReceiver, address stakingRouter, address vaultHub ); }
// SPDX-FileCopyrightText: 2025 Lido <[email protected]> // SPDX-License-Identifier: GPL-3.0 // See contracts/COMPILERS.md // solhint-disable-next-line pragma solidity >=0.4.24; interface IVersioned { /// @notice Returns the current contract version. function getContractVersion() external view returns (uint256); }
// SPDX-FileCopyrightText: 2023 Lido <[email protected]> // SPDX-License-Identifier: MIT // Copied from: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/0457042d93d9dfd760dbaa06a4d2f1216fdbe297/contracts/utils/math/Math.sol // See contracts/COMPILERS.md // solhint-disable-next-line pragma solidity >=0.4.24 <0.9.0; library Math256 { /// @dev Returns the largest of two numbers. function max(uint256 a, uint256 b) internal pure returns (uint256) { return a > b ? a : b; } /// @dev Returns the smallest of two numbers. function min(uint256 a, uint256 b) internal pure returns (uint256) { return a < b ? a : b; } /// @dev Returns the largest of two numbers. function max(int256 a, int256 b) internal pure returns (int256) { return a > b ? a : b; } /// @dev Returns the smallest of two numbers. function min(int256 a, int256 b) internal pure returns (int256) { return a < b ? a : b; } /// @dev Returns the ceiling of the division of two numbers. /// /// This differs from standard division with `/` in that it rounds up instead /// of rounding down. function ceilDiv(uint256 a, uint256 b) internal pure returns (uint256) { // (a + b - 1) / b can overflow on addition, so we distribute. return a == 0 ? 0 : (a - 1) / b + 1; } /// @dev Returns absolute difference of two numbers. function absDiff(uint256 a, uint256 b) internal pure returns (uint256) { return a > b ? a - b : b - a; } }
// SPDX-FileCopyrightText: 2023 Lido <[email protected]>, Aragon // SPDX-License-Identifier: MIT // solhint-disable-next-line lido/fixed-compiler-version pragma solidity ^0.8.9; library UnstructuredStorage { function getStorageBool(bytes32 position) internal view returns (bool data) { assembly { data := sload(position) } } function getStorageAddress(bytes32 position) internal view returns (address data) { assembly { data := sload(position) } } function getStorageBytes32(bytes32 position) internal view returns (bytes32 data) { assembly { data := sload(position) } } function getStorageUint256(bytes32 position) internal view returns (uint256 data) { assembly { data := sload(position) } } function setStorageBool(bytes32 position, bool data) internal { assembly { sstore(position, data) } } function setStorageAddress(bytes32 position, address data) internal { assembly { sstore(position, data) } } function setStorageBytes32(bytes32 position, bytes32 data) internal { assembly { sstore(position, data) } } function setStorageUint256(bytes32 position, uint256 data) internal { assembly { sstore(position, data) } } }
// SPDX-FileCopyrightText: 2025 Lido <[email protected]> // SPDX-License-Identifier: GPL-3.0 // solhint-disable-next-line lido/fixed-compiler-version pragma solidity ^0.8.9; import {UnstructuredStorage} from "contracts/common/lib/UnstructuredStorage.sol"; /** * @title PausableUntil * @notice allows to pause the contract for a specific duration or indefinitely */ abstract contract PausableUntil { using UnstructuredStorage for bytes32; /// Contract resume/pause control storage slot bytes32 internal constant RESUME_SINCE_TIMESTAMP_POSITION = keccak256("lido.PausableUntil.resumeSinceTimestamp"); /// Special value for the infinite pause uint256 public constant PAUSE_INFINITELY = type(uint256).max; /// @notice Emitted when paused by the `pauseFor` or `pauseUntil` call event Paused(uint256 duration); /// @notice Emitted when resumed by the `resume` call event Resumed(); error ZeroPauseDuration(); error PausedExpected(); error ResumedExpected(); error PauseUntilMustBeInFuture(); /// @notice Reverts if paused modifier whenResumed() { _checkResumed(); _; } /// @notice Returns whether the contract is paused function isPaused() public view returns (bool) { return block.timestamp < RESUME_SINCE_TIMESTAMP_POSITION.getStorageUint256(); } /// @notice Returns one of: /// - PAUSE_INFINITELY if paused infinitely returns /// - the timestamp when the contract get resumed if paused for specific duration /// - some timestamp in past if not paused function getResumeSinceTimestamp() external view returns (uint256) { return RESUME_SINCE_TIMESTAMP_POSITION.getStorageUint256(); } function _checkPaused() internal view { if (!isPaused()) { revert PausedExpected(); } } function _checkResumed() internal view { if (isPaused()) { revert ResumedExpected(); } } function _resume() internal { _checkPaused(); RESUME_SINCE_TIMESTAMP_POSITION.setStorageUint256(block.timestamp); emit Resumed(); } function _pauseFor(uint256 _duration) internal { _checkResumed(); if (_duration == 0) revert ZeroPauseDuration(); uint256 resumeSince; if (_duration == PAUSE_INFINITELY) { resumeSince = PAUSE_INFINITELY; } else { resumeSince = block.timestamp + _duration; } _setPausedState(resumeSince); } function _pauseUntil(uint256 _pauseUntilInclusive) internal { _checkResumed(); if (_pauseUntilInclusive < block.timestamp) revert PauseUntilMustBeInFuture(); uint256 resumeSince; if (_pauseUntilInclusive != PAUSE_INFINITELY) { resumeSince = _pauseUntilInclusive + 1; } else { resumeSince = PAUSE_INFINITELY; } _setPausedState(resumeSince); } function _setPausedState(uint256 _resumeSince) internal { RESUME_SINCE_TIMESTAMP_POSITION.setStorageUint256(_resumeSince); if (_resumeSince == PAUSE_INFINITELY) { emit Paused(PAUSE_INFINITELY); } else { emit Paused(_resumeSince - block.timestamp); } } }
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (access/AccessControl.sol)
pragma solidity ^0.8.20;
import {IAccessControl} from "@openzeppelin/contracts-v5.2/access/IAccessControl.sol";
import {ContextUpgradeable} from "../utils/ContextUpgradeable.sol";
import {ERC165Upgradeable} from "../utils/introspection/ERC165Upgradeable.sol";
import {Initializable} from "../proxy/utils/Initializable.sol";
/**
* @dev Contract module that allows children to implement role-based access
* control mechanisms. This is a lightweight version that doesn't allow enumerating role
* members except through off-chain means by accessing the contract event logs. Some
* applications may benefit from on-chain enumerability, for those cases see
* {AccessControlEnumerable}.
*
* Roles are referred to by their `bytes32` identifier. These should be exposed
* in the external API and be unique. The best way to achieve this is by
* using `public constant` hash digests:
*
* ```solidity
* bytes32 public constant MY_ROLE = keccak256("MY_ROLE");
* ```
*
* Roles can be used to represent a set of permissions. To restrict access to a
* function call, use {hasRole}:
*
* ```solidity
* function foo() public {
* require(hasRole(MY_ROLE, msg.sender));
* ...
* }
* ```
*
* Roles can be granted and revoked dynamically via the {grantRole} and
* {revokeRole} functions. Each role has an associated admin role, and only
* accounts that have a role's admin role can call {grantRole} and {revokeRole}.
*
* By default, the admin role for all roles is `DEFAULT_ADMIN_ROLE`, which means
* that only accounts with this role will be able to grant or revoke other
* roles. More complex role relationships can be created by using
* {_setRoleAdmin}.
*
* WARNING: The `DEFAULT_ADMIN_ROLE` is also its own admin: it has permission to
* grant and revoke this role. Extra precautions should be taken to secure
* accounts that have been granted it. We recommend using {AccessControlDefaultAdminRules}
* to enforce additional security measures for this role.
*/
abstract contract AccessControlUpgradeable is Initializable, ContextUpgradeable, IAccessControl, ERC165Upgradeable {
struct RoleData {
mapping(address account => bool) hasRole;
bytes32 adminRole;
}
bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00;
/// @custom:storage-location erc7201:openzeppelin.storage.AccessControl
struct AccessControlStorage {
mapping(bytes32 role => RoleData) _roles;
}
// keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.AccessControl")) - 1)) & ~bytes32(uint256(0xff))
bytes32 private constant AccessControlStorageLocation = 0x02dd7bc7dec4dceedda775e58dd541e08a116c6c53815c0bd028192f7b626800;
function _getAccessControlStorage() private pure returns (AccessControlStorage storage $) {
assembly {
$.slot := AccessControlStorageLocation
}
}
/**
* @dev Modifier that checks that an account has a specific role. Reverts
* with an {AccessControlUnauthorizedAccount} error including the required role.
*/
modifier onlyRole(bytes32 role) {
_checkRole(role);
_;
}
function __AccessControl_init() internal onlyInitializing {
}
function __AccessControl_init_unchained() internal onlyInitializing {
}
/**
* @dev See {IERC165-supportsInterface}.
*/
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
return interfaceId == type(IAccessControl).interfaceId || super.supportsInterface(interfaceId);
}
/**
* @dev Returns `true` if `account` has been granted `role`.
*/
function hasRole(bytes32 role, address account) public view virtual returns (bool) {
AccessControlStorage storage $ = _getAccessControlStorage();
return $._roles[role].hasRole[account];
}
/**
* @dev Reverts with an {AccessControlUnauthorizedAccount} error if `_msgSender()`
* is missing `role`. Overriding this function changes the behavior of the {onlyRole} modifier.
*/
function _checkRole(bytes32 role) internal view virtual {
_checkRole(role, _msgSender());
}
/**
* @dev Reverts with an {AccessControlUnauthorizedAccount} error if `account`
* is missing `role`.
*/
function _checkRole(bytes32 role, address account) internal view virtual {
if (!hasRole(role, account)) {
revert AccessControlUnauthorizedAccount(account, role);
}
}
/**
* @dev Returns the admin role that controls `role`. See {grantRole} and
* {revokeRole}.
*
* To change a role's admin, use {_setRoleAdmin}.
*/
function getRoleAdmin(bytes32 role) public view virtual returns (bytes32) {
AccessControlStorage storage $ = _getAccessControlStorage();
return $._roles[role].adminRole;
}
/**
* @dev Grants `role` to `account`.
*
* If `account` had not been already granted `role`, emits a {RoleGranted}
* event.
*
* Requirements:
*
* - the caller must have ``role``'s admin role.
*
* May emit a {RoleGranted} event.
*/
function grantRole(bytes32 role, address account) public virtual onlyRole(getRoleAdmin(role)) {
_grantRole(role, account);
}
/**
* @dev Revokes `role` from `account`.
*
* If `account` had been granted `role`, emits a {RoleRevoked} event.
*
* Requirements:
*
* - the caller must have ``role``'s admin role.
*
* May emit a {RoleRevoked} event.
*/
function revokeRole(bytes32 role, address account) public virtual onlyRole(getRoleAdmin(role)) {
_revokeRole(role, account);
}
/**
* @dev Revokes `role` from the calling account.
*
* Roles are often managed via {grantRole} and {revokeRole}: this function's
* purpose is to provide a mechanism for accounts to lose their privileges
* if they are compromised (such as when a trusted device is misplaced).
*
* If the calling account had been revoked `role`, emits a {RoleRevoked}
* event.
*
* Requirements:
*
* - the caller must be `callerConfirmation`.
*
* May emit a {RoleRevoked} event.
*/
function renounceRole(bytes32 role, address callerConfirmation) public virtual {
if (callerConfirmation != _msgSender()) {
revert AccessControlBadConfirmation();
}
_revokeRole(role, callerConfirmation);
}
/**
* @dev Sets `adminRole` as ``role``'s admin role.
*
* Emits a {RoleAdminChanged} event.
*/
function _setRoleAdmin(bytes32 role, bytes32 adminRole) internal virtual {
AccessControlStorage storage $ = _getAccessControlStorage();
bytes32 previousAdminRole = getRoleAdmin(role);
$._roles[role].adminRole = adminRole;
emit RoleAdminChanged(role, previousAdminRole, adminRole);
}
/**
* @dev Attempts to grant `role` to `account` and returns a boolean indicating if `role` was granted.
*
* Internal function without access restriction.
*
* May emit a {RoleGranted} event.
*/
function _grantRole(bytes32 role, address account) internal virtual returns (bool) {
AccessControlStorage storage $ = _getAccessControlStorage();
if (!hasRole(role, account)) {
$._roles[role].hasRole[account] = true;
emit RoleGranted(role, account, _msgSender());
return true;
} else {
return false;
}
}
/**
* @dev Attempts to revoke `role` from `account` and returns a boolean indicating if `role` was revoked.
*
* Internal function without access restriction.
*
* May emit a {RoleRevoked} event.
*/
function _revokeRole(bytes32 role, address account) internal virtual returns (bool) {
AccessControlStorage storage $ = _getAccessControlStorage();
if (hasRole(role, account)) {
$._roles[role].hasRole[account] = false;
emit RoleRevoked(role, account, _msgSender());
return true;
} else {
return false;
}
}
}// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (access/extensions/AccessControlEnumerable.sol)
pragma solidity ^0.8.20;
import {IAccessControlEnumerable} from "@openzeppelin/contracts-v5.2/access/extensions/IAccessControlEnumerable.sol";
import {AccessControlUpgradeable} from "../AccessControlUpgradeable.sol";
import {EnumerableSet} from "@openzeppelin/contracts-v5.2/utils/structs/EnumerableSet.sol";
import {Initializable} from "../../proxy/utils/Initializable.sol";
/**
* @dev Extension of {AccessControl} that allows enumerating the members of each role.
*/
abstract contract AccessControlEnumerableUpgradeable is Initializable, IAccessControlEnumerable, AccessControlUpgradeable {
using EnumerableSet for EnumerableSet.AddressSet;
/// @custom:storage-location erc7201:openzeppelin.storage.AccessControlEnumerable
struct AccessControlEnumerableStorage {
mapping(bytes32 role => EnumerableSet.AddressSet) _roleMembers;
}
// keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.AccessControlEnumerable")) - 1)) & ~bytes32(uint256(0xff))
bytes32 private constant AccessControlEnumerableStorageLocation = 0xc1f6fe24621ce81ec5827caf0253cadb74709b061630e6b55e82371705932000;
function _getAccessControlEnumerableStorage() private pure returns (AccessControlEnumerableStorage storage $) {
assembly {
$.slot := AccessControlEnumerableStorageLocation
}
}
function __AccessControlEnumerable_init() internal onlyInitializing {
}
function __AccessControlEnumerable_init_unchained() internal onlyInitializing {
}
/**
* @dev See {IERC165-supportsInterface}.
*/
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
return interfaceId == type(IAccessControlEnumerable).interfaceId || super.supportsInterface(interfaceId);
}
/**
* @dev Returns one of the accounts that have `role`. `index` must be a
* value between 0 and {getRoleMemberCount}, non-inclusive.
*
* Role bearers are not sorted in any particular way, and their ordering may
* change at any point.
*
* WARNING: When using {getRoleMember} and {getRoleMemberCount}, make sure
* you perform all queries on the same block. See the following
* https://forum.openzeppelin.com/t/iterating-over-elements-on-enumerableset-in-openzeppelin-contracts/2296[forum post]
* for more information.
*/
function getRoleMember(bytes32 role, uint256 index) public view virtual returns (address) {
AccessControlEnumerableStorage storage $ = _getAccessControlEnumerableStorage();
return $._roleMembers[role].at(index);
}
/**
* @dev Returns the number of accounts that have `role`. Can be used
* together with {getRoleMember} to enumerate all bearers of a role.
*/
function getRoleMemberCount(bytes32 role) public view virtual returns (uint256) {
AccessControlEnumerableStorage storage $ = _getAccessControlEnumerableStorage();
return $._roleMembers[role].length();
}
/**
* @dev Return all accounts that have `role`
*
* WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed
* to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that
* this function has an unbounded cost, and using it as part of a state-changing function may render the function
* uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.
*/
function getRoleMembers(bytes32 role) public view virtual returns (address[] memory) {
AccessControlEnumerableStorage storage $ = _getAccessControlEnumerableStorage();
return $._roleMembers[role].values();
}
/**
* @dev Overload {AccessControl-_grantRole} to track enumerable memberships
*/
function _grantRole(bytes32 role, address account) internal virtual override returns (bool) {
AccessControlEnumerableStorage storage $ = _getAccessControlEnumerableStorage();
bool granted = super._grantRole(role, account);
if (granted) {
$._roleMembers[role].add(account);
}
return granted;
}
/**
* @dev Overload {AccessControl-_revokeRole} to track enumerable memberships
*/
function _revokeRole(bytes32 role, address account) internal virtual override returns (bool) {
AccessControlEnumerableStorage storage $ = _getAccessControlEnumerableStorage();
bool revoked = super._revokeRole(role, account);
if (revoked) {
$._roleMembers[role].remove(account);
}
return revoked;
}
}// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (proxy/utils/Initializable.sol)
pragma solidity ^0.8.20;
/**
* @dev This is a base contract to aid in writing upgradeable contracts, or any kind of contract that will be deployed
* behind a proxy. Since proxied contracts do not make use of a constructor, it's common to move constructor logic to an
* external initializer function, usually called `initialize`. It then becomes necessary to protect this initializer
* function so it can only be called once. The {initializer} modifier provided by this contract will have this effect.
*
* The initialization functions use a version number. Once a version number is used, it is consumed and cannot be
* reused. This mechanism prevents re-execution of each "step" but allows the creation of new initialization steps in
* case an upgrade adds a module that needs to be initialized.
*
* For example:
*
* [.hljs-theme-light.nopadding]
* ```solidity
* contract MyToken is ERC20Upgradeable {
* function initialize() initializer public {
* __ERC20_init("MyToken", "MTK");
* }
* }
*
* contract MyTokenV2 is MyToken, ERC20PermitUpgradeable {
* function initializeV2() reinitializer(2) public {
* __ERC20Permit_init("MyToken");
* }
* }
* ```
*
* TIP: To avoid leaving the proxy in an uninitialized state, the initializer function should be called as early as
* possible by providing the encoded function call as the `_data` argument to {ERC1967Proxy-constructor}.
*
* CAUTION: When used with inheritance, manual care must be taken to not invoke a parent initializer twice, or to ensure
* that all initializers are idempotent. This is not verified automatically as constructors are by Solidity.
*
* [CAUTION]
* ====
* Avoid leaving a contract uninitialized.
*
* An uninitialized contract can be taken over by an attacker. This applies to both a proxy and its implementation
* contract, which may impact the proxy. To prevent the implementation contract from being used, you should invoke
* the {_disableInitializers} function in the constructor to automatically lock it when it is deployed:
*
* [.hljs-theme-light.nopadding]
* ```
* /// @custom:oz-upgrades-unsafe-allow constructor
* constructor() {
* _disableInitializers();
* }
* ```
* ====
*/
abstract contract Initializable {
/**
* @dev Storage of the initializable contract.
*
* It's implemented on a custom ERC-7201 namespace to reduce the risk of storage collisions
* when using with upgradeable contracts.
*
* @custom:storage-location erc7201:openzeppelin.storage.Initializable
*/
struct InitializableStorage {
/**
* @dev Indicates that the contract has been initialized.
*/
uint64 _initialized;
/**
* @dev Indicates that the contract is in the process of being initialized.
*/
bool _initializing;
}
// keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.Initializable")) - 1)) & ~bytes32(uint256(0xff))
bytes32 private constant INITIALIZABLE_STORAGE = 0xf0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00;
/**
* @dev The contract is already initialized.
*/
error InvalidInitialization();
/**
* @dev The contract is not initializing.
*/
error NotInitializing();
/**
* @dev Triggered when the contract has been initialized or reinitialized.
*/
event Initialized(uint64 version);
/**
* @dev A modifier that defines a protected initializer function that can be invoked at most once. In its scope,
* `onlyInitializing` functions can be used to initialize parent contracts.
*
* Similar to `reinitializer(1)`, except that in the context of a constructor an `initializer` may be invoked any
* number of times. This behavior in the constructor can be useful during testing and is not expected to be used in
* production.
*
* Emits an {Initialized} event.
*/
modifier initializer() {
// solhint-disable-next-line var-name-mixedcase
InitializableStorage storage $ = _getInitializableStorage();
// Cache values to avoid duplicated sloads
bool isTopLevelCall = !$._initializing;
uint64 initialized = $._initialized;
// Allowed calls:
// - initialSetup: the contract is not in the initializing state and no previous version was
// initialized
// - construction: the contract is initialized at version 1 (no reininitialization) and the
// current contract is just being deployed
bool initialSetup = initialized == 0 && isTopLevelCall;
bool construction = initialized == 1 && address(this).code.length == 0;
if (!initialSetup && !construction) {
revert InvalidInitialization();
}
$._initialized = 1;
if (isTopLevelCall) {
$._initializing = true;
}
_;
if (isTopLevelCall) {
$._initializing = false;
emit Initialized(1);
}
}
/**
* @dev A modifier that defines a protected reinitializer function that can be invoked at most once, and only if the
* contract hasn't been initialized to a greater version before. In its scope, `onlyInitializing` functions can be
* used to initialize parent contracts.
*
* A reinitializer may be used after the original initialization step. This is essential to configure modules that
* are added through upgrades and that require initialization.
*
* When `version` is 1, this modifier is similar to `initializer`, except that functions marked with `reinitializer`
* cannot be nested. If one is invoked in the context of another, execution will revert.
*
* Note that versions can jump in increments greater than 1; this implies that if multiple reinitializers coexist in
* a contract, executing them in the right order is up to the developer or operator.
*
* WARNING: Setting the version to 2**64 - 1 will prevent any future reinitialization.
*
* Emits an {Initialized} event.
*/
modifier reinitializer(uint64 version) {
// solhint-disable-next-line var-name-mixedcase
InitializableStorage storage $ = _getInitializableStorage();
if ($._initializing || $._initialized >= version) {
revert InvalidInitialization();
}
$._initialized = version;
$._initializing = true;
_;
$._initializing = false;
emit Initialized(version);
}
/**
* @dev Modifier to protect an initialization function so that it can only be invoked by functions with the
* {initializer} and {reinitializer} modifiers, directly or indirectly.
*/
modifier onlyInitializing() {
_checkInitializing();
_;
}
/**
* @dev Reverts if the contract is not in an initializing state. See {onlyInitializing}.
*/
function _checkInitializing() internal view virtual {
if (!_isInitializing()) {
revert NotInitializing();
}
}
/**
* @dev Locks the contract, preventing any future reinitialization. This cannot be part of an initializer call.
* Calling this in the constructor of a contract will prevent that contract from being initialized or reinitialized
* to any version. It is recommended to use this to lock implementation contracts that are designed to be called
* through proxies.
*
* Emits an {Initialized} event the first time it is successfully executed.
*/
function _disableInitializers() internal virtual {
// solhint-disable-next-line var-name-mixedcase
InitializableStorage storage $ = _getInitializableStorage();
if ($._initializing) {
revert InvalidInitialization();
}
if ($._initialized != type(uint64).max) {
$._initialized = type(uint64).max;
emit Initialized(type(uint64).max);
}
}
/**
* @dev Returns the highest version that has been initialized. See {reinitializer}.
*/
function _getInitializedVersion() internal view returns (uint64) {
return _getInitializableStorage()._initialized;
}
/**
* @dev Returns `true` if the contract is currently initializing. See {onlyInitializing}.
*/
function _isInitializing() internal view returns (bool) {
return _getInitializableStorage()._initializing;
}
/**
* @dev Returns a pointer to the storage namespace.
*/
// solhint-disable-next-line var-name-mixedcase
function _getInitializableStorage() private pure returns (InitializableStorage storage $) {
assembly {
$.slot := INITIALIZABLE_STORAGE
}
}
}// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.1) (utils/Context.sol)
pragma solidity ^0.8.20;
import {Initializable} from "../proxy/utils/Initializable.sol";
/**
* @dev Provides information about the current execution context, including the
* sender of the transaction and its data. While these are generally available
* via msg.sender and msg.data, they should not be accessed in such a direct
* manner, since when dealing with meta-transactions the account sending and
* paying for execution may not be the actual sender (as far as an application
* is concerned).
*
* This contract is only required for intermediate, library-like contracts.
*/
abstract contract ContextUpgradeable is Initializable {
function __Context_init() internal onlyInitializing {
}
function __Context_init_unchained() internal onlyInitializing {
}
function _msgSender() internal view virtual returns (address) {
return msg.sender;
}
function _msgData() internal view virtual returns (bytes calldata) {
return msg.data;
}
function _contextSuffixLength() internal view virtual returns (uint256) {
return 0;
}
}// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (utils/introspection/ERC165.sol)
pragma solidity ^0.8.20;
import {IERC165} from "@openzeppelin/contracts-v5.2/utils/introspection/IERC165.sol";
import {Initializable} from "../../proxy/utils/Initializable.sol";
/**
* @dev Implementation of the {IERC165} interface.
*
* Contracts that want to implement ERC-165 should inherit from this contract and override {supportsInterface} to check
* for the additional interface id that will be supported. For example:
*
* ```solidity
* function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
* return interfaceId == type(MyInterface).interfaceId || super.supportsInterface(interfaceId);
* }
* ```
*/
abstract contract ERC165Upgradeable is Initializable, IERC165 {
function __ERC165_init() internal onlyInitializing {
}
function __ERC165_init_unchained() internal onlyInitializing {
}
/**
* @dev See {IERC165-supportsInterface}.
*/
function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) {
return interfaceId == type(IERC165).interfaceId;
}
}{
"optimizer": {
"enabled": true,
"runs": 200
},
"viaIR": true,
"evmVersion": "cancun",
"outputSelection": {
"*": {
"*": [
"evm.bytecode",
"evm.deployedBytecode",
"devdoc",
"userdoc",
"metadata",
"abi"
]
}
}
}Contract ABI
API[{"inputs":[{"internalType":"address","name":"_stETH","type":"address"},{"internalType":"address","name":"_wstETH","type":"address"},{"internalType":"address","name":"_vaultHub","type":"address"},{"internalType":"address","name":"_lidoLocator","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"AbnormallyHighFee","type":"error"},{"inputs":[],"name":"AccessControlBadConfirmation","type":"error"},{"inputs":[{"internalType":"address","name":"account","type":"address"},{"internalType":"bytes32","name":"neededRole","type":"bytes32"}],"name":"AccessControlUnauthorizedAccount","type":"error"},{"inputs":[],"name":"AlreadyInitialized","type":"error"},{"inputs":[],"name":"ConfirmExpiryOutOfBounds","type":"error"},{"inputs":[],"name":"ConnectedToVaultHub","type":"error"},{"inputs":[],"name":"CorrectionAfterReport","type":"error"},{"inputs":[],"name":"DashboardNotAllowed","type":"error"},{"inputs":[{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"EthTransferFailed","type":"error"},{"inputs":[{"internalType":"uint256","name":"requestedShares","type":"uint256"},{"internalType":"uint256","name":"remainingShares","type":"uint256"}],"name":"ExceedsMintingCapacity","type":"error"},{"inputs":[{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"uint256","name":"withdrawableValue","type":"uint256"}],"name":"ExceedsWithdrawable","type":"error"},{"inputs":[],"name":"FeeValueExceed100Percent","type":"error"},{"inputs":[],"name":"ForbiddenByPDGPolicy","type":"error"},{"inputs":[],"name":"PDGPolicyAlreadyActive","type":"error"},{"inputs":[],"name":"ReportStale","type":"error"},{"inputs":[{"internalType":"uint8","name":"bits","type":"uint8"},{"internalType":"int256","name":"value","type":"int256"}],"name":"SafeCastOverflowedIntDowncast","type":"error"},{"inputs":[{"internalType":"address","name":"token","type":"address"}],"name":"SafeERC20FailedOperation","type":"error"},{"inputs":[],"name":"SameRecipient","type":"error"},{"inputs":[],"name":"SameSettledGrowth","type":"error"},{"inputs":[],"name":"SenderNotMember","type":"error"},{"inputs":[],"name":"SettledGrowthMismatch","type":"error"},{"inputs":[],"name":"TierChangeNotConfirmed","type":"error"},{"inputs":[],"name":"UnexpectedFeeExemptionAmount","type":"error"},{"inputs":[],"name":"UnexpectedSettledGrowth","type":"error"},{"inputs":[],"name":"VaultQuarantined","type":"error"},{"inputs":[],"name":"ZeroAddress","type":"error"},{"inputs":[],"name":"ZeroArgument","type":"error"},{"inputs":[],"name":"ZeroConfirmingRoles","type":"error"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":true,"internalType":"address","name":"assetAddress","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"AssetsRecovered","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"sender","type":"address"},{"indexed":false,"internalType":"uint256","name":"oldConfirmExpiry","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"newConfirmExpiry","type":"uint256"}],"name":"ConfirmExpirySet","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"timestamp","type":"uint256"}],"name":"CorrectionTimestampUpdated","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"sender","type":"address"},{"indexed":false,"internalType":"uint256","name":"fee","type":"uint256"}],"name":"FeeDisbursed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"sender","type":"address"},{"indexed":false,"internalType":"uint256","name":"oldFeeRate","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"newFeeRate","type":"uint256"}],"name":"FeeRateSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"sender","type":"address"},{"indexed":false,"internalType":"address","name":"oldFeeRecipient","type":"address"},{"indexed":false,"internalType":"address","name":"newFeeRecipient","type":"address"}],"name":"FeeRecipientSet","type":"event"},{"anonymous":false,"inputs":[],"name":"Initialized","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"enum Dashboard.PDGPolicy","name":"pdgPolicy","type":"uint8"}],"name":"PDGPolicyEnacted","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"role","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"previousAdminRole","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"newAdminRole","type":"bytes32"}],"name":"RoleAdminChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"role","type":"bytes32"},{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":true,"internalType":"address","name":"sender","type":"address"}],"name":"RoleGranted","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"member","type":"address"},{"indexed":true,"internalType":"bytes32","name":"roleOrAddress","type":"bytes32"},{"indexed":false,"internalType":"uint256","name":"confirmTimestamp","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"expiryTimestamp","type":"uint256"},{"indexed":false,"internalType":"bytes","name":"data","type":"bytes"}],"name":"RoleMemberConfirmed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"role","type":"bytes32"},{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":true,"internalType":"address","name":"sender","type":"address"}],"name":"RoleRevoked","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"int128","name":"oldSettledGrowth","type":"int128"},{"indexed":false,"internalType":"int128","name":"newSettledGrowth","type":"int128"}],"name":"SettledGrowthSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"stakingVault","type":"address"},{"indexed":false,"internalType":"uint256","name":"deposits","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"totalAmount","type":"uint256"}],"name":"UnguaranteedDeposits","type":"event"},{"inputs":[],"name":"BURN_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"COLLECT_VAULT_ERC20_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"DEFAULT_ADMIN_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"FUND_ON_RECEIVE_FLAG_SLOT","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"FUND_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"LIDO_LOCATOR","outputs":[{"internalType":"contract ILidoLocator","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MAX_CONFIRM_EXPIRY","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MINT_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MIN_CONFIRM_EXPIRY","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"NODE_OPERATOR_FEE_EXEMPT_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"NODE_OPERATOR_MANAGER_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"NODE_OPERATOR_PROVE_UNKNOWN_VALIDATOR_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"NODE_OPERATOR_UNGUARANTEED_DEPOSIT_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"PAUSE_BEACON_CHAIN_DEPOSITS_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"REBALANCE_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"REQUEST_VALIDATOR_EXIT_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"RESUME_BEACON_CHAIN_DEPOSITS_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"STETH","outputs":[{"internalType":"contract ILido","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"TRIGGER_VALIDATOR_WITHDRAWAL_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"VAULT_CONFIGURATION_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"VAULT_HUB","outputs":[{"internalType":"contract VaultHub","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"VOLUNTARY_DISCONNECT_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"WITHDRAW_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"WSTETH","outputs":[{"internalType":"contract IWstETH","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_newOwner","type":"address"}],"name":"abandonDashboard","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"accruedFee","outputs":[{"internalType":"uint256","name":"fee","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_exemptedAmount","type":"uint256"}],"name":"addFeeExemption","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_amountOfShares","type":"uint256"}],"name":"burnShares","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_amountOfStETH","type":"uint256"}],"name":"burnStETH","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_amountOfWstETH","type":"uint256"}],"name":"burnWstETH","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_tierId","type":"uint256"},{"internalType":"uint256","name":"_requestedShareLimit","type":"uint256"}],"name":"changeTier","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"},{"internalType":"address","name":"_recipient","type":"address"},{"internalType":"uint256","name":"_amount","type":"uint256"}],"name":"collectERC20FromVault","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes","name":"_callData","type":"bytes"},{"internalType":"bytes32","name":"_role","type":"bytes32"}],"name":"confirmation","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"confirmingRoles","outputs":[{"internalType":"bytes32[]","name":"roles","type":"bytes32[]"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"_tierId","type":"uint256"},{"internalType":"uint256","name":"_requestedShareLimit","type":"uint256"},{"internalType":"uint256","name":"_currentSettledGrowth","type":"uint256"}],"name":"connectAndAcceptTier","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_currentSettledGrowth","type":"uint256"}],"name":"connectToVaultHub","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"int256","name":"_newSettledGrowth","type":"int256"},{"internalType":"int256","name":"_expectedSettledGrowth","type":"int256"}],"name":"correctSettledGrowth","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"disburseAbnormallyHighFee","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"disburseFee","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"feeRate","outputs":[{"internalType":"uint16","name":"","type":"uint16"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"feeRecipient","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"fund","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"getConfirmExpiry","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"}],"name":"getRoleAdmin","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"uint256","name":"index","type":"uint256"}],"name":"getRoleMember","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"}],"name":"getRoleMemberCount","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"}],"name":"getRoleMembers","outputs":[{"internalType":"address[]","name":"","type":"address[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"grantRole","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"account","type":"address"},{"internalType":"bytes32","name":"role","type":"bytes32"}],"internalType":"struct Permissions.RoleAssignment[]","name":"_assignments","type":"tuple[]"}],"name":"grantRoles","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"hasRole","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"healthShortfallShares","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_defaultAdmin","type":"address"},{"internalType":"address","name":"_nodeOperatorManager","type":"address"},{"internalType":"address","name":"_nodeOperatorFeeRecipient","type":"address"},{"internalType":"uint256","name":"_nodeOperatorFeeBP","type":"uint256"},{"internalType":"uint256","name":"_confirmExpiry","type":"uint256"}],"name":"initialize","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"initialized","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"latestCorrectionTimestamp","outputs":[{"internalType":"uint64","name":"","type":"uint64"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"latestReport","outputs":[{"components":[{"internalType":"uint104","name":"totalValue","type":"uint104"},{"internalType":"int104","name":"inOutDelta","type":"int104"},{"internalType":"uint48","name":"timestamp","type":"uint48"}],"internalType":"struct VaultHub.Report","name":"","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"liabilityShares","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"locked","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"maxLockableValue","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"minimalReserve","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_recipient","type":"address"},{"internalType":"uint256","name":"_amountOfShares","type":"uint256"}],"name":"mintShares","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"_recipient","type":"address"},{"internalType":"uint256","name":"_amountOfStETH","type":"uint256"}],"name":"mintStETH","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"_recipient","type":"address"},{"internalType":"uint256","name":"_amountOfWstETH","type":"uint256"}],"name":"mintWstETH","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"obligations","outputs":[{"internalType":"uint256","name":"sharesToBurn","type":"uint256"},{"internalType":"uint256","name":"feesToSettle","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"obligationsShortfallValue","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"pauseBeaconChainDeposits","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"pdgPolicy","outputs":[{"internalType":"enum Dashboard.PDGPolicy","name":"","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[{"components":[{"internalType":"bytes32[]","name":"proof","type":"bytes32[]"},{"internalType":"bytes","name":"pubkey","type":"bytes"},{"internalType":"uint256","name":"validatorIndex","type":"uint256"},{"internalType":"uint64","name":"childBlockTimestamp","type":"uint64"},{"internalType":"uint64","name":"slot","type":"uint64"},{"internalType":"uint64","name":"proposerIndex","type":"uint64"}],"internalType":"struct IPredepositGuarantee.ValidatorWitness[]","name":"_witnesses","type":"tuple[]"}],"name":"proveUnknownValidatorsToPDG","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_ether","type":"uint256"}],"name":"rebalanceVaultWithEther","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_shares","type":"uint256"}],"name":"rebalanceVaultWithShares","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_currentSettledGrowth","type":"uint256"}],"name":"reconnectToVaultHub","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"},{"internalType":"address","name":"_recipient","type":"address"},{"internalType":"uint256","name":"_amount","type":"uint256"}],"name":"recoverERC20","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_etherToFund","type":"uint256"}],"name":"remainingMintingCapacityShares","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"callerConfirmation","type":"address"}],"name":"renounceRole","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes","name":"_pubkeys","type":"bytes"}],"name":"requestValidatorExit","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"resumeBeaconChainDeposits","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"revokeRole","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"account","type":"address"},{"internalType":"bytes32","name":"role","type":"bytes32"}],"internalType":"struct Permissions.RoleAssignment[]","name":"_assignments","type":"tuple[]"}],"name":"revokeRoles","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_newConfirmExpiry","type":"uint256"}],"name":"setConfirmExpiry","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_newFeeRate","type":"uint256"}],"name":"setFeeRate","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_newFeeRecipient","type":"address"}],"name":"setFeeRecipient","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"enum Dashboard.PDGPolicy","name":"_pdgPolicy","type":"uint8"}],"name":"setPDGPolicy","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"settledGrowth","outputs":[{"internalType":"int128","name":"","type":"int128"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"stakingVault","outputs":[{"internalType":"contract IStakingVault","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes4","name":"interfaceId","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"syncTier","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"totalMintingCapacityShares","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalValue","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_newOwner","type":"address"}],"name":"transferVaultOwnership","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes","name":"_pubkeys","type":"bytes"},{"internalType":"uint64[]","name":"_amountsInGwei","type":"uint64[]"},{"internalType":"address","name":"_refundRecipient","type":"address"}],"name":"triggerValidatorWithdrawals","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"components":[{"internalType":"bytes","name":"pubkey","type":"bytes"},{"internalType":"bytes","name":"signature","type":"bytes"},{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"bytes32","name":"depositDataRoot","type":"bytes32"}],"internalType":"struct IStakingVault.Deposit[]","name":"_deposits","type":"tuple[]"}],"name":"unguaranteedDepositToBeaconChain","outputs":[{"internalType":"uint256","name":"totalAmount","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_requestedShareLimit","type":"uint256"}],"name":"updateShareLimit","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"vaultConnection","outputs":[{"components":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"uint96","name":"shareLimit","type":"uint96"},{"internalType":"uint96","name":"vaultIndex","type":"uint96"},{"internalType":"uint48","name":"disconnectInitiatedTs","type":"uint48"},{"internalType":"uint16","name":"reserveRatioBP","type":"uint16"},{"internalType":"uint16","name":"forcedRebalanceThresholdBP","type":"uint16"},{"internalType":"uint16","name":"infraFeeBP","type":"uint16"},{"internalType":"uint16","name":"liquidityFeeBP","type":"uint16"},{"internalType":"uint16","name":"reservationFeeBP","type":"uint16"},{"internalType":"bool","name":"beaconChainDepositsPauseIntent","type":"bool"}],"internalType":"struct VaultHub.VaultConnection","name":"","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"voluntaryDisconnect","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_recipient","type":"address"},{"internalType":"uint256","name":"_ether","type":"uint256"}],"name":"withdraw","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"withdrawableValue","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"stateMutability":"payable","type":"receive"}]Loading...
Loading
Loading...
Loading
Loading...
Loading
[ Download: CSV Export ]
[ Download: CSV Export ]
A contract address hosts a smart contract, which is a set of code stored on the blockchain that runs when predetermined conditions are met. Learn more about addresses in our Knowledge Base.