Overview
ETH Balance
0 ETH
More Info
ContractCreator
Multichain Info
N/A
Latest 1 from a total of 1 transactions
| Transaction Hash |
Method
|
Block
|
From
|
To
|
Amount
|
||||
|---|---|---|---|---|---|---|---|---|---|
| Set Node Operato... | 1326611 | 47 days ago | IN | 0 ETH | 0.00011754 |
Latest 23 internal transactions
Advanced mode:
| Parent Transaction Hash | Method | Block |
From
|
To
|
Amount
|
||
|---|---|---|---|---|---|---|---|
| Vault Quarantine | 1326611 | 47 days ago | 0 ETH | ||||
| Lazy Oracle | 1326611 | 47 days ago | 0 ETH | ||||
| Latest Report Ti... | 1326611 | 47 days ago | 0 ETH | ||||
| Lazy Oracle | 1326611 | 47 days ago | 0 ETH | ||||
| Is Report Fresh | 1326611 | 47 days ago | 0 ETH | ||||
| Set Node Operato... | 1326611 | 47 days ago | 0 ETH | ||||
| Revoke Role | 1325497 | 47 days ago | 0 ETH | ||||
| Revoke Role | 1325497 | 47 days ago | 0 ETH | ||||
| DEFAULT_ADMIN_RO... | 1325497 | 47 days ago | 0 ETH | ||||
| DEFAULT_ADMIN_RO... | 1325497 | 47 days ago | 0 ETH | ||||
| Grant Role | 1325497 | 47 days ago | 0 ETH | ||||
| Grant Role | 1325497 | 47 days ago | 0 ETH | ||||
| DEFAULT_ADMIN_RO... | 1325497 | 47 days ago | 0 ETH | ||||
| DEFAULT_ADMIN_RO... | 1325497 | 47 days ago | 0 ETH | ||||
| Connect Vault | 1325497 | 47 days ago | 0 ETH | ||||
| Transfer Ownersh... | 1325497 | 47 days ago | 0 ETH | ||||
| Fund | 1325497 | 47 days ago | 1 ETH | ||||
| Connect To Vault... | 1325497 | 47 days ago | 1 ETH | ||||
| Connect To Vault... | 1325497 | 47 days ago | 1 ETH | ||||
| Approve | 1325497 | 47 days ago | 0 ETH | ||||
| Initialize | 1325497 | 47 days ago | 0 ETH | ||||
| Initialize | 1325497 | 47 days ago | 0 ETH | ||||
| 0x61004d3d | 1325497 | 47 days ago | Contract Creation | 0 ETH |
Loading...
Loading
Loading...
Loading
Minimal Proxy Contract for 0xcb3bb848252f7ca05ed7753ead0eb2bdfd2ba878
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 {IERC721} from "@openzeppelin/contracts-v5.2/token/ERC721/IERC721.sol"; import {Math256} from "contracts/common/lib/Math256.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) external returns (uint256); function unwrap(uint256) 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 { bytes32 public constant RECOVER_ASSETS_ROLE = keccak256("vaults.Dashboard.RecoverAssets"); /** * @notice The stETH token contract */ IStETH public immutable STETH; /** * @notice The wstETH token contract */ IWstETH public immutable WSTETH; /** * @notice ETH address convention per EIP-7528 */ address public constant ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; /** * @notice Slot for the fund-on-receive flag * keccak256("vaults.Dashboard.fundOnReceive") */ bytes32 public constant FUND_ON_RECEIVE_FLAG_SLOT = 0x7408b7b034fda7051615c19182918ecb91d753231cffd86f81a45d996d63e038; /** * @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 _nodeOperatorFeeBP The node operator fee in basis points * @param _confirmExpiry The confirmation expiry time in seconds */ function initialize( address _defaultAdmin, address _nodeOperatorManager, uint256 _nodeOperatorFeeBP, uint256 _confirmExpiry ) external { super._initialize(_defaultAdmin, _nodeOperatorManager, _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 stETH share limit of the vault */ function shareLimit() external view returns (uint256) { return vaultConnection().shareLimit; } /** * @notice Returns the number of stETH shares minted */ function liabilityShares() public view returns (uint256) { return VAULT_HUB.liabilityShares(address(_stakingVault())); } /** * @notice Returns the reserve ratio of the vault in basis points */ function reserveRatioBP() public view returns (uint16) { return vaultConnection().reserveRatioBP; } /** * @notice Returns the rebalance threshold of the vault in basis points. */ function forcedRebalanceThresholdBP() external view returns (uint16) { return vaultConnection().forcedRebalanceThresholdBP; } /** * @notice Returns the infra fee basis points. */ function infraFeeBP() external view returns (uint16) { return vaultConnection().infraFeeBP; } /** * @notice Returns the liquidity fee basis points. */ function liquidityFeeBP() external view returns (uint16) { return vaultConnection().liquidityFeeBP; } /** * @notice Returns the reservation fee basis points. */ function reservationFeeBP() external view returns (uint16) { return vaultConnection().reservationFeeBP; } /** * @notice Returns the total value of the vault in ether. */ function totalValue() public view returns (uint256) { return VAULT_HUB.totalValue(address(_stakingVault())); } /** * @notice Returns the overall unsettled obligations of the vault in ether * @dev includes the node operator fee */ function unsettledObligations() external view returns (uint256) { VaultHub.VaultObligations memory obligations = VAULT_HUB.vaultObligations(address(_stakingVault())); return uint256(obligations.unsettledLidoFees) + uint256(obligations.redemptions) + nodeOperatorDisbursableFee(); } /** * @notice Returns the locked amount of ether for the vault */ function locked() public view returns (uint256) { return VAULT_HUB.locked(address(_stakingVault())); } /** * @notice Returns the max total lockable amount of ether for the vault (excluding the Lido and node operator fees) */ function maxLockableValue() public view returns (uint256) { uint256 maxLockableValue_ = VAULT_HUB.maxLockableValue(address(_stakingVault())); uint256 nodeOperatorFee = nodeOperatorDisbursableFee(); return maxLockableValue_ > nodeOperatorFee ? maxLockableValue_ - nodeOperatorFee : 0; } /** * @notice Returns the overall capacity for stETH shares that can be minted by the vault */ function totalMintingCapacityShares() public view returns (uint256) { uint256 effectiveShareLimit = _operatorGrid().effectiveShareLimit(address(_stakingVault())); return Math256.min(effectiveShareLimit, _mintableShares(maxLockableValue())); } /** * @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) { uint256 effectiveShareLimit = _operatorGrid().effectiveShareLimit(address(_stakingVault())); uint256 vaultMintableSharesByRR = _mintableShares(maxLockableValue() + _etherToFund); uint256 vaultLiabilityShares = liabilityShares(); return Math256.min( effectiveShareLimit > vaultLiabilityShares ? effectiveShareLimit - vaultLiabilityShares : 0, vaultMintableSharesByRR > vaultLiabilityShares ? vaultMintableSharesByRR - vaultLiabilityShares : 0 ); } /** * @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) { // On pending disconnect, the vault does not allow any withdrawals, so need to return 0 here if (VAULT_HUB.vaultConnection(address(_stakingVault())).pendingDisconnect) return 0; uint256 withdrawable = VAULT_HUB.withdrawableValue(address(_stakingVault())); uint256 nodeOperatorFee = nodeOperatorDisbursableFee(); 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. */ function transferVaultOwnership(address _newOwner) external { _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 { disburseNodeOperatorFee(); _voluntaryDisconnect(); } /** * @notice Accepts the ownership over the StakingVault transferred from VaultHub on disconnect * and immediately transfers it to a new pending owner. This new owner will have to accept the ownership * on the StakingVault contract. * @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(); _acceptOwnership(); _transferOwnership(_newOwner); } /** * @notice Accepts the ownership over the StakingVault and connects to VaultHub. Can be called to reconnect * to the hub after voluntaryDisconnect() */ function reconnectToVaultHub() external { _acceptOwnership(); connectToVaultHub(); } /** * @notice Connects to VaultHub, transferring ownership to VaultHub. */ function connectToVaultHub() public payable { 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 */ function connectAndAcceptTier(uint256 _tierId, uint256 _requestedShareLimit) external payable { connectToVaultHub(); 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`VaultHub.ZeroArgument("_amountOfShares")` 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 `VaultHub.ZeroArgument("_amountOfShares")` 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. * !NB: this will revert with `VaultHub.ZeroArgument("_amountOfShares")` 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 StakingVault by withdrawing ether to VaultHub corresponding to shares amount provided * @param _shares amount of shares to rebalance */ function rebalanceVaultWithShares(uint256 _shares) external { _rebalanceVault(_shares); } /** * @notice Rebalances the vault by transferring ether given the shares amount * @param _ether amount of ether to rebalance */ function rebalanceVaultWithEther(uint256 _ether) external payable fundable { _rebalanceVault(_getSharesByPooledEth(_ether)); } /** * @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.depositToBeaconChain` * @param _deposits array of IStakingVault.Deposit structs containing deposit data * @return totalAmount total amount of ether deposited to beacon chain * @dev requires the caller to have the `UNGUARANTEED_BEACON_CHAIN_DEPOSIT_ROLE` * @dev can be used as PDG shortcut if the node operator is trusted to not frontrun provided deposits */ function unguaranteedDepositToBeaconChain( IStakingVault.Deposit[] calldata _deposits ) external returns (uint256 totalAmount) { 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(); _setRewardsAdjustment(rewardsAdjustment.amount + 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 caller to have the `PDG_PROVE_VALIDATOR_ROLE` */ function proveUnknownValidatorsToPDG(IPredepositGuarantee.ValidatorWitness[] calldata _witnesses) external { _proveUnknownValidatorsToPDG(_witnesses); } /** * @notice Compensates ether of disproven validator's predeposit from PDG to the recipient. * Can be called if validator which was predeposited via `PDG.predeposit` with vault funds * was frontrun by NO's with non-vault WC (effectively NO's stealing the predeposit) and then * proof of the validator's invalidity has been provided via `PDG.proveInvalidValidatorWC`. * @param _pubkey of validator that was proven invalid in PDG * @param _recipient address to receive the `PDG.PREDEPOSIT_AMOUNT` * @dev PDG will revert if _recipient is vault address, use fund() instead to return ether to vault * @dev requires the caller to have the `PDG_COMPENSATE_PREDEPOSIT_ROLE` */ function compensateDisprovenPredepositFromPDG(bytes calldata _pubkey, address _recipient) external { _compensateDisprovenPredepositFromPDG(_pubkey, _recipient); } /** * @notice Recovers ERC20 tokens or ether from the dashboard contract to sender * @param _token Address of the token to recover or 0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee for ether * @param _recipient Address of the recovery recipient */ function recoverERC20( address _token, address _recipient, uint256 _amount ) external onlyRoleMemberOrAdmin(RECOVER_ASSETS_ROLE) { _requireNotZero(_token); _requireNotZero(_recipient); _requireNotZero(_amount); if (_token == ETH) { (bool success,) = payable(_recipient).call{value: _amount}(""); if (!success) revert EthTransferFailed(_recipient, _amount); } else { SafeERC20.safeTransfer(IERC20(_token), _recipient, _amount); } emit ERC20Recovered(_recipient, _token, _amount); } /** * @notice Transfers a given token_id of an ERC721-compatible NFT (defined by the token contract address) * from the dashboard contract to sender * * @param _token an ERC721-compatible token * @param _tokenId token id to recover * @param _recipient Address of the recovery recipient */ function recoverERC721( address _token, uint256 _tokenId, address _recipient ) external onlyRoleMemberOrAdmin(RECOVER_ASSETS_ROLE) { _requireNotZero(_token); _requireNotZero(_recipient); IERC721(_token).safeTransferFrom(address(this), _recipient, _tokenId); emit ERC721Recovered(_recipient, _token, _tokenId); } /** * @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 _amounts Withdrawal amounts in wei for each validator key and must match _pubkeys length. * Set amount to 0 for a full validator exit. * For partial withdrawals, amounts will be trimmed to keep MIN_ACTIVATION_BALANCE on the validator to avoid deactivation * @param _refundRecipient Address to receive any fee refunds, if zero, refunds go to msg.sender. * @dev A withdrawal fee must be paid via msg.value. * Use `StakingVault.calculateValidatorWithdrawalFee()` to determine the required fee for the current block. */ function triggerValidatorWithdrawals( bytes calldata _pubkeys, uint64[] calldata _amounts, address _refundRecipient ) external payable { _triggerValidatorWithdrawals(_pubkeys, _amounts, _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 Whether the tier change was confirmed. */ function changeTier(uint256 _tierId, uint256 _requestedShareLimit) external returns (bool) { return _changeTier(_tierId, _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 can be minted by the vault /// @param _ether The amount of ether to consider for minting function _mintableShares(uint256 _ether) internal view returns (uint256) { uint256 mintableStETH = (_ether * (TOTAL_BASIS_POINTS - reserveRatioBP())) / TOTAL_BASIS_POINTS; return _getSharesByPooledEth(mintableStETH); } /// @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) } } // ==================== 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 ERC20 `token` or ether is recovered (i.e. transferred) * @param to The address of the recovery recipient * @param token The address of the recovered ERC20 token (0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee for ether) * @param amount The amount of the token recovered */ event ERC20Recovered(address indexed to, address indexed token, uint256 amount); /** * @notice Emitted when the ERC721-compatible `token` (NFT) recovered (i.e. transferred) * @param to The address of the recovery recipient * @param token The address of the recovered ERC721 token * @param tokenId id of token recovered */ event ERC721Recovered(address indexed to, address indexed token, uint256 tokenId); // ==================== 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 thrown when recovery of ETH fails on transfer to recipient */ error EthTransferFailed(address recipient, uint256 amount); /** * @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(); }
// 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.1.0) (token/ERC721/IERC721.sol)
pragma solidity ^0.8.20;
import {IERC165} from "../../utils/introspection/IERC165.sol";
/**
* @dev Required interface of an ERC-721 compliant contract.
*/
interface IERC721 is IERC165 {
/**
* @dev Emitted when `tokenId` token is transferred from `from` to `to`.
*/
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
/**
* @dev Emitted when `owner` enables `approved` to manage the `tokenId` token.
*/
event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
/**
* @dev Emitted when `owner` enables or disables (`approved`) `operator` to manage all of its assets.
*/
event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
/**
* @dev Returns the number of tokens in ``owner``'s account.
*/
function balanceOf(address owner) external view returns (uint256 balance);
/**
* @dev Returns the owner of the `tokenId` token.
*
* Requirements:
*
* - `tokenId` must exist.
*/
function ownerOf(uint256 tokenId) external view returns (address owner);
/**
* @dev Safely transfers `tokenId` token from `from` to `to`.
*
* Requirements:
*
* - `from` cannot be the zero address.
* - `to` cannot be the zero address.
* - `tokenId` token must exist and be owned by `from`.
* - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}.
* - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon
* a safe transfer.
*
* Emits a {Transfer} event.
*/
function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external;
/**
* @dev Safely transfers `tokenId` token from `from` to `to`, checking first that contract recipients
* are aware of the ERC-721 protocol to prevent tokens from being forever locked.
*
* Requirements:
*
* - `from` cannot be the zero address.
* - `to` cannot be the zero address.
* - `tokenId` token must exist and be owned by `from`.
* - If the caller is not `from`, it must have been allowed to move this token by either {approve} or
* {setApprovalForAll}.
* - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon
* a safe transfer.
*
* Emits a {Transfer} event.
*/
function safeTransferFrom(address from, address to, uint256 tokenId) external;
/**
* @dev Transfers `tokenId` token from `from` to `to`.
*
* WARNING: Note that the caller is responsible to confirm that the recipient is capable of receiving ERC-721
* or else they may be permanently lost. Usage of {safeTransferFrom} prevents loss, though the caller must
* understand this adds an external call which potentially creates a reentrancy vulnerability.
*
* Requirements:
*
* - `from` cannot be the zero address.
* - `to` cannot be the zero address.
* - `tokenId` token must be owned by `from`.
* - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}.
*
* Emits a {Transfer} event.
*/
function transferFrom(address from, address to, uint256 tokenId) external;
/**
* @dev Gives permission to `to` to transfer `tokenId` token to another account.
* The approval is cleared when the token is transferred.
*
* Only a single account can be approved at a time, so approving the zero address clears previous approvals.
*
* Requirements:
*
* - The caller must own the token or be an approved operator.
* - `tokenId` must exist.
*
* Emits an {Approval} event.
*/
function approve(address to, uint256 tokenId) external;
/**
* @dev Approve or remove `operator` as an operator for the caller.
* Operators can call {transferFrom} or {safeTransferFrom} for any token owned by the caller.
*
* Requirements:
*
* - The `operator` cannot be the address zero.
*
* Emits an {ApprovalForAll} event.
*/
function setApprovalForAll(address operator, bool approved) external;
/**
* @dev Returns the account approved for `tokenId` token.
*
* Requirements:
*
* - `tokenId` must exist.
*/
function getApproved(uint256 tokenId) external view returns (address operator);
/**
* @dev Returns if the `operator` is allowed to manage all of the assets of `owner`.
*
* See {setApprovalForAll}
*/
function isApprovedForAll(address owner, address operator) external view returns (bool);
}// 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/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 exectuing 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 * @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` is restricted by * the `onlyConfirmed` modifier, the confirmation expiry will be tricky to change. * This is why confirmExpiry is private, set to a default value of 1 hour 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.Vaults.storage.Confirmations")) - 1)) & ~bytes32(uint256(0xff)) */ bytes32 private constant CONFIRMATIONS_STORAGE_LOCATION = 0x1b8b5828bd311c11f60881dedc705c95b2fbc3408c25f5c1964af0a81ceb0900; /** * @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 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 role The role 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 role, 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 bytes32 public constant PAUSE_ROLE = keccak256("PausableUntilWithRoles.PauseRole"); /// @notice role that allows to resume the contract 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"; /** * @title NodeOperatorFee * @author Lido * @notice An accounting contract for a vault's node operator fee: * • Calculates the node operator's share of each reward period, * • Ignores any vault value changes that aren't true rewards, * • Permissionless on-demand fee disbursement, * • Critical parameter changes require vault-owner<>node operator approval. */ contract NodeOperatorFee is Permissions { /** * @notice Total basis points; 1bp = 0.01%, 100_00bp = 100.00%. */ uint256 internal constant TOTAL_BASIS_POINTS = 100_00; /** * @notice Maximum value that can be set via manual adjustment */ uint256 public constant MANUAL_REWARDS_ADJUSTMENT_LIMIT = 10_000_000 ether; /** * @notice Node operator manager role: * - confirms confirm expiry; * - confirms node operator fee changes; * - confirms the transfer of the StakingVault ownership; * - sets the node operator fee recipient. */ bytes32 public constant NODE_OPERATOR_MANAGER_ROLE = keccak256("vaults.NodeOperatorFee.NodeOperatorManagerRole"); /** * @notice Adjusts rewards to allow fee correction during side deposits or consolidations */ bytes32 public constant NODE_OPERATOR_REWARDS_ADJUST_ROLE = keccak256("vaults.NodeOperatorFee.RewardsAdjustRole"); /** * @notice Node operator fee in basis points; cannot exceed 100.00%. * The node operator's disbursable fee in ether is returned by `nodeOperatorDisbursableFee()`. */ uint256 public nodeOperatorFeeRate; /** * @notice The last report for which node operator fee was disbursed. Updated on each disbursement. */ VaultHub.Report public feePeriodStartReport; /** * @notice The address of the node operator fee recipient. */ address public nodeOperatorFeeRecipient; struct RewardsAdjustment { uint128 amount; uint64 latestTimestamp; } /** * @notice Adjustment to allow fee correction during side deposits or consolidations. * - can be increased manually by `increaseRewardsAdjustment` by NODE_OPERATOR_REWARDS_ADJUST_ROLE * - can be set via `setRewardsAdjustment` by `confirmingRoles()` * - increased automatically with `unguaranteedDepositToBeaconChain` by total ether amount of deposits * - reset to zero after `disburseNodeOperatorFee` * This amount will be deducted from rewards during NO fee calculation and can be used effectively write off NO's accrued fees. * */ RewardsAdjustment public rewardsAdjustment; /** * @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 _nodeOperatorFeeRate The node operator fee rate * @param _confirmExpiry The confirmation expiry time in seconds */ function _initialize( address _defaultAdmin, address _nodeOperatorManager, uint256 _nodeOperatorFeeRate, uint256 _confirmExpiry ) internal { _requireNotZero(_nodeOperatorManager); super._initialize(_defaultAdmin, _confirmExpiry); _validateNodeOperatorFeeRate(_nodeOperatorFeeRate); _setNodeOperatorFeeRate(_nodeOperatorFeeRate); _setNodeOperatorFeeRecipient(_nodeOperatorManager); _grantRole(NODE_OPERATOR_MANAGER_ROLE, _nodeOperatorManager); _setRoleAdmin(NODE_OPERATOR_MANAGER_ROLE, NODE_OPERATOR_MANAGER_ROLE); _setRoleAdmin(NODE_OPERATOR_REWARDS_ADJUST_ROLE, NODE_OPERATOR_MANAGER_ROLE); } /** * @notice Returns the roles that can: * - change the confirm expiry; * - set the node operator fee; * - set a new owner of the StakingVault. * @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 Returns the latest report data containing the total value and in-out delta. * @return report The latest report. */ function latestReport() public view returns (VaultHub.Report memory) { return VAULT_HUB.latestReport(address(_stakingVault())); } /** * @notice Calculates the node operator's disbursable fee. * * The fee presently owed to the node-operator, * computed as a portion of staking rewards accrued between * `feePeriodStartReport` and `latestReport()`. * * Staking rewards for an accounting interval are derived as: * rewards = Δ(totalValue) − Δ(inOutDelta) − rewardsAdjustment * * where * • Δ(totalValue) — change in totalValue (CL + EL balances) between reports; * • Δ(inOutDelta) — net funds/withdrawals in the same interval; * • rewardsAdjustment — rewards offset that excludes side deposits and consolidations * (e.g. CL topups that are not subject to node operator fee). * * If the rewards are negative, for the purposes of fee calculation, they are considered to be zero. * The node-operator's fee is therefore: * fee = max(0, rewards) × nodeOperatorFeeBP / TOTAL_BASIS_POINTS * * @return fee The node operator's disbursable fee. */ function nodeOperatorDisbursableFee() public view returns (uint256) { VaultHub.Report memory periodStart = feePeriodStartReport; VaultHub.Report memory periodEnd = latestReport(); int256 adjustment = _toSignedClamped(rewardsAdjustment.amount); // the total increase/decrease of the vault value during the fee period int256 growth = int112(periodEnd.totalValue) - int112(periodStart.totalValue) - (periodEnd.inOutDelta - periodStart.inOutDelta); // the actual rewards that are subject to the fee int256 rewards = growth - adjustment; return rewards <= 0 ? 0 : (uint256(rewards) * nodeOperatorFeeRate) / TOTAL_BASIS_POINTS; } /** * @notice Transfers the node-operator's accrued fee (if any). * Steps: * • Compute the current fee via `nodeOperatorDisbursableFee()`. * • If there are no rewards, do nothing. * • Otherwise, move `feePeriodStartReport` to `latestReport()`, * reset `rewardsAdjustment` and transfer `fee` wei to `nodeOperatorFeeRecipient`. */ function disburseNodeOperatorFee() public { uint256 fee = nodeOperatorDisbursableFee(); // it's important not to revert here if there is no fee, // because the fee is automatically disbursed during `voluntaryDisconnect` if (fee == 0) return; if (rewardsAdjustment.amount != 0) _setRewardsAdjustment(0); feePeriodStartReport = latestReport(); VAULT_HUB.withdraw(address(_stakingVault()), nodeOperatorFeeRecipient, fee); emit NodeOperatorFeeDisbursed(msg.sender, fee); } /** * @notice Updates the node-operator's fee rate (basis-points share). * @param _newNodeOperatorFeeRate The new node operator fee rate. * @return bool Whether the node operator fee rate was set. */ function setNodeOperatorFeeRate(uint256 _newNodeOperatorFeeRate) 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 adjustment must be earlier than the latest fresh report timestamp if (rewardsAdjustment.latestTimestamp >= _lazyOracle().latestReportTimestamp()) revert AdjustmentNotReported(); // Adjustment must be settled before the fee rate change if (rewardsAdjustment.amount != 0) revert AdjustmentNotSettled(); // If the vault is quarantined, the total value is reduced and may not reflect the adjustment if (_lazyOracle().vaultQuarantine(address(_stakingVault())).isActive) revert VaultQuarantined(); // Validate fee rate before collecting confirmations _validateNodeOperatorFeeRate(_newNodeOperatorFeeRate); // store the caller's confirmation; only proceed if the required number of confirmations is met. if (!_collectAndCheckConfirmations(msg.data, confirmingRoles())) return false; // To follow the check-effects-interaction pattern, we need to remember the fee here // because the fee calculation variables will be reset in the following lines uint256 fee = nodeOperatorDisbursableFee(); // Start a new fee period feePeriodStartReport = latestReport(); _setNodeOperatorFeeRate(_newNodeOperatorFeeRate); if (fee > 0) { VAULT_HUB.withdraw(address(_stakingVault()), nodeOperatorFeeRecipient, fee); emit NodeOperatorFeeDisbursed(msg.sender, fee); } return true; } /** * @notice Sets the confirm expiry. * Confirm expiry is a period during which the confirm is counted. Once the period is over, * the confirm is considered expired, no longer counts and must be recasted. * @param _newConfirmExpiry The new confirm expiry in seconds. * @return bool Whether the confirm expiry was set. */ function setConfirmExpiry(uint256 _newConfirmExpiry) external returns (bool) { _validateConfirmExpiry(_newConfirmExpiry); if (!_collectAndCheckConfirmations(msg.data, confirmingRoles())) return false; _setConfirmExpiry(_newConfirmExpiry); return true; } /** * @notice Sets the node operator fee recipient. * @param _newNodeOperatorFeeRecipient The address of the new node operator fee recipient. */ function setNodeOperatorFeeRecipient( address _newNodeOperatorFeeRecipient ) external onlyRoleMemberOrAdmin(NODE_OPERATOR_MANAGER_ROLE) { _setNodeOperatorFeeRecipient(_newNodeOperatorFeeRecipient); } /** * @notice Increases rewards adjustment to correct fee calculation due to non-rewards ether on CL * @param _adjustmentIncrease amount to increase adjustment by * @dev will revert if final adjustment is more than `MANUAL_REWARDS_ADJUSTMENT_LIMIT` */ function increaseRewardsAdjustment( uint256 _adjustmentIncrease ) external onlyRoleMemberOrAdmin(NODE_OPERATOR_REWARDS_ADJUST_ROLE) { uint256 newAdjustment = rewardsAdjustment.amount + _adjustmentIncrease; // sanity check, though value will be cast safely during fee calculation if (newAdjustment > MANUAL_REWARDS_ADJUSTMENT_LIMIT) revert IncreasedOverLimit(); _setRewardsAdjustment(newAdjustment); } /** * @notice set `rewardsAdjustment` to a new proposed value if `confirmingRoles()` agree * @param _proposedAdjustment new adjustment amount * @param _expectedAdjustment current adjustment value for invalidating old confirmations * @return bool Whether the rewards adjustment was set. * @dev will revert if new adjustment is more than `MANUAL_REWARDS_ADJUSTMENT_LIMIT` */ function setRewardsAdjustment( uint256 _proposedAdjustment, uint256 _expectedAdjustment ) external returns (bool) { if (rewardsAdjustment.amount != _expectedAdjustment) revert InvalidatedAdjustmentVote(rewardsAdjustment.amount, _expectedAdjustment); if (_proposedAdjustment > MANUAL_REWARDS_ADJUSTMENT_LIMIT) revert IncreasedOverLimit(); if (!_collectAndCheckConfirmations(msg.data, confirmingRoles())) return false; _setRewardsAdjustment(_proposedAdjustment); return true; } function _setNodeOperatorFeeRate(uint256 _newNodeOperatorFeeRate) internal { _validateNodeOperatorFeeRate(_newNodeOperatorFeeRate); uint256 oldNodeOperatorFeeRate = nodeOperatorFeeRate; nodeOperatorFeeRate = _newNodeOperatorFeeRate; emit NodeOperatorFeeRateSet(msg.sender, oldNodeOperatorFeeRate, _newNodeOperatorFeeRate); } function _setNodeOperatorFeeRecipient(address _newNodeOperatorFeeRecipient) internal { _requireNotZero(_newNodeOperatorFeeRecipient); if (_newNodeOperatorFeeRecipient == nodeOperatorFeeRecipient) revert SameRecipient(); address oldNodeOperatorFeeRecipient = nodeOperatorFeeRecipient; nodeOperatorFeeRecipient = _newNodeOperatorFeeRecipient; emit NodeOperatorFeeRecipientSet(msg.sender, oldNodeOperatorFeeRecipient, _newNodeOperatorFeeRecipient); } /** * @notice sets InOut adjustment for correct fee calculation * @param _newAdjustment new adjustment value */ function _setRewardsAdjustment(uint256 _newAdjustment) internal { uint256 oldAdjustment = rewardsAdjustment.amount; if (_newAdjustment == oldAdjustment) revert SameAdjustment(); rewardsAdjustment.amount = uint128(_newAdjustment); rewardsAdjustment.latestTimestamp = uint64(block.timestamp); emit RewardsAdjustmentSet(_newAdjustment, oldAdjustment); } function _toSignedClamped(uint128 _adjustment) internal pure returns (int128) { if (_adjustment > uint128(type(int128).max)) return type(int128).max; return int128(_adjustment); } /** * @notice Validates that the node operator fee rate is within acceptable bounds * @param _nodeOperatorFeeRate The fee rate to validate */ function _validateNodeOperatorFeeRate(uint256 _nodeOperatorFeeRate) internal pure { if (_nodeOperatorFeeRate > TOTAL_BASIS_POINTS) revert FeeValueExceed100Percent(); } function _lazyOracle() internal view returns (LazyOracle) { return LazyOracle(LIDO_LOCATOR.lazyOracle()); } // ==================== Events ==================== /** * @dev Emitted when the node operator fee is set. * @param oldNodeOperatorFeeRate The old node operator fee rate. * @param newNodeOperatorFeeRate The new node operator fee rate. */ event NodeOperatorFeeRateSet(address indexed sender, uint256 oldNodeOperatorFeeRate, uint256 newNodeOperatorFeeRate); /** * @dev Emitted when the node operator fee is disbursed. * @param fee the amount of disbursed fee. */ event NodeOperatorFeeDisbursed(address indexed sender, uint256 fee); /** * @dev Emitted when the new rewards adjustment is set. * @param newAdjustment the new adjustment value * @param oldAdjustment previous adjustment value */ event RewardsAdjustmentSet(uint256 newAdjustment, uint256 oldAdjustment); /** * @dev Emitted when the node operator fee recipient is set. * @param sender the address of the sender who set the recipient * @param oldNodeOperatorFeeRecipient the old node operator fee recipient * @param newNodeOperatorFeeRecipient the new node operator fee recipient */ event NodeOperatorFeeRecipientSet(address indexed sender, address oldNodeOperatorFeeRecipient, address newNodeOperatorFeeRecipient); // ==================== Errors ==================== /** * @dev Error emitted when the combined feeBPs exceed 100%. */ error FeeValueExceed100Percent(); /** * @dev Error emitted when the increased adjustment exceeds the `MANUAL_REWARDS_ADJUSTMENT_LIMIT`. */ error IncreasedOverLimit(); /** * @dev Error emitted when the adjustment setting vote is not valid due to changed state */ error InvalidatedAdjustmentVote(uint256 currentAdjustment, uint256 currentAtPropositionAdjustment); /** * @dev Error emitted when trying to set same value for adjustment */ error SameAdjustment(); /** * @dev Error emitted when trying to set same value for recipient */ error SameRecipient(); /** * @dev Error emitted when the report is stale. */ error ReportStale(); /** * @dev Error emitted when the adjustment has not been reported yet. */ error AdjustmentNotReported(); /** * @dev Error emitted when the adjustment is not settled. */ error AdjustmentNotSettled(); /** * @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 {IPredepositGuarantee} from "../interfaces/IPredepositGuarantee.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. */ bytes32 public constant FUND_ROLE = keccak256("vaults.Permissions.Fund"); /** * @notice Permission for withdrawing funds from the StakingVault. */ bytes32 public constant WITHDRAW_ROLE = keccak256("vaults.Permissions.Withdraw"); /** * @notice Permission for minting stETH shares backed by the StakingVault. */ bytes32 public constant MINT_ROLE = keccak256("vaults.Permissions.Mint"); /** * @notice Permission for burning stETH shares backed by the StakingVault. */ bytes32 public constant BURN_ROLE = keccak256("vaults.Permissions.Burn"); /** * @notice Permission for rebalancing the StakingVault. */ bytes32 public constant REBALANCE_ROLE = keccak256("vaults.Permissions.Rebalance"); /** * @notice Permission for pausing beacon chain deposits on the StakingVault. */ bytes32 public constant PAUSE_BEACON_CHAIN_DEPOSITS_ROLE = keccak256("vaults.Permissions.PauseDeposits"); /** * @notice Permission for resuming beacon chain deposits on the StakingVault. */ bytes32 public constant RESUME_BEACON_CHAIN_DEPOSITS_ROLE = keccak256("vaults.Permissions.ResumeDeposits"); /** * @notice Permission for requesting validator exit from the StakingVault. */ 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. */ bytes32 public constant TRIGGER_VALIDATOR_WITHDRAWAL_ROLE = keccak256("vaults.Permissions.TriggerValidatorWithdrawal"); /** * @notice Permission for voluntary disconnecting the StakingVault. */ bytes32 public constant VOLUNTARY_DISCONNECT_ROLE = keccak256("vaults.Permissions.VoluntaryDisconnect"); /** * @notice Permission for getting compensation for disproven validator predeposit from PDG */ bytes32 public constant PDG_COMPENSATE_PREDEPOSIT_ROLE = keccak256("vaults.Permissions.PDGCompensatePredeposit"); /** * @notice Permission for proving valid vault validators unknown to the PDG */ bytes32 public constant PDG_PROVE_VALIDATOR_ROLE = keccak256("vaults.Permissions.PDGProveValidator"); /** * @notice Permission for unguaranteed deposit to trusted validators */ bytes32 public constant UNGUARANTEED_BEACON_CHAIN_DEPOSIT_ROLE = keccak256("vaults.Permissions.UnguaranteedBeaconChainDeposit"); /** * @dev Permission for requesting change of tier on the OperatorGrid. */ bytes32 public constant CHANGE_TIER_ROLE = keccak256("vaults.Permissions.ChangeTier"); /** * @notice Address of the implementation contract * @dev Used to prevent initialization in the implementation */ address private immutable _SELF; 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); _SELF = address(this); // @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(); if (address(this) == _SELF) revert NonProxyCallsForbidden(); 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 { if (_assignments.length == 0) revert ZeroArgument(); 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 call * used for the `onlyConfirmed` modifier. * @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)) { _; } else { 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. * @dev The zero checks for recipient and ether are performed in the StakingVault contract. */ 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. * @dev The zero checks for parameters are performed in the VaultHub contract. */ 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. * @dev The zero check for parameters is performed in the VaultHub contract. */ 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. * @dev The zero check for parameters is performed in the StakingVault contract. */ 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. * @dev The zero check for _pubkeys is performed in the StakingVault contract. */ 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. * @dev The zero checks for parameters are performed in the StakingVault contract. */ function _triggerValidatorWithdrawals( bytes calldata _pubkeys, uint64[] calldata _amounts, address _refundRecipient ) internal onlyRoleMemberOrAdmin(TRIGGER_VALIDATOR_WITHDRAWAL_ROLE) { VAULT_HUB.triggerValidatorWithdrawals{value: msg.value}(address(_stakingVault()), _pubkeys, _amounts, _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 PDG_COMPENSATE_PREDEPOSIT_ROLE and claims disproven predeposit from PDG. * @param _pubkey The pubkey of the validator. * @param _recipient The address to compensate the disproven validator predeposit to. * @return The amount of ether compensated. */ function _compensateDisprovenPredepositFromPDG( bytes calldata _pubkey, address _recipient ) internal onlyRoleMemberOrAdmin(PDG_COMPENSATE_PREDEPOSIT_ROLE) returns (uint256) { return VAULT_HUB.compensateDisprovenPredepositFromPDG(address(_stakingVault()), _pubkey, _recipient); } /** * @dev Proves validators unknown to PDG that have correct vault WC */ function _proveUnknownValidatorsToPDG( IPredepositGuarantee.ValidatorWitness[] calldata _witnesses ) internal onlyRoleMemberOrAdmin(PDG_PROVE_VALIDATOR_ROLE) { for (uint256 i = 0; i < _witnesses.length; i++) { VAULT_HUB.proveUnknownValidatorToPDG(address(_stakingVault()), _witnesses[i]); } } /** * @dev Withdraws ether from vault to this contract for unguaranteed deposit to validators */ function _withdrawForUnguaranteedDepositToBeaconChain( uint256 _ether ) internal onlyRoleMemberOrAdmin(UNGUARANTEED_BEACON_CHAIN_DEPOSIT_ROLE) { VAULT_HUB.withdraw(address(_stakingVault()), address(this), _ether); } /** * @dev Checks the confirming roles and sets the owner on the StakingVault. * @param _newOwner The address to set the owner to. */ function _transferVaultOwnership(address _newOwner) internal { if (!_collectAndCheckConfirmations(msg.data, confirmingRoles())) return; VAULT_HUB.transferVaultOwnership(address(_stakingVault()), _newOwner); } /** * @dev Checks the CHANGE_TIER_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 confirmed. */ function _changeTier(uint256 _tierId, uint256 _requestedShareLimit) internal onlyRoleMemberOrAdmin(CHANGE_TIER_ROLE) returns (bool) { return _operatorGrid().changeTier(address(_stakingVault()), _tierId, _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 direct calls to the implementation are forbidden */ error NonProxyCallsForbidden(); /** * @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; import {IStakingVault} from "./IStakingVault.sol"; /** * @title IPredepositGuarantee * @author Lido * @notice Interface for the `PredepositGuarantee` contract */ interface IPredepositGuarantee { /** * @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 compensateDisprovenPredeposit( bytes calldata _validatorPubkey, address _recipient ) external returns (uint256 compensatedEther); 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 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 isOssified() external view returns (bool); 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 _amounts, address _refundRecipient) external payable; function ejectValidators(bytes calldata _pubkeys, address _refundRecipient) external payable; function setDepositor(address _depositor) external; function ossify() external; }
// 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 {Math256} from "contracts/common/lib/Math256.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 {IHashConsensus} from "contracts/common/interfaces/IHashConsensus.sol"; import {VaultHub} from "./VaultHub.sol"; import {OperatorGrid} from "./OperatorGrid.sol"; import {IStakingVault} from "./interfaces/IStakingVault.sol"; contract LazyOracle is ILazyOracle, AccessControlEnumerableUpgradeable { /// @custom:storage-location erc7201: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 total value increase quarantine period uint64 quarantinePeriod; /// @notice max reward ratio for refSlot-observed total value, basis points uint16 maxRewardRatioBP; /// @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; uint96 vaultIndex; uint256 balance; bytes32 withdrawalCredentials; uint256 liabilityShares; uint256 mintableStETH; uint96 shareLimit; uint16 reserveRatioBP; uint16 forcedRebalanceThresholdBP; uint16 infraFeeBP; uint16 liquidityFeeBP; uint16 reservationFeeBP; bool pendingDisconnect; } // keccak256(abi.encode(uint256(keccak256("LazyOracle")) - 1)) & ~bytes32(uint256(0xff)) bytes32 private constant LAZY_ORACLE_STORAGE_LOCATION = 0xe5459f2b48ec5df2407caac4ec464a5cb0f7f31a1f22f649728a9579b25c1d00; bytes32 public constant UPDATE_SANITY_PARAMS_ROLE = keccak256("UPDATE_SANITY_PARAMS_ROLE"); 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; 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 function initialize(address _admin, uint64 _quarantinePeriod, uint16 _maxRewardRatioBP) external initializer { if (_admin == address(0)) revert AdminCannotBeZero(); _grantRole(DEFAULT_ADMIN_ROLE, _admin); _updateSanityParams(_quarantinePeriod, _maxRewardRatioBP); } /// @notice returns the latest report data /// @return timestamp of the report /// @return treeRoot merkle root of the report /// @return reportCid IPFS CID for the report JSON file function latestReportData() external view returns (uint64 timestamp, bytes32 treeRoot, string memory reportCid) { Storage storage $ = _storage(); return ($.vaultsDataTimestamp, $.vaultsDataTreeRoot, $.vaultsDataReportCid); } /// @notice returns the latest report timestamp function latestReportTimestamp() external view returns (uint64) { return _storage().vaultsDataTimestamp; } /// @notice returns the quarantine period function quarantinePeriod() external view returns (uint64) { return _storage().quarantinePeriod; } /// @notice returns the max reward ratio for refSlot total value, basis points function maxRewardRatioBP() external view returns (uint16) { return _storage().maxRewardRatioBP; } /// @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 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); IStakingVault vault = IStakingVault(vaultAddress); VaultHub.VaultConnection memory connection = vaultHub.vaultConnection(vaultAddress); VaultHub.VaultRecord memory record = vaultHub.vaultRecord(vaultAddress); batch[i] = VaultInfo( vaultAddress, connection.vaultIndex, address(vault).balance, vault.withdrawalCredentials(), record.liabilityShares, _mintableStETH(vaultAddress), connection.shareLimit, connection.reserveRatioBP, connection.forcedRebalanceThresholdBP, connection.infraFeeBP, connection.liquidityFeeBP, connection.reservationFeeBP, connection.pendingDisconnect ); } return batch; } /// @notice update the sanity parameters /// @param _quarantinePeriod the quarantine period /// @param _maxRewardRatioBP the max EL CL rewards function updateSanityParams( uint64 _quarantinePeriod, uint16 _maxRewardRatioBP ) external onlyRole(UPDATE_SANITY_PARAMS_ROLE) { _updateSanityParams(_quarantinePeriod, _maxRewardRatioBP); } /// @notice Store the report root and its meta information /// @param _vaultsDataTimestamp the timestamp of the report /// @param _vaultsDataTreeRoot the root of the report /// @param _vaultsDataReportCid the CID of the report function updateReportData( uint256 _vaultsDataTimestamp, bytes32 _vaultsDataTreeRoot, string memory _vaultsDataReportCid ) external override(ILazyOracle) { if (msg.sender != LIDO_LOCATOR.accountingOracle()) revert NotAuthorized(); Storage storage $ = _storage(); $.vaultsDataTimestamp = uint64(_vaultsDataTimestamp); $.vaultsDataTreeRoot = _vaultsDataTreeRoot; $.vaultsDataReportCid = _vaultsDataReportCid; emit VaultsReportDataUpdated(_vaultsDataTimestamp, _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 of the vault /// @param _proof the proof of the reported data function updateVaultData( address _vault, uint256 _totalValue, uint256 _cumulativeLidoFees, uint256 _liabilityShares, uint256 _slashingReserve, bytes32[] calldata _proof ) external { bytes32 leaf = keccak256( bytes.concat( keccak256( abi.encode( _vault, _totalValue, _cumulativeLidoFees, _liabilityShares, _slashingReserve ) ) ) ); if (!MerkleProof.verify(_proof, _storage().vaultsDataTreeRoot, leaf)) revert InvalidProof(); int256 inOutDelta; (_totalValue, inOutDelta) = _handleSanityChecks(_vault, _totalValue); _vaultHub().applyVaultReport( _vault, _storage().vaultsDataTimestamp, _totalValue, inOutDelta, _cumulativeLidoFees, _liabilityShares, _slashingReserve ); } /// @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 /// @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 ) public returns (uint256 totalValueWithoutQuarantine, int256 inOutDeltaOnRefSlot) { VaultHub vaultHub = _vaultHub(); VaultHub.VaultRecord memory record = vaultHub.vaultRecord(_vault); // 1. Calculate inOutDelta in the refSlot int256 currentInOutDelta = record.inOutDelta.value; inOutDeltaOnRefSlot = vaultHub.inOutDeltaAsOfLastRefSlot(_vault); // 2. Sanity check for total value increase totalValueWithoutQuarantine = _processTotalValue(_vault, _totalValue, inOutDeltaOnRefSlot, record); // 3. Sanity check for dynamic total value underflow if (int256(totalValueWithoutQuarantine) + currentInOutDelta - inOutDeltaOnRefSlot < 0) { revert UnderflowInTotalValueCalculation(); } } function _processTotalValue( address _vault, uint256 _reportedTotalValue, int256 _inOutDeltaOnRefSlot, VaultHub.VaultRecord memory record ) internal returns (uint256 totalValueWithoutQuarantine) { if (_reportedTotalValue > MAX_SANE_TOTAL_VALUE) { revert TotalValueTooLarge(); } Storage storage $ = _storage(); // total value from the previous report with inOutDelta correction till the current 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); // some percentage of funds hasn't passed through the vault's balance is allowed for the EL and CL rewards handling uint256 maxSaneTotalValue = onchainTotalValueOnRefSlot * (TOTAL_BASIS_POINTS + $.maxRewardRatioBP) / TOTAL_BASIS_POINTS; if (_reportedTotalValue > maxSaneTotalValue) { Quarantine storage q = $.vaultQuarantines[_vault]; uint64 reportTs = $.vaultsDataTimestamp; uint128 quarDelta = q.pendingTotalValueIncrease; uint128 delta = uint128(_reportedTotalValue - onchainTotalValueOnRefSlot); if (quarDelta == 0) { // first overlimit report _reportedTotalValue = onchainTotalValueOnRefSlot; q.pendingTotalValueIncrease = delta; q.startTimestamp = reportTs; emit QuarantinedDeposit(_vault, delta); } else if (reportTs - q.startTimestamp < $.quarantinePeriod) { // quarantine not expired _reportedTotalValue = onchainTotalValueOnRefSlot; } else if (delta <= quarDelta + onchainTotalValueOnRefSlot * $.maxRewardRatioBP / TOTAL_BASIS_POINTS) { // quarantine expired q.pendingTotalValueIncrease = 0; emit QuarantineExpired(_vault, delta); } else { // start new quarantine _reportedTotalValue = onchainTotalValueOnRefSlot + quarDelta; q.pendingTotalValueIncrease = delta - quarDelta; q.startTimestamp = reportTs; emit QuarantinedDeposit(_vault, delta - quarDelta); } } return _reportedTotalValue; } function _updateSanityParams(uint64 _quarantinePeriod, uint16 _maxRewardRatioBP) internal { Storage storage $ = _storage(); $.quarantinePeriod = _quarantinePeriod; $.maxRewardRatioBP = _maxRewardRatioBP; emit SanityParamsUpdated(_quarantinePeriod, _maxRewardRatioBP); } function _mintableStETH(address _vault) internal view returns (uint256) { VaultHub vaultHub = _vaultHub(); uint256 maxLockableValue = vaultHub.maxLockableValue(_vault); uint256 reserveRatioBP = vaultHub.vaultConnection(_vault).reserveRatioBP; uint256 mintableStETHByRR = maxLockableValue * (TOTAL_BASIS_POINTS - reserveRatioBP) / TOTAL_BASIS_POINTS; uint256 effectiveShareLimit = _operatorGrid().effectiveShareLimit(_vault); uint256 mintableStEthByShareLimit = ILido(LIDO_LOCATOR.lido()).getPooledEthBySharesRoundUp(effectiveShareLimit); return Math256.min(mintableStETHByRR, mintableStEthByShareLimit); } function _storage() internal pure returns (Storage storage $) { assembly { $.slot := LAZY_ORACLE_STORAGE_LOCATION } } function _vaultHub() internal view returns (VaultHub) { return VaultHub(payable(LIDO_LOCATOR.vaultHub())); } function _operatorGrid() internal view returns (OperatorGrid) { return OperatorGrid(LIDO_LOCATOR.operatorGrid()); } event VaultsReportDataUpdated(uint256 indexed timestamp, bytes32 indexed root, string cid); event QuarantinedDeposit(address indexed vault, uint128 delta); event SanityParamsUpdated(uint64 quarantinePeriod, uint16 maxRewardRatioBP); event QuarantineExpired(address indexed vault, uint128 delta); error AdminCannotBeZero(); error NotAuthorized(); error InvalidProof(); error UnderflowInTotalValueCalculation(); error TotalValueTooLarge(); }
// SPDX-FileCopyrightText: 2025 Lido <[email protected]> // SPDX-License-Identifier: GPL-3.0 // See contracts/COMPILERS.md pragma solidity 0.8.25; import {IHashConsensus} from "contracts/common/interfaces/IHashConsensus.sol"; library RefSlotCache { struct Int112WithRefSlotCache { int112 value; int112 valueOnRefSlot; uint32 refSlot; } struct Uint112WithRefSlotCache { uint112 value; uint112 valueOnRefSlot; uint32 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( Uint112WithRefSlotCache storage _storage, IHashConsensus _consensus, uint112 _increment ) internal view returns (Uint112WithRefSlotCache memory) { (uint256 refSlot, ) = _consensus.getCurrentFrame(); Uint112WithRefSlotCache memory newStorage = _storage; if (newStorage.refSlot != uint32(refSlot)) { // 32 bits is enough precision for this kind of comparison newStorage.valueOnRefSlot = _storage.value; newStorage.refSlot = uint32(refSlot); } newStorage.value += _increment; return newStorage; } /// @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( Int112WithRefSlotCache storage _storage, IHashConsensus _consensus, int112 _increment ) internal view returns (Int112WithRefSlotCache memory) { (uint256 refSlot, ) = _consensus.getCurrentFrame(); Int112WithRefSlotCache memory newStorage = _storage; if (newStorage.refSlot != uint32(refSlot)) { // 32 bits is enough precision for this kind of comparison newStorage.valueOnRefSlot = _storage.value; newStorage.refSlot = uint32(refSlot); } newStorage.value += _increment; return newStorage; } /// @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( Uint112WithRefSlotCache storage _storage, IHashConsensus _consensus ) internal view returns (uint112) { (uint256 refSlot, ) = _consensus.getCurrentFrame(); if (uint32(refSlot) > _storage.refSlot) { return _storage.value; } else { return _storage.valueOnRefSlot; } } /// @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( Int112WithRefSlotCache storage _storage, IHashConsensus _consensus ) internal view returns (int112) { (uint256 refSlot, ) = _consensus.getCurrentFrame(); if (uint32(refSlot) > _storage.refSlot) { return _storage.value; } else { return _storage.valueOnRefSlot; } } }
// 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 {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 ┌──────────────────────────────────────────────────────┐ │ Group 1 = operator 1 │ │ ┌────────────────────────────────────────────────┐ │ │ │ groupShareLimit = 1kk │ │ │ └────────────────────────────────────────────────┘ │ │ ┌──────────────────────┐ ┌──────────────────────┐ │ │ │ Tier 1 │ │ Tier 2 │ │ │ │ tierShareLimit = x │ │ tierShareLimit = y │ │ │ │ Vault_2 ... Vault_k │ │ │ │ │ └──────────────────────┘ └──────────────────────┘ │ └──────────────────────────────────────────────────────┘ */ 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; // ----------------------------- // 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 */ struct ERC7201Storage { Tier[] tiers; mapping(address vault => uint256 tierId) vaultTier; mapping(address nodeOperator => Group) groups; address[] nodeOperators; } /** * @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); 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 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: uint96(_shareLimit), liabilityShares: 0, tierIds: new uint256[](0) }); $.nodeOperators.push(_nodeOperator); emit GroupAdded(_nodeOperator, uint96(_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 = uint96(_shareLimit); emit GroupShareLimitUpdated(_nodeOperator, uint96(_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 ); } } /// @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 confirmed. /* 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. */ 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; // Vault may not be connected to VaultHub yet. // There are two possible flows: // 1. Vault is created and connected to VaultHub immediately with the default tier. // In this case, `VaultConnection` is non-zero and updateConnection must be called. // 2. Vault is created, its tier is changed before connecting to VaultHub. // In this case, `VaultConnection` is still zero, and updateConnection must be skipped. // Hence, we update the VaultHub connection only if the vault is already connected. vaultHub.updateConnection( _vault, _requestedShareLimit, requestedTier.reserveRatioBP, requestedTier.forcedRebalanceThresholdBP, requestedTier.infraFeeBP, requestedTier.liquidityFeeBP, requestedTier.reservationFeeBP ); emit TierChanged(_vault, _requestedTierId, _requestedShareLimit); 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); } } // ----------------------------- // MINT / BURN // ----------------------------- /// @notice Mint shares limit check /// @param _vault address of the vault /// @param _amount amount of shares will be minted function onMintedShares( address _vault, uint256 _amount ) external { if (msg.sender != LIDO_LOCATOR.vaultHub()) revert NotAuthorized("onMintedShares", msg.sender); ERC7201Storage storage $ = _getStorage(); uint256 tierId = $.vaultTier[_vault]; Tier storage tier_ = $.tiers[tierId]; uint96 tierLiabilityShares = tier_.liabilityShares; if (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 (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 Get vault 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 vaultInfo(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 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 > TOTAL_BASIS_POINTS) revert ReserveRatioTooHigh(_tierId, _reserveRatioBP, TOTAL_BASIS_POINTS); 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 } } // ----------------------------- // 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 ); // ----------------------------- // ERRORS // ----------------------------- error NotAuthorized(string operation, address sender); error ZeroArgument(string argument); error GroupExists(); error GroupNotExists(); error GroupLimitExceeded(); error NodeOperatorNotExists(); error TierLimitExceeded(); 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); }
// SPDX-FileCopyrightText: 2025 Lido <[email protected]> // SPDX-License-Identifier: GPL-3.0 // See contracts/COMPILERS.md pragma solidity 0.8.25; 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 {OperatorGrid} from "./OperatorGrid.sol"; import {LazyOracle} from "./LazyOracle.sol"; import {PausableUntilWithRoles} from "../utils/PausableUntilWithRoles.sol"; import {RefSlotCache} 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.Uint112WithRefSlotCache; using RefSlotCache for RefSlotCache.Int112WithRefSlotCache; // ----------------------------- // STORAGE STRUCTS // ----------------------------- /// @custom:storage-location erc7201:VaultHub struct Storage { /// @notice vault proxy contract codehashes allowed for connecting mapping(bytes32 codehash => bool allowed) codehashes; /// @notice accounting records for each vault mapping(address vault => VaultRecord) records; /// @notice connection parameters for each vault mapping(address vault => VaultConnection) connections; /// @notice obligation values for each vault mapping(address vault => VaultObligations) obligations; /// @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.Uint112WithRefSlotCache 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 is guaranteed to be stable only if there was no deletions. /// @dev vaultIndex is always greater than 0 uint96 vaultIndex; /// @notice if true, vault is disconnected and fee is not accrued bool pendingDisconnect; /// @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 manually paused the beacon chain deposits bool isBeaconDepositsManuallyPaused; /// 64 bits gap } struct VaultRecord { // ### 1st slot /// @notice latest report for the vault Report report; // ### 2nd slot /// @notice amount of ether that is locked from withdrawal on the vault uint128 locked; /// @notice liability shares of the vault uint96 liabilityShares; // ### 3rd slot /// @notice inOutDelta of the vault (all deposits - all withdrawals) RefSlotCache.Int112WithRefSlotCache inOutDelta; } struct Report { /// @notice total value of the vault uint112 totalValue; /// @notice inOutDelta of the report int112 inOutDelta; /// @notice last 32 bits of the timestamp (in seconds) uint32 timestamp; } /** * Obligations of the vaults towards the Lido protocol. * While any part of those obligations remains unsettled, VaultHub may want to limit what the vault can do. * * Obligations have two types: * 1. Redemptions. Under extreme conditions Lido protocol may rebalance the part of the vault's liability to serve * the Lido Core withdrawal queue requests to guarantee that every stETH is redeemable. Calculated in ether. * 2. Lido fees. Record of infra, liquidity and reservation fees charged to the vault. Charged in ether on every * oracle report. * * Obligations settlement: * - Lido fees are settled by transferring ether to the Lido protocol treasury * - Redemptions are settled by rebalancing the vault or by burning stETH on the vault * - Obligations may be settled manually using the `settleVaultObligations` function * - Obligations try to automatically settle: * - every time oracle report is applied to the vault * - on resume of the beacon chain deposits * - on disconnect initiation * - Lido fees are automatically settled on the final report that completes the disconnection process * * Constraints until obligations settled: * - Beacon chain deposits are paused while unsettled obligations ≥ OBLIGATIONS_THRESHOLD (1 ETH) * - Unsettled obligations can't be withdrawn * - Minting new stETH is limited by unsettled Lido fees (NB: redemptions do not affect minting capacity) * - Vault disconnect is refused until both unsettled redemptions and Lido fees obligations hit zero * * @dev NB: Under extreme conditions, Lido protocol may trigger validator exits to withdraw ether to the vault and * rebalance it to settle redemptions. */ struct VaultObligations { /// @notice cumulative value for Lido fees that were settled on the vault uint128 settledLidoFees; /// @notice current unsettled Lido fees amount uint128 unsettledLidoFees; /// @notice current unsettled redemptions amount uint128 redemptions; } // ----------------------------- // CONSTANTS // ----------------------------- // keccak256(abi.encode(uint256(keccak256("VaultHub")) - 1)) & ~bytes32(uint256(0xff)) bytes32 private constant STORAGE_LOCATION = 0xb158a1a9015c52036ff69e7937a7bb424e82a8c4cbec5c5309994af06d825300; /// @notice role that allows to connect vaults to the hub bytes32 public constant VAULT_MASTER_ROLE = keccak256("vaults.VaultHub.VaultMasterRole"); /// @notice role that allows to set allowed codehashes bytes32 public constant VAULT_CODEHASH_SET_ROLE = keccak256("vaults.VaultHub.VaultCodehashSetRole"); /// @notice role that allows to accrue Lido Core redemptions on the vault bytes32 public constant REDEMPTION_MASTER_ROLE = keccak256("vaults.VaultHub.RedemptionMasterRole"); /// @notice role that allows to trigger validator exits under extreme conditions bytes32 public constant VALIDATOR_EXIT_ROLE = keccak256("vaults.VaultHub.ValidatorExitRole"); /// @notice role that allows to bail out vaults with bad debt bytes32 public constant 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; /// @notice length of the validator pubkey in bytes uint256 internal constant PUBLIC_KEY_LENGTH = 48; /// @dev max value for fees in basis points - it's about 650% uint256 internal constant MAX_FEE_BP = type(uint16).max; /// @notice codehash of the account with no code bytes32 private constant EMPTY_CODEHASH = keccak256(""); /// @notice no limit for the unsettled obligations on settlement uint256 internal constant MAX_UNSETTLED_ALLOWED = type(uint256).max; /// @notice threshold for the unsettled obligations that will activate the beacon chain deposits pause uint256 internal constant UNSETTLED_THRESHOLD = 1 ether; /// @notice no unsettled obligations allowed on settlement uint256 internal constant NO_UNSETTLED_ALLOWED = 0; // ----------------------------- // 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; 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); _requireLessThanBP(_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 is 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 the obligations struct for the given vault /// @dev returns empty struct if the vault is not connected to the hub function vaultObligations(address _vault) external view returns (VaultObligations memory) { return _vaultObligations(_vault); } /// @return true if the vault is connected to the hub function isVaultConnected(address _vault) external view returns (bool) { return _vaultConnection(_vault).vaultIndex != 0; } /// @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 _vaultRecord(_vault).locked; } /// @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), _vaultObligations(_vault)); } /// @return the amount of ether that can be instantly withdrawn from the staking vault /// @dev returns 0 if the vault is not connected /// @dev check for `pendingDisconnect = false` before using this function to avoid reverts function withdrawableValue(address _vault) external view returns (uint256) { return _withdrawableValue(_vault, _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 amount of shares to rebalance or UINT256_MAX if it's impossible to make the vault healthy using rebalance /// @dev returns 0 if the vault is not connected function rebalanceShortfall(address _vault) external view returns (uint256) { return _rebalanceShortfall(_vaultConnection(_vault), _vaultRecord(_vault)); } /// @notice amount of bad debt to be internalized to become the protocol loss function badDebtToInternalizeAsOfLastRefSlot() external view returns (uint256) { return _storage().badDebtToInternalize.getValueForLastRefSlot(CONSENSUS_CONTRACT); } /// @notice inOutDelta of the vault as of the last refSlot /// @param _vault vault address /// @return inOutDelta of the vault as of the last refSlot /// @dev returns 0 if the vault is not connected function inOutDeltaAsOfLastRefSlot(address _vault) external view returns (int256) { return _vaultRecord(_vault).inOutDelta.getValueForLastRefSlot(CONSENSUS_CONTRACT); } /// @notice Set if a vault proxy codehash is allowed to be connected to the hub /// @param _codehash vault proxy codehash /// @param _allowed true to add, false to remove /// @dev msg.sender must have VAULT_CODEHASH_SET_ROLE function setAllowedCodehash(bytes32 _codehash, bool _allowed) external onlyRole(VAULT_CODEHASH_SET_ROLE) { _requireNotZero(uint256(_codehash)); if (_codehash == EMPTY_CODEHASH) revert ZeroCodehash(); _storage().codehashes[_codehash] = _allowed; emit AllowedCodehashUpdated(_codehash, _allowed); } /// @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); IStakingVault vault_ = IStakingVault(_vault); if (vault_.pendingOwner() != address(this)) revert VaultHubNotPendingOwner(_vault); if (vault_.isOssified()) revert VaultOssified(_vault); if (vault_.depositor() != address(_predepositGuarantee())) revert PDGNotDepositor(_vault); ( , // nodeOperatorInTier , // tierId uint256 shareLimit, uint256 reserveRatioBP, uint256 forcedRebalanceThresholdBP, uint256 infraFeeBP, uint256 liquidityFeeBP, uint256 reservationFeeBP ) = _operatorGrid().vaultInfo(_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 share limit for the vault /// Setting share limit to zero actually pause the vault's ability to mint /// @param _vault vault address /// @param _shareLimit new share limit /// @dev msg.sender must have VAULT_MASTER_ROLE function updateShareLimit(address _vault, uint256 _shareLimit) external onlyRole(VAULT_MASTER_ROLE) { _requireNotZero(_vault); _requireSaneShareLimit(_shareLimit); VaultConnection storage connection = _checkConnection(_vault); connection.shareLimit = uint96(_shareLimit); emit VaultShareLimitUpdated(_vault, _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 /// @dev msg.sender must have VAULT_MASTER_ROLE function updateVaultFees( address _vault, uint256 _infraFeeBP, uint256 _liquidityFeeBP, uint256 _reservationFeeBP ) external onlyRole(VAULT_MASTER_ROLE) { _requireNotZero(_vault); _requireLessThanBP(_infraFeeBP, MAX_FEE_BP); _requireLessThanBP(_liquidityFeeBP, MAX_FEE_BP); _requireLessThanBP(_reservationFeeBP, MAX_FEE_BP); VaultConnection storage connection = _checkConnection(_vault); uint16 preInfraFeeBP = connection.infraFeeBP; uint16 preLiquidityFeeBP = connection.liquidityFeeBP; uint16 preReservationFeeBP = connection.reservationFeeBP; connection.infraFeeBP = uint16(_infraFeeBP); connection.liquidityFeeBP = uint16(_liquidityFeeBP); connection.reservationFeeBP = uint16(_reservationFeeBP); emit VaultFeesUpdated({ vault: _vault, preInfraFeeBP: preInfraFeeBP, preLiquidityFeeBP: preLiquidityFeeBP, preReservationFeeBP: preReservationFeeBP, infraFeeBP: _infraFeeBP, liquidityFeeBP: _liquidityFeeBP, reservationFeeBP: _reservationFeeBP }); } /// @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 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); uint256 totalValue_ = _totalValue(record); uint256 liabilityShares_ = record.liabilityShares; if (_isThresholdBreached(totalValue_, liabilityShares_, _reserveRatioBP)) { revert VaultMintingCapacityExceeded(_vault, totalValue_, liabilityShares_, _reserveRatioBP); } 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, shareLimit: _shareLimit, reserveRatioBP: _reserveRatioBP, forcedRebalanceThresholdBP: _forcedRebalanceThresholdBP, infraFeeBP: _infraFeeBP, liquidityFeeBP: _liquidityFeeBP, reservationFeeBP: _reservationFeeBP }); } /// @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 function disconnect(address _vault) external onlyRole(VAULT_MASTER_ROLE) { _initiateDisconnection(_vault, _checkConnection(_vault), _vaultRecord(_vault)); 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 function applyVaultReport( address _vault, uint256 _reportTimestamp, uint256 _reportTotalValue, int256 _reportInOutDelta, uint256 _reportCumulativeLidoFees, uint256 _reportLiabilityShares, uint256 _reportSlashingReserve ) external whenResumed { _requireSender(address(_lazyOracle())); VaultConnection storage connection = _vaultConnection(_vault); VaultRecord storage record = _vaultRecord(_vault); VaultObligations storage obligations = _vaultObligations(_vault); _checkAndUpdateLidoFeesObligations(_vault, obligations, _reportCumulativeLidoFees); if (connection.pendingDisconnect) { if (_reportSlashingReserve == 0 && record.liabilityShares == 0) { _settleObligations(_vault, record, obligations, NO_UNSETTLED_ALLOWED); 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.pendingDisconnect = false; emit VaultDisconnectAborted(_vault, _reportSlashingReserve); } } _applyVaultReport( record, connection, _reportTimestamp, _reportTotalValue, _reportLiabilityShares, _reportInOutDelta ); emit VaultReportApplied({ vault: _vault, reportTimestamp: _reportTimestamp, reportTotalValue: _reportTotalValue, reportInOutDelta: _reportInOutDelta, reportCumulativeLidoFees: _reportCumulativeLidoFees, reportLiabilityShares: _reportLiabilityShares, reportSlashingReserve: _reportSlashingReserve }); _settleObligations(_vault, record, obligations, MAX_UNSETTLED_ALLOWED); _checkAndUpdateBeaconChainDepositsPause(_vault, connection, record); } /// @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 /// @dev msg.sender must have BAD_DEBT_MASTER_ROLE function socializeBadDebt( address _badDebtVault, address _vaultAcceptor, uint256 _maxSharesToSocialize ) external onlyRole(BAD_DEBT_MASTER_ROLE) { _requireNotZero(_badDebtVault); _requireNotZero(_vaultAcceptor); _requireNotZero(_maxSharesToSocialize); if (_nodeOperator(_vaultAcceptor) != _nodeOperator(_badDebtVault)) revert BadDebtSocializationNotAllowed(); VaultConnection storage badDebtConnection = _vaultConnection(_badDebtVault); _requireConnected(badDebtConnection, _badDebtVault); // require connected but may be pending disconnect uint256 badDebtToSocialize = _writeOffBadDebt({ _vault: _badDebtVault, _record: _vaultRecord(_badDebtVault), _maxSharesToWriteOff: _maxSharesToSocialize }); VaultConnection storage connectionAcceptor = _vaultConnection(_vaultAcceptor); _requireConnected(connectionAcceptor, _vaultAcceptor); VaultRecord storage recordAcceptor = _vaultRecord(_vaultAcceptor); _increaseLiability({ _vault: _vaultAcceptor, _record: recordAcceptor, _amountOfShares: badDebtToSocialize, _reserveRatioBP: connectionAcceptor.reserveRatioBP, _maxMintableRatioBP: TOTAL_BASIS_POINTS, // maxMintableRatio up to 100% of total value _shareLimit: _getSharesByPooledEth(recordAcceptor.locked) // we can occupy all the locked amount }); emit BadDebtSocialized(_badDebtVault, _vaultAcceptor, badDebtToSocialize); } /// @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 /// @dev msg.sender must have BAD_DEBT_MASTER_ROLE function internalizeBadDebt( address _badDebtVault, uint256 _maxSharesToInternalize ) external onlyRole(BAD_DEBT_MASTER_ROLE) { _requireNotZero(_badDebtVault); _requireNotZero(_maxSharesToInternalize); VaultConnection storage badDebtConnection = _vaultConnection(_badDebtVault); _requireConnected(badDebtConnection, _badDebtVault); uint256 badDebtToInternalize = _writeOffBadDebt({ _vault: _badDebtVault, _record: _vaultRecord(_badDebtVault), _maxSharesToWriteOff: _maxSharesToInternalize }); // internalize the bad debt to the protocol _storage().badDebtToInternalize = _storage().badDebtToInternalize.withValueIncrease({ _consensus: CONSENSUS_CONTRACT, _increment: uint112(badDebtToInternalize) }); emit BadDebtWrittenOffToBeInternalized(_badDebtVault, 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 -= uint112(_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 function voluntaryDisconnect(address _vault) external whenResumed { VaultConnection storage connection = _checkConnectionAndOwner(_vault); _initiateDisconnection(_vault, connection, _vaultRecord(_vault)); 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); if (connection.vaultIndex == 0) revert NotConnectedToHub(_vault); if (msg.sender != connection.owner) revert NotAuthorized(); _updateInOutDelta(_vault, _vaultRecord(_vault), int112(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 function withdraw(address _vault, address _recipient, uint256 _ether) external whenResumed { _checkConnectionAndOwner(_vault); VaultRecord storage record = _vaultRecord(_vault); _requireFreshReport(_vault, record); uint256 withdrawable = _withdrawableValue(_vault, 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 function rebalance(address _vault, uint256 _shares) external whenResumed { _requireNotZero(_shares); _checkConnectionAndOwner(_vault); _rebalance(_vault, _vaultRecord(_vault), _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 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); uint256 reserveRatioBP = connection.reserveRatioBP; _increaseLiability({ _vault: _vault, _record: record, _amountOfShares: _amountOfShares, _reserveRatioBP: reserveRatioBP, _maxMintableRatioBP: TOTAL_BASIS_POINTS - reserveRatioBP, _shareLimit: connection.shareLimit }); LIDO.mintExternalShares(_recipient, _amountOfShares); emit MintedSharesOnVault(_vault, _amountOfShares, record.locked); } /// @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); 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); connection.isBeaconDepositsManuallyPaused = true; IStakingVault(_vault).pauseBeaconChainDeposits(); } /// @notice resumes beacon chain deposits for the vault /// @param _vault vault address /// @dev msg.sender should be vault's owner function resumeBeaconChainDeposits(address _vault) external { VaultConnection storage connection = _checkConnectionAndOwner(_vault); VaultRecord storage record = _vaultRecord(_vault); if (!_isVaultHealthy(connection, record)) revert UnhealthyVaultCannotDeposit(_vault); _settleObligations(_vault, record, _vaultObligations(_vault), UNSETTLED_THRESHOLD); connection.isBeaconDepositsManuallyPaused = false; IStakingVault(_vault).resumeBeaconChainDeposits(); } /// @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 _amounts 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 function triggerValidatorWithdrawals( address _vault, bytes calldata _pubkeys, uint64[] calldata _amounts, address _refundRecipient ) external payable { VaultConnection storage connection = _checkConnectionAndOwner(_vault); VaultRecord storage record = _vaultRecord(_vault); VaultObligations storage obligations = _vaultObligations(_vault); /// @dev NB: Disallow partial withdrawals when the vault is unhealthy or has redemptions over the threshold /// in order to prevent the vault owner from clogging the consensus layer withdrawal queue /// front-running and delaying the forceful validator exits required for rebalancing the vault, /// unless the requested amount of withdrawals is enough to recover the vault to healthy state and /// settle the unsettled obligations if (!_isVaultHealthy(connection, record) || obligations.redemptions >= UNSETTLED_THRESHOLD) { uint256 minPartialAmount = type(uint256).max; for (uint256 i = 0; i < _amounts.length; i++) { if (_amounts[i] > 0 && _amounts[i] < minPartialAmount) minPartialAmount = _amounts[i]; } if (minPartialAmount < type(uint256).max) { uint256 currentVaultBalance = _vault.balance; uint256 required = _totalUnsettledObligations(obligations) + _rebalanceShortfall(connection, record); uint256 amountToCover = required > currentVaultBalance ? required - currentVaultBalance : 0; if (minPartialAmount < amountToCover) revert PartialValidatorWithdrawalNotAllowed(); } } IStakingVault(_vault).triggerValidatorWithdrawals{value: msg.value}(_pubkeys, _amounts, _refundRecipient); } /// @notice Triggers validator full withdrawals for the vault using EIP-7002 permissionlessly if the vault is /// unhealthy or has redemptions obligation over the threshold /// @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 When the vault becomes unhealthy, 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 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); if ( _isVaultHealthy(connection, record) && // Check if the vault has redemptions under the threshold, or enough balance to cover the redemptions fully _vaultObligations(_vault).redemptions < Math256.max(UNSETTLED_THRESHOLD, _vault.balance) ) { revert ForcedValidatorExitNotAllowed(); } uint64[] memory amounts = new uint64[](0); IStakingVault(_vault).triggerValidatorWithdrawals{value: msg.value}(_pubkeys, amounts, _refundRecipient); emit ForcedValidatorExitTriggered(_vault, _pubkeys, _refundRecipient); } /// @notice Permissionless rebalance for unhealthy vaults /// @param _vault vault address /// @dev rebalance all available amount of ether until the vault is healthy function forceRebalance(address _vault) external { VaultConnection storage connection = _checkConnection(_vault); VaultRecord storage record = _vaultRecord(_vault); uint256 sharesToRebalance = Math256.min( _rebalanceShortfall(connection, record), _getSharesByPooledEth(_vault.balance) ); if (sharesToRebalance == 0) revert AlreadyHealthy(_vault); _rebalance(_vault, record, sharesToRebalance); } /// @notice Accrues a redemption obligation on the vault under extreme conditions /// @param _vault The address of the vault /// @param _redemptionsValue The value of the redemptions obligation function setVaultRedemptions(address _vault, uint256 _redemptionsValue) external onlyRole(REDEMPTION_MASTER_ROLE) { VaultRecord storage record = _vaultRecord(_vault); uint256 liabilityShares_ = record.liabilityShares; // This function may intentionally perform no action in some cases, as these are EasyTrack motions if (liabilityShares_ > 0) { uint256 newRedemptions = Math256.min(_redemptionsValue, _getPooledEthBySharesRoundUp(liabilityShares_)); _vaultObligations(_vault).redemptions = uint128(newRedemptions); emit RedemptionsUpdated(_vault, newRedemptions); _checkAndUpdateBeaconChainDepositsPause(_vault, _vaultConnection(_vault), record); } else { emit RedemptionsNotSet(_vault, _redemptionsValue); } } /// @notice Allows permissionless full or partial settlement of unsettled obligations on the vault /// @param _vault The address of the vault function settleVaultObligations(address _vault) external whenResumed { if (_vault.balance == 0) revert ZeroBalance(); VaultRecord storage record = _vaultRecord(_vault); _settleObligations(_vault, record, _vaultObligations(_vault), MAX_UNSETTLED_ALLOWED); _checkAndUpdateBeaconChainDepositsPause(_vault, _vaultConnection(_vault), record); } /// @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 Compensates disproven predeposit from PDG to the recipient /// @param _vault vault address /// @param _pubkey pubkey of the validator /// @param _recipient address to compensate the disproven validator predeposit to /// @return amount of compensated ether function compensateDisprovenPredepositFromPDG( address _vault, bytes calldata _pubkey, address _recipient ) external returns (uint256) { _checkConnectionAndOwner(_vault); return _predepositGuarantee().compensateDisprovenPredeposit(_pubkey, _recipient); } function _connectVault( address _vault, uint256 _shareLimit, uint256 _reserveRatioBP, uint256 _forcedRebalanceThresholdBP, uint256 _infraFeeBP, uint256 _liquidityFeeBP, uint256 _reservationFeeBP ) internal { _requireSaneShareLimit(_shareLimit); _requireNotZero(_reserveRatioBP); _requireLessThanBP(_reserveRatioBP, TOTAL_BASIS_POINTS); _requireNotZero(_forcedRebalanceThresholdBP); _requireLessThanBP(_forcedRebalanceThresholdBP, _reserveRatioBP); _requireLessThanBP(_infraFeeBP, MAX_FEE_BP); _requireLessThanBP(_liquidityFeeBP, MAX_FEE_BP); _requireLessThanBP(_reservationFeeBP, MAX_FEE_BP); VaultConnection memory connection = _vaultConnection(_vault); if (connection.pendingDisconnect) revert VaultIsDisconnecting(_vault); if (connection.vaultIndex != 0) revert AlreadyConnected(_vault, connection.vaultIndex); bytes32 codehash = address(_vault).codehash; if (!_storage().codehashes[codehash]) revert CodehashNotAllowed(_vault, codehash); uint256 vaultBalance = _vault.balance; if (vaultBalance < CONNECT_DEPOSIT) revert VaultInsufficientBalance(_vault, vaultBalance, CONNECT_DEPOSIT); // Connecting a new vault with totalValue == balance VaultRecord memory record = VaultRecord({ report: Report({ totalValue: uint112(vaultBalance), inOutDelta: int112(int256(vaultBalance)), timestamp: uint32(_lazyOracle().latestReportTimestamp()) }), locked: uint128(CONNECT_DEPOSIT), liabilityShares: 0, inOutDelta: RefSlotCache.Int112WithRefSlotCache({ value: int112(int256(vaultBalance)), valueOnRefSlot: 0, refSlot: 0 }) }); connection = VaultConnection({ owner: IStakingVault(_vault).owner(), shareLimit: uint96(_shareLimit), vaultIndex: uint96(_storage().vaults.length), pendingDisconnect: false, reserveRatioBP: uint16(_reserveRatioBP), forcedRebalanceThresholdBP: uint16(_forcedRebalanceThresholdBP), infraFeeBP: uint16(_infraFeeBP), liquidityFeeBP: uint16(_liquidityFeeBP), reservationFeeBP: uint16(_reservationFeeBP), isBeaconDepositsManuallyPaused: false }); _addVault(_vault, connection, record); } function _initiateDisconnection( address _vault, VaultConnection storage _connection, VaultRecord storage _record ) internal { uint256 liabilityShares_ = _record.liabilityShares; if (liabilityShares_ > 0) { revert NoLiabilitySharesShouldBeLeft(_vault, liabilityShares_); } _record.locked = 0; // unlock the connection deposit to allow fees settlement _settleObligations(_vault, _record, _vaultObligations(_vault), NO_UNSETTLED_ALLOWED); _connection.pendingDisconnect = true; _operatorGrid().resetVaultTier(_vault); } function _applyVaultReport( VaultRecord storage _record, VaultConnection storage _connection, uint256 _reportTimestamp, uint256 _reportTotalValue, uint256 _reportLiabilityShares, int256 _reportInOutDelta ) internal { uint256 liabilityShares_ = Math256.max(_record.liabilityShares, _reportLiabilityShares); uint256 liability = _getPooledEthBySharesRoundUp(liabilityShares_); uint256 lockedEther = Math256.max( liability * TOTAL_BASIS_POINTS / (TOTAL_BASIS_POINTS - _connection.reserveRatioBP), CONNECT_DEPOSIT ); _record.locked = uint128(lockedEther); _record.report = Report({ totalValue: uint112(_reportTotalValue), inOutDelta: int112(_reportInOutDelta), timestamp: uint32(_reportTimestamp) }); } function _rebalance(address _vault, VaultRecord storage _record, uint256 _shares) internal { uint256 valueToRebalance = _getPooledEthBySharesRoundUp(_shares); uint256 totalValue_ = _totalValue(_record); if (valueToRebalance > totalValue_) revert RebalanceAmountExceedsTotalValue(totalValue_, valueToRebalance); _decreaseLiability(_vault, _record, _shares); _withdraw(_vault, _record, address(this), valueToRebalance); _rebalanceExternalEtherToInternal(valueToRebalance); emit VaultRebalanced(_vault, _shares, valueToRebalance); } function _withdraw( address _vault, VaultRecord storage _record, address _recipient, uint256 _amount ) internal { _updateInOutDelta(_vault, _record, -int112(int256(_amount))); IStakingVault(_vault).withdraw(_recipient, _amount); } function _increaseLiability( address _vault, VaultRecord storage _record, uint256 _amountOfShares, uint256 _reserveRatioBP, uint256 _maxMintableRatioBP, uint256 _shareLimit ) internal { uint256 sharesAfterMint = _record.liabilityShares + _amountOfShares; if (sharesAfterMint > _shareLimit) revert ShareLimitExceeded(_vault, sharesAfterMint, _shareLimit); uint256 stETHAfterMint = _getPooledEthBySharesRoundUp(sharesAfterMint); uint256 maxLockableValue_ = _maxLockableValue(_record, _vaultObligations(_vault)); uint256 maxMintableEther = (maxLockableValue_ * _maxMintableRatioBP) / TOTAL_BASIS_POINTS; if (stETHAfterMint > maxMintableEther) { revert InsufficientValueToMint(_vault, maxLockableValue_); } // Calculate the minimum ETH that needs to be locked in the vault to maintain the reserve ratio uint256 etherToLock = (stETHAfterMint * TOTAL_BASIS_POINTS) / (TOTAL_BASIS_POINTS - _reserveRatioBP); if (etherToLock > _record.locked) { _record.locked = uint128(etherToLock); } _record.liabilityShares = uint96(sharesAfterMint); _operatorGrid().onMintedShares(_vault, _amountOfShares); } 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); _decreaseRedemptions(_vault, _amountOfShares); _operatorGrid().onBurnedShares(_vault, _amountOfShares); } function _writeOffBadDebt( address _vault, VaultRecord storage _record, uint256 _maxSharesToWriteOff ) internal returns (uint256 badDebtWrittenOff) { uint256 liabilityShares_ = _record.liabilityShares; uint256 totalValueShares = _getSharesByPooledEth(_totalValue(_record)); if (totalValueShares > liabilityShares_) { revert NoBadDebtToWriteOff(_vault, totalValueShares, liabilityShares_); } badDebtWrittenOff = Math256.min(liabilityShares_ - totalValueShares, _maxSharesToWriteOff); _decreaseLiability(_vault, _record, badDebtWrittenOff); } function _rebalanceShortfall( 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 sharesByTotalValue = _getSharesByPooledEth(totalValue_); // Impossible to rebalance a vault with bad debt if (liabilityShares_ >= sharesByTotalValue) { // return MAX_UINT_256 return type(uint256).max; } // Solve the equation for X: // LS - liabilityShares, TV - sharesByTotalValue // 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 LS/TVS ratio back to MR // (LS - X) / (TV - X) = MR / 100 // (LS - X) * 100 = (TV - X) * MR // LS * 100 - X * 100 = TV * MR - X * MR // X * MR - X * 100 = TV * MR - LS * 100 // X * (MR - 100) = TV * MR - LS * 100 // X = (TV * MR - LS * 100) / (MR - 100) // X = (LS * 100 - TV * MR) / (100 - MR) // RR = 100 - MR // X = (LS * 100 - TV * MR) / RR return (liabilityShares_ * TOTAL_BASIS_POINTS - sharesByTotalValue * maxMintableRatio) / reserveRatioBP; } function _totalValue(VaultRecord storage _record) internal view returns (uint256) { Report memory report = _record.report; return uint256(int256(uint256(report.totalValue)) + _record.inOutDelta.value - report.inOutDelta); } function _maxLockableValue(VaultRecord storage _record, VaultObligations storage _obligations) internal view returns (uint256) { return _totalValue(_record) - _obligations.unsettledLidoFees; } function _isReportFresh(VaultRecord storage _record) internal view returns (bool) { uint256 latestReportTimestamp = _lazyOracle().latestReportTimestamp(); return // check if AccountingOracle brought fresh report uint32(latestReportTimestamp) == _record.report.timestamp && // if Accounting Oracle stop bringing the report, last report is fresh for 2 days 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; } 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]; delete $.obligations[_vault]; } function _checkConnectionAndOwner(address _vault) internal view returns (VaultConnection storage connection) { connection = _checkConnection(_vault); _requireSender(connection.owner); } function _checkConnection(address _vault) internal view returns (VaultConnection storage) { _requireNotZero(_vault); VaultConnection storage connection = _vaultConnection(_vault); _requireConnected(connection, _vault); if (connection.pendingDisconnect) revert VaultIsDisconnecting(_vault); return connection; } /// @dev Caches the inOutDelta of the latest refSlot and updates the value function _updateInOutDelta(address _vault, VaultRecord storage _record, int112 _increment) internal { _record.inOutDelta = _record.inOutDelta.withValueIncrease({ _consensus: CONSENSUS_CONTRACT, _increment: _increment }); emit VaultInOutDeltaUpdated(_vault, _record.inOutDelta.value); } /** * @notice Updates the unsettled Lido fees obligations based on the report cumulative Lido fees * @param _vault The address of the vault * @param _reportCumulativeLidoFees The cumulative Lido fees reported in the report */ function _checkAndUpdateLidoFeesObligations( address _vault, VaultObligations storage _obligations, uint256 _reportCumulativeLidoFees ) internal { uint256 cumulativeSettledLidoFees = _obligations.settledLidoFees; uint256 cumulativeLidoFees = cumulativeSettledLidoFees + _obligations.unsettledLidoFees; if (_reportCumulativeLidoFees < cumulativeLidoFees) { revert InvalidFees(_vault, _reportCumulativeLidoFees, cumulativeLidoFees); } // update unsettled lido fees uint256 unsettledLidoFees = _reportCumulativeLidoFees - cumulativeSettledLidoFees; if (unsettledLidoFees != _obligations.unsettledLidoFees) { _obligations.unsettledLidoFees = uint128(unsettledLidoFees); emit LidoFeesUpdated(_vault, unsettledLidoFees, cumulativeSettledLidoFees); } } /** * @notice Calculates a settlement plan based on vault balance and obligations * @param _vault The address of the vault * @param _record The record of the vault * @param _obligations The obligations of the vault to be settled * @return valueToRebalance The ETH amount to be rebalanced for redemptions * @return sharesToRebalance The shares to be rebalanced for redemptions * @return valueToTransferToLido The ETH amount to be sent to the Lido * @return unsettledRedemptions The remaining redemptions after the planned settlement * @return unsettledLidoFees The remaining Lido fees after the planned settlement * @return totalUnsettled The total ETH value of obligations remaining after the planned settlement */ function _planSettlement( address _vault, VaultRecord storage _record, VaultObligations storage _obligations ) internal view returns ( uint256 valueToRebalance, uint256 sharesToRebalance, uint256 valueToTransferToLido, uint256 unsettledRedemptions, uint256 unsettledLidoFees, uint256 totalUnsettled ) { (valueToRebalance, sharesToRebalance, unsettledRedemptions) = _planRebalance(_vault, _record, _obligations); (valueToTransferToLido, unsettledLidoFees) = _planLidoTransfer(_vault, _record, _obligations, valueToRebalance); totalUnsettled = unsettledRedemptions + unsettledLidoFees; } /** * @notice Plans the amounts and shares to rebalance for redemptions * @param _vault The address of the vault * @param _record The record of the vault * @param _obligations The obligations of the vault * @return valueToRebalance The ETH amount to be rebalanced for redemptions * @return sharesToRebalance The shares to be rebalanced for redemptions * @return unsettledRedemptions The remaining redemptions after the planned settlement */ function _planRebalance( address _vault, VaultRecord storage _record, VaultObligations storage _obligations ) internal view returns (uint256 valueToRebalance, uint256 sharesToRebalance, uint256 unsettledRedemptions) { uint256 redemptionShares = _getSharesByPooledEth(_obligations.redemptions); uint256 maxRedemptionsValue = _getPooledEthBySharesRoundUp(redemptionShares); // if the max redemptions value is less than the redemptions, we need to round up the redemptions shares if (maxRedemptionsValue < _obligations.redemptions) redemptionShares += 1; uint256 cappedRedemptionsShares = Math256.min(_record.liabilityShares, redemptionShares); sharesToRebalance = Math256.min(cappedRedemptionsShares, _getSharesByPooledEth(_vault.balance)); valueToRebalance = _getPooledEthBySharesRoundUp(sharesToRebalance); unsettledRedemptions = _getPooledEthBySharesRoundUp(redemptionShares - sharesToRebalance); } /** * @notice Plans the amount to transfer to Lido for fees * @param _vault The address of the vault * @param _record The record of the vault * @param _obligations The obligations of the vault * @param _valueToRebalance The ETH amount already allocated for rebalancing * @return valueToTransferToLido The ETH amount to be sent to the Lido * @return unsettledLidoFees The remaining Lido fees after the planned settlement */ function _planLidoTransfer( address _vault, VaultRecord storage _record, VaultObligations storage _obligations, uint256 _valueToRebalance ) internal view returns (uint256 valueToTransferToLido, uint256 unsettledLidoFees) { uint256 vaultBalance = _vault.balance; uint256 remainingBalance = vaultBalance - _valueToRebalance; if (_vaultConnection(_vault).pendingDisconnect) { /// @dev connection deposit is unlocked, so it's available for fees valueToTransferToLido = Math256.min(_obligations.unsettledLidoFees, remainingBalance); } else { /// @dev connection deposit is permanently locked, so it's not available for fees /// @dev NB: Fees are deducted from the vault's current balance, which reduces the total value, so the /// current locked value must be considered to prevent the vault from entering an unhealthy state uint256 lockedValue = _record.locked; uint256 totalValue_ = _totalValue(_record); uint256 unlockedValue = totalValue_ > lockedValue ? totalValue_ - lockedValue : 0; uint256 availableForFees = Math256.min( unlockedValue > _valueToRebalance ? unlockedValue - _valueToRebalance : 0, remainingBalance ); valueToTransferToLido = Math256.min(_obligations.unsettledLidoFees, availableForFees); } unsettledLidoFees = _obligations.unsettledLidoFees - valueToTransferToLido; } /** * @notice Settles redemptions and Lido fee obligations for a vault * @param _vault The address of the vault to settle obligations for * @param _record The record of the vault to settle obligations for * @param _obligations The obligations of the vault to be settled * @param _allowedUnsettled The maximum allowable unsettled obligations post-settlement (triggers reverts) */ function _settleObligations( address _vault, VaultRecord storage _record, VaultObligations storage _obligations, uint256 _allowedUnsettled ) internal { ( uint256 valueToRebalance, uint256 sharesToRebalance, uint256 valueToTransferToLido, uint256 unsettledRedemptions, uint256 unsettledLidoFees, uint256 totalUnsettled ) = _planSettlement(_vault, _record, _obligations); // Enforce requirement for settlement completeness if (totalUnsettled > _allowedUnsettled) { revert VaultHasUnsettledObligations(_vault, totalUnsettled, _allowedUnsettled); } // Skip if no changes to obligations if (valueToTransferToLido == 0 && valueToRebalance == 0) { return; } if (valueToRebalance > 0) { _decreaseLiability(_vault, _record, sharesToRebalance); _withdraw(_vault, _record, address(this), valueToRebalance); _rebalanceExternalEtherToInternal(valueToRebalance); } if (valueToTransferToLido > 0) { _withdraw(_vault, _record, LIDO_LOCATOR.treasury(), valueToTransferToLido); _obligations.settledLidoFees += uint128(valueToTransferToLido); } _obligations.redemptions = uint128(unsettledRedemptions); _obligations.unsettledLidoFees = uint128(unsettledLidoFees); emit VaultObligationsSettled({ vault: _vault, rebalanced: valueToRebalance, transferredToLido: valueToTransferToLido, unsettledRedemptions: unsettledRedemptions, unsettledLidoFees: unsettledLidoFees, settledLidoFees: _obligations.settledLidoFees }); } function _decreaseRedemptions(address _vault, uint256 _shares) internal { VaultObligations storage obligations = _vaultObligations(_vault); if (obligations.redemptions > 0) { uint256 redemptionsValue = _getPooledEthBySharesRoundUp(_shares); uint256 decrease = Math256.min(obligations.redemptions, redemptionsValue); if (decrease > 0) { obligations.redemptions -= uint128(decrease); emit RedemptionsUpdated(_vault, obligations.redemptions); } } } function _totalUnsettledObligations(VaultObligations storage _obligations) internal view returns (uint256) { return _obligations.unsettledLidoFees + _obligations.redemptions; } function _checkAndUpdateBeaconChainDepositsPause( address _vault, VaultConnection storage _connection, VaultRecord storage _record ) internal { IStakingVault vault_ = IStakingVault(_vault); bool isHealthy = _isVaultHealthy(_connection, _record); bool isBeaconDepositsPaused = vault_.beaconChainDepositsPaused(); if (_totalUnsettledObligations(_vaultObligations(_vault)) >= UNSETTLED_THRESHOLD || !isHealthy) { if (!isBeaconDepositsPaused) vault_.pauseBeaconChainDeposits(); } else if (!_connection.isBeaconDepositsManuallyPaused) { if (isBeaconDepositsPaused) vault_.resumeBeaconChainDeposits(); } } /// @return the amount of ether that can be instantly withdrawn from the staking vault /// @dev this amount already accounts locked value and unsettled obligations function _withdrawableValue( address _vault, VaultRecord storage _record ) internal view returns (uint256) { uint256 totalValue_ = _totalValue(_record); uint256 lockedPlusUnsettled = _record.locked + _totalUnsettledObligations(_vaultObligations(_vault)); return Math256.min( _vault.balance, totalValue_ > lockedPlusUnsettled ? totalValue_ - lockedPlusUnsettled : 0 ); } 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]; } function _vaultObligations(address _vault) internal view returns (VaultObligations storage) { return _storage().obligations[_vault]; } 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 _getPooledEthByShares(uint256 _ether) internal view returns (uint256) { return LIDO.getPooledEthByShares(_ether); } function _getPooledEthBySharesRoundUp(uint256 _shares) internal view returns (uint256) { return LIDO.getPooledEthBySharesRoundUp(_shares); } function _rebalanceExternalEtherToInternal(uint256 _ether) internal { LIDO.rebalanceExternalEtherToInternal{value: _ether}(); } function _nodeOperator(address _vault) internal view returns (address) { return IStakingVault(_vault).nodeOperator(); } 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 _requireLessThanBP(uint256 _valueBP, uint256 _maxValueBP) internal pure { if (_valueBP > _maxValueBP) revert InvalidBasisPoints(_valueBP, _maxValueBP); } 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); } // ----------------------------- // EVENTS // ----------------------------- event AllowedCodehashUpdated(bytes32 indexed codehash, bool allowed); event VaultConnected( address indexed vault, uint256 shareLimit, uint256 reserveRatioBP, uint256 forcedRebalanceThresholdBP, uint256 infraFeeBP, uint256 liquidityFeeBP, uint256 reservationFeeBP ); event VaultConnectionUpdated( address indexed vault, uint256 shareLimit, uint256 reserveRatioBP, uint256 forcedRebalanceThresholdBP, uint256 infraFeeBP, uint256 liquidityFeeBP, uint256 reservationFeeBP ); event VaultShareLimitUpdated(address indexed vault, uint256 newShareLimit); 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 reportSlashingReserve ); event MintedSharesOnVault(address indexed vault, uint256 amountOfShares, uint256 lockedAmount); event BurnedSharesOnVault(address indexed vault, uint256 amountOfShares); event VaultRebalanced(address indexed vault, uint256 sharesBurned, uint256 etherWithdrawn); event VaultInOutDeltaUpdated(address indexed vault, int112 inOutDelta); event ForcedValidatorExitTriggered(address indexed vault, bytes pubkeys, address refundRecipient); /** * @notice Emitted when the manager is set * @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 LidoFeesUpdated(address indexed vault, uint256 unsettledLidoFees, uint256 settledLidoFees); event RedemptionsUpdated(address indexed vault, uint256 unsettledRedemptions); event RedemptionsNotSet(address indexed vault, uint256 redemptionsValue); event VaultObligationsSettled( address indexed vault, uint256 rebalanced, uint256 transferredToLido, uint256 unsettledRedemptions, uint256 unsettledLidoFees, uint256 settledLidoFees ); // ----------------------------- // ERRORS // ----------------------------- event BadDebtSocialized(address indexed vaultDonor, address indexed vaultAcceptor, uint256 badDebtShares); event BadDebtWrittenOffToBeInternalized(address indexed vault, uint256 badDebtShares); error ZeroBalance(); /** * @notice Thrown when attempting to rebalance more ether than the current total value of the vault * @param totalValue Current total value of the vault * @param rebalanceAmount Amount attempting to rebalance (in ether) */ error RebalanceAmountExceedsTotalValue(uint256 totalValue, uint256 rebalanceAmount); /** * @notice Thrown when attempting to withdraw more ether than the available value of the vault * @param vault The address of the vault * @param withdrawable The available value of the vault * @param requested The amount attempting to withdraw */ error AmountExceedsWithdrawableValue(address vault, uint256 withdrawable, uint256 requested); error AlreadyHealthy(address vault); 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 NotConnectedToHub(address vault); error NotAuthorized(); error ZeroAddress(); error ZeroArgument(); error InvalidBasisPoints(uint256 valueBP, uint256 maxValueBP); error ShareLimitTooHigh(uint256 shareLimit, uint256 maxShareLimit); error InsufficientValueToMint(address vault, uint256 maxLockableValue); error NoLiabilitySharesShouldBeLeft(address vault, uint256 liabilityShares); error CodehashNotAllowed(address vault, bytes32 codehash); error InvalidFees(address vault, uint256 newFees, uint256 oldFees); error VaultOssified(address vault); error VaultInsufficientBalance(address vault, uint256 currentBalance, uint256 expectedBalance); error VaultReportStale(address vault); error PDGNotDepositor(address vault); error ZeroCodehash(); error VaultHubNotPendingOwner(address vault); error UnhealthyVaultCannotDeposit(address vault); error VaultIsDisconnecting(address vault); error VaultHasUnsettledObligations(address vault, uint256 unsettledObligations, uint256 allowedUnsettled); error PartialValidatorWithdrawalNotAllowed(); error ForcedValidatorExitNotAllowed(); error NoBadDebtToWriteOff(address vault, uint256 totalValueShares, uint256 liabilityShares); error BadDebtSocializationNotAllowed(); }
// 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 _timestamp, 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() 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 validatorExitDelayVerifier() external view returns (address); function triggerableWithdrawalsGateway() 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"
]
}
},
"libraries": {}
}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":"AccessControlBadConfirmation","type":"error"},{"inputs":[{"internalType":"address","name":"account","type":"address"},{"internalType":"bytes32","name":"neededRole","type":"bytes32"}],"name":"AccessControlUnauthorizedAccount","type":"error"},{"inputs":[],"name":"AdjustmentNotReported","type":"error"},{"inputs":[],"name":"AdjustmentNotSettled","type":"error"},{"inputs":[],"name":"AlreadyInitialized","type":"error"},{"inputs":[],"name":"ConfirmExpiryOutOfBounds","type":"error"},{"inputs":[],"name":"ConnectedToVaultHub","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":"IncreasedOverLimit","type":"error"},{"inputs":[{"internalType":"uint256","name":"currentAdjustment","type":"uint256"},{"internalType":"uint256","name":"currentAtPropositionAdjustment","type":"uint256"}],"name":"InvalidatedAdjustmentVote","type":"error"},{"inputs":[],"name":"NonProxyCallsForbidden","type":"error"},{"inputs":[],"name":"ReportStale","type":"error"},{"inputs":[{"internalType":"address","name":"token","type":"address"}],"name":"SafeERC20FailedOperation","type":"error"},{"inputs":[],"name":"SameAdjustment","type":"error"},{"inputs":[],"name":"SameRecipient","type":"error"},{"inputs":[],"name":"SenderNotMember","type":"error"},{"inputs":[],"name":"TierChangeNotConfirmed","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":"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":true,"internalType":"address","name":"to","type":"address"},{"indexed":true,"internalType":"address","name":"token","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"ERC20Recovered","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":true,"internalType":"address","name":"token","type":"address"},{"indexed":false,"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"ERC721Recovered","type":"event"},{"anonymous":false,"inputs":[],"name":"Initialized","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"sender","type":"address"},{"indexed":false,"internalType":"uint256","name":"fee","type":"uint256"}],"name":"NodeOperatorFeeDisbursed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"sender","type":"address"},{"indexed":false,"internalType":"uint256","name":"oldNodeOperatorFeeRate","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"newNodeOperatorFeeRate","type":"uint256"}],"name":"NodeOperatorFeeRateSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"sender","type":"address"},{"indexed":false,"internalType":"address","name":"oldNodeOperatorFeeRecipient","type":"address"},{"indexed":false,"internalType":"address","name":"newNodeOperatorFeeRecipient","type":"address"}],"name":"NodeOperatorFeeRecipientSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"newAdjustment","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"oldAdjustment","type":"uint256"}],"name":"RewardsAdjustmentSet","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":"role","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":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":"CHANGE_TIER_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":"ETH","outputs":[{"internalType":"address","name":"","type":"address"}],"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":"MANUAL_REWARDS_ADJUSTMENT_LIMIT","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"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_MANAGER_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"NODE_OPERATOR_REWARDS_ADJUST_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":"PDG_COMPENSATE_PREDEPOSIT_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"PDG_PROVE_VALIDATOR_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":"RECOVER_ASSETS_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":"UNGUARANTEED_BEACON_CHAIN_DEPOSIT_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":[{"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":"bytes","name":"_pubkey","type":"bytes"},{"internalType":"address","name":"_recipient","type":"address"}],"name":"compensateDisprovenPredepositFromPDG","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"}],"name":"connectAndAcceptTier","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"connectToVaultHub","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"disburseNodeOperatorFee","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"feePeriodStartReport","outputs":[{"internalType":"uint112","name":"totalValue","type":"uint112"},{"internalType":"int112","name":"inOutDelta","type":"int112"},{"internalType":"uint32","name":"timestamp","type":"uint32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"forcedRebalanceThresholdBP","outputs":[{"internalType":"uint16","name":"","type":"uint16"}],"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":[{"internalType":"uint256","name":"_adjustmentIncrease","type":"uint256"}],"name":"increaseRewardsAdjustment","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"infraFeeBP","outputs":[{"internalType":"uint16","name":"","type":"uint16"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_defaultAdmin","type":"address"},{"internalType":"address","name":"_nodeOperatorManager","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":"latestReport","outputs":[{"components":[{"internalType":"uint112","name":"totalValue","type":"uint112"},{"internalType":"int112","name":"inOutDelta","type":"int112"},{"internalType":"uint32","name":"timestamp","type":"uint32"}],"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":"liquidityFeeBP","outputs":[{"internalType":"uint16","name":"","type":"uint16"}],"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":[{"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":"nodeOperatorDisbursableFee","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"nodeOperatorFeeRate","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"nodeOperatorFeeRecipient","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"pauseBeaconChainDeposits","outputs":[],"stateMutability":"nonpayable","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":[],"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":"address","name":"_token","type":"address"},{"internalType":"uint256","name":"_tokenId","type":"uint256"},{"internalType":"address","name":"_recipient","type":"address"}],"name":"recoverERC721","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":"reservationFeeBP","outputs":[{"internalType":"uint16","name":"","type":"uint16"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"reserveRatioBP","outputs":[{"internalType":"uint16","name":"","type":"uint16"}],"stateMutability":"view","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":[],"name":"rewardsAdjustment","outputs":[{"internalType":"uint128","name":"amount","type":"uint128"},{"internalType":"uint64","name":"latestTimestamp","type":"uint64"}],"stateMutability":"view","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":"_newNodeOperatorFeeRate","type":"uint256"}],"name":"setNodeOperatorFeeRate","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_newNodeOperatorFeeRecipient","type":"address"}],"name":"setNodeOperatorFeeRecipient","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_proposedAdjustment","type":"uint256"},{"internalType":"uint256","name":"_expectedAdjustment","type":"uint256"}],"name":"setRewardsAdjustment","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"shareLimit","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"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":"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":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes","name":"_pubkeys","type":"bytes"},{"internalType":"uint64[]","name":"_amounts","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":[],"name":"unsettledObligations","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","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":"bool","name":"pendingDisconnect","type":"bool"},{"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":"isBeaconDepositsManuallyPaused","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.