Hoodi Testnet

Contract

0xC0F6BaAABA3397FDeD43a0f4e421D2F4574dfc04

Overview

ETH Balance

0 ETH

More Info

Multichain Info

N/A
Transaction Hash
Method
Block
From
To
Amount
Burn Shares16668112025-11-21 14:25:4817 days ago1763735148IN
0xC0F6BaAA...4574dfc04
0 ETH0.000410122.05779287
Prove Unknown Va...16453162025-11-18 9:13:4820 days ago1763457228IN
0xC0F6BaAA...4574dfc04
0 ETH0.000601543.02565712
Unguaranteed Dep...16400552025-11-17 14:06:3621 days ago1763388396IN
0xC0F6BaAA...4574dfc04
0 ETH0.000249671.00589671
Fund16121112025-11-13 10:33:4825 days ago1763030028IN
0xC0F6BaAA...4574dfc04
0.02 ETH0.000125421.27733726
Fund16120852025-11-13 10:28:0025 days ago1763029680IN
0xC0F6BaAA...4574dfc04
0.02 ETH0.000092520.9422235
Fund16119842025-11-13 10:06:3625 days ago1763028396IN
0xC0F6BaAA...4574dfc04
0.02 ETH0.000107641.0910804
Trigger Validato...15999732025-11-11 15:48:4827 days ago1762876128IN
0xC0F6BaAA...4574dfc04
1 wei0.000243811.05788411
Trigger Validato...15991092025-11-11 12:41:3627 days ago1762864896IN
0xC0F6BaAA...4574dfc04
0.1 ETH0.000387461.63238698
Burn Wst ETH15934502025-11-10 16:17:4828 days ago1762791468IN
0xC0F6BaAA...4574dfc04
0 ETH0.000273041.10273041
Prove Unknown Va...15928802025-11-10 14:18:0028 days ago1762784280IN
0xC0F6BaAA...4574dfc04
0 ETH0.000409182.05800204
Burn St ETH15928312025-11-10 14:07:4828 days ago1762783668IN
0xC0F6BaAA...4574dfc04
0 ETH0.000232951.13112822
Burn Shares15926092025-11-10 13:19:1228 days ago1762780752IN
0xC0F6BaAA...4574dfc04
0 ETH0.000223141.11926532
Fund15925872025-11-10 13:14:0028 days ago1762780440IN
0xC0F6BaAA...4574dfc04
0.02 ETH0.00010361.05513704
Fund15925722025-11-10 13:10:3628 days ago1762780236IN
0xC0F6BaAA...4574dfc04
0.02 ETH0.000097720.99056147
Unguaranteed Dep...15923272025-11-10 12:19:2428 days ago1762777164IN
0xC0F6BaAA...4574dfc04
0 ETH0.000263711.07739111
Fund15923222025-11-10 12:18:2428 days ago1762777104IN
0xC0F6BaAA...4574dfc04
2,048 ETH0.000107831.09296968
Prove Unknown Va...15750762025-11-07 20:16:0031 days ago1762546560IN
0xC0F6BaAA...4574dfc04
0 ETH0.000395551.9891941
Grant Role15735852025-11-07 14:48:0031 days ago1762526880IN
0xC0F6BaAA...4574dfc04
0 ETH0.000150511.24075513
Prove Unknown Va...15734552025-11-07 14:18:0031 days ago1762525080IN
0xC0F6BaAA...4574dfc04
0 ETH0.000410392.06383336
Mint Wst ETH15734132025-11-07 14:08:4831 days ago1762524528IN
0xC0F6BaAA...4574dfc04
0 ETH0.000356791.25059241
Mint Shares15734052025-11-07 14:07:0031 days ago1762524420IN
0xC0F6BaAA...4574dfc04
0 ETH0.000644562.97276565
Mint Shares15733862025-11-07 14:03:1231 days ago1762524192IN
0xC0F6BaAA...4574dfc04
0 ETH0.000235211.0848349
Mint St ETH15733752025-11-07 14:00:4831 days ago1762524048IN
0xC0F6BaAA...4574dfc04
0 ETH0.000294471.32264769
Withdraw15733582025-11-07 13:57:1231 days ago1762523832IN
0xC0F6BaAA...4574dfc04
0 ETH0.000228861.15818125
Withdraw15733532025-11-07 13:56:1231 days ago1762523772IN
0xC0F6BaAA...4574dfc04
0 ETH0.00022431.13510681
View all transactions

Latest 25 internal transactions (View All)

Advanced mode:
Parent Transaction Hash Method Block
From
To
Amount
Burn Shares16668112025-11-21 14:25:4817 days ago1763735148
0xC0F6BaAA...4574dfc04
0 ETH
Transfer*16668112025-11-21 14:25:4817 days ago1763735148
0xC0F6BaAA...4574dfc04
0 ETH
Burn Shares16668112025-11-21 14:25:4817 days ago1763735148
0xC0F6BaAA...4574dfc04
0 ETH
Prove Unknown Va...16453162025-11-18 9:13:4820 days ago1763457228
0xC0F6BaAA...4574dfc04
0 ETH
Prove Unknown Va...16453162025-11-18 9:13:4820 days ago1763457228
0xC0F6BaAA...4574dfc04
0 ETH
Transfer*16400552025-11-17 14:06:3621 days ago1763388396
0xC0F6BaAA...4574dfc04
36 ETH
Withdrawal Crede...16400552025-11-17 14:06:3621 days ago1763388396
0xC0F6BaAA...4574dfc04
0 ETH
Transfer16400552025-11-17 14:06:3621 days ago1763388396
0xC0F6BaAA...4574dfc04
36 ETH
Transfer16400552025-11-17 14:06:3621 days ago1763388396
0xC0F6BaAA...4574dfc04
36 ETH
Withdraw16400552025-11-17 14:06:3621 days ago1763388396
0xC0F6BaAA...4574dfc04
0 ETH
Latest Report16400552025-11-17 14:06:3621 days ago1763388396
0xC0F6BaAA...4574dfc04
0 ETH
Withdrawable Val...16400552025-11-17 14:06:3621 days ago1763388396
0xC0F6BaAA...4574dfc04
0 ETH
DEPOSIT_CONTRACT16400552025-11-17 14:06:3621 days ago1763388396
0xC0F6BaAA...4574dfc04
0 ETH
Unguaranteed Dep...16400552025-11-17 14:06:3621 days ago1763388396
0xC0F6BaAA...4574dfc04
0 ETH
Fund16121112025-11-13 10:33:4825 days ago1763030028
0xC0F6BaAA...4574dfc04
0.02 ETH
Fund16121112025-11-13 10:33:4825 days ago1763030028
0xC0F6BaAA...4574dfc04
0.02 ETH
Fund16120852025-11-13 10:28:0025 days ago1763029680
0xC0F6BaAA...4574dfc04
0.02 ETH
Fund16120852025-11-13 10:28:0025 days ago1763029680
0xC0F6BaAA...4574dfc04
0.02 ETH
Fund16119842025-11-13 10:06:3625 days ago1763028396
0xC0F6BaAA...4574dfc04
0.02 ETH
Fund16119842025-11-13 10:06:3625 days ago1763028396
0xC0F6BaAA...4574dfc04
0.02 ETH
Trigger Validato...15999732025-11-11 15:48:4827 days ago1762876128
0xC0F6BaAA...4574dfc04
1 wei
Trigger Validato...15999732025-11-11 15:48:4827 days ago1762876128
0xC0F6BaAA...4574dfc04
1 wei
Trigger Validato...15991092025-11-11 12:41:3627 days ago1762864896
0xC0F6BaAA...4574dfc04
0.1 ETH
Trigger Validato...15991092025-11-11 12:41:3627 days ago1762864896
0xC0F6BaAA...4574dfc04
0.1 ETH
Burn Shares15934502025-11-10 16:17:4828 days ago1762791468
0xC0F6BaAA...4574dfc04
0 ETH
View All Internal Transactions
Loading...
Loading

Block Transaction Difficulty Gas Used Reward
View All Blocks Produced

Validator Index Block Amount
View All Withdrawals

Transaction Hash Block Value Eth2 PubKey Valid
View All Deposits
Loading...
Loading

Minimal Proxy Contract for 0x7ca203e3b7341341a4a83086780137eb283a9338

Contract Name:
Dashboard

Compiler Version
v0.8.25+commit.b61c2a91

Optimization Enabled:
Yes with 200 runs

Other Settings:
cancun EvmVersion

Contract Source Code (Solidity Standard Json-Input format)

File 1 of 50 : Dashboard.sol
// SPDX-FileCopyrightText: 2025 Lido <[email protected]>
// SPDX-License-Identifier: GPL-3.0

// See contracts/COMPILERS.md
pragma solidity 0.8.25;

import {SafeERC20} from "@openzeppelin/contracts-v5.2/token/ERC20/utils/SafeERC20.sol";
import {IERC20} from "@openzeppelin/contracts-v5.2/token/ERC20/IERC20.sol";
import {RecoverTokens} from "../lib/RecoverTokens.sol";

import {ILido as IStETH} from "contracts/common/interfaces/ILido.sol";
import {IDepositContract} from "contracts/common/interfaces/IDepositContract.sol";

import {IStakingVault} from "../interfaces/IStakingVault.sol";
import {IPredepositGuarantee} from "../interfaces/IPredepositGuarantee.sol";
import {NodeOperatorFee} from "./NodeOperatorFee.sol";
import {VaultHub} from "../VaultHub.sol";

interface IWstETH is IERC20 {
    function wrap(uint256 _stETHAmount) external returns (uint256);

    function unwrap(uint256 _wstETHAmount) external returns (uint256);
}

/**
 * @title Dashboard
 * @notice This contract is a UX-layer for StakingVault and meant to be used as its owner.
 * This contract improves the vault UX by bundling all functions from the StakingVault and VaultHub
 * in this single contract. It provides administrative functions for managing the StakingVault,
 * including funding, withdrawing, minting, burning, and rebalancing operations.
 */
contract Dashboard is NodeOperatorFee {
    /// @dev 0xb694d4d19c77484e8f232470d9bf7e10450638db998b577a833d46df71fb6d97
    bytes32 public constant COLLECT_VAULT_ERC20_ROLE = keccak256("vaults.Dashboard.CollectVaultERC20");

    /**
     * @notice The stETH token contract
     */
    IStETH public immutable STETH;

    /**
     * @notice The wstETH token contract
     */
    IWstETH public immutable WSTETH;

    /**
     * @notice Slot for the fund-on-receive flag
     *         keccak256("vaults.Dashboard.fundOnReceive")
     */
    bytes32 public constant FUND_ON_RECEIVE_FLAG_SLOT =
        0x7408b7b034fda7051615c19182918ecb91d753231cffd86f81a45d996d63e038;

    /**
     * @notice The PDG policy modes.
     * "STRICT": deposits require the full PDG process.
     * "ALLOW_PROVE": allows the node operator to prove unknown validators to PDG.
     * "ALLOW_DEPOSIT_AND_PROVE": allows the node operator to perform unguaranteed deposits
     * (bypassing the predeposit requirement) and proving unknown validators.
     */
    enum PDGPolicy {
        STRICT,
        ALLOW_PROVE,
        ALLOW_DEPOSIT_AND_PROVE
    }

    /**
     * @notice Current active PDG policy set by `DEFAULT_ADMIN_ROLE`.
     */
    PDGPolicy public pdgPolicy = PDGPolicy.STRICT;

    /**
     * @notice Constructor sets the stETH, and WSTETH token addresses,
     * and passes the address of the vault hub up the inheritance chain.
     * @param _stETH Address of the stETH token contract.
     * @param _wstETH Address of the wstETH token contract.
     * @param _vaultHub Address of the vault hub contract.
     * @param _lidoLocator Address of the Lido locator contract.
     */
    constructor(
        address _stETH,
        address _wstETH,
        address _vaultHub,
        address _lidoLocator
    ) NodeOperatorFee(_vaultHub, _lidoLocator) {
        _requireNotZero(_stETH);
        _requireNotZero(_wstETH);

        // stETH and wstETH are cached as immutable to save gas for main operations
        STETH = IStETH(_stETH);
        WSTETH = IWstETH(_wstETH);
    }

    /**
     * @notice Calls the parent's initializer and approves the max allowance for WSTETH for gas savings
     * @param _defaultAdmin The address of the default admin
     * @param _nodeOperatorManager The address of the node operator manager
     * @param _nodeOperatorFeeRecipient The address of the node operator fee recipient
     * @param _nodeOperatorFeeBP The node operator fee in basis points
     * @param _confirmExpiry The confirmation expiry time in seconds
     */
    function initialize(
        address _defaultAdmin,
        address _nodeOperatorManager,
        address _nodeOperatorFeeRecipient,
        uint256 _nodeOperatorFeeBP,
        uint256 _confirmExpiry
    ) external {
        super._initialize(
            _defaultAdmin,
            _nodeOperatorManager,
            _nodeOperatorFeeRecipient,
            _nodeOperatorFeeBP,
            _confirmExpiry
        );

        // reduces gas cost for `mintWsteth`
        // invariant: dashboard does not hold stETH on its balance
        STETH.approve(address(WSTETH), type(uint256).max);
    }

    // ==================== View Functions ====================

    /**
     * @notice Returns the vault connection data for the staking vault.
     * @return VaultConnection struct containing vault data
     */
    function vaultConnection() public view returns (VaultHub.VaultConnection memory) {
        return VAULT_HUB.vaultConnection(address(_stakingVault()));
    }

    /**
     * @notice Returns the number of stETH shares minted
     */
    function liabilityShares() public view returns (uint256) {
        return VAULT_HUB.liabilityShares(address(_stakingVault()));
    }

    /**
     * @notice Returns the total value of the vault in ether.
     */
    function totalValue() external view returns (uint256) {
        return VAULT_HUB.totalValue(address(_stakingVault()));
    }

    /**
     * @notice Returns the locked amount of ether for the vault
     */
    function locked() external view returns (uint256) {
        return VAULT_HUB.locked(address(_stakingVault()));
    }

    /**
     * @notice Returns the amount of shares to burn to restore vault healthiness or to cover redemptions and the
     *         amount of outstanding Lido fees
     * @return sharesToBurn amount of shares to burn or to rebalance
     * @return feesToSettle amount of Lido fees to be settled
     */
    function obligations() external view returns (uint256 sharesToBurn, uint256 feesToSettle) {
        (sharesToBurn, feesToSettle) = VAULT_HUB.obligations(address(_stakingVault()));
    }

    /**
     * @notice Returns the amount of shares to rebalance to restore vault healthiness or to cover redemptions
     * @dev returns UINT256_MAX if it's impossible to make the vault healthy using rebalance
     */
    function healthShortfallShares() external view returns (uint256) {
        return VAULT_HUB.healthShortfallShares(address(_stakingVault()));
    }

    /**
     * @notice Returns the amount of ether required to cover obligations shortfall of the vault
     * @dev returns UINT256_MAX if it's impossible to cover obligations shortfall
     * @dev NB: obligationsShortfallValue includes healthShortfallShares converted to ether and any unsettled Lido fees
     *          in case they are greater than the minimum beacon deposit
     */
    function obligationsShortfallValue() external view returns (uint256) {
        return VAULT_HUB.obligationsShortfallValue(address(_stakingVault()));
    }

    /**
     * @notice Returns the amount of ether that is locked on the vault only as a reserve.
     * @dev There is no way to mint stETH for it (it includes connection deposit and slashing reserve)
     */
    function minimalReserve() public view returns (uint256) {
        return VAULT_HUB.vaultRecord(address(_stakingVault())).minimalReserve;
    }

    /**
     * @notice Returns the max total lockable amount of ether for the vault (excluding the Lido and node operator fees)
     */
    function maxLockableValue() external view returns (uint256) {
        uint256 maxLockableValue_ = VAULT_HUB.maxLockableValue(address(_stakingVault()));
        uint256 nodeOperatorFee = accruedFee();

        return maxLockableValue_ > nodeOperatorFee ? maxLockableValue_ - nodeOperatorFee : 0;
    }

    /**
     * @notice Returns the overall capacity for stETH shares that can be minted by the vault
     */
    function totalMintingCapacityShares() external view returns (uint256) {
        return _totalMintingCapacityShares(-int256(accruedFee()));
    }

    /**
     * @notice Returns the remaining capacity for stETH shares that can be minted
     *         by the vault if additional ether is funded
     * @param _etherToFund the amount of ether to be funded, can be zero
     * @return the number of shares that can be minted using additional ether
     */
    function remainingMintingCapacityShares(uint256 _etherToFund) public view returns (uint256) {
        int256 deltaValue = int256(_etherToFund) - int256(accruedFee());
        uint256 vaultTotalMintingCapacityShares = _totalMintingCapacityShares(deltaValue);
        uint256 vaultLiabilityShares = liabilityShares();

        if (vaultTotalMintingCapacityShares <= vaultLiabilityShares) return 0;

        return vaultTotalMintingCapacityShares - vaultLiabilityShares;
    }

    /**
     * @notice Returns the amount of ether that can be instantly withdrawn from the staking vault.
     * @dev This is the amount of ether that is not locked in the StakingVault and not reserved for fees and obligations.
     */
    function withdrawableValue() public view returns (uint256) {
        uint256 withdrawable = VAULT_HUB.withdrawableValue(address(_stakingVault()));
        uint256 nodeOperatorFee = accruedFee();

        return withdrawable > nodeOperatorFee ? withdrawable - nodeOperatorFee : 0;
    }

    // ==================== Vault Management Functions ====================

    /**
     * @dev Automatically funds the staking vault with ether
     */
    receive() external payable {
        if (_shouldFundOnReceive()) _fund(msg.value);
    }

    /**
     * @notice Transfers the ownership of the underlying StakingVault from this contract to a new owner
     *         without disconnecting it from the hub
     * @param _newOwner Address of the new owner.
     * @return bool True if the ownership transfer was executed, false if pending for confirmation
     */
    function transferVaultOwnership(address _newOwner) external returns (bool) {
        return _transferVaultOwnership(_newOwner);
    }

    /**
     * @notice Disconnects the underlying StakingVault from the hub and passing its ownership to Dashboard.
     *         After receiving the final report, one can call reconnectToVaultHub() to reconnect to the hub
     *         or abandonDashboard() to transfer the ownership to a new owner.
     */
    function voluntaryDisconnect() external {
        disburseFee();
        _voluntaryDisconnect();
    }

    /**
     * @notice Accepts the ownership over the disconnected StakingVault transferred from VaultHub
     *         and immediately passes it to a new pending owner. This new owner will have to accept the ownership
     *         on the StakingVault contract.
     *         Resets the settled growth to 0 to encourage correction before reconnection.
     * @param _newOwner The address to transfer the StakingVault ownership to.
     */
    function abandonDashboard(address _newOwner) external {
        if (VAULT_HUB.isVaultConnected(address(_stakingVault()))) revert ConnectedToVaultHub();
        if (_newOwner == address(this)) revert DashboardNotAllowed();
        if (settledGrowth != 0) _setSettledGrowth(0);

        _acceptOwnership();
        _transferOwnership(_newOwner);
    }

    /**
     * @notice Accepts the ownership over the StakingVault and connects to VaultHub. Can be called to reconnect
     *         to the hub after voluntaryDisconnect()
     * @param _currentSettledGrowth The current settled growth value to verify against the stored one
     */
    function reconnectToVaultHub(uint256 _currentSettledGrowth) external {
        _acceptOwnership();
        connectToVaultHub(_currentSettledGrowth);
    }

    /**
     * @notice Connects to VaultHub, transferring underlying StakingVault ownership to VaultHub.
     * @param _currentSettledGrowth The current settled growth value to verify against the stored one
     */
    function connectToVaultHub(uint256 _currentSettledGrowth) public payable {
        if (settledGrowth != int256(_currentSettledGrowth)) {
            revert SettledGrowthMismatch();
        }
        if (msg.value > 0) _stakingVault().fund{value: msg.value}();
        _transferOwnership(address(VAULT_HUB));
        VAULT_HUB.connectVault(address(_stakingVault()));
    }

    /**
     * @notice Changes the tier of the vault and connects to VaultHub
     * @param _tierId The tier to change to
     * @param _requestedShareLimit The requested share limit
     * @param _currentSettledGrowth The current settled growth value to verify against the stored one
     */
    function connectAndAcceptTier(uint256 _tierId, uint256 _requestedShareLimit, uint256 _currentSettledGrowth) external payable {
        connectToVaultHub(_currentSettledGrowth);
        if (!_changeTier(_tierId, _requestedShareLimit)) {
            revert TierChangeNotConfirmed();
        }
    }

    /**
     * @notice Funds the staking vault with ether
     */
    function fund() external payable {
        _fund(msg.value);
    }

    /**
     * @notice Withdraws ether from the staking vault to a recipient
     * @param _recipient Address of the recipient
     * @param _ether Amount of ether to withdraw
     */
    function withdraw(address _recipient, uint256 _ether) external {
        uint256 withdrawableEther = withdrawableValue();
        if (_ether > withdrawableEther) {
            revert ExceedsWithdrawable(_ether, withdrawableEther);
        }

        _withdraw(_recipient, _ether);
    }

    /**
     * @notice Mints stETH shares backed by the vault to the recipient.
     * @param _recipient Address of the recipient
     * @param _amountOfShares Amount of stETH shares to mint
     */
    function mintShares(address _recipient, uint256 _amountOfShares) external payable fundable {
        _mintSharesWithinMintingCapacity(_recipient, _amountOfShares);
    }

    /**
     * @notice Mints stETH tokens backed by the vault to the recipient.
     * !NB: this will revert with `ZeroArgument()` if the amount of stETH is less than 1 share
     * @param _recipient Address of the recipient
     * @param _amountOfStETH Amount of stETH to mint
     */
    function mintStETH(address _recipient, uint256 _amountOfStETH) external payable fundable {
        _mintSharesWithinMintingCapacity(_recipient, _getSharesByPooledEth(_amountOfStETH));
    }

    /**
     * @notice Mints wstETH tokens backed by the vault to a recipient.
     * @param _recipient Address of the recipient
     * @param _amountOfWstETH Amount of tokens to mint
     */
    function mintWstETH(address _recipient, uint256 _amountOfWstETH) external payable fundable {
        _mintSharesWithinMintingCapacity(address(this), _amountOfWstETH);

        uint256 mintedStETH = STETH.getPooledEthBySharesRoundUp(_amountOfWstETH);

        uint256 wrappedWstETH = WSTETH.wrap(mintedStETH);
        SafeERC20.safeTransfer(WSTETH, _recipient, wrappedWstETH);
    }

    /**
     * @notice Burns stETH shares from the sender backed by the vault.
     *         Expects corresponding amount of stETH approved to this contract.
     * @param _amountOfShares Amount of stETH shares to burn
     */
    function burnShares(uint256 _amountOfShares) external {
        STETH.transferSharesFrom(msg.sender, address(VAULT_HUB), _amountOfShares);
        _burnShares(_amountOfShares);
    }

    /**
     * @notice Burns stETH tokens from the sender backed by the vault. Expects stETH amount approved to this contract.
     * !NB: this will revert with `ZeroArgument()` if the amount of stETH is less than 1 share
     * @param _amountOfStETH Amount of stETH tokens to burn
     */
    function burnStETH(uint256 _amountOfStETH) external {
        _burnStETH(_amountOfStETH);
    }

    /**
     * @notice Burns wstETH tokens from the sender backed by the vault. Expects wstETH amount approved to this contract.
     * @dev !NB: this will revert with `ZeroArgument()` on 1 wei of wstETH due to rounding inside wstETH unwrap method
     * @param _amountOfWstETH Amount of wstETH tokens to burn
     */
    function burnWstETH(uint256 _amountOfWstETH) external {
        _burnWstETH(_amountOfWstETH);
    }

    /**
     * @notice Rebalances the vault's position by transferring ether corresponding to the passed `_shares`
     *         number to Lido Core and writing it off from the vault's liability.
     * @param _shares amount of shares to rebalance
     */
    function rebalanceVaultWithShares(uint256 _shares) external {
        _rebalanceVault(_shares);
    }

    /**
     * @notice Rebalances the vault by transferring ether and writing off the respective shares amount fro the vault's
     *         liability
     * @param _ether amount of ether to rebalance
     * @dev the amount of ether transferred can differ a bit because of the rounding
     */
    function rebalanceVaultWithEther(uint256 _ether) external payable fundable {
        _rebalanceVault(_getSharesByPooledEth(_ether));
    }

    /**
     * @notice Changes the PDG policy. PDGPolicy regulates the possibility of deposits without PredepositGuarantee
     * @param _pdgPolicy new PDG policy
     */
    function setPDGPolicy(PDGPolicy _pdgPolicy) external onlyRoleMemberOrAdmin(DEFAULT_ADMIN_ROLE) {
        if (_pdgPolicy == pdgPolicy) revert PDGPolicyAlreadyActive();

        pdgPolicy = _pdgPolicy;

        emit PDGPolicyEnacted(_pdgPolicy);
    }

    /**
     * @notice Withdraws ether from vault and deposits directly to provided validators bypassing the default PDG process,
     *          allowing validators to be proven post-factum via `proveUnknownValidatorsToPDG`
     *          clearing them for future deposits via `PDG.topUpValidators`
     * @param _deposits array of IStakingVault.Deposit structs containing deposit data
     * @return totalAmount total amount of ether deposited to beacon chain
     * @dev requires the PDG policy set to `ALLOW_DEPOSIT_AND_PROVE`
     * @dev requires the caller to have the `NODE_OPERATOR_UNGUARANTEED_DEPOSIT_ROLE`
     * @dev Warning! vulnerable to deposit frontrunning and requires putting trust on the node operator
     */
    function unguaranteedDepositToBeaconChain(
        IStakingVault.Deposit[] calldata _deposits
    ) external returns (uint256 totalAmount) {
        if (pdgPolicy != PDGPolicy.ALLOW_DEPOSIT_AND_PROVE) revert ForbiddenByPDGPolicy();

        IStakingVault stakingVault_ = _stakingVault();
        IDepositContract depositContract = stakingVault_.DEPOSIT_CONTRACT();

        for (uint256 i = 0; i < _deposits.length; i++) {
            totalAmount += _deposits[i].amount;
        }

        uint256 withdrawableEther = withdrawableValue();
        if (totalAmount > withdrawableEther) {
            revert ExceedsWithdrawable(totalAmount, withdrawableEther);
        }

        _disableFundOnReceive();
        _withdrawForUnguaranteedDepositToBeaconChain(totalAmount);
        // Instead of relying on auto-reset at the end of the transaction,
        // re-enable fund-on-receive manually to restore the default receive() behavior in the same transaction
        _enableFundOnReceive();
        _addFeeExemption(totalAmount);

        bytes memory withdrawalCredentials = bytes.concat(stakingVault_.withdrawalCredentials());

        IStakingVault.Deposit calldata deposit;
        for (uint256 i = 0; i < _deposits.length; i++) {
            deposit = _deposits[i];
            depositContract.deposit{value: deposit.amount}(
                deposit.pubkey,
                withdrawalCredentials,
                deposit.signature,
                deposit.depositDataRoot
            );
        }

        emit UnguaranteedDeposits(address(stakingVault_), _deposits.length, totalAmount);
    }

    /**
     * @notice Proves validators with correct vault WC if they are unknown to PDG
     * @param _witnesses array of IPredepositGuarantee.ValidatorWitness structs containing proof data for validators
     * @dev requires the PDG policy set to `ALLOW_PROVE` or `ALLOW_DEPOSIT_AND_PROVE`
     * @dev requires the caller to have the `NODE_OPERATOR_PROVE_UNKNOWN_VALIDATOR_ROLE`
     */
    function proveUnknownValidatorsToPDG(IPredepositGuarantee.ValidatorWitness[] calldata _witnesses) external {
        if (pdgPolicy == PDGPolicy.STRICT) revert ForbiddenByPDGPolicy();

        _proveUnknownValidatorsToPDG(_witnesses);
    }

    /**
     * @notice Recovers ERC20 tokens or ether from the dashboard contract to the recipient
     * @param _token Address of the token to recover or 0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee for ether (EIP-7528)
     * @param _recipient Address of the recovery recipient
     * @param _amount Amount of tokens or ether to recover
     */
    function recoverERC20(
        address _token,
        address _recipient,
        uint256 _amount
    ) external onlyRoleMemberOrAdmin(DEFAULT_ADMIN_ROLE) {
        _requireNotZero(_token);
        _requireNotZero(_recipient);
        _requireNotZero(_amount);

        if (_token == RecoverTokens.ETH) {
            RecoverTokens._recoverEth(_recipient, _amount);
        } else {
            RecoverTokens._recoverERC20(_token, _recipient, _amount);
        }
    }

    /**
     * @notice Collects ERC20 tokens from vault contract balance to the recipient
     * @param _token Address of the token to collect
     * @param _recipient Address of the recipient
     * @param _amount Amount of tokens to collect
     * @dev will revert on EIP-7528 ETH address with EthCollectionNotAllowed() or on zero arguments with ZeroArgument()
     */
    function collectERC20FromVault(
        address _token,
        address _recipient,
        uint256 _amount
    ) external onlyRoleMemberOrAdmin(COLLECT_VAULT_ERC20_ROLE) {
        VAULT_HUB.collectERC20FromVault(address(_stakingVault()), _token, _recipient, _amount);
    }

    /**
     * @notice Pauses beacon chain deposits on the StakingVault.
     */
    function pauseBeaconChainDeposits() external {
        _pauseBeaconChainDeposits();
    }

    /**
     * @notice Resumes beacon chain deposits on the StakingVault.
     */
    function resumeBeaconChainDeposits() external {
        _resumeBeaconChainDeposits();
    }

    /**
     * @notice Signals to node operators that specific validators should exit from the beacon chain. It DOES NOT
     *         directly trigger the exit - node operators must monitor for request events and handle the exits.
     * @param _pubkeys Concatenated validator public keys (48 bytes each).
     * @dev    Emits `ValidatorExitRequested` event for each validator public key through the `StakingVault`.
     *         This is a voluntary exit request - node operators can choose whether to act on it or not.
     */
    function requestValidatorExit(bytes calldata _pubkeys) external {
        _requestValidatorExit(_pubkeys);
    }

    /**
     * @notice Initiates a withdrawal from validator(s) on the beacon chain using EIP-7002 triggerable withdrawals
     *         Both partial withdrawals (disabled for if vault is unhealthy) and full validator exits are supported.
     * @param _pubkeys Concatenated validator public keys (48 bytes each).
     * @param _amountsInGwei Withdrawal amounts in Gwei for each validator key. Must match _pubkeys length.
     *         Set amount to 0 for a full validator exit. For partial withdrawals, amounts may be trimmed to keep
     *         MIN_ACTIVATION_BALANCE on the validator to avoid deactivation.
     * @param _refundRecipient Address to receive any fee refunds
     * @dev    A withdrawal fee must be paid via msg.value.
     *         You can use `StakingVault.calculateValidatorWithdrawalFee()` to calculate the approximate fee amount but
     *         it's accurate only for the current block. The fee may change when the tx is included, so it's recommended
     *         to send some surplus. The exact amount required will be paid and the excess will be refunded to the
     *         `_refundRecipient` address. The fee required can grow exponentially, so limit msg.value wisely to avoid
     *         overspending.
     */
    function triggerValidatorWithdrawals(
        bytes calldata _pubkeys,
        uint64[] calldata _amountsInGwei,
        address _refundRecipient
    ) external payable {
        _triggerValidatorWithdrawals(_pubkeys, _amountsInGwei, _refundRecipient);
    }

    /**
     * @notice Requests a change of tier on the OperatorGrid.
     * @param _tierId The tier to change to.
     * @param _requestedShareLimit The requested share limit.
     * @return bool True if the tier change was executed, false if pending for confirmation.
     * @dev Tier change confirmation logic:
     *      - Both vault owner (via this function) AND node operator (via OperatorGrid) confirmations are always required
     *      - First call returns false (pending), second call with both confirmations completes the tier change
     *      - Confirmations expire after the configured period (default: 1 day)
     */
    function changeTier(uint256 _tierId, uint256 _requestedShareLimit) external returns (bool) {
        return _changeTier(_tierId, _requestedShareLimit);
    }

    /**
     * @notice Requests a sync of tier on the OperatorGrid.
     * @return bool True if the tier sync was executed, false if pending for confirmation.
     * @dev Tier sync confirmation logic:
     *      - Both vault owner (via this function) AND node operator (via OperatorGrid) confirmations are required
     *      - First call returns false (pending), second call with both confirmations completes the operation
     *      - Confirmations expire after the configured period (default: 1 day)
     */
    function syncTier() external returns (bool) {
        return _syncTier();
    }

    /**
     * @notice Requests a change of share limit on the OperatorGrid.
     * @param _requestedShareLimit The requested share limit.
     * @return bool True if the share limit change was executed, false if pending for confirmation.
     * @dev Share limit update confirmation logic:
     *      - Both vault owner (via this function) AND node operator (via OperatorGrid) confirmations required
     *      - First call returns false (pending), second call with node operator confirmation completes the operation
     *      - Confirmations expire after the configured period (default: 1 day)
     */
    function updateShareLimit(uint256 _requestedShareLimit) external returns (bool) {
        return _updateVaultShareLimit(_requestedShareLimit);
    }

    // ==================== Internal Functions ====================

    /**
     * @dev Modifier to fund the staking vault if msg.value > 0
     */
    modifier fundable() {
        if (msg.value > 0) {
            _fund(msg.value);
        }
        _;
    }

    /**
     * @notice Mints shares within the mintable capacity,
     *         and reverts if the resulting backing is greater than the mintable capacity.
     * @param _recipient The address of the recipient.
     * @param _amountOfShares The amount of shares to mint.
     */
    function _mintSharesWithinMintingCapacity(address _recipient, uint256 _amountOfShares) internal {
        uint256 remainingShares = remainingMintingCapacityShares(0);
        if (_amountOfShares > remainingShares) revert ExceedsMintingCapacity(_amountOfShares, remainingShares);

        _mintShares(_recipient, _amountOfShares);
    }

    /**
     * @dev Burns stETH tokens from the sender backed by the vault
     * @param _amountOfStETH Amount of tokens to burn
     */
    function _burnStETH(uint256 _amountOfStETH) internal {
        uint256 _amountOfShares = _getSharesByPooledEth(_amountOfStETH);
        STETH.transferSharesFrom(msg.sender, address(VAULT_HUB), _amountOfShares);
        _burnShares(_amountOfShares);
    }

    /**
     * @dev Burns wstETH tokens from the sender backed by the vault
     * @param _amountOfWstETH Amount of tokens to burn
     */
    function _burnWstETH(uint256 _amountOfWstETH) internal {
        SafeERC20.safeTransferFrom(WSTETH, msg.sender, address(this), _amountOfWstETH);
        uint256 unwrappedStETH = WSTETH.unwrap(_amountOfWstETH);
        uint256 unwrappedShares = _getSharesByPooledEth(unwrappedStETH);

        STETH.transferShares(address(VAULT_HUB), unwrappedShares);
        _burnShares(unwrappedShares);
    }

    /// @notice Calculates the total number of shares that is possible to mint on the vault
    /// @dev the delta value is the amount of ether to add or subtract from the total value of the vault
    function _totalMintingCapacityShares(int256 _deltaValue) internal view returns (uint256) {
        return VAULT_HUB.totalMintingCapacityShares(address(_stakingVault()), _deltaValue);
    }

    /// @notice Converts the given amount of stETH to shares
    function _getSharesByPooledEth(uint256 _amountOfStETH) internal view returns (uint256) {
        return STETH.getSharesByPooledEth(_amountOfStETH);
    }

    // @dev The logic is inverted, 0 means fund-on-receive is enabled,
    // so that fund-on-receive is enabled by default
    function _shouldFundOnReceive() internal view returns (bool shouldFund) {
        assembly {
            shouldFund := iszero(tload(FUND_ON_RECEIVE_FLAG_SLOT))
        }
    }

    function _enableFundOnReceive() internal {
        assembly {
            tstore(FUND_ON_RECEIVE_FLAG_SLOT, 0)
        }
    }

    function _disableFundOnReceive() internal {
        assembly {
            tstore(FUND_ON_RECEIVE_FLAG_SLOT, 1)
        }
    }

    /**
     * @dev Withdraws ether from vault to this contract for unguaranteed deposit to validators
     * Requires the caller to have the `NODE_OPERATOR_UNGUARANTEED_DEPOSIT_ROLE`.
     */
    function _withdrawForUnguaranteedDepositToBeaconChain(
        uint256 _ether
    ) internal onlyRoleMemberOrAdmin(NODE_OPERATOR_UNGUARANTEED_DEPOSIT_ROLE) {
        VAULT_HUB.withdraw(address(_stakingVault()), address(this), _ether);
    }

    /**
     * @dev Proves validators unknown to PDG that have correct vault WC
     * Requires the caller to have the `NODE_OPERATOR_PROVE_UNKNOWN_VALIDATOR_ROLE`.
     */
    function _proveUnknownValidatorsToPDG(
        IPredepositGuarantee.ValidatorWitness[] calldata _witnesses
    ) internal onlyRoleMemberOrAdmin(NODE_OPERATOR_PROVE_UNKNOWN_VALIDATOR_ROLE) {
        for (uint256 i = 0; i < _witnesses.length; i++) {
            VAULT_HUB.proveUnknownValidatorToPDG(address(_stakingVault()), _witnesses[i]);
        }
    }

    // ==================== Events ====================

    /**
     * @notice Emitted when ether was withdrawn from the staking vault and deposited to validators directly bypassing PDG
     * @param stakingVault the address of owned staking vault
     * @param deposits the number of deposits
     * @param totalAmount the total amount of ether deposited to beacon chain
     */
    event UnguaranteedDeposits(address indexed stakingVault, uint256 deposits, uint256 totalAmount);

    /**
     * @notice Emitted when the PDG policy is updated.
     */
    event PDGPolicyEnacted(PDGPolicy pdgPolicy);

    // ==================== Errors ====================

    /**
     * @notice Emitted when the withdrawable amount of ether is exceeded
     * @param amount The amount of ether that was attempted to be withdrawn
     * @param withdrawableValue The amount of withdrawable ether available
     */
    error ExceedsWithdrawable(uint256 amount, uint256 withdrawableValue);

    /**
     * @notice Error thrown when minting capacity is exceeded
     */
    error ExceedsMintingCapacity(uint256 requestedShares, uint256 remainingShares);

    /**
     * @notice Error when the StakingVault is still connected to the VaultHub.
     */
    error ConnectedToVaultHub();

    /**
     * @notice Error thrown when attempting to connect to VaultHub without confirmed tier change
     */
    error TierChangeNotConfirmed();

    /**
     * @notice Error when attempting to abandon the Dashboard contract itself.
     */
    error DashboardNotAllowed();

    /**
     * @notice Error when attempting to set the same PDG policy that is already active.
     */
    error PDGPolicyAlreadyActive();

    /**
     * @notice Error when attempting to perform an operation that is not allowed
     * by the current active PDG policy.
     */
    error ForbiddenByPDGPolicy();
}

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (token/ERC20/IERC20.sol)

pragma solidity ^0.8.0;

/**
 * @dev Interface of the ERC20 standard as defined in the EIP.
 */
interface IERC20 {
    /**
     * @dev Returns the amount of tokens in existence.
     */
    function totalSupply() external view returns (uint256);

    /**
     * @dev Returns the amount of tokens owned by `account`.
     */
    function balanceOf(address account) external view returns (uint256);

    /**
     * @dev Moves `amount` tokens from the caller's account to `recipient`.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * Emits a {Transfer} event.
     */
    function transfer(address recipient, uint256 amount) external returns (bool);

    /**
     * @dev Returns the remaining number of tokens that `spender` will be
     * allowed to spend on behalf of `owner` through {transferFrom}. This is
     * zero by default.
     *
     * This value changes when {approve} or {transferFrom} are called.
     */
    function allowance(address owner, address spender) external view returns (uint256);

    /**
     * @dev Sets `amount` as the allowance of `spender` over the caller's tokens.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * IMPORTANT: Beware that changing an allowance with this method brings the risk
     * that someone may use both the old and the new allowance by unfortunate
     * transaction ordering. One possible solution to mitigate this race
     * condition is to first reduce the spender's allowance to 0 and set the
     * desired value afterwards:
     * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
     *
     * Emits an {Approval} event.
     */
    function approve(address spender, uint256 amount) external returns (bool);

    /**
     * @dev Moves `amount` tokens from `sender` to `recipient` using the
     * allowance mechanism. `amount` is then deducted from the caller's
     * allowance.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * Emits a {Transfer} event.
     */
    function transferFrom(
        address sender,
        address recipient,
        uint256 amount
    ) external returns (bool);

    /**
     * @dev Emitted when `value` tokens are moved from one account (`from`) to
     * another (`to`).
     *
     * Note that `value` may be zero.
     */
    event Transfer(address indexed from, address indexed to, uint256 value);

    /**
     * @dev Emitted when the allowance of a `spender` for an `owner` is set by
     * a call to {approve}. `value` is the new allowance.
     */
    event Approval(address indexed owner, address indexed spender, uint256 value);
}

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (access/AccessControl.sol)

pragma solidity ^0.8.20;

import {IAccessControl} from "./IAccessControl.sol";
import {Context} from "../utils/Context.sol";
import {ERC165} from "../utils/introspection/ERC165.sol";

/**
 * @dev Contract module that allows children to implement role-based access
 * control mechanisms. This is a lightweight version that doesn't allow enumerating role
 * members except through off-chain means by accessing the contract event logs. Some
 * applications may benefit from on-chain enumerability, for those cases see
 * {AccessControlEnumerable}.
 *
 * Roles are referred to by their `bytes32` identifier. These should be exposed
 * in the external API and be unique. The best way to achieve this is by
 * using `public constant` hash digests:
 *
 * ```solidity
 * bytes32 public constant MY_ROLE = keccak256("MY_ROLE");
 * ```
 *
 * Roles can be used to represent a set of permissions. To restrict access to a
 * function call, use {hasRole}:
 *
 * ```solidity
 * function foo() public {
 *     require(hasRole(MY_ROLE, msg.sender));
 *     ...
 * }
 * ```
 *
 * Roles can be granted and revoked dynamically via the {grantRole} and
 * {revokeRole} functions. Each role has an associated admin role, and only
 * accounts that have a role's admin role can call {grantRole} and {revokeRole}.
 *
 * By default, the admin role for all roles is `DEFAULT_ADMIN_ROLE`, which means
 * that only accounts with this role will be able to grant or revoke other
 * roles. More complex role relationships can be created by using
 * {_setRoleAdmin}.
 *
 * WARNING: The `DEFAULT_ADMIN_ROLE` is also its own admin: it has permission to
 * grant and revoke this role. Extra precautions should be taken to secure
 * accounts that have been granted it. We recommend using {AccessControlDefaultAdminRules}
 * to enforce additional security measures for this role.
 */
abstract contract AccessControl is Context, IAccessControl, ERC165 {
    struct RoleData {
        mapping(address account => bool) hasRole;
        bytes32 adminRole;
    }

    mapping(bytes32 role => RoleData) private _roles;

    bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00;

    /**
     * @dev Modifier that checks that an account has a specific role. Reverts
     * with an {AccessControlUnauthorizedAccount} error including the required role.
     */
    modifier onlyRole(bytes32 role) {
        _checkRole(role);
        _;
    }

    /**
     * @dev See {IERC165-supportsInterface}.
     */
    function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
        return interfaceId == type(IAccessControl).interfaceId || super.supportsInterface(interfaceId);
    }

    /**
     * @dev Returns `true` if `account` has been granted `role`.
     */
    function hasRole(bytes32 role, address account) public view virtual returns (bool) {
        return _roles[role].hasRole[account];
    }

    /**
     * @dev Reverts with an {AccessControlUnauthorizedAccount} error if `_msgSender()`
     * is missing `role`. Overriding this function changes the behavior of the {onlyRole} modifier.
     */
    function _checkRole(bytes32 role) internal view virtual {
        _checkRole(role, _msgSender());
    }

    /**
     * @dev Reverts with an {AccessControlUnauthorizedAccount} error if `account`
     * is missing `role`.
     */
    function _checkRole(bytes32 role, address account) internal view virtual {
        if (!hasRole(role, account)) {
            revert AccessControlUnauthorizedAccount(account, role);
        }
    }

    /**
     * @dev Returns the admin role that controls `role`. See {grantRole} and
     * {revokeRole}.
     *
     * To change a role's admin, use {_setRoleAdmin}.
     */
    function getRoleAdmin(bytes32 role) public view virtual returns (bytes32) {
        return _roles[role].adminRole;
    }

    /**
     * @dev Grants `role` to `account`.
     *
     * If `account` had not been already granted `role`, emits a {RoleGranted}
     * event.
     *
     * Requirements:
     *
     * - the caller must have ``role``'s admin role.
     *
     * May emit a {RoleGranted} event.
     */
    function grantRole(bytes32 role, address account) public virtual onlyRole(getRoleAdmin(role)) {
        _grantRole(role, account);
    }

    /**
     * @dev Revokes `role` from `account`.
     *
     * If `account` had been granted `role`, emits a {RoleRevoked} event.
     *
     * Requirements:
     *
     * - the caller must have ``role``'s admin role.
     *
     * May emit a {RoleRevoked} event.
     */
    function revokeRole(bytes32 role, address account) public virtual onlyRole(getRoleAdmin(role)) {
        _revokeRole(role, account);
    }

    /**
     * @dev Revokes `role` from the calling account.
     *
     * Roles are often managed via {grantRole} and {revokeRole}: this function's
     * purpose is to provide a mechanism for accounts to lose their privileges
     * if they are compromised (such as when a trusted device is misplaced).
     *
     * If the calling account had been revoked `role`, emits a {RoleRevoked}
     * event.
     *
     * Requirements:
     *
     * - the caller must be `callerConfirmation`.
     *
     * May emit a {RoleRevoked} event.
     */
    function renounceRole(bytes32 role, address callerConfirmation) public virtual {
        if (callerConfirmation != _msgSender()) {
            revert AccessControlBadConfirmation();
        }

        _revokeRole(role, callerConfirmation);
    }

    /**
     * @dev Sets `adminRole` as ``role``'s admin role.
     *
     * Emits a {RoleAdminChanged} event.
     */
    function _setRoleAdmin(bytes32 role, bytes32 adminRole) internal virtual {
        bytes32 previousAdminRole = getRoleAdmin(role);
        _roles[role].adminRole = adminRole;
        emit RoleAdminChanged(role, previousAdminRole, adminRole);
    }

    /**
     * @dev Attempts to grant `role` to `account` and returns a boolean indicating if `role` was granted.
     *
     * Internal function without access restriction.
     *
     * May emit a {RoleGranted} event.
     */
    function _grantRole(bytes32 role, address account) internal virtual returns (bool) {
        if (!hasRole(role, account)) {
            _roles[role].hasRole[account] = true;
            emit RoleGranted(role, account, _msgSender());
            return true;
        } else {
            return false;
        }
    }

    /**
     * @dev Attempts to revoke `role` to `account` and returns a boolean indicating if `role` was revoked.
     *
     * Internal function without access restriction.
     *
     * May emit a {RoleRevoked} event.
     */
    function _revokeRole(bytes32 role, address account) internal virtual returns (bool) {
        if (hasRole(role, account)) {
            _roles[role].hasRole[account] = false;
            emit RoleRevoked(role, account, _msgSender());
            return true;
        } else {
            return false;
        }
    }
}

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (access/extensions/AccessControlEnumerable.sol)

pragma solidity ^0.8.20;

import {IAccessControlEnumerable} from "./IAccessControlEnumerable.sol";
import {AccessControl} from "../AccessControl.sol";
import {EnumerableSet} from "../../utils/structs/EnumerableSet.sol";

/**
 * @dev Extension of {AccessControl} that allows enumerating the members of each role.
 */
abstract contract AccessControlEnumerable is IAccessControlEnumerable, AccessControl {
    using EnumerableSet for EnumerableSet.AddressSet;

    mapping(bytes32 role => EnumerableSet.AddressSet) private _roleMembers;

    /**
     * @dev See {IERC165-supportsInterface}.
     */
    function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
        return interfaceId == type(IAccessControlEnumerable).interfaceId || super.supportsInterface(interfaceId);
    }

    /**
     * @dev Returns one of the accounts that have `role`. `index` must be a
     * value between 0 and {getRoleMemberCount}, non-inclusive.
     *
     * Role bearers are not sorted in any particular way, and their ordering may
     * change at any point.
     *
     * WARNING: When using {getRoleMember} and {getRoleMemberCount}, make sure
     * you perform all queries on the same block. See the following
     * https://forum.openzeppelin.com/t/iterating-over-elements-on-enumerableset-in-openzeppelin-contracts/2296[forum post]
     * for more information.
     */
    function getRoleMember(bytes32 role, uint256 index) public view virtual returns (address) {
        return _roleMembers[role].at(index);
    }

    /**
     * @dev Returns the number of accounts that have `role`. Can be used
     * together with {getRoleMember} to enumerate all bearers of a role.
     */
    function getRoleMemberCount(bytes32 role) public view virtual returns (uint256) {
        return _roleMembers[role].length();
    }

    /**
     * @dev Return all accounts that have `role`
     *
     * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed
     * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that
     * this function has an unbounded cost, and using it as part of a state-changing function may render the function
     * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.
     */
    function getRoleMembers(bytes32 role) public view virtual returns (address[] memory) {
        return _roleMembers[role].values();
    }

    /**
     * @dev Overload {AccessControl-_grantRole} to track enumerable memberships
     */
    function _grantRole(bytes32 role, address account) internal virtual override returns (bool) {
        bool granted = super._grantRole(role, account);
        if (granted) {
            _roleMembers[role].add(account);
        }
        return granted;
    }

    /**
     * @dev Overload {AccessControl-_revokeRole} to track enumerable memberships
     */
    function _revokeRole(bytes32 role, address account) internal virtual override returns (bool) {
        bool revoked = super._revokeRole(role, account);
        if (revoked) {
            _roleMembers[role].remove(account);
        }
        return revoked;
    }
}

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (access/extensions/IAccessControlEnumerable.sol)

pragma solidity ^0.8.20;

import {IAccessControl} from "../IAccessControl.sol";

/**
 * @dev External interface of AccessControlEnumerable declared to support ERC-165 detection.
 */
interface IAccessControlEnumerable is IAccessControl {
    /**
     * @dev Returns one of the accounts that have `role`. `index` must be a
     * value between 0 and {getRoleMemberCount}, non-inclusive.
     *
     * Role bearers are not sorted in any particular way, and their ordering may
     * change at any point.
     *
     * WARNING: When using {getRoleMember} and {getRoleMemberCount}, make sure
     * you perform all queries on the same block. See the following
     * https://forum.openzeppelin.com/t/iterating-over-elements-on-enumerableset-in-openzeppelin-contracts/2296[forum post]
     * for more information.
     */
    function getRoleMember(bytes32 role, uint256 index) external view returns (address);

    /**
     * @dev Returns the number of accounts that have `role`. Can be used
     * together with {getRoleMember} to enumerate all bearers of a role.
     */
    function getRoleMemberCount(bytes32 role) external view returns (uint256);
}

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (access/IAccessControl.sol)

pragma solidity ^0.8.20;

/**
 * @dev External interface of AccessControl declared to support ERC-165 detection.
 */
interface IAccessControl {
    /**
     * @dev The `account` is missing a role.
     */
    error AccessControlUnauthorizedAccount(address account, bytes32 neededRole);

    /**
     * @dev The caller of a function is not the expected one.
     *
     * NOTE: Don't confuse with {AccessControlUnauthorizedAccount}.
     */
    error AccessControlBadConfirmation();

    /**
     * @dev Emitted when `newAdminRole` is set as ``role``'s admin role, replacing `previousAdminRole`
     *
     * `DEFAULT_ADMIN_ROLE` is the starting admin for all roles, despite
     * {RoleAdminChanged} not being emitted signaling this.
     */
    event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole);

    /**
     * @dev Emitted when `account` is granted `role`.
     *
     * `sender` is the account that originated the contract call. This account bears the admin role (for the granted role).
     * Expected in cases where the role was granted using the internal {AccessControl-_grantRole}.
     */
    event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender);

    /**
     * @dev Emitted when `account` is revoked `role`.
     *
     * `sender` is the account that originated the contract call:
     *   - if using `revokeRole`, it is the admin role bearer
     *   - if using `renounceRole`, it is the role bearer (i.e. `account`)
     */
    event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender);

    /**
     * @dev Returns `true` if `account` has been granted `role`.
     */
    function hasRole(bytes32 role, address account) external view returns (bool);

    /**
     * @dev Returns the admin role that controls `role`. See {grantRole} and
     * {revokeRole}.
     *
     * To change a role's admin, use {AccessControl-_setRoleAdmin}.
     */
    function getRoleAdmin(bytes32 role) external view returns (bytes32);

    /**
     * @dev Grants `role` to `account`.
     *
     * If `account` had not been already granted `role`, emits a {RoleGranted}
     * event.
     *
     * Requirements:
     *
     * - the caller must have ``role``'s admin role.
     */
    function grantRole(bytes32 role, address account) external;

    /**
     * @dev Revokes `role` from `account`.
     *
     * If `account` had been granted `role`, emits a {RoleRevoked} event.
     *
     * Requirements:
     *
     * - the caller must have ``role``'s admin role.
     */
    function revokeRole(bytes32 role, address account) external;

    /**
     * @dev Revokes `role` from the calling account.
     *
     * Roles are often managed via {grantRole} and {revokeRole}: this function's
     * purpose is to provide a mechanism for accounts to lose their privileges
     * if they are compromised (such as when a trusted device is misplaced).
     *
     * If the calling account had been granted `role`, emits a {RoleRevoked}
     * event.
     *
     * Requirements:
     *
     * - the caller must be `callerConfirmation`.
     */
    function renounceRole(bytes32 role, address callerConfirmation) external;
}

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (interfaces/IERC1363.sol)

pragma solidity ^0.8.20;

import {IERC20} from "./IERC20.sol";
import {IERC165} from "./IERC165.sol";

/**
 * @title IERC1363
 * @dev Interface of the ERC-1363 standard as defined in the https://eips.ethereum.org/EIPS/eip-1363[ERC-1363].
 *
 * Defines an extension interface for ERC-20 tokens that supports executing code on a recipient contract
 * after `transfer` or `transferFrom`, or code on a spender contract after `approve`, in a single transaction.
 */
interface IERC1363 is IERC20, IERC165 {
    /*
     * Note: the ERC-165 identifier for this interface is 0xb0202a11.
     * 0xb0202a11 ===
     *   bytes4(keccak256('transferAndCall(address,uint256)')) ^
     *   bytes4(keccak256('transferAndCall(address,uint256,bytes)')) ^
     *   bytes4(keccak256('transferFromAndCall(address,address,uint256)')) ^
     *   bytes4(keccak256('transferFromAndCall(address,address,uint256,bytes)')) ^
     *   bytes4(keccak256('approveAndCall(address,uint256)')) ^
     *   bytes4(keccak256('approveAndCall(address,uint256,bytes)'))
     */

    /**
     * @dev Moves a `value` amount of tokens from the caller's account to `to`
     * and then calls {IERC1363Receiver-onTransferReceived} on `to`.
     * @param to The address which you want to transfer to.
     * @param value The amount of tokens to be transferred.
     * @return A boolean value indicating whether the operation succeeded unless throwing.
     */
    function transferAndCall(address to, uint256 value) external returns (bool);

    /**
     * @dev Moves a `value` amount of tokens from the caller's account to `to`
     * and then calls {IERC1363Receiver-onTransferReceived} on `to`.
     * @param to The address which you want to transfer to.
     * @param value The amount of tokens to be transferred.
     * @param data Additional data with no specified format, sent in call to `to`.
     * @return A boolean value indicating whether the operation succeeded unless throwing.
     */
    function transferAndCall(address to, uint256 value, bytes calldata data) external returns (bool);

    /**
     * @dev Moves a `value` amount of tokens from `from` to `to` using the allowance mechanism
     * and then calls {IERC1363Receiver-onTransferReceived} on `to`.
     * @param from The address which you want to send tokens from.
     * @param to The address which you want to transfer to.
     * @param value The amount of tokens to be transferred.
     * @return A boolean value indicating whether the operation succeeded unless throwing.
     */
    function transferFromAndCall(address from, address to, uint256 value) external returns (bool);

    /**
     * @dev Moves a `value` amount of tokens from `from` to `to` using the allowance mechanism
     * and then calls {IERC1363Receiver-onTransferReceived} on `to`.
     * @param from The address which you want to send tokens from.
     * @param to The address which you want to transfer to.
     * @param value The amount of tokens to be transferred.
     * @param data Additional data with no specified format, sent in call to `to`.
     * @return A boolean value indicating whether the operation succeeded unless throwing.
     */
    function transferFromAndCall(address from, address to, uint256 value, bytes calldata data) external returns (bool);

    /**
     * @dev Sets a `value` amount of tokens as the allowance of `spender` over the
     * caller's tokens and then calls {IERC1363Spender-onApprovalReceived} on `spender`.
     * @param spender The address which will spend the funds.
     * @param value The amount of tokens to be spent.
     * @return A boolean value indicating whether the operation succeeded unless throwing.
     */
    function approveAndCall(address spender, uint256 value) external returns (bool);

    /**
     * @dev Sets a `value` amount of tokens as the allowance of `spender` over the
     * caller's tokens and then calls {IERC1363Spender-onApprovalReceived} on `spender`.
     * @param spender The address which will spend the funds.
     * @param value The amount of tokens to be spent.
     * @param data Additional data with no specified format, sent in call to `spender`.
     * @return A boolean value indicating whether the operation succeeded unless throwing.
     */
    function approveAndCall(address spender, uint256 value, bytes calldata data) external returns (bool);
}

File 8 of 50 : IERC165.sol
// 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";

File 9 of 50 : IERC20.sol
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (interfaces/IERC20.sol)

pragma solidity ^0.8.20;

import {IERC20} from "../token/ERC20/IERC20.sol";

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.2.0) (proxy/Clones.sol)

pragma solidity ^0.8.20;

import {Create2} from "../utils/Create2.sol";
import {Errors} from "../utils/Errors.sol";

/**
 * @dev https://eips.ethereum.org/EIPS/eip-1167[ERC-1167] is a standard for
 * deploying minimal proxy contracts, also known as "clones".
 *
 * > To simply and cheaply clone contract functionality in an immutable way, this standard specifies
 * > a minimal bytecode implementation that delegates all calls to a known, fixed address.
 *
 * The library includes functions to deploy a proxy using either `create` (traditional deployment) or `create2`
 * (salted deterministic deployment). It also includes functions to predict the addresses of clones deployed using the
 * deterministic method.
 */
library Clones {
    error CloneArgumentsTooLong();

    /**
     * @dev Deploys and returns the address of a clone that mimics the behaviour of `implementation`.
     *
     * This function uses the create opcode, which should never revert.
     */
    function clone(address implementation) internal returns (address instance) {
        return clone(implementation, 0);
    }

    /**
     * @dev Same as {xref-Clones-clone-address-}[clone], but with a `value` parameter to send native currency
     * to the new contract.
     *
     * NOTE: Using a non-zero value at creation will require the contract using this function (e.g. a factory)
     * to always have enough balance for new deployments. Consider exposing this function under a payable method.
     */
    function clone(address implementation, uint256 value) internal returns (address instance) {
        if (address(this).balance < value) {
            revert Errors.InsufficientBalance(address(this).balance, value);
        }
        assembly ("memory-safe") {
            // Cleans the upper 96 bits of the `implementation` word, then packs the first 3 bytes
            // of the `implementation` address with the bytecode before the address.
            mstore(0x00, or(shr(0xe8, shl(0x60, implementation)), 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000))
            // Packs the remaining 17 bytes of `implementation` with the bytecode after the address.
            mstore(0x20, or(shl(0x78, implementation), 0x5af43d82803e903d91602b57fd5bf3))
            instance := create(value, 0x09, 0x37)
        }
        if (instance == address(0)) {
            revert Errors.FailedDeployment();
        }
    }

    /**
     * @dev Deploys and returns the address of a clone that mimics the behaviour of `implementation`.
     *
     * This function uses the create2 opcode and a `salt` to deterministically deploy
     * the clone. Using the same `implementation` and `salt` multiple times will revert, since
     * the clones cannot be deployed twice at the same address.
     */
    function cloneDeterministic(address implementation, bytes32 salt) internal returns (address instance) {
        return cloneDeterministic(implementation, salt, 0);
    }

    /**
     * @dev Same as {xref-Clones-cloneDeterministic-address-bytes32-}[cloneDeterministic], but with
     * a `value` parameter to send native currency to the new contract.
     *
     * NOTE: Using a non-zero value at creation will require the contract using this function (e.g. a factory)
     * to always have enough balance for new deployments. Consider exposing this function under a payable method.
     */
    function cloneDeterministic(
        address implementation,
        bytes32 salt,
        uint256 value
    ) internal returns (address instance) {
        if (address(this).balance < value) {
            revert Errors.InsufficientBalance(address(this).balance, value);
        }
        assembly ("memory-safe") {
            // Cleans the upper 96 bits of the `implementation` word, then packs the first 3 bytes
            // of the `implementation` address with the bytecode before the address.
            mstore(0x00, or(shr(0xe8, shl(0x60, implementation)), 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000))
            // Packs the remaining 17 bytes of `implementation` with the bytecode after the address.
            mstore(0x20, or(shl(0x78, implementation), 0x5af43d82803e903d91602b57fd5bf3))
            instance := create2(value, 0x09, 0x37, salt)
        }
        if (instance == address(0)) {
            revert Errors.FailedDeployment();
        }
    }

    /**
     * @dev Computes the address of a clone deployed using {Clones-cloneDeterministic}.
     */
    function predictDeterministicAddress(
        address implementation,
        bytes32 salt,
        address deployer
    ) internal pure returns (address predicted) {
        assembly ("memory-safe") {
            let ptr := mload(0x40)
            mstore(add(ptr, 0x38), deployer)
            mstore(add(ptr, 0x24), 0x5af43d82803e903d91602b57fd5bf3ff)
            mstore(add(ptr, 0x14), implementation)
            mstore(ptr, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73)
            mstore(add(ptr, 0x58), salt)
            mstore(add(ptr, 0x78), keccak256(add(ptr, 0x0c), 0x37))
            predicted := and(keccak256(add(ptr, 0x43), 0x55), 0xffffffffffffffffffffffffffffffffffffffff)
        }
    }

    /**
     * @dev Computes the address of a clone deployed using {Clones-cloneDeterministic}.
     */
    function predictDeterministicAddress(
        address implementation,
        bytes32 salt
    ) internal view returns (address predicted) {
        return predictDeterministicAddress(implementation, salt, address(this));
    }

    /**
     * @dev Deploys and returns the address of a clone that mimics the behavior of `implementation` with custom
     * immutable arguments. These are provided through `args` and cannot be changed after deployment. To
     * access the arguments within the implementation, use {fetchCloneArgs}.
     *
     * This function uses the create opcode, which should never revert.
     */
    function cloneWithImmutableArgs(address implementation, bytes memory args) internal returns (address instance) {
        return cloneWithImmutableArgs(implementation, args, 0);
    }

    /**
     * @dev Same as {xref-Clones-cloneWithImmutableArgs-address-bytes-}[cloneWithImmutableArgs], but with a `value`
     * parameter to send native currency to the new contract.
     *
     * NOTE: Using a non-zero value at creation will require the contract using this function (e.g. a factory)
     * to always have enough balance for new deployments. Consider exposing this function under a payable method.
     */
    function cloneWithImmutableArgs(
        address implementation,
        bytes memory args,
        uint256 value
    ) internal returns (address instance) {
        if (address(this).balance < value) {
            revert Errors.InsufficientBalance(address(this).balance, value);
        }
        bytes memory bytecode = _cloneCodeWithImmutableArgs(implementation, args);
        assembly ("memory-safe") {
            instance := create(value, add(bytecode, 0x20), mload(bytecode))
        }
        if (instance == address(0)) {
            revert Errors.FailedDeployment();
        }
    }

    /**
     * @dev Deploys and returns the address of a clone that mimics the behaviour of `implementation` with custom
     * immutable arguments. These are provided through `args` and cannot be changed after deployment. To
     * access the arguments within the implementation, use {fetchCloneArgs}.
     *
     * This function uses the create2 opcode and a `salt` to deterministically deploy the clone. Using the same
     * `implementation`, `args` and `salt` multiple times will revert, since the clones cannot be deployed twice
     * at the same address.
     */
    function cloneDeterministicWithImmutableArgs(
        address implementation,
        bytes memory args,
        bytes32 salt
    ) internal returns (address instance) {
        return cloneDeterministicWithImmutableArgs(implementation, args, salt, 0);
    }

    /**
     * @dev Same as {xref-Clones-cloneDeterministicWithImmutableArgs-address-bytes-bytes32-}[cloneDeterministicWithImmutableArgs],
     * but with a `value` parameter to send native currency to the new contract.
     *
     * NOTE: Using a non-zero value at creation will require the contract using this function (e.g. a factory)
     * to always have enough balance for new deployments. Consider exposing this function under a payable method.
     */
    function cloneDeterministicWithImmutableArgs(
        address implementation,
        bytes memory args,
        bytes32 salt,
        uint256 value
    ) internal returns (address instance) {
        bytes memory bytecode = _cloneCodeWithImmutableArgs(implementation, args);
        return Create2.deploy(value, salt, bytecode);
    }

    /**
     * @dev Computes the address of a clone deployed using {Clones-cloneDeterministicWithImmutableArgs}.
     */
    function predictDeterministicAddressWithImmutableArgs(
        address implementation,
        bytes memory args,
        bytes32 salt,
        address deployer
    ) internal pure returns (address predicted) {
        bytes memory bytecode = _cloneCodeWithImmutableArgs(implementation, args);
        return Create2.computeAddress(salt, keccak256(bytecode), deployer);
    }

    /**
     * @dev Computes the address of a clone deployed using {Clones-cloneDeterministicWithImmutableArgs}.
     */
    function predictDeterministicAddressWithImmutableArgs(
        address implementation,
        bytes memory args,
        bytes32 salt
    ) internal view returns (address predicted) {
        return predictDeterministicAddressWithImmutableArgs(implementation, args, salt, address(this));
    }

    /**
     * @dev Get the immutable args attached to a clone.
     *
     * - If `instance` is a clone that was deployed using `clone` or `cloneDeterministic`, this
     *   function will return an empty array.
     * - If `instance` is a clone that was deployed using `cloneWithImmutableArgs` or
     *   `cloneDeterministicWithImmutableArgs`, this function will return the args array used at
     *   creation.
     * - If `instance` is NOT a clone deployed using this library, the behavior is undefined. This
     *   function should only be used to check addresses that are known to be clones.
     */
    function fetchCloneArgs(address instance) internal view returns (bytes memory) {
        bytes memory result = new bytes(instance.code.length - 45); // revert if length is too short
        assembly ("memory-safe") {
            extcodecopy(instance, add(result, 32), 45, mload(result))
        }
        return result;
    }

    /**
     * @dev Helper that prepares the initcode of the proxy with immutable args.
     *
     * An assembly variant of this function requires copying the `args` array, which can be efficiently done using
     * `mcopy`. Unfortunately, that opcode is not available before cancun. A pure solidity implementation using
     * abi.encodePacked is more expensive but also more portable and easier to review.
     *
     * NOTE: https://eips.ethereum.org/EIPS/eip-170[EIP-170] limits the length of the contract code to 24576 bytes.
     * With the proxy code taking 45 bytes, that limits the length of the immutable args to 24531 bytes.
     */
    function _cloneCodeWithImmutableArgs(
        address implementation,
        bytes memory args
    ) private pure returns (bytes memory) {
        if (args.length > 24531) revert CloneArgumentsTooLong();
        return
            abi.encodePacked(
                hex"61",
                uint16(args.length + 45),
                hex"3d81600a3d39f3363d3d373d3d3d363d73",
                implementation,
                hex"5af43d82803e903d91602b57fd5bf3",
                args
            );
    }
}

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/IERC20.sol)

pragma solidity ^0.8.20;

/**
 * @dev Interface of the ERC-20 standard as defined in the ERC.
 */
interface IERC20 {
    /**
     * @dev Emitted when `value` tokens are moved from one account (`from`) to
     * another (`to`).
     *
     * Note that `value` may be zero.
     */
    event Transfer(address indexed from, address indexed to, uint256 value);

    /**
     * @dev Emitted when the allowance of a `spender` for an `owner` is set by
     * a call to {approve}. `value` is the new allowance.
     */
    event Approval(address indexed owner, address indexed spender, uint256 value);

    /**
     * @dev Returns the value of tokens in existence.
     */
    function totalSupply() external view returns (uint256);

    /**
     * @dev Returns the value of tokens owned by `account`.
     */
    function balanceOf(address account) external view returns (uint256);

    /**
     * @dev Moves a `value` amount of tokens from the caller's account to `to`.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * Emits a {Transfer} event.
     */
    function transfer(address to, uint256 value) external returns (bool);

    /**
     * @dev Returns the remaining number of tokens that `spender` will be
     * allowed to spend on behalf of `owner` through {transferFrom}. This is
     * zero by default.
     *
     * This value changes when {approve} or {transferFrom} are called.
     */
    function allowance(address owner, address spender) external view returns (uint256);

    /**
     * @dev Sets a `value` amount of tokens as the allowance of `spender` over the
     * caller's tokens.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * IMPORTANT: Beware that changing an allowance with this method brings the risk
     * that someone may use both the old and the new allowance by unfortunate
     * transaction ordering. One possible solution to mitigate this race
     * condition is to first reduce the spender's allowance to 0 and set the
     * desired value afterwards:
     * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
     *
     * Emits an {Approval} event.
     */
    function approve(address spender, uint256 value) external returns (bool);

    /**
     * @dev Moves a `value` amount of tokens from `from` to `to` using the
     * allowance mechanism. `value` is then deducted from the caller's
     * allowance.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * Emits a {Transfer} event.
     */
    function transferFrom(address from, address to, uint256 value) external returns (bool);
}

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.2.0) (token/ERC20/utils/SafeERC20.sol)

pragma solidity ^0.8.20;

import {IERC20} from "../IERC20.sol";
import {IERC1363} from "../../../interfaces/IERC1363.sol";

/**
 * @title SafeERC20
 * @dev Wrappers around ERC-20 operations that throw on failure (when the token
 * contract returns false). Tokens that return no value (and instead revert or
 * throw on failure) are also supported, non-reverting calls are assumed to be
 * successful.
 * To use this library you can add a `using SafeERC20 for IERC20;` statement to your contract,
 * which allows you to call the safe operations as `token.safeTransfer(...)`, etc.
 */
library SafeERC20 {
    /**
     * @dev An operation with an ERC-20 token failed.
     */
    error SafeERC20FailedOperation(address token);

    /**
     * @dev Indicates a failed `decreaseAllowance` request.
     */
    error SafeERC20FailedDecreaseAllowance(address spender, uint256 currentAllowance, uint256 requestedDecrease);

    /**
     * @dev Transfer `value` amount of `token` from the calling contract to `to`. If `token` returns no value,
     * non-reverting calls are assumed to be successful.
     */
    function safeTransfer(IERC20 token, address to, uint256 value) internal {
        _callOptionalReturn(token, abi.encodeCall(token.transfer, (to, value)));
    }

    /**
     * @dev Transfer `value` amount of `token` from `from` to `to`, spending the approval given by `from` to the
     * calling contract. If `token` returns no value, non-reverting calls are assumed to be successful.
     */
    function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal {
        _callOptionalReturn(token, abi.encodeCall(token.transferFrom, (from, to, value)));
    }

    /**
     * @dev Increase the calling contract's allowance toward `spender` by `value`. If `token` returns no value,
     * non-reverting calls are assumed to be successful.
     *
     * IMPORTANT: If the token implements ERC-7674 (ERC-20 with temporary allowance), and if the "client"
     * smart contract uses ERC-7674 to set temporary allowances, then the "client" smart contract should avoid using
     * this function. Performing a {safeIncreaseAllowance} or {safeDecreaseAllowance} operation on a token contract
     * that has a non-zero temporary allowance (for that particular owner-spender) will result in unexpected behavior.
     */
    function safeIncreaseAllowance(IERC20 token, address spender, uint256 value) internal {
        uint256 oldAllowance = token.allowance(address(this), spender);
        forceApprove(token, spender, oldAllowance + value);
    }

    /**
     * @dev Decrease the calling contract's allowance toward `spender` by `requestedDecrease`. If `token` returns no
     * value, non-reverting calls are assumed to be successful.
     *
     * IMPORTANT: If the token implements ERC-7674 (ERC-20 with temporary allowance), and if the "client"
     * smart contract uses ERC-7674 to set temporary allowances, then the "client" smart contract should avoid using
     * this function. Performing a {safeIncreaseAllowance} or {safeDecreaseAllowance} operation on a token contract
     * that has a non-zero temporary allowance (for that particular owner-spender) will result in unexpected behavior.
     */
    function safeDecreaseAllowance(IERC20 token, address spender, uint256 requestedDecrease) internal {
        unchecked {
            uint256 currentAllowance = token.allowance(address(this), spender);
            if (currentAllowance < requestedDecrease) {
                revert SafeERC20FailedDecreaseAllowance(spender, currentAllowance, requestedDecrease);
            }
            forceApprove(token, spender, currentAllowance - requestedDecrease);
        }
    }

    /**
     * @dev Set the calling contract's allowance toward `spender` to `value`. If `token` returns no value,
     * non-reverting calls are assumed to be successful. Meant to be used with tokens that require the approval
     * to be set to zero before setting it to a non-zero value, such as USDT.
     *
     * NOTE: If the token implements ERC-7674, this function will not modify any temporary allowance. This function
     * only sets the "standard" allowance. Any temporary allowance will remain active, in addition to the value being
     * set here.
     */
    function forceApprove(IERC20 token, address spender, uint256 value) internal {
        bytes memory approvalCall = abi.encodeCall(token.approve, (spender, value));

        if (!_callOptionalReturnBool(token, approvalCall)) {
            _callOptionalReturn(token, abi.encodeCall(token.approve, (spender, 0)));
            _callOptionalReturn(token, approvalCall);
        }
    }

    /**
     * @dev Performs an {ERC1363} transferAndCall, with a fallback to the simple {ERC20} transfer if the target has no
     * code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when
     * targeting contracts.
     *
     * Reverts if the returned value is other than `true`.
     */
    function transferAndCallRelaxed(IERC1363 token, address to, uint256 value, bytes memory data) internal {
        if (to.code.length == 0) {
            safeTransfer(token, to, value);
        } else if (!token.transferAndCall(to, value, data)) {
            revert SafeERC20FailedOperation(address(token));
        }
    }

    /**
     * @dev Performs an {ERC1363} transferFromAndCall, with a fallback to the simple {ERC20} transferFrom if the target
     * has no code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when
     * targeting contracts.
     *
     * Reverts if the returned value is other than `true`.
     */
    function transferFromAndCallRelaxed(
        IERC1363 token,
        address from,
        address to,
        uint256 value,
        bytes memory data
    ) internal {
        if (to.code.length == 0) {
            safeTransferFrom(token, from, to, value);
        } else if (!token.transferFromAndCall(from, to, value, data)) {
            revert SafeERC20FailedOperation(address(token));
        }
    }

    /**
     * @dev Performs an {ERC1363} approveAndCall, with a fallback to the simple {ERC20} approve if the target has no
     * code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when
     * targeting contracts.
     *
     * NOTE: When the recipient address (`to`) has no code (i.e. is an EOA), this function behaves as {forceApprove}.
     * Opposedly, when the recipient address (`to`) has code, this function only attempts to call {ERC1363-approveAndCall}
     * once without retrying, and relies on the returned value to be true.
     *
     * Reverts if the returned value is other than `true`.
     */
    function approveAndCallRelaxed(IERC1363 token, address to, uint256 value, bytes memory data) internal {
        if (to.code.length == 0) {
            forceApprove(token, to, value);
        } else if (!token.approveAndCall(to, value, data)) {
            revert SafeERC20FailedOperation(address(token));
        }
    }

    /**
     * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement
     * on the return value: the return value is optional (but if data is returned, it must not be false).
     * @param token The token targeted by the call.
     * @param data The call data (encoded using abi.encode or one of its variants).
     *
     * This is a variant of {_callOptionalReturnBool} that reverts if call fails to meet the requirements.
     */
    function _callOptionalReturn(IERC20 token, bytes memory data) private {
        uint256 returnSize;
        uint256 returnValue;
        assembly ("memory-safe") {
            let success := call(gas(), token, 0, add(data, 0x20), mload(data), 0, 0x20)
            // bubble errors
            if iszero(success) {
                let ptr := mload(0x40)
                returndatacopy(ptr, 0, returndatasize())
                revert(ptr, returndatasize())
            }
            returnSize := returndatasize()
            returnValue := mload(0)
        }

        if (returnSize == 0 ? address(token).code.length == 0 : returnValue != 1) {
            revert SafeERC20FailedOperation(address(token));
        }
    }

    /**
     * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement
     * on the return value: the return value is optional (but if data is returned, it must not be false).
     * @param token The token targeted by the call.
     * @param data The call data (encoded using abi.encode or one of its variants).
     *
     * This is a variant of {_callOptionalReturn} that silently catches all reverts and returns a bool instead.
     */
    function _callOptionalReturnBool(IERC20 token, bytes memory data) private returns (bool) {
        bool success;
        uint256 returnSize;
        uint256 returnValue;
        assembly ("memory-safe") {
            success := call(gas(), token, 0, add(data, 0x20), mload(data), 0, 0x20)
            returnSize := returndatasize()
            returnValue := mload(0)
        }
        return success && (returnSize == 0 ? address(token).code.length > 0 : returnValue == 1);
    }
}

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.1) (utils/Context.sol)

pragma solidity ^0.8.20;

/**
 * @dev Provides information about the current execution context, including the
 * sender of the transaction and its data. While these are generally available
 * via msg.sender and msg.data, they should not be accessed in such a direct
 * manner, since when dealing with meta-transactions the account sending and
 * paying for execution may not be the actual sender (as far as an application
 * is concerned).
 *
 * This contract is only required for intermediate, library-like contracts.
 */
abstract contract Context {
    function _msgSender() internal view virtual returns (address) {
        return msg.sender;
    }

    function _msgData() internal view virtual returns (bytes calldata) {
        return msg.data;
    }

    function _contextSuffixLength() internal view virtual returns (uint256) {
        return 0;
    }
}

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (utils/Create2.sol)

pragma solidity ^0.8.20;

import {Errors} from "./Errors.sol";

/**
 * @dev Helper to make usage of the `CREATE2` EVM opcode easier and safer.
 * `CREATE2` can be used to compute in advance the address where a smart
 * contract will be deployed, which allows for interesting new mechanisms known
 * as 'counterfactual interactions'.
 *
 * See the https://eips.ethereum.org/EIPS/eip-1014#motivation[EIP] for more
 * information.
 */
library Create2 {
    /**
     * @dev There's no code to deploy.
     */
    error Create2EmptyBytecode();

    /**
     * @dev Deploys a contract using `CREATE2`. The address where the contract
     * will be deployed can be known in advance via {computeAddress}.
     *
     * The bytecode for a contract can be obtained from Solidity with
     * `type(contractName).creationCode`.
     *
     * Requirements:
     *
     * - `bytecode` must not be empty.
     * - `salt` must have not been used for `bytecode` already.
     * - the factory must have a balance of at least `amount`.
     * - if `amount` is non-zero, `bytecode` must have a `payable` constructor.
     */
    function deploy(uint256 amount, bytes32 salt, bytes memory bytecode) internal returns (address addr) {
        if (address(this).balance < amount) {
            revert Errors.InsufficientBalance(address(this).balance, amount);
        }
        if (bytecode.length == 0) {
            revert Create2EmptyBytecode();
        }
        assembly ("memory-safe") {
            addr := create2(amount, add(bytecode, 0x20), mload(bytecode), salt)
            // if no address was created, and returndata is not empty, bubble revert
            if and(iszero(addr), not(iszero(returndatasize()))) {
                let p := mload(0x40)
                returndatacopy(p, 0, returndatasize())
                revert(p, returndatasize())
            }
        }
        if (addr == address(0)) {
            revert Errors.FailedDeployment();
        }
    }

    /**
     * @dev Returns the address where a contract will be stored if deployed via {deploy}. Any change in the
     * `bytecodeHash` or `salt` will result in a new destination address.
     */
    function computeAddress(bytes32 salt, bytes32 bytecodeHash) internal view returns (address) {
        return computeAddress(salt, bytecodeHash, address(this));
    }

    /**
     * @dev Returns the address where a contract will be stored if deployed via {deploy} from a contract located at
     * `deployer`. If `deployer` is this contract's address, returns the same value as {computeAddress}.
     */
    function computeAddress(bytes32 salt, bytes32 bytecodeHash, address deployer) internal pure returns (address addr) {
        assembly ("memory-safe") {
            let ptr := mload(0x40) // Get free memory pointer

            // |                   | ↓ ptr ...  ↓ ptr + 0x0B (start) ...  ↓ ptr + 0x20 ...  ↓ ptr + 0x40 ...   |
            // |-------------------|---------------------------------------------------------------------------|
            // | bytecodeHash      |                                                        CCCCCCCCCCCCC...CC |
            // | salt              |                                      BBBBBBBBBBBBB...BB                   |
            // | deployer          | 000000...0000AAAAAAAAAAAAAAAAAAA...AA                                     |
            // | 0xFF              |            FF                                                             |
            // |-------------------|---------------------------------------------------------------------------|
            // | memory            | 000000...00FFAAAAAAAAAAAAAAAAAAA...AABBBBBBBBBBBBB...BBCCCCCCCCCCCCC...CC |
            // | keccak(start, 85) |            ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑ |

            mstore(add(ptr, 0x40), bytecodeHash)
            mstore(add(ptr, 0x20), salt)
            mstore(ptr, deployer) // Right-aligned with 12 preceding garbage bytes
            let start := add(ptr, 0x0b) // The hashed data starts at the final garbage byte which we will set to 0xff
            mstore8(start, 0xff)
            addr := and(keccak256(start, 85), 0xffffffffffffffffffffffffffffffffffffffff)
        }
    }
}

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (utils/cryptography/Hashes.sol)

pragma solidity ^0.8.20;

/**
 * @dev Library of standard hash functions.
 *
 * _Available since v5.1._
 */
library Hashes {
    /**
     * @dev Commutative Keccak256 hash of a sorted pair of bytes32. Frequently used when working with merkle proofs.
     *
     * NOTE: Equivalent to the `standardNodeHash` in our https://github.com/OpenZeppelin/merkle-tree[JavaScript library].
     */
    function commutativeKeccak256(bytes32 a, bytes32 b) internal pure returns (bytes32) {
        return a < b ? _efficientKeccak256(a, b) : _efficientKeccak256(b, a);
    }

    /**
     * @dev Implementation of keccak256(abi.encode(a, b)) that doesn't allocate or expand memory.
     */
    function _efficientKeccak256(bytes32 a, bytes32 b) private pure returns (bytes32 value) {
        assembly ("memory-safe") {
            mstore(0x00, a)
            mstore(0x20, b)
            value := keccak256(0x00, 0x40)
        }
    }
}

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (utils/cryptography/MerkleProof.sol)
// This file was procedurally generated from scripts/generate/templates/MerkleProof.js.

pragma solidity ^0.8.20;

import {Hashes} from "./Hashes.sol";

/**
 * @dev These functions deal with verification of Merkle Tree proofs.
 *
 * The tree and the proofs can be generated using our
 * https://github.com/OpenZeppelin/merkle-tree[JavaScript library].
 * You will find a quickstart guide in the readme.
 *
 * WARNING: You should avoid using leaf values that are 64 bytes long prior to
 * hashing, or use a hash function other than keccak256 for hashing leaves.
 * This is because the concatenation of a sorted pair of internal nodes in
 * the Merkle tree could be reinterpreted as a leaf value.
 * OpenZeppelin's JavaScript library generates Merkle trees that are safe
 * against this attack out of the box.
 *
 * IMPORTANT: Consider memory side-effects when using custom hashing functions
 * that access memory in an unsafe way.
 *
 * NOTE: This library supports proof verification for merkle trees built using
 * custom _commutative_ hashing functions (i.e. `H(a, b) == H(b, a)`). Proving
 * leaf inclusion in trees built using non-commutative hashing functions requires
 * additional logic that is not supported by this library.
 */
library MerkleProof {
    /**
     *@dev The multiproof provided is not valid.
     */
    error MerkleProofInvalidMultiproof();

    /**
     * @dev Returns true if a `leaf` can be proved to be a part of a Merkle tree
     * defined by `root`. For this, a `proof` must be provided, containing
     * sibling hashes on the branch from the leaf to the root of the tree. Each
     * pair of leaves and each pair of pre-images are assumed to be sorted.
     *
     * This version handles proofs in memory with the default hashing function.
     */
    function verify(bytes32[] memory proof, bytes32 root, bytes32 leaf) internal pure returns (bool) {
        return processProof(proof, leaf) == root;
    }

    /**
     * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up
     * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt
     * hash matches the root of the tree. When processing the proof, the pairs
     * of leaves & pre-images are assumed to be sorted.
     *
     * This version handles proofs in memory with the default hashing function.
     */
    function processProof(bytes32[] memory proof, bytes32 leaf) internal pure returns (bytes32) {
        bytes32 computedHash = leaf;
        for (uint256 i = 0; i < proof.length; i++) {
            computedHash = Hashes.commutativeKeccak256(computedHash, proof[i]);
        }
        return computedHash;
    }

    /**
     * @dev Returns true if a `leaf` can be proved to be a part of a Merkle tree
     * defined by `root`. For this, a `proof` must be provided, containing
     * sibling hashes on the branch from the leaf to the root of the tree. Each
     * pair of leaves and each pair of pre-images are assumed to be sorted.
     *
     * This version handles proofs in memory with a custom hashing function.
     */
    function verify(
        bytes32[] memory proof,
        bytes32 root,
        bytes32 leaf,
        function(bytes32, bytes32) view returns (bytes32) hasher
    ) internal view returns (bool) {
        return processProof(proof, leaf, hasher) == root;
    }

    /**
     * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up
     * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt
     * hash matches the root of the tree. When processing the proof, the pairs
     * of leaves & pre-images are assumed to be sorted.
     *
     * This version handles proofs in memory with a custom hashing function.
     */
    function processProof(
        bytes32[] memory proof,
        bytes32 leaf,
        function(bytes32, bytes32) view returns (bytes32) hasher
    ) internal view returns (bytes32) {
        bytes32 computedHash = leaf;
        for (uint256 i = 0; i < proof.length; i++) {
            computedHash = hasher(computedHash, proof[i]);
        }
        return computedHash;
    }

    /**
     * @dev Returns true if a `leaf` can be proved to be a part of a Merkle tree
     * defined by `root`. For this, a `proof` must be provided, containing
     * sibling hashes on the branch from the leaf to the root of the tree. Each
     * pair of leaves and each pair of pre-images are assumed to be sorted.
     *
     * This version handles proofs in calldata with the default hashing function.
     */
    function verifyCalldata(bytes32[] calldata proof, bytes32 root, bytes32 leaf) internal pure returns (bool) {
        return processProofCalldata(proof, leaf) == root;
    }

    /**
     * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up
     * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt
     * hash matches the root of the tree. When processing the proof, the pairs
     * of leaves & pre-images are assumed to be sorted.
     *
     * This version handles proofs in calldata with the default hashing function.
     */
    function processProofCalldata(bytes32[] calldata proof, bytes32 leaf) internal pure returns (bytes32) {
        bytes32 computedHash = leaf;
        for (uint256 i = 0; i < proof.length; i++) {
            computedHash = Hashes.commutativeKeccak256(computedHash, proof[i]);
        }
        return computedHash;
    }

    /**
     * @dev Returns true if a `leaf` can be proved to be a part of a Merkle tree
     * defined by `root`. For this, a `proof` must be provided, containing
     * sibling hashes on the branch from the leaf to the root of the tree. Each
     * pair of leaves and each pair of pre-images are assumed to be sorted.
     *
     * This version handles proofs in calldata with a custom hashing function.
     */
    function verifyCalldata(
        bytes32[] calldata proof,
        bytes32 root,
        bytes32 leaf,
        function(bytes32, bytes32) view returns (bytes32) hasher
    ) internal view returns (bool) {
        return processProofCalldata(proof, leaf, hasher) == root;
    }

    /**
     * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up
     * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt
     * hash matches the root of the tree. When processing the proof, the pairs
     * of leaves & pre-images are assumed to be sorted.
     *
     * This version handles proofs in calldata with a custom hashing function.
     */
    function processProofCalldata(
        bytes32[] calldata proof,
        bytes32 leaf,
        function(bytes32, bytes32) view returns (bytes32) hasher
    ) internal view returns (bytes32) {
        bytes32 computedHash = leaf;
        for (uint256 i = 0; i < proof.length; i++) {
            computedHash = hasher(computedHash, proof[i]);
        }
        return computedHash;
    }

    /**
     * @dev Returns true if the `leaves` can be simultaneously proven to be a part of a Merkle tree defined by
     * `root`, according to `proof` and `proofFlags` as described in {processMultiProof}.
     *
     * This version handles multiproofs in memory with the default hashing function.
     *
     * CAUTION: Not all Merkle trees admit multiproofs. See {processMultiProof} for details.
     *
     * NOTE: Consider the case where `root == proof[0] && leaves.length == 0` as it will return `true`.
     * The `leaves` must be validated independently. See {processMultiProof}.
     */
    function multiProofVerify(
        bytes32[] memory proof,
        bool[] memory proofFlags,
        bytes32 root,
        bytes32[] memory leaves
    ) internal pure returns (bool) {
        return processMultiProof(proof, proofFlags, leaves) == root;
    }

    /**
     * @dev Returns the root of a tree reconstructed from `leaves` and sibling nodes in `proof`. The reconstruction
     * proceeds by incrementally reconstructing all inner nodes by combining a leaf/inner node with either another
     * leaf/inner node or a proof sibling node, depending on whether each `proofFlags` item is true or false
     * respectively.
     *
     * This version handles multiproofs in memory with the default hashing function.
     *
     * CAUTION: Not all Merkle trees admit multiproofs. To use multiproofs, it is sufficient to ensure that: 1) the tree
     * is complete (but not necessarily perfect), 2) the leaves to be proven are in the opposite order they are in the
     * tree (i.e., as seen from right to left starting at the deepest layer and continuing at the next layer).
     *
     * NOTE: The _empty set_ (i.e. the case where `proof.length == 1 && leaves.length == 0`) is considered a no-op,
     * and therefore a valid multiproof (i.e. it returns `proof[0]`). Consider disallowing this case if you're not
     * validating the leaves elsewhere.
     */
    function processMultiProof(
        bytes32[] memory proof,
        bool[] memory proofFlags,
        bytes32[] memory leaves
    ) internal pure returns (bytes32 merkleRoot) {
        // This function rebuilds the root hash by traversing the tree up from the leaves. The root is rebuilt by
        // consuming and producing values on a queue. The queue starts with the `leaves` array, then goes onto the
        // `hashes` array. At the end of the process, the last hash in the `hashes` array should contain the root of
        // the Merkle tree.
        uint256 leavesLen = leaves.length;
        uint256 proofFlagsLen = proofFlags.length;

        // Check proof validity.
        if (leavesLen + proof.length != proofFlagsLen + 1) {
            revert MerkleProofInvalidMultiproof();
        }

        // The xxxPos values are "pointers" to the next value to consume in each array. All accesses are done using
        // `xxx[xxxPos++]`, which return the current value and increment the pointer, thus mimicking a queue's "pop".
        bytes32[] memory hashes = new bytes32[](proofFlagsLen);
        uint256 leafPos = 0;
        uint256 hashPos = 0;
        uint256 proofPos = 0;
        // At each step, we compute the next hash using two values:
        // - a value from the "main queue". If not all leaves have been consumed, we get the next leaf, otherwise we
        //   get the next hash.
        // - depending on the flag, either another value from the "main queue" (merging branches) or an element from the
        //   `proof` array.
        for (uint256 i = 0; i < proofFlagsLen; i++) {
            bytes32 a = leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++];
            bytes32 b = proofFlags[i]
                ? (leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++])
                : proof[proofPos++];
            hashes[i] = Hashes.commutativeKeccak256(a, b);
        }

        if (proofFlagsLen > 0) {
            if (proofPos != proof.length) {
                revert MerkleProofInvalidMultiproof();
            }
            unchecked {
                return hashes[proofFlagsLen - 1];
            }
        } else if (leavesLen > 0) {
            return leaves[0];
        } else {
            return proof[0];
        }
    }

    /**
     * @dev Returns true if the `leaves` can be simultaneously proven to be a part of a Merkle tree defined by
     * `root`, according to `proof` and `proofFlags` as described in {processMultiProof}.
     *
     * This version handles multiproofs in memory with a custom hashing function.
     *
     * CAUTION: Not all Merkle trees admit multiproofs. See {processMultiProof} for details.
     *
     * NOTE: Consider the case where `root == proof[0] && leaves.length == 0` as it will return `true`.
     * The `leaves` must be validated independently. See {processMultiProof}.
     */
    function multiProofVerify(
        bytes32[] memory proof,
        bool[] memory proofFlags,
        bytes32 root,
        bytes32[] memory leaves,
        function(bytes32, bytes32) view returns (bytes32) hasher
    ) internal view returns (bool) {
        return processMultiProof(proof, proofFlags, leaves, hasher) == root;
    }

    /**
     * @dev Returns the root of a tree reconstructed from `leaves` and sibling nodes in `proof`. The reconstruction
     * proceeds by incrementally reconstructing all inner nodes by combining a leaf/inner node with either another
     * leaf/inner node or a proof sibling node, depending on whether each `proofFlags` item is true or false
     * respectively.
     *
     * This version handles multiproofs in memory with a custom hashing function.
     *
     * CAUTION: Not all Merkle trees admit multiproofs. To use multiproofs, it is sufficient to ensure that: 1) the tree
     * is complete (but not necessarily perfect), 2) the leaves to be proven are in the opposite order they are in the
     * tree (i.e., as seen from right to left starting at the deepest layer and continuing at the next layer).
     *
     * NOTE: The _empty set_ (i.e. the case where `proof.length == 1 && leaves.length == 0`) is considered a no-op,
     * and therefore a valid multiproof (i.e. it returns `proof[0]`). Consider disallowing this case if you're not
     * validating the leaves elsewhere.
     */
    function processMultiProof(
        bytes32[] memory proof,
        bool[] memory proofFlags,
        bytes32[] memory leaves,
        function(bytes32, bytes32) view returns (bytes32) hasher
    ) internal view returns (bytes32 merkleRoot) {
        // This function rebuilds the root hash by traversing the tree up from the leaves. The root is rebuilt by
        // consuming and producing values on a queue. The queue starts with the `leaves` array, then goes onto the
        // `hashes` array. At the end of the process, the last hash in the `hashes` array should contain the root of
        // the Merkle tree.
        uint256 leavesLen = leaves.length;
        uint256 proofFlagsLen = proofFlags.length;

        // Check proof validity.
        if (leavesLen + proof.length != proofFlagsLen + 1) {
            revert MerkleProofInvalidMultiproof();
        }

        // The xxxPos values are "pointers" to the next value to consume in each array. All accesses are done using
        // `xxx[xxxPos++]`, which return the current value and increment the pointer, thus mimicking a queue's "pop".
        bytes32[] memory hashes = new bytes32[](proofFlagsLen);
        uint256 leafPos = 0;
        uint256 hashPos = 0;
        uint256 proofPos = 0;
        // At each step, we compute the next hash using two values:
        // - a value from the "main queue". If not all leaves have been consumed, we get the next leaf, otherwise we
        //   get the next hash.
        // - depending on the flag, either another value from the "main queue" (merging branches) or an element from the
        //   `proof` array.
        for (uint256 i = 0; i < proofFlagsLen; i++) {
            bytes32 a = leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++];
            bytes32 b = proofFlags[i]
                ? (leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++])
                : proof[proofPos++];
            hashes[i] = hasher(a, b);
        }

        if (proofFlagsLen > 0) {
            if (proofPos != proof.length) {
                revert MerkleProofInvalidMultiproof();
            }
            unchecked {
                return hashes[proofFlagsLen - 1];
            }
        } else if (leavesLen > 0) {
            return leaves[0];
        } else {
            return proof[0];
        }
    }

    /**
     * @dev Returns true if the `leaves` can be simultaneously proven to be a part of a Merkle tree defined by
     * `root`, according to `proof` and `proofFlags` as described in {processMultiProof}.
     *
     * This version handles multiproofs in calldata with the default hashing function.
     *
     * CAUTION: Not all Merkle trees admit multiproofs. See {processMultiProof} for details.
     *
     * NOTE: Consider the case where `root == proof[0] && leaves.length == 0` as it will return `true`.
     * The `leaves` must be validated independently. See {processMultiProofCalldata}.
     */
    function multiProofVerifyCalldata(
        bytes32[] calldata proof,
        bool[] calldata proofFlags,
        bytes32 root,
        bytes32[] memory leaves
    ) internal pure returns (bool) {
        return processMultiProofCalldata(proof, proofFlags, leaves) == root;
    }

    /**
     * @dev Returns the root of a tree reconstructed from `leaves` and sibling nodes in `proof`. The reconstruction
     * proceeds by incrementally reconstructing all inner nodes by combining a leaf/inner node with either another
     * leaf/inner node or a proof sibling node, depending on whether each `proofFlags` item is true or false
     * respectively.
     *
     * This version handles multiproofs in calldata with the default hashing function.
     *
     * CAUTION: Not all Merkle trees admit multiproofs. To use multiproofs, it is sufficient to ensure that: 1) the tree
     * is complete (but not necessarily perfect), 2) the leaves to be proven are in the opposite order they are in the
     * tree (i.e., as seen from right to left starting at the deepest layer and continuing at the next layer).
     *
     * NOTE: The _empty set_ (i.e. the case where `proof.length == 1 && leaves.length == 0`) is considered a no-op,
     * and therefore a valid multiproof (i.e. it returns `proof[0]`). Consider disallowing this case if you're not
     * validating the leaves elsewhere.
     */
    function processMultiProofCalldata(
        bytes32[] calldata proof,
        bool[] calldata proofFlags,
        bytes32[] memory leaves
    ) internal pure returns (bytes32 merkleRoot) {
        // This function rebuilds the root hash by traversing the tree up from the leaves. The root is rebuilt by
        // consuming and producing values on a queue. The queue starts with the `leaves` array, then goes onto the
        // `hashes` array. At the end of the process, the last hash in the `hashes` array should contain the root of
        // the Merkle tree.
        uint256 leavesLen = leaves.length;
        uint256 proofFlagsLen = proofFlags.length;

        // Check proof validity.
        if (leavesLen + proof.length != proofFlagsLen + 1) {
            revert MerkleProofInvalidMultiproof();
        }

        // The xxxPos values are "pointers" to the next value to consume in each array. All accesses are done using
        // `xxx[xxxPos++]`, which return the current value and increment the pointer, thus mimicking a queue's "pop".
        bytes32[] memory hashes = new bytes32[](proofFlagsLen);
        uint256 leafPos = 0;
        uint256 hashPos = 0;
        uint256 proofPos = 0;
        // At each step, we compute the next hash using two values:
        // - a value from the "main queue". If not all leaves have been consumed, we get the next leaf, otherwise we
        //   get the next hash.
        // - depending on the flag, either another value from the "main queue" (merging branches) or an element from the
        //   `proof` array.
        for (uint256 i = 0; i < proofFlagsLen; i++) {
            bytes32 a = leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++];
            bytes32 b = proofFlags[i]
                ? (leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++])
                : proof[proofPos++];
            hashes[i] = Hashes.commutativeKeccak256(a, b);
        }

        if (proofFlagsLen > 0) {
            if (proofPos != proof.length) {
                revert MerkleProofInvalidMultiproof();
            }
            unchecked {
                return hashes[proofFlagsLen - 1];
            }
        } else if (leavesLen > 0) {
            return leaves[0];
        } else {
            return proof[0];
        }
    }

    /**
     * @dev Returns true if the `leaves` can be simultaneously proven to be a part of a Merkle tree defined by
     * `root`, according to `proof` and `proofFlags` as described in {processMultiProof}.
     *
     * This version handles multiproofs in calldata with a custom hashing function.
     *
     * CAUTION: Not all Merkle trees admit multiproofs. See {processMultiProof} for details.
     *
     * NOTE: Consider the case where `root == proof[0] && leaves.length == 0` as it will return `true`.
     * The `leaves` must be validated independently. See {processMultiProofCalldata}.
     */
    function multiProofVerifyCalldata(
        bytes32[] calldata proof,
        bool[] calldata proofFlags,
        bytes32 root,
        bytes32[] memory leaves,
        function(bytes32, bytes32) view returns (bytes32) hasher
    ) internal view returns (bool) {
        return processMultiProofCalldata(proof, proofFlags, leaves, hasher) == root;
    }

    /**
     * @dev Returns the root of a tree reconstructed from `leaves` and sibling nodes in `proof`. The reconstruction
     * proceeds by incrementally reconstructing all inner nodes by combining a leaf/inner node with either another
     * leaf/inner node or a proof sibling node, depending on whether each `proofFlags` item is true or false
     * respectively.
     *
     * This version handles multiproofs in calldata with a custom hashing function.
     *
     * CAUTION: Not all Merkle trees admit multiproofs. To use multiproofs, it is sufficient to ensure that: 1) the tree
     * is complete (but not necessarily perfect), 2) the leaves to be proven are in the opposite order they are in the
     * tree (i.e., as seen from right to left starting at the deepest layer and continuing at the next layer).
     *
     * NOTE: The _empty set_ (i.e. the case where `proof.length == 1 && leaves.length == 0`) is considered a no-op,
     * and therefore a valid multiproof (i.e. it returns `proof[0]`). Consider disallowing this case if you're not
     * validating the leaves elsewhere.
     */
    function processMultiProofCalldata(
        bytes32[] calldata proof,
        bool[] calldata proofFlags,
        bytes32[] memory leaves,
        function(bytes32, bytes32) view returns (bytes32) hasher
    ) internal view returns (bytes32 merkleRoot) {
        // This function rebuilds the root hash by traversing the tree up from the leaves. The root is rebuilt by
        // consuming and producing values on a queue. The queue starts with the `leaves` array, then goes onto the
        // `hashes` array. At the end of the process, the last hash in the `hashes` array should contain the root of
        // the Merkle tree.
        uint256 leavesLen = leaves.length;
        uint256 proofFlagsLen = proofFlags.length;

        // Check proof validity.
        if (leavesLen + proof.length != proofFlagsLen + 1) {
            revert MerkleProofInvalidMultiproof();
        }

        // The xxxPos values are "pointers" to the next value to consume in each array. All accesses are done using
        // `xxx[xxxPos++]`, which return the current value and increment the pointer, thus mimicking a queue's "pop".
        bytes32[] memory hashes = new bytes32[](proofFlagsLen);
        uint256 leafPos = 0;
        uint256 hashPos = 0;
        uint256 proofPos = 0;
        // At each step, we compute the next hash using two values:
        // - a value from the "main queue". If not all leaves have been consumed, we get the next leaf, otherwise we
        //   get the next hash.
        // - depending on the flag, either another value from the "main queue" (merging branches) or an element from the
        //   `proof` array.
        for (uint256 i = 0; i < proofFlagsLen; i++) {
            bytes32 a = leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++];
            bytes32 b = proofFlags[i]
                ? (leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++])
                : proof[proofPos++];
            hashes[i] = hasher(a, b);
        }

        if (proofFlagsLen > 0) {
            if (proofPos != proof.length) {
                revert MerkleProofInvalidMultiproof();
            }
            unchecked {
                return hashes[proofFlagsLen - 1];
            }
        } else if (leavesLen > 0) {
            return leaves[0];
        } else {
            return proof[0];
        }
    }
}

File 17 of 50 : Errors.sol
// 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);
}

File 20 of 50 : SafeCast.sol
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (utils/math/SafeCast.sol)
// This file was procedurally generated from scripts/generate/templates/SafeCast.js.

pragma solidity ^0.8.20;

/**
 * @dev Wrappers over Solidity's uintXX/intXX/bool casting operators with added overflow
 * checks.
 *
 * Downcasting from uint256/int256 in Solidity does not revert on overflow. This can
 * easily result in undesired exploitation or bugs, since developers usually
 * assume that overflows raise errors. `SafeCast` restores this intuition by
 * reverting the transaction when such an operation overflows.
 *
 * Using this library instead of the unchecked operations eliminates an entire
 * class of bugs, so it's recommended to use it always.
 */
library SafeCast {
    /**
     * @dev Value doesn't fit in an uint of `bits` size.
     */
    error SafeCastOverflowedUintDowncast(uint8 bits, uint256 value);

    /**
     * @dev An int value doesn't fit in an uint of `bits` size.
     */
    error SafeCastOverflowedIntToUint(int256 value);

    /**
     * @dev Value doesn't fit in an int of `bits` size.
     */
    error SafeCastOverflowedIntDowncast(uint8 bits, int256 value);

    /**
     * @dev An uint value doesn't fit in an int of `bits` size.
     */
    error SafeCastOverflowedUintToInt(uint256 value);

    /**
     * @dev Returns the downcasted uint248 from uint256, reverting on
     * overflow (when the input is greater than largest uint248).
     *
     * Counterpart to Solidity's `uint248` operator.
     *
     * Requirements:
     *
     * - input must fit into 248 bits
     */
    function toUint248(uint256 value) internal pure returns (uint248) {
        if (value > type(uint248).max) {
            revert SafeCastOverflowedUintDowncast(248, value);
        }
        return uint248(value);
    }

    /**
     * @dev Returns the downcasted uint240 from uint256, reverting on
     * overflow (when the input is greater than largest uint240).
     *
     * Counterpart to Solidity's `uint240` operator.
     *
     * Requirements:
     *
     * - input must fit into 240 bits
     */
    function toUint240(uint256 value) internal pure returns (uint240) {
        if (value > type(uint240).max) {
            revert SafeCastOverflowedUintDowncast(240, value);
        }
        return uint240(value);
    }

    /**
     * @dev Returns the downcasted uint232 from uint256, reverting on
     * overflow (when the input is greater than largest uint232).
     *
     * Counterpart to Solidity's `uint232` operator.
     *
     * Requirements:
     *
     * - input must fit into 232 bits
     */
    function toUint232(uint256 value) internal pure returns (uint232) {
        if (value > type(uint232).max) {
            revert SafeCastOverflowedUintDowncast(232, value);
        }
        return uint232(value);
    }

    /**
     * @dev Returns the downcasted uint224 from uint256, reverting on
     * overflow (when the input is greater than largest uint224).
     *
     * Counterpart to Solidity's `uint224` operator.
     *
     * Requirements:
     *
     * - input must fit into 224 bits
     */
    function toUint224(uint256 value) internal pure returns (uint224) {
        if (value > type(uint224).max) {
            revert SafeCastOverflowedUintDowncast(224, value);
        }
        return uint224(value);
    }

    /**
     * @dev Returns the downcasted uint216 from uint256, reverting on
     * overflow (when the input is greater than largest uint216).
     *
     * Counterpart to Solidity's `uint216` operator.
     *
     * Requirements:
     *
     * - input must fit into 216 bits
     */
    function toUint216(uint256 value) internal pure returns (uint216) {
        if (value > type(uint216).max) {
            revert SafeCastOverflowedUintDowncast(216, value);
        }
        return uint216(value);
    }

    /**
     * @dev Returns the downcasted uint208 from uint256, reverting on
     * overflow (when the input is greater than largest uint208).
     *
     * Counterpart to Solidity's `uint208` operator.
     *
     * Requirements:
     *
     * - input must fit into 208 bits
     */
    function toUint208(uint256 value) internal pure returns (uint208) {
        if (value > type(uint208).max) {
            revert SafeCastOverflowedUintDowncast(208, value);
        }
        return uint208(value);
    }

    /**
     * @dev Returns the downcasted uint200 from uint256, reverting on
     * overflow (when the input is greater than largest uint200).
     *
     * Counterpart to Solidity's `uint200` operator.
     *
     * Requirements:
     *
     * - input must fit into 200 bits
     */
    function toUint200(uint256 value) internal pure returns (uint200) {
        if (value > type(uint200).max) {
            revert SafeCastOverflowedUintDowncast(200, value);
        }
        return uint200(value);
    }

    /**
     * @dev Returns the downcasted uint192 from uint256, reverting on
     * overflow (when the input is greater than largest uint192).
     *
     * Counterpart to Solidity's `uint192` operator.
     *
     * Requirements:
     *
     * - input must fit into 192 bits
     */
    function toUint192(uint256 value) internal pure returns (uint192) {
        if (value > type(uint192).max) {
            revert SafeCastOverflowedUintDowncast(192, value);
        }
        return uint192(value);
    }

    /**
     * @dev Returns the downcasted uint184 from uint256, reverting on
     * overflow (when the input is greater than largest uint184).
     *
     * Counterpart to Solidity's `uint184` operator.
     *
     * Requirements:
     *
     * - input must fit into 184 bits
     */
    function toUint184(uint256 value) internal pure returns (uint184) {
        if (value > type(uint184).max) {
            revert SafeCastOverflowedUintDowncast(184, value);
        }
        return uint184(value);
    }

    /**
     * @dev Returns the downcasted uint176 from uint256, reverting on
     * overflow (when the input is greater than largest uint176).
     *
     * Counterpart to Solidity's `uint176` operator.
     *
     * Requirements:
     *
     * - input must fit into 176 bits
     */
    function toUint176(uint256 value) internal pure returns (uint176) {
        if (value > type(uint176).max) {
            revert SafeCastOverflowedUintDowncast(176, value);
        }
        return uint176(value);
    }

    /**
     * @dev Returns the downcasted uint168 from uint256, reverting on
     * overflow (when the input is greater than largest uint168).
     *
     * Counterpart to Solidity's `uint168` operator.
     *
     * Requirements:
     *
     * - input must fit into 168 bits
     */
    function toUint168(uint256 value) internal pure returns (uint168) {
        if (value > type(uint168).max) {
            revert SafeCastOverflowedUintDowncast(168, value);
        }
        return uint168(value);
    }

    /**
     * @dev Returns the downcasted uint160 from uint256, reverting on
     * overflow (when the input is greater than largest uint160).
     *
     * Counterpart to Solidity's `uint160` operator.
     *
     * Requirements:
     *
     * - input must fit into 160 bits
     */
    function toUint160(uint256 value) internal pure returns (uint160) {
        if (value > type(uint160).max) {
            revert SafeCastOverflowedUintDowncast(160, value);
        }
        return uint160(value);
    }

    /**
     * @dev Returns the downcasted uint152 from uint256, reverting on
     * overflow (when the input is greater than largest uint152).
     *
     * Counterpart to Solidity's `uint152` operator.
     *
     * Requirements:
     *
     * - input must fit into 152 bits
     */
    function toUint152(uint256 value) internal pure returns (uint152) {
        if (value > type(uint152).max) {
            revert SafeCastOverflowedUintDowncast(152, value);
        }
        return uint152(value);
    }

    /**
     * @dev Returns the downcasted uint144 from uint256, reverting on
     * overflow (when the input is greater than largest uint144).
     *
     * Counterpart to Solidity's `uint144` operator.
     *
     * Requirements:
     *
     * - input must fit into 144 bits
     */
    function toUint144(uint256 value) internal pure returns (uint144) {
        if (value > type(uint144).max) {
            revert SafeCastOverflowedUintDowncast(144, value);
        }
        return uint144(value);
    }

    /**
     * @dev Returns the downcasted uint136 from uint256, reverting on
     * overflow (when the input is greater than largest uint136).
     *
     * Counterpart to Solidity's `uint136` operator.
     *
     * Requirements:
     *
     * - input must fit into 136 bits
     */
    function toUint136(uint256 value) internal pure returns (uint136) {
        if (value > type(uint136).max) {
            revert SafeCastOverflowedUintDowncast(136, value);
        }
        return uint136(value);
    }

    /**
     * @dev Returns the downcasted uint128 from uint256, reverting on
     * overflow (when the input is greater than largest uint128).
     *
     * Counterpart to Solidity's `uint128` operator.
     *
     * Requirements:
     *
     * - input must fit into 128 bits
     */
    function toUint128(uint256 value) internal pure returns (uint128) {
        if (value > type(uint128).max) {
            revert SafeCastOverflowedUintDowncast(128, value);
        }
        return uint128(value);
    }

    /**
     * @dev Returns the downcasted uint120 from uint256, reverting on
     * overflow (when the input is greater than largest uint120).
     *
     * Counterpart to Solidity's `uint120` operator.
     *
     * Requirements:
     *
     * - input must fit into 120 bits
     */
    function toUint120(uint256 value) internal pure returns (uint120) {
        if (value > type(uint120).max) {
            revert SafeCastOverflowedUintDowncast(120, value);
        }
        return uint120(value);
    }

    /**
     * @dev Returns the downcasted uint112 from uint256, reverting on
     * overflow (when the input is greater than largest uint112).
     *
     * Counterpart to Solidity's `uint112` operator.
     *
     * Requirements:
     *
     * - input must fit into 112 bits
     */
    function toUint112(uint256 value) internal pure returns (uint112) {
        if (value > type(uint112).max) {
            revert SafeCastOverflowedUintDowncast(112, value);
        }
        return uint112(value);
    }

    /**
     * @dev Returns the downcasted uint104 from uint256, reverting on
     * overflow (when the input is greater than largest uint104).
     *
     * Counterpart to Solidity's `uint104` operator.
     *
     * Requirements:
     *
     * - input must fit into 104 bits
     */
    function toUint104(uint256 value) internal pure returns (uint104) {
        if (value > type(uint104).max) {
            revert SafeCastOverflowedUintDowncast(104, value);
        }
        return uint104(value);
    }

    /**
     * @dev Returns the downcasted uint96 from uint256, reverting on
     * overflow (when the input is greater than largest uint96).
     *
     * Counterpart to Solidity's `uint96` operator.
     *
     * Requirements:
     *
     * - input must fit into 96 bits
     */
    function toUint96(uint256 value) internal pure returns (uint96) {
        if (value > type(uint96).max) {
            revert SafeCastOverflowedUintDowncast(96, value);
        }
        return uint96(value);
    }

    /**
     * @dev Returns the downcasted uint88 from uint256, reverting on
     * overflow (when the input is greater than largest uint88).
     *
     * Counterpart to Solidity's `uint88` operator.
     *
     * Requirements:
     *
     * - input must fit into 88 bits
     */
    function toUint88(uint256 value) internal pure returns (uint88) {
        if (value > type(uint88).max) {
            revert SafeCastOverflowedUintDowncast(88, value);
        }
        return uint88(value);
    }

    /**
     * @dev Returns the downcasted uint80 from uint256, reverting on
     * overflow (when the input is greater than largest uint80).
     *
     * Counterpart to Solidity's `uint80` operator.
     *
     * Requirements:
     *
     * - input must fit into 80 bits
     */
    function toUint80(uint256 value) internal pure returns (uint80) {
        if (value > type(uint80).max) {
            revert SafeCastOverflowedUintDowncast(80, value);
        }
        return uint80(value);
    }

    /**
     * @dev Returns the downcasted uint72 from uint256, reverting on
     * overflow (when the input is greater than largest uint72).
     *
     * Counterpart to Solidity's `uint72` operator.
     *
     * Requirements:
     *
     * - input must fit into 72 bits
     */
    function toUint72(uint256 value) internal pure returns (uint72) {
        if (value > type(uint72).max) {
            revert SafeCastOverflowedUintDowncast(72, value);
        }
        return uint72(value);
    }

    /**
     * @dev Returns the downcasted uint64 from uint256, reverting on
     * overflow (when the input is greater than largest uint64).
     *
     * Counterpart to Solidity's `uint64` operator.
     *
     * Requirements:
     *
     * - input must fit into 64 bits
     */
    function toUint64(uint256 value) internal pure returns (uint64) {
        if (value > type(uint64).max) {
            revert SafeCastOverflowedUintDowncast(64, value);
        }
        return uint64(value);
    }

    /**
     * @dev Returns the downcasted uint56 from uint256, reverting on
     * overflow (when the input is greater than largest uint56).
     *
     * Counterpart to Solidity's `uint56` operator.
     *
     * Requirements:
     *
     * - input must fit into 56 bits
     */
    function toUint56(uint256 value) internal pure returns (uint56) {
        if (value > type(uint56).max) {
            revert SafeCastOverflowedUintDowncast(56, value);
        }
        return uint56(value);
    }

    /**
     * @dev Returns the downcasted uint48 from uint256, reverting on
     * overflow (when the input is greater than largest uint48).
     *
     * Counterpart to Solidity's `uint48` operator.
     *
     * Requirements:
     *
     * - input must fit into 48 bits
     */
    function toUint48(uint256 value) internal pure returns (uint48) {
        if (value > type(uint48).max) {
            revert SafeCastOverflowedUintDowncast(48, value);
        }
        return uint48(value);
    }

    /**
     * @dev Returns the downcasted uint40 from uint256, reverting on
     * overflow (when the input is greater than largest uint40).
     *
     * Counterpart to Solidity's `uint40` operator.
     *
     * Requirements:
     *
     * - input must fit into 40 bits
     */
    function toUint40(uint256 value) internal pure returns (uint40) {
        if (value > type(uint40).max) {
            revert SafeCastOverflowedUintDowncast(40, value);
        }
        return uint40(value);
    }

    /**
     * @dev Returns the downcasted uint32 from uint256, reverting on
     * overflow (when the input is greater than largest uint32).
     *
     * Counterpart to Solidity's `uint32` operator.
     *
     * Requirements:
     *
     * - input must fit into 32 bits
     */
    function toUint32(uint256 value) internal pure returns (uint32) {
        if (value > type(uint32).max) {
            revert SafeCastOverflowedUintDowncast(32, value);
        }
        return uint32(value);
    }

    /**
     * @dev Returns the downcasted uint24 from uint256, reverting on
     * overflow (when the input is greater than largest uint24).
     *
     * Counterpart to Solidity's `uint24` operator.
     *
     * Requirements:
     *
     * - input must fit into 24 bits
     */
    function toUint24(uint256 value) internal pure returns (uint24) {
        if (value > type(uint24).max) {
            revert SafeCastOverflowedUintDowncast(24, value);
        }
        return uint24(value);
    }

    /**
     * @dev Returns the downcasted uint16 from uint256, reverting on
     * overflow (when the input is greater than largest uint16).
     *
     * Counterpart to Solidity's `uint16` operator.
     *
     * Requirements:
     *
     * - input must fit into 16 bits
     */
    function toUint16(uint256 value) internal pure returns (uint16) {
        if (value > type(uint16).max) {
            revert SafeCastOverflowedUintDowncast(16, value);
        }
        return uint16(value);
    }

    /**
     * @dev Returns the downcasted uint8 from uint256, reverting on
     * overflow (when the input is greater than largest uint8).
     *
     * Counterpart to Solidity's `uint8` operator.
     *
     * Requirements:
     *
     * - input must fit into 8 bits
     */
    function toUint8(uint256 value) internal pure returns (uint8) {
        if (value > type(uint8).max) {
            revert SafeCastOverflowedUintDowncast(8, value);
        }
        return uint8(value);
    }

    /**
     * @dev Converts a signed int256 into an unsigned uint256.
     *
     * Requirements:
     *
     * - input must be greater than or equal to 0.
     */
    function toUint256(int256 value) internal pure returns (uint256) {
        if (value < 0) {
            revert SafeCastOverflowedIntToUint(value);
        }
        return uint256(value);
    }

    /**
     * @dev Returns the downcasted int248 from int256, reverting on
     * overflow (when the input is less than smallest int248 or
     * greater than largest int248).
     *
     * Counterpart to Solidity's `int248` operator.
     *
     * Requirements:
     *
     * - input must fit into 248 bits
     */
    function toInt248(int256 value) internal pure returns (int248 downcasted) {
        downcasted = int248(value);
        if (downcasted != value) {
            revert SafeCastOverflowedIntDowncast(248, value);
        }
    }

    /**
     * @dev Returns the downcasted int240 from int256, reverting on
     * overflow (when the input is less than smallest int240 or
     * greater than largest int240).
     *
     * Counterpart to Solidity's `int240` operator.
     *
     * Requirements:
     *
     * - input must fit into 240 bits
     */
    function toInt240(int256 value) internal pure returns (int240 downcasted) {
        downcasted = int240(value);
        if (downcasted != value) {
            revert SafeCastOverflowedIntDowncast(240, value);
        }
    }

    /**
     * @dev Returns the downcasted int232 from int256, reverting on
     * overflow (when the input is less than smallest int232 or
     * greater than largest int232).
     *
     * Counterpart to Solidity's `int232` operator.
     *
     * Requirements:
     *
     * - input must fit into 232 bits
     */
    function toInt232(int256 value) internal pure returns (int232 downcasted) {
        downcasted = int232(value);
        if (downcasted != value) {
            revert SafeCastOverflowedIntDowncast(232, value);
        }
    }

    /**
     * @dev Returns the downcasted int224 from int256, reverting on
     * overflow (when the input is less than smallest int224 or
     * greater than largest int224).
     *
     * Counterpart to Solidity's `int224` operator.
     *
     * Requirements:
     *
     * - input must fit into 224 bits
     */
    function toInt224(int256 value) internal pure returns (int224 downcasted) {
        downcasted = int224(value);
        if (downcasted != value) {
            revert SafeCastOverflowedIntDowncast(224, value);
        }
    }

    /**
     * @dev Returns the downcasted int216 from int256, reverting on
     * overflow (when the input is less than smallest int216 or
     * greater than largest int216).
     *
     * Counterpart to Solidity's `int216` operator.
     *
     * Requirements:
     *
     * - input must fit into 216 bits
     */
    function toInt216(int256 value) internal pure returns (int216 downcasted) {
        downcasted = int216(value);
        if (downcasted != value) {
            revert SafeCastOverflowedIntDowncast(216, value);
        }
    }

    /**
     * @dev Returns the downcasted int208 from int256, reverting on
     * overflow (when the input is less than smallest int208 or
     * greater than largest int208).
     *
     * Counterpart to Solidity's `int208` operator.
     *
     * Requirements:
     *
     * - input must fit into 208 bits
     */
    function toInt208(int256 value) internal pure returns (int208 downcasted) {
        downcasted = int208(value);
        if (downcasted != value) {
            revert SafeCastOverflowedIntDowncast(208, value);
        }
    }

    /**
     * @dev Returns the downcasted int200 from int256, reverting on
     * overflow (when the input is less than smallest int200 or
     * greater than largest int200).
     *
     * Counterpart to Solidity's `int200` operator.
     *
     * Requirements:
     *
     * - input must fit into 200 bits
     */
    function toInt200(int256 value) internal pure returns (int200 downcasted) {
        downcasted = int200(value);
        if (downcasted != value) {
            revert SafeCastOverflowedIntDowncast(200, value);
        }
    }

    /**
     * @dev Returns the downcasted int192 from int256, reverting on
     * overflow (when the input is less than smallest int192 or
     * greater than largest int192).
     *
     * Counterpart to Solidity's `int192` operator.
     *
     * Requirements:
     *
     * - input must fit into 192 bits
     */
    function toInt192(int256 value) internal pure returns (int192 downcasted) {
        downcasted = int192(value);
        if (downcasted != value) {
            revert SafeCastOverflowedIntDowncast(192, value);
        }
    }

    /**
     * @dev Returns the downcasted int184 from int256, reverting on
     * overflow (when the input is less than smallest int184 or
     * greater than largest int184).
     *
     * Counterpart to Solidity's `int184` operator.
     *
     * Requirements:
     *
     * - input must fit into 184 bits
     */
    function toInt184(int256 value) internal pure returns (int184 downcasted) {
        downcasted = int184(value);
        if (downcasted != value) {
            revert SafeCastOverflowedIntDowncast(184, value);
        }
    }

    /**
     * @dev Returns the downcasted int176 from int256, reverting on
     * overflow (when the input is less than smallest int176 or
     * greater than largest int176).
     *
     * Counterpart to Solidity's `int176` operator.
     *
     * Requirements:
     *
     * - input must fit into 176 bits
     */
    function toInt176(int256 value) internal pure returns (int176 downcasted) {
        downcasted = int176(value);
        if (downcasted != value) {
            revert SafeCastOverflowedIntDowncast(176, value);
        }
    }

    /**
     * @dev Returns the downcasted int168 from int256, reverting on
     * overflow (when the input is less than smallest int168 or
     * greater than largest int168).
     *
     * Counterpart to Solidity's `int168` operator.
     *
     * Requirements:
     *
     * - input must fit into 168 bits
     */
    function toInt168(int256 value) internal pure returns (int168 downcasted) {
        downcasted = int168(value);
        if (downcasted != value) {
            revert SafeCastOverflowedIntDowncast(168, value);
        }
    }

    /**
     * @dev Returns the downcasted int160 from int256, reverting on
     * overflow (when the input is less than smallest int160 or
     * greater than largest int160).
     *
     * Counterpart to Solidity's `int160` operator.
     *
     * Requirements:
     *
     * - input must fit into 160 bits
     */
    function toInt160(int256 value) internal pure returns (int160 downcasted) {
        downcasted = int160(value);
        if (downcasted != value) {
            revert SafeCastOverflowedIntDowncast(160, value);
        }
    }

    /**
     * @dev Returns the downcasted int152 from int256, reverting on
     * overflow (when the input is less than smallest int152 or
     * greater than largest int152).
     *
     * Counterpart to Solidity's `int152` operator.
     *
     * Requirements:
     *
     * - input must fit into 152 bits
     */
    function toInt152(int256 value) internal pure returns (int152 downcasted) {
        downcasted = int152(value);
        if (downcasted != value) {
            revert SafeCastOverflowedIntDowncast(152, value);
        }
    }

    /**
     * @dev Returns the downcasted int144 from int256, reverting on
     * overflow (when the input is less than smallest int144 or
     * greater than largest int144).
     *
     * Counterpart to Solidity's `int144` operator.
     *
     * Requirements:
     *
     * - input must fit into 144 bits
     */
    function toInt144(int256 value) internal pure returns (int144 downcasted) {
        downcasted = int144(value);
        if (downcasted != value) {
            revert SafeCastOverflowedIntDowncast(144, value);
        }
    }

    /**
     * @dev Returns the downcasted int136 from int256, reverting on
     * overflow (when the input is less than smallest int136 or
     * greater than largest int136).
     *
     * Counterpart to Solidity's `int136` operator.
     *
     * Requirements:
     *
     * - input must fit into 136 bits
     */
    function toInt136(int256 value) internal pure returns (int136 downcasted) {
        downcasted = int136(value);
        if (downcasted != value) {
            revert SafeCastOverflowedIntDowncast(136, value);
        }
    }

    /**
     * @dev Returns the downcasted int128 from int256, reverting on
     * overflow (when the input is less than smallest int128 or
     * greater than largest int128).
     *
     * Counterpart to Solidity's `int128` operator.
     *
     * Requirements:
     *
     * - input must fit into 128 bits
     */
    function toInt128(int256 value) internal pure returns (int128 downcasted) {
        downcasted = int128(value);
        if (downcasted != value) {
            revert SafeCastOverflowedIntDowncast(128, value);
        }
    }

    /**
     * @dev Returns the downcasted int120 from int256, reverting on
     * overflow (when the input is less than smallest int120 or
     * greater than largest int120).
     *
     * Counterpart to Solidity's `int120` operator.
     *
     * Requirements:
     *
     * - input must fit into 120 bits
     */
    function toInt120(int256 value) internal pure returns (int120 downcasted) {
        downcasted = int120(value);
        if (downcasted != value) {
            revert SafeCastOverflowedIntDowncast(120, value);
        }
    }

    /**
     * @dev Returns the downcasted int112 from int256, reverting on
     * overflow (when the input is less than smallest int112 or
     * greater than largest int112).
     *
     * Counterpart to Solidity's `int112` operator.
     *
     * Requirements:
     *
     * - input must fit into 112 bits
     */
    function toInt112(int256 value) internal pure returns (int112 downcasted) {
        downcasted = int112(value);
        if (downcasted != value) {
            revert SafeCastOverflowedIntDowncast(112, value);
        }
    }

    /**
     * @dev Returns the downcasted int104 from int256, reverting on
     * overflow (when the input is less than smallest int104 or
     * greater than largest int104).
     *
     * Counterpart to Solidity's `int104` operator.
     *
     * Requirements:
     *
     * - input must fit into 104 bits
     */
    function toInt104(int256 value) internal pure returns (int104 downcasted) {
        downcasted = int104(value);
        if (downcasted != value) {
            revert SafeCastOverflowedIntDowncast(104, value);
        }
    }

    /**
     * @dev Returns the downcasted int96 from int256, reverting on
     * overflow (when the input is less than smallest int96 or
     * greater than largest int96).
     *
     * Counterpart to Solidity's `int96` operator.
     *
     * Requirements:
     *
     * - input must fit into 96 bits
     */
    function toInt96(int256 value) internal pure returns (int96 downcasted) {
        downcasted = int96(value);
        if (downcasted != value) {
            revert SafeCastOverflowedIntDowncast(96, value);
        }
    }

    /**
     * @dev Returns the downcasted int88 from int256, reverting on
     * overflow (when the input is less than smallest int88 or
     * greater than largest int88).
     *
     * Counterpart to Solidity's `int88` operator.
     *
     * Requirements:
     *
     * - input must fit into 88 bits
     */
    function toInt88(int256 value) internal pure returns (int88 downcasted) {
        downcasted = int88(value);
        if (downcasted != value) {
            revert SafeCastOverflowedIntDowncast(88, value);
        }
    }

    /**
     * @dev Returns the downcasted int80 from int256, reverting on
     * overflow (when the input is less than smallest int80 or
     * greater than largest int80).
     *
     * Counterpart to Solidity's `int80` operator.
     *
     * Requirements:
     *
     * - input must fit into 80 bits
     */
    function toInt80(int256 value) internal pure returns (int80 downcasted) {
        downcasted = int80(value);
        if (downcasted != value) {
            revert SafeCastOverflowedIntDowncast(80, value);
        }
    }

    /**
     * @dev Returns the downcasted int72 from int256, reverting on
     * overflow (when the input is less than smallest int72 or
     * greater than largest int72).
     *
     * Counterpart to Solidity's `int72` operator.
     *
     * Requirements:
     *
     * - input must fit into 72 bits
     */
    function toInt72(int256 value) internal pure returns (int72 downcasted) {
        downcasted = int72(value);
        if (downcasted != value) {
            revert SafeCastOverflowedIntDowncast(72, value);
        }
    }

    /**
     * @dev Returns the downcasted int64 from int256, reverting on
     * overflow (when the input is less than smallest int64 or
     * greater than largest int64).
     *
     * Counterpart to Solidity's `int64` operator.
     *
     * Requirements:
     *
     * - input must fit into 64 bits
     */
    function toInt64(int256 value) internal pure returns (int64 downcasted) {
        downcasted = int64(value);
        if (downcasted != value) {
            revert SafeCastOverflowedIntDowncast(64, value);
        }
    }

    /**
     * @dev Returns the downcasted int56 from int256, reverting on
     * overflow (when the input is less than smallest int56 or
     * greater than largest int56).
     *
     * Counterpart to Solidity's `int56` operator.
     *
     * Requirements:
     *
     * - input must fit into 56 bits
     */
    function toInt56(int256 value) internal pure returns (int56 downcasted) {
        downcasted = int56(value);
        if (downcasted != value) {
            revert SafeCastOverflowedIntDowncast(56, value);
        }
    }

    /**
     * @dev Returns the downcasted int48 from int256, reverting on
     * overflow (when the input is less than smallest int48 or
     * greater than largest int48).
     *
     * Counterpart to Solidity's `int48` operator.
     *
     * Requirements:
     *
     * - input must fit into 48 bits
     */
    function toInt48(int256 value) internal pure returns (int48 downcasted) {
        downcasted = int48(value);
        if (downcasted != value) {
            revert SafeCastOverflowedIntDowncast(48, value);
        }
    }

    /**
     * @dev Returns the downcasted int40 from int256, reverting on
     * overflow (when the input is less than smallest int40 or
     * greater than largest int40).
     *
     * Counterpart to Solidity's `int40` operator.
     *
     * Requirements:
     *
     * - input must fit into 40 bits
     */
    function toInt40(int256 value) internal pure returns (int40 downcasted) {
        downcasted = int40(value);
        if (downcasted != value) {
            revert SafeCastOverflowedIntDowncast(40, value);
        }
    }

    /**
     * @dev Returns the downcasted int32 from int256, reverting on
     * overflow (when the input is less than smallest int32 or
     * greater than largest int32).
     *
     * Counterpart to Solidity's `int32` operator.
     *
     * Requirements:
     *
     * - input must fit into 32 bits
     */
    function toInt32(int256 value) internal pure returns (int32 downcasted) {
        downcasted = int32(value);
        if (downcasted != value) {
            revert SafeCastOverflowedIntDowncast(32, value);
        }
    }

    /**
     * @dev Returns the downcasted int24 from int256, reverting on
     * overflow (when the input is less than smallest int24 or
     * greater than largest int24).
     *
     * Counterpart to Solidity's `int24` operator.
     *
     * Requirements:
     *
     * - input must fit into 24 bits
     */
    function toInt24(int256 value) internal pure returns (int24 downcasted) {
        downcasted = int24(value);
        if (downcasted != value) {
            revert SafeCastOverflowedIntDowncast(24, value);
        }
    }

    /**
     * @dev Returns the downcasted int16 from int256, reverting on
     * overflow (when the input is less than smallest int16 or
     * greater than largest int16).
     *
     * Counterpart to Solidity's `int16` operator.
     *
     * Requirements:
     *
     * - input must fit into 16 bits
     */
    function toInt16(int256 value) internal pure returns (int16 downcasted) {
        downcasted = int16(value);
        if (downcasted != value) {
            revert SafeCastOverflowedIntDowncast(16, value);
        }
    }

    /**
     * @dev Returns the downcasted int8 from int256, reverting on
     * overflow (when the input is less than smallest int8 or
     * greater than largest int8).
     *
     * Counterpart to Solidity's `int8` operator.
     *
     * Requirements:
     *
     * - input must fit into 8 bits
     */
    function toInt8(int256 value) internal pure returns (int8 downcasted) {
        downcasted = int8(value);
        if (downcasted != value) {
            revert SafeCastOverflowedIntDowncast(8, value);
        }
    }

    /**
     * @dev Converts an unsigned uint256 into a signed int256.
     *
     * Requirements:
     *
     * - input must be less than or equal to maxInt256.
     */
    function toInt256(uint256 value) internal pure returns (int256) {
        // Note: Unsafe cast below is okay because `type(int256).max` is guaranteed to be positive
        if (value > uint256(type(int256).max)) {
            revert SafeCastOverflowedUintToInt(value);
        }
        return int256(value);
    }

    /**
     * @dev Cast a boolean (false or true) to a uint256 (0 or 1) with no jump.
     */
    function toUint(bool b) internal pure returns (uint256 u) {
        assembly ("memory-safe") {
            u := iszero(iszero(b))
        }
    }
}

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (utils/structs/EnumerableSet.sol)
// This file was procedurally generated from scripts/generate/templates/EnumerableSet.js.

pragma solidity ^0.8.20;

/**
 * @dev Library for managing
 * https://en.wikipedia.org/wiki/Set_(abstract_data_type)[sets] of primitive
 * types.
 *
 * Sets have the following properties:
 *
 * - Elements are added, removed, and checked for existence in constant time
 * (O(1)).
 * - Elements are enumerated in O(n). No guarantees are made on the ordering.
 *
 * ```solidity
 * contract Example {
 *     // Add the library methods
 *     using EnumerableSet for EnumerableSet.AddressSet;
 *
 *     // Declare a set state variable
 *     EnumerableSet.AddressSet private mySet;
 * }
 * ```
 *
 * As of v3.3.0, sets of type `bytes32` (`Bytes32Set`), `address` (`AddressSet`)
 * and `uint256` (`UintSet`) are supported.
 *
 * [WARNING]
 * ====
 * Trying to delete such a structure from storage will likely result in data corruption, rendering the structure
 * unusable.
 * See https://github.com/ethereum/solidity/pull/11843[ethereum/solidity#11843] for more info.
 *
 * In order to clean an EnumerableSet, you can either remove all elements one by one or create a fresh instance using an
 * array of EnumerableSet.
 * ====
 */
library EnumerableSet {
    // To implement this library for multiple types with as little code
    // repetition as possible, we write it in terms of a generic Set type with
    // bytes32 values.
    // The Set implementation uses private functions, and user-facing
    // implementations (such as AddressSet) are just wrappers around the
    // underlying Set.
    // This means that we can only create new EnumerableSets for types that fit
    // in bytes32.

    struct Set {
        // Storage of set values
        bytes32[] _values;
        // Position is the index of the value in the `values` array plus 1.
        // Position 0 is used to mean a value is not in the set.
        mapping(bytes32 value => uint256) _positions;
    }

    /**
     * @dev Add a value to a set. O(1).
     *
     * Returns true if the value was added to the set, that is if it was not
     * already present.
     */
    function _add(Set storage set, bytes32 value) private returns (bool) {
        if (!_contains(set, value)) {
            set._values.push(value);
            // The value is stored at length-1, but we add 1 to all indexes
            // and use 0 as a sentinel value
            set._positions[value] = set._values.length;
            return true;
        } else {
            return false;
        }
    }

    /**
     * @dev Removes a value from a set. O(1).
     *
     * Returns true if the value was removed from the set, that is if it was
     * present.
     */
    function _remove(Set storage set, bytes32 value) private returns (bool) {
        // We cache the value's position to prevent multiple reads from the same storage slot
        uint256 position = set._positions[value];

        if (position != 0) {
            // Equivalent to contains(set, value)
            // To delete an element from the _values array in O(1), we swap the element to delete with the last one in
            // the array, and then remove the last element (sometimes called as 'swap and pop').
            // This modifies the order of the array, as noted in {at}.

            uint256 valueIndex = position - 1;
            uint256 lastIndex = set._values.length - 1;

            if (valueIndex != lastIndex) {
                bytes32 lastValue = set._values[lastIndex];

                // Move the lastValue to the index where the value to delete is
                set._values[valueIndex] = lastValue;
                // Update the tracked position of the lastValue (that was just moved)
                set._positions[lastValue] = position;
            }

            // Delete the slot where the moved value was stored
            set._values.pop();

            // Delete the tracked position for the deleted slot
            delete set._positions[value];

            return true;
        } else {
            return false;
        }
    }

    /**
     * @dev Returns true if the value is in the set. O(1).
     */
    function _contains(Set storage set, bytes32 value) private view returns (bool) {
        return set._positions[value] != 0;
    }

    /**
     * @dev Returns the number of values on the set. O(1).
     */
    function _length(Set storage set) private view returns (uint256) {
        return set._values.length;
    }

    /**
     * @dev Returns the value stored at position `index` in the set. O(1).
     *
     * Note that there are no guarantees on the ordering of values inside the
     * array, and it may change when more values are added or removed.
     *
     * Requirements:
     *
     * - `index` must be strictly less than {length}.
     */
    function _at(Set storage set, uint256 index) private view returns (bytes32) {
        return set._values[index];
    }

    /**
     * @dev Return the entire set in an array
     *
     * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed
     * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that
     * this function has an unbounded cost, and using it as part of a state-changing function may render the function
     * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.
     */
    function _values(Set storage set) private view returns (bytes32[] memory) {
        return set._values;
    }

    // Bytes32Set

    struct Bytes32Set {
        Set _inner;
    }

    /**
     * @dev Add a value to a set. O(1).
     *
     * Returns true if the value was added to the set, that is if it was not
     * already present.
     */
    function add(Bytes32Set storage set, bytes32 value) internal returns (bool) {
        return _add(set._inner, value);
    }

    /**
     * @dev Removes a value from a set. O(1).
     *
     * Returns true if the value was removed from the set, that is if it was
     * present.
     */
    function remove(Bytes32Set storage set, bytes32 value) internal returns (bool) {
        return _remove(set._inner, value);
    }

    /**
     * @dev Returns true if the value is in the set. O(1).
     */
    function contains(Bytes32Set storage set, bytes32 value) internal view returns (bool) {
        return _contains(set._inner, value);
    }

    /**
     * @dev Returns the number of values in the set. O(1).
     */
    function length(Bytes32Set storage set) internal view returns (uint256) {
        return _length(set._inner);
    }

    /**
     * @dev Returns the value stored at position `index` in the set. O(1).
     *
     * Note that there are no guarantees on the ordering of values inside the
     * array, and it may change when more values are added or removed.
     *
     * Requirements:
     *
     * - `index` must be strictly less than {length}.
     */
    function at(Bytes32Set storage set, uint256 index) internal view returns (bytes32) {
        return _at(set._inner, index);
    }

    /**
     * @dev Return the entire set in an array
     *
     * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed
     * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that
     * this function has an unbounded cost, and using it as part of a state-changing function may render the function
     * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.
     */
    function values(Bytes32Set storage set) internal view returns (bytes32[] memory) {
        bytes32[] memory store = _values(set._inner);
        bytes32[] memory result;

        assembly ("memory-safe") {
            result := store
        }

        return result;
    }

    // AddressSet

    struct AddressSet {
        Set _inner;
    }

    /**
     * @dev Add a value to a set. O(1).
     *
     * Returns true if the value was added to the set, that is if it was not
     * already present.
     */
    function add(AddressSet storage set, address value) internal returns (bool) {
        return _add(set._inner, bytes32(uint256(uint160(value))));
    }

    /**
     * @dev Removes a value from a set. O(1).
     *
     * Returns true if the value was removed from the set, that is if it was
     * present.
     */
    function remove(AddressSet storage set, address value) internal returns (bool) {
        return _remove(set._inner, bytes32(uint256(uint160(value))));
    }

    /**
     * @dev Returns true if the value is in the set. O(1).
     */
    function contains(AddressSet storage set, address value) internal view returns (bool) {
        return _contains(set._inner, bytes32(uint256(uint160(value))));
    }

    /**
     * @dev Returns the number of values in the set. O(1).
     */
    function length(AddressSet storage set) internal view returns (uint256) {
        return _length(set._inner);
    }

    /**
     * @dev Returns the value stored at position `index` in the set. O(1).
     *
     * Note that there are no guarantees on the ordering of values inside the
     * array, and it may change when more values are added or removed.
     *
     * Requirements:
     *
     * - `index` must be strictly less than {length}.
     */
    function at(AddressSet storage set, uint256 index) internal view returns (address) {
        return address(uint160(uint256(_at(set._inner, index))));
    }

    /**
     * @dev Return the entire set in an array
     *
     * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed
     * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that
     * this function has an unbounded cost, and using it as part of a state-changing function may render the function
     * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.
     */
    function values(AddressSet storage set) internal view returns (address[] memory) {
        bytes32[] memory store = _values(set._inner);
        address[] memory result;

        assembly ("memory-safe") {
            result := store
        }

        return result;
    }

    // UintSet

    struct UintSet {
        Set _inner;
    }

    /**
     * @dev Add a value to a set. O(1).
     *
     * Returns true if the value was added to the set, that is if it was not
     * already present.
     */
    function add(UintSet storage set, uint256 value) internal returns (bool) {
        return _add(set._inner, bytes32(value));
    }

    /**
     * @dev Removes a value from a set. O(1).
     *
     * Returns true if the value was removed from the set, that is if it was
     * present.
     */
    function remove(UintSet storage set, uint256 value) internal returns (bool) {
        return _remove(set._inner, bytes32(value));
    }

    /**
     * @dev Returns true if the value is in the set. O(1).
     */
    function contains(UintSet storage set, uint256 value) internal view returns (bool) {
        return _contains(set._inner, bytes32(value));
    }

    /**
     * @dev Returns the number of values in the set. O(1).
     */
    function length(UintSet storage set) internal view returns (uint256) {
        return _length(set._inner);
    }

    /**
     * @dev Returns the value stored at position `index` in the set. O(1).
     *
     * Note that there are no guarantees on the ordering of values inside the
     * array, and it may change when more values are added or removed.
     *
     * Requirements:
     *
     * - `index` must be strictly less than {length}.
     */
    function at(UintSet storage set, uint256 index) internal view returns (uint256) {
        return uint256(_at(set._inner, index));
    }

    /**
     * @dev Return the entire set in an array
     *
     * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed
     * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that
     * this function has an unbounded cost, and using it as part of a state-changing function may render the function
     * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.
     */
    function values(UintSet storage set) internal view returns (uint256[] memory) {
        bytes32[] memory store = _values(set._inner);
        uint256[] memory result;

        assembly ("memory-safe") {
            result := store
        }

        return result;
    }
}

File 22 of 50 : AccessControlConfirmable.sol
// SPDX-FileCopyrightText: 2025 Lido <[email protected]>
// SPDX-License-Identifier: GPL-3.0

// See contracts/COMPILERS.md
pragma solidity 0.8.25;

import {AccessControlEnumerable} from "@openzeppelin/contracts-v5.2/access/extensions/AccessControlEnumerable.sol";
import {Confirmations} from "./Confirmations.sol";

/**
 * @title AccessControlConfirmable
 * @author Lido
 * @notice An extension of AccessControlEnumerable that allows executing functions by mutual confirmation.
 * @dev This contract extends Confirmations and AccessControlEnumerable and adds a confirmation mechanism.
 */
abstract contract AccessControlConfirmable is AccessControlEnumerable, Confirmations {

    constructor() {
        __Confirmations_init();
    }

    function _isValidConfirmer(bytes32 _role) internal view override returns (bool) {
        return hasRole(_role, msg.sender);
    }
}

// SPDX-FileCopyrightText: 2025 Lido <[email protected]>
// SPDX-License-Identifier: GPL-3.0

// See contracts/COMPILERS.md
pragma solidity 0.8.25;

import {Confirmations} from "./Confirmations.sol";

/**
 * @title Confirmable2Addresses
 * @author Lido
 * @notice An extension of Confirmations that allows executing functions by mutual confirmation.
 * @dev In this implementation, roles are treated as addresses.
 */
abstract contract Confirmable2Addresses is Confirmations {

    function _collectAndCheckConfirmations(bytes calldata _calldata, address _role1, address _role2) internal returns (bool) {
        bytes32[] memory roles = new bytes32[](2);
        roles[0] = bytes32(uint256(uint160(_role1)));
        roles[1] = bytes32(uint256(uint160(_role2)));

        return _collectAndCheckConfirmations(_calldata, roles);
    }

    function _isValidConfirmer(bytes32 _roleAsAddress) internal view override returns (bool) {
        return _roleAsAddress == bytes32(uint256(uint160(msg.sender)));
    }
}

// SPDX-FileCopyrightText: 2025 Lido <[email protected]>
// SPDX-License-Identifier: GPL-3.0

// See contracts/COMPILERS.md
pragma solidity 0.8.25;

/**
 * @title Confirmations
 * @author Lido
 * @notice A contract that allows executing functions by mutual confirmation.
 */
abstract contract Confirmations {

    /**
     * @notice Tracks confirmations
     * @custom:storage-location erc7201:Lido.Utils.Confirmations
     * @dev We cannot set confirmExpiry to 0 because this means that all confirmations have to be in the same block,
     *      which can never be guaranteed. And, more importantly, if the `_setConfirmExpiry` requires confirmation,
     *      the confirmation expiry will be tricky to change.
     *      This is why confirmExpiry is private, set to a default value of 1 days and cannot be set to 0.
     *
     * Storage layout:
     * - callData: msg.data of the call (selector + arguments)
     * - role: role that confirmed the action
     * - expiryTimestamp: timestamp of the confirmation
     *
     * - confirmExpiry: confirmation expiry period in seconds
     */
    struct ConfirmationStorage {
        mapping(bytes callData => mapping(bytes32 role => uint256 expiryTimestamp)) confirmations;
        uint256 confirmExpiry;
    }

    /**
     * @notice Storage offset slot for ERC-7201 namespace
     *         The storage namespace is used to prevent upgrade collisions
     *         keccak256(abi.encode(uint256(keccak256("Lido.Utils.Confirmations")) - 1)) & ~bytes32(uint256(0xff))
     */
    bytes32 private constant CONFIRMATIONS_STORAGE_LOCATION =
        0xe4ca011a1344eb515c922209bf867930fc05bf79f4b0e3bb4ec9938eedd47700;


    /**
     * @notice Minimal confirmation expiry in seconds.
     */
    uint256 public constant MIN_CONFIRM_EXPIRY = 1 hours;

    /**
     * @notice Maximal confirmation expiry in seconds.
     */
    uint256 public constant MAX_CONFIRM_EXPIRY = 30 days;

    function __Confirmations_init() internal {
        _setConfirmExpiry(1 days);
    }


    /**
     * @notice Returns the confirmation expiry.
     * @return The confirmation expiry in seconds.
     */
    function getConfirmExpiry() public view returns (uint256) {
        return _getConfirmationsStorage().confirmExpiry;
    }

    /**
     * @notice Returns the confirmation expiry for a given call data and confirmer.
     * @param _callData The call data of the function.
     * @param _role The role of the confirmer.
     * @return The confirmation expiration timestamp or 0 if there was no confirmation from this role to this _callData
     */
    function confirmation(bytes memory _callData, bytes32 _role) external view returns (uint256) {
        return _getConfirmationsStorage().confirmations[_callData][_role];
    }

    /**
     * @dev Processes a confirmation from the current caller and checks if all required confirmations are present.
     * Confirmation, in this context, is a call to the same function with the same arguments.
     * This is a one-off operation that either:
     * - Collects the current caller's confirmation and returns false if not enough confirmations
     * - Or clears all confirmations and returns true if all required confirmations are present
     *
     * The confirmation process works as follows:
     * 1. When a role member calls the function:
     *    - Their confirmation is counted immediately
     *    - If not enough confirmations exist, their confirmation is recorded
     *    - If they're not a member of any of the specified roles, the call reverts
     *
     * 2. Confirmation counting:
     *    - Counts the current caller's confirmations if they're a member of any of the specified roles
     *    - Counts existing confirmations that are not expired, i.e. expiry is not exceeded
     *
     * 3. Confirmation Management:
     *    - If all members of the specified roles have confirmed:
     *      a. Clears all confirmations for this call
     *      b. Returns true to indicate that the function can be executed
     *    - If not enough confirmations:
     *      a. Stores the current confirmations
     *      b. Returns false to indicate that the function cannot be executed yet
     *    - Thus, if the caller has all the roles, returns true immediately
     *
     * 4. Gas Optimization:
     *    - Confirmations are stored in a deferred manner using a memory array
     *    - Confirmation storage writes only occur if the function cannot be executed immediately
     *    - This prevents unnecessary storage writes when all confirmations are present,
     *      because the confirmations are cleared anyway after the function is executed,
     *    - i.e. this optimization is beneficial for the deciding caller and
     *      saves 1 storage write for each role the deciding caller has
     *
     * @param _calldata msg.data of the call (selector + arguments)
     * @param _roles Array of role identifiers that must confirm the call in order to execute it
     * @return bool True if all required confirmations are present and the function can be executed, false otherwise
     *
     * @notice Confirmations past their expiry are not counted and must be recast
     * @notice Only members of the specified roles can submit confirmations
     * @notice The order of confirmations does not matter
     *
     */
    function _collectAndCheckConfirmations(bytes calldata _calldata, bytes32[] memory _roles) internal returns (bool) {
        if (_roles.length == 0) revert ZeroConfirmingRoles();

        uint256 numberOfRoles = _roles.length;
        uint256 numberOfConfirms = 0;
        bool[] memory deferredConfirms = new bool[](numberOfRoles);
        bool isRoleMember = false;

        ConfirmationStorage storage $ = _getConfirmationsStorage();
        uint256 expiryTimestamp = block.timestamp + $.confirmExpiry;

        for (uint256 i = 0; i < numberOfRoles; ++i) {
            bytes32 role = _roles[i];
            if (_isValidConfirmer(role)) {
                isRoleMember = true;
                numberOfConfirms++;
                deferredConfirms[i] = true;

                emit RoleMemberConfirmed(msg.sender, role, block.timestamp, expiryTimestamp, msg.data);
            } else if ($.confirmations[_calldata][role] >= block.timestamp) {
                numberOfConfirms++;
            }
        }

        if (!isRoleMember) revert SenderNotMember();

        if (numberOfConfirms == numberOfRoles) {
            for (uint256 i = 0; i < numberOfRoles; ++i) {
                bytes32 role = _roles[i];
                delete $.confirmations[_calldata][role];
            }
            return true;
        } else {
            for (uint256 i = 0; i < numberOfRoles; ++i) {
                if (deferredConfirms[i]) {
                    bytes32 role = _roles[i];
                    $.confirmations[_calldata][role] = expiryTimestamp;
                }
            }
            return false;
        }
    }

    /**
     * @notice Checks if the caller is a valid confirmer
     * @param _role The role to check
     * @return bool True if the caller is a valid confirmer
     */
    function _isValidConfirmer(bytes32 _role) internal view virtual returns (bool);

    /**
     * @dev Sets the confirmation expiry.
     * Confirmation expiry is a period during which the confirmation is counted. Once expired,
     * the confirmation no longer counts and must be recasted for the confirmation to go through.
     * @dev Does not retroactively apply to existing confirmations.
     * @param _newConfirmExpiry The new confirmation expiry in seconds.
     */
    function _setConfirmExpiry(uint256 _newConfirmExpiry) internal {
        _validateConfirmExpiry(_newConfirmExpiry);

        ConfirmationStorage storage $ = _getConfirmationsStorage();

        uint256 oldConfirmExpiry = $.confirmExpiry;
        $.confirmExpiry = _newConfirmExpiry;

        emit ConfirmExpirySet(msg.sender, oldConfirmExpiry, _newConfirmExpiry);
    }

    function _validateConfirmExpiry(uint256 _newConfirmExpiry) internal pure {
        if (_newConfirmExpiry < MIN_CONFIRM_EXPIRY || _newConfirmExpiry > MAX_CONFIRM_EXPIRY)
            revert ConfirmExpiryOutOfBounds();
    }

    function _getConfirmationsStorage() private pure returns (ConfirmationStorage storage $) {
        assembly {
            $.slot := CONFIRMATIONS_STORAGE_LOCATION
        }
    }

    /**
     * @dev Emitted when the confirmation expiry is set.
     * @param sender msg.sender of the call
     * @param oldConfirmExpiry The old confirmation expiry.
     * @param newConfirmExpiry The new confirmation expiry.
     */
    event ConfirmExpirySet(address indexed sender, uint256 oldConfirmExpiry, uint256 newConfirmExpiry);

    /**
     * @dev Emitted when a role member confirms.
     * @param member The address of the confirming member.
     * @param roleOrAddress The role or address of the confirming member.
     * @param confirmTimestamp The timestamp of the confirmation.
     * @param expiryTimestamp The timestamp when this confirmation expires.
     * @param data The msg.data of the confirmation (selector + arguments).
     */
    event RoleMemberConfirmed(address indexed member, bytes32 indexed roleOrAddress, uint256 confirmTimestamp, uint256 expiryTimestamp, bytes data);

    /**
     * @dev Thrown when attempting to set confirmation expiry out of bounds.
     */
    error ConfirmExpiryOutOfBounds();

    /**
     * @dev Thrown when a caller without a required role attempts to confirm.
     */
    error SenderNotMember();

    /**
     * @dev Thrown when the roles array is empty.
     */
    error ZeroConfirmingRoles();
}

// SPDX-FileCopyrightText: 2025 Lido <[email protected]>
// SPDX-License-Identifier: GPL-3.0

// See contracts/COMPILERS.md
pragma solidity 0.8.25;

import {AccessControlEnumerableUpgradeable} from "contracts/openzeppelin/5.2/upgradeable/access/extensions/AccessControlEnumerableUpgradeable.sol";

import {PausableUntil} from "contracts/common/utils/PausableUntil.sol";

/**
 * @title PausableUntilWithRoles
 * @notice a `PausableUntil` implementation using OpenZeppelin's `AccessControlEnumerableUpgradeable`
 * @dev the inheriting contract must use `whenNotPaused` modifier from `PausableUntil` to block some functions on pause
 */
abstract contract PausableUntilWithRoles is PausableUntil, AccessControlEnumerableUpgradeable {

    /// @notice role that allows to pause the contract
    /// @dev 0x8d0e4ae4847b49935b55c99f9c3ce025c87e7c4604c35b7ae56929bd32fa5a78
    bytes32 public constant PAUSE_ROLE = keccak256("PausableUntilWithRoles.PauseRole");

    /// @notice role that allows to resume the contract
    /// @dev 0xa79a6aede309e0d48bf2ef0f71355c06ad317956d4c0da2deb0dc47cc34f826c
    bytes32 public constant RESUME_ROLE = keccak256("PausableUntilWithRoles.ResumeRole");

    /**
     * @notice Resume the contract
     * @dev Reverts if contracts is not paused
     * @dev Reverts if sender has no `RESUME_ROLE`
     */
    function resume() external onlyRole(RESUME_ROLE) {
        _resume();
    }

    /**
     * @notice Pause the contract for a specified period
     * @param _duration pause duration in seconds (use `PAUSE_INFINITELY` for unlimited)
     * @dev Reverts if contract is already paused
     * @dev Reverts if sender has no `PAUSE_ROLE`
     * @dev Reverts if zero duration is passed
     */
    function pauseFor(uint256 _duration) external onlyRole(PAUSE_ROLE) {
        _pauseFor(_duration);
    }

    /**
     * @notice Pause the contract until a specified timestamp
     * @param _pauseUntilInclusive the last second to pause until inclusive
     * @dev Reverts if the timestamp is in the past
     * @dev Reverts if sender has no `PAUSE_ROLE`
     * @dev Reverts if contract is already paused
     */
    function pauseUntil(uint256 _pauseUntilInclusive) external onlyRole(PAUSE_ROLE) {
        _pauseUntil(_pauseUntilInclusive);
    }
}

// SPDX-FileCopyrightText: 2025 Lido <[email protected]>
// SPDX-License-Identifier: GPL-3.0

// See contracts/COMPILERS.md
pragma solidity 0.8.25;

import {VaultHub} from "../VaultHub.sol";
import {LazyOracle} from "../LazyOracle.sol";
import {Permissions} from "./Permissions.sol";
import {SafeCast} from "@openzeppelin/contracts-v5.2/utils/math/SafeCast.sol";

/**
 * @title NodeOperatorFee
 * @author Lido
 * @notice A contract that manages the node operator fee.
 */
contract NodeOperatorFee is Permissions {
    using SafeCast for uint256;
    using SafeCast for int256;

    /**
     * @notice Total basis points; 1bp = 0.01%, 100_00bp = 100.00%.
     */
    uint256 internal constant TOTAL_BASIS_POINTS = 100_00;

    /**
     * @notice Parent role representing the node operator of the underlying StakingVault.
     * The members may not include the node operator address recorded in the underlying StakingVault
     * but it is assumed that the members of this role act in the interest of that node operator.
     *
     * @dev 0x59783a4ae82167eefad593739a5430c1d9e896a16c35f1e5285ddd0c0980885c
     */
    bytes32 public constant NODE_OPERATOR_MANAGER_ROLE = keccak256("vaults.NodeOperatorFee.NodeOperatorManagerRole");

    /**
     * @notice Node operator's sub-role for fee exemptions.
     * Managed by `NODE_OPERATOR_MANAGER_ROLE`.
     *
     * @dev 0xcceeef0309e9a678ed7f11f20499aeb00a9a4b0d50e53daa428f8591debc583a
     */
    bytes32 public constant NODE_OPERATOR_FEE_EXEMPT_ROLE = keccak256("vaults.NodeOperatorFee.FeeExemptRole");

    /**
     * @notice Node operator's sub-role for unguaranteed deposit
     * Managed by `NODE_OPERATOR_MANAGER_ROLE`.
     *
     * @dev 0x5c17b14b08ace6dda14c9642528ae92de2a73d59eacb65c71f39f309a5611063
     */
    bytes32 public constant NODE_OPERATOR_UNGUARANTEED_DEPOSIT_ROLE =
        keccak256("vaults.NodeOperatorFee.UnguaranteedDepositRole");

    /**
     * @notice Node operator's sub-role for proving unknown validators.
     * Managed by `NODE_OPERATOR_MANAGER_ROLE`.
     *
     * @dev 0x7b564705f4e61596c4a9469b6884980f89e475befabdb849d69719f0791628be
     */
    bytes32 public constant NODE_OPERATOR_PROVE_UNKNOWN_VALIDATOR_ROLE =
        keccak256("vaults.NodeOperatorFee.ProveUnknownValidatorsRole");

    /**
     * @notice If the accrued fee exceeds this BP of the total value, it is considered abnormally high.
     * An abnormally high fee can only be disbursed by `DEFAULT_ADMIN_ROLE`.
     * This threshold is to prevent accidental overpayment due to outdated settled growth.
     *
     * Why 1% threshold?
     *
     * - Assume a very generous annual staking APR of ~5% (3% CL + 2% EL).
     * - A very high node operator fee rate of 10% translates to a 0.5% annual fee.
     * - Thus, a 1% fee threshold would therefore be reached in 2 years.
     * - Meaning: as long as the operator disburses fees at least once every 2 years,
     *   the threshold will never be hit.
     *
     * Since these assumptions are highly conservative, in practice the operator
     * would need to disburse even less frequently before approaching the threshold.
     */
    uint256 constant internal ABNORMALLY_HIGH_FEE_THRESHOLD_BP = 1_00;

    // ==================== Packed Storage Slot 1 ====================
    /**
     * @notice Address that receives node operator fee disbursements.
     * This address is set by the node operator manager and receives disbursed fees.
     */
    address public feeRecipient;

    /**
     * @notice Node operator fee rate in basis points (1 bp = 0.01%).
     * Cannot exceed 100.00% (10000 basis points).
     */
    uint16 public feeRate;

    // ==================== Packed Storage Slot 2 ====================
    /**
     * @notice Growth of the vault not subject to fees.
     *
     * Growth is the difference between inOutDelta and totalValue,
     * i.e. the component of totalValue that has not been directly funded to the underlying StakingVault via `fund()`:
     *    inOutDelta + growth = totalValue
     *
     * Settled growth is the portion of the total growth that:
     * - has already been charged by the node operator,
     * - or is not subject to fee (exempted) such as unguaranteed/side deposits, consolidations.
     */
    int128 public settledGrowth;

    /**
     * @notice Timestamp of the most recent settled growth correction.
     * This timestamp is used to prevent retroactive fees after a fee rate change.
     * The timestamp ensures that all fee exemptions and corrections are fully reported before changing the fee rate.
     * Regular fee disbursements do not update this timestamp.
     */
    uint64 public latestCorrectionTimestamp;

    /**
     * @notice Passes the address of the vault hub up the inheritance chain.
     * @param _vaultHub The address of the vault hub.
     * @param _lidoLocator The address of the Lido locator.
     */
    constructor(address _vaultHub, address _lidoLocator) Permissions(_vaultHub, _lidoLocator) {}

    /**
     * @dev Calls the parent's initializer, sets the node operator fee, assigns the node operator manager role,
     * and makes the node operator manager the admin for the node operator roles.
     * @param _defaultAdmin The address of the default admin
     * @param _nodeOperatorManager The address of the node operator manager
     * @param _feeRecipient The node operator fee recipient address
     * @param _feeRate The node operator fee rate
     * @param _confirmExpiry The confirmation expiry time in seconds
     */
    function _initialize(
        address _defaultAdmin,
        address _nodeOperatorManager,
        address _feeRecipient,
        uint256 _feeRate,
        uint256 _confirmExpiry
    ) internal {
        _requireNotZero(_nodeOperatorManager);

        super._initialize(_defaultAdmin, _confirmExpiry);

        _setFeeRate(_feeRate);
        _setFeeRecipient(_feeRecipient);

        _grantRole(NODE_OPERATOR_MANAGER_ROLE, _nodeOperatorManager);
        _setRoleAdmin(NODE_OPERATOR_MANAGER_ROLE, NODE_OPERATOR_MANAGER_ROLE);
        _setRoleAdmin(NODE_OPERATOR_FEE_EXEMPT_ROLE, NODE_OPERATOR_MANAGER_ROLE);
        _setRoleAdmin(NODE_OPERATOR_UNGUARANTEED_DEPOSIT_ROLE, NODE_OPERATOR_MANAGER_ROLE);
        _setRoleAdmin(NODE_OPERATOR_PROVE_UNKNOWN_VALIDATOR_ROLE, NODE_OPERATOR_MANAGER_ROLE);
    }

    /**
     * @notice The roles that must confirm critical parameter changes in the contract.
     * @return roles is an array of roles that form the confirming roles.
     */
    function confirmingRoles() public pure override returns (bytes32[] memory roles) {
        roles = new bytes32[](2);
        roles[0] = DEFAULT_ADMIN_ROLE;
        roles[1] = NODE_OPERATOR_MANAGER_ROLE;
    }

    /**
     * @notice The latest vault report for the underlying StakingVault.
     * @return report The latest report containing totalValue, inOutDelta, and timestamp
     */
    function latestReport() public view returns (VaultHub.Report memory) {
        return VAULT_HUB.latestReport(address(_stakingVault()));
    }

    /**
     * @notice Calculates the current node operator fee amount in ETH.
     *
     * Fee calculation steps:
     * 1. Retrieve latest vault report (totalValue, inOutDelta)
     * 2. Calculate current growth: totalValue - inOutDelta
     * 3. Determine unsettled growth: currentGrowth - settledGrowth
     * 4. Apply fee rate: unsettledGrowth × feeRate / 10000
     *
     * @return fee The amount of ETH accrued as fee
     */
    function accruedFee() public view returns (uint256 fee) {
        (fee,, ) = _calculateFee();
    }

    /**
     * @notice Disburses node operator fees permissionlessly.
     * Can be called by anyone as long as fee is not abnormally high.
     *
     * Fee disbursement steps:
     * 1. Calculate current vault growth from latest report
     * 2. Determine fee amount on unsettled growth
     * 3. Update settled growth to current growth (marking fees as paid)
     * 4. Withdraws fee amount from vault to node operator recipient
     */
    function disburseFee() public {
        (uint256 fee, int128 growth, uint256 abnormallyHighFeeThreshold) = _calculateFee();
        if (fee > abnormallyHighFeeThreshold) revert AbnormallyHighFee();

       _disburseFee(fee, growth);
    }

    /**
     * @notice Disburses an abnormally high fee as `DEFAULT_ADMIN_ROLE`.
     * Before calling this function, the caller must ensure that the high fee is expected,
     * and the settled growth (used as baseline for fee) is set correctly.
     */
    function disburseAbnormallyHighFee() external onlyRoleMemberOrAdmin(DEFAULT_ADMIN_ROLE) {
        (uint256 fee, int128 growth,) = _calculateFee();
        _disburseFee(fee, growth);
    }

    /**
     * @notice Updates the node operator's fee rate with dual confirmation.
     * @param _newFeeRate The new fee rate in basis points (max 10000 = 100%)
     * @return bool True if fee rate was updated, false if still awaiting confirmations
     */
    function setFeeRate(uint256 _newFeeRate) external returns (bool) {
        // The report must be fresh so that the total value of the vault is up to date
        // and all the node operator fees are paid out fairly up to the moment of the latest fresh report
        if (!VAULT_HUB.isReportFresh(address(_stakingVault()))) revert ReportStale();

        // Latest fee exemption must be earlier than the latest fresh report timestamp
        if (latestCorrectionTimestamp >= _lazyOracle().latestReportTimestamp()) revert CorrectionAfterReport();

        // If the vault is quarantined, the total value is reduced and may not reflect the exemption
        if (_lazyOracle().vaultQuarantine(address(_stakingVault())).isActive) revert VaultQuarantined();

        // store the caller's confirmation; only proceed if the required number of confirmations is met.
        if (!_collectAndCheckConfirmations(msg.data, confirmingRoles())) return false;

        // Disburse any outstanding fees at the current rate before changing it
        disburseFee();

        _setFeeRate(_newFeeRate);

        return true;
    }

    /**
     * @notice Manually corrects the settled growth value with dual confirmation.
     * Used to correct fee calculation.
     *
     * @param _newSettledGrowth The corrected settled growth value
     * @param _expectedSettledGrowth The expected current settled growth
     * @return bool True if correction was applied, false if awaiting confirmations
     */
    function correctSettledGrowth(int256 _newSettledGrowth, int256 _expectedSettledGrowth) public returns (bool) {
        if (settledGrowth != _expectedSettledGrowth) revert UnexpectedSettledGrowth();
        if (!_collectAndCheckConfirmations(msg.data, confirmingRoles())) return false;

        _correctSettledGrowth(_newSettledGrowth);

        return true;
    }

    /**
     * @notice Adds a fee exemption to exclude this value from node operator fee base.
     * The exemption works by increasing the settled growth,
     * effectively treating the exempted amount as if fees were already paid on it.
     *
     * @param _exemptedAmount Amount in ETH to exempt from fee calculations
     */
    function addFeeExemption(uint256 _exemptedAmount) external onlyRoleMemberOrAdmin(NODE_OPERATOR_FEE_EXEMPT_ROLE) {
        _addFeeExemption(_exemptedAmount);
    }

    /**
     * @notice Sets the confirmation expiry period with dual confirmation.
     * @param _newConfirmExpiry The new confirmation expiry period in seconds
     * @return bool True if expiry was updated, false if awaiting confirmations
     */
    function setConfirmExpiry(uint256 _newConfirmExpiry) external returns (bool) {
        _validateConfirmExpiry(_newConfirmExpiry);

        if (!_collectAndCheckConfirmations(msg.data, confirmingRoles())) return false;

        _setConfirmExpiry(_newConfirmExpiry);

        return true;
    }

    /**
     * @notice Sets the address that receives node operator fee disbursements.
     * @param _newFeeRecipient The new recipient address for fee payments
     */
    function setFeeRecipient(address _newFeeRecipient) external onlyRoleMemberOrAdmin(NODE_OPERATOR_MANAGER_ROLE) {
        _setFeeRecipient(_newFeeRecipient);
    }

    // ==================== Internal Functions ====================

    function _lazyOracle() internal view returns (LazyOracle) {
        return LazyOracle(LIDO_LOCATOR.lazyOracle());
    }

    function _disburseFee(uint256 fee, int128 growth) internal {
        // it's important not to revert here so as not to block disconnect
        if (fee == 0) return;

        _setSettledGrowth(growth);

        VAULT_HUB.withdraw(address(_stakingVault()), feeRecipient, fee);
        emit FeeDisbursed(msg.sender, fee);
    }

    function _setSettledGrowth(int256 _newSettledGrowth) internal {
        int128 oldSettledGrowth = settledGrowth;
        if (oldSettledGrowth == _newSettledGrowth) revert SameSettledGrowth();

        int128 newSettledGrowth = _newSettledGrowth.toInt128();
        settledGrowth = newSettledGrowth;

        emit SettledGrowthSet(oldSettledGrowth, newSettledGrowth);
    }

    /**
     * @dev Set a new settled growth and updates the timestamp.
     * Should be used to correct settled growth for total value change that might not have been reported yet
     */
    function _correctSettledGrowth(int256 _newSettledGrowth) internal {
        _setSettledGrowth(_newSettledGrowth);
        latestCorrectionTimestamp = uint64(block.timestamp);

        emit CorrectionTimestampUpdated(block.timestamp);
    }

    /**
     * @dev Increases settled growth for total value increases not subject to fee,
     * which is why it updates the timestamp to ensure that the exemption comes before
     * the total value report during the fee rate change, which guarantees that the exemption is reported
     * @dev fee exemption can only be positive
     */
    function _addFeeExemption(uint256 _amount) internal {
        if (_amount > type(uint104).max) revert UnexpectedFeeExemptionAmount();

        _correctSettledGrowth(settledGrowth + int256(_amount));
    }

    function _calculateFee() internal view returns (uint256 fee, int128 growth, uint256 abnormallyHighFeeThreshold) {
        VaultHub.Report memory report = latestReport();
        growth = int128(uint128(report.totalValue)) - int128(report.inOutDelta);
        int256 unsettledGrowth = growth - settledGrowth;

        if (unsettledGrowth > 0) {
            fee = (uint256(unsettledGrowth) * feeRate) / TOTAL_BASIS_POINTS;
        }

        abnormallyHighFeeThreshold = (report.totalValue * ABNORMALLY_HIGH_FEE_THRESHOLD_BP) / TOTAL_BASIS_POINTS;
    }

    function _setFeeRate(uint256 _newFeeRate) internal {
        if (_newFeeRate > TOTAL_BASIS_POINTS) revert FeeValueExceed100Percent();

        uint256 oldFeeRate = feeRate;
        uint256 newFeeRate = _newFeeRate;

        feeRate = uint16(newFeeRate);

        emit FeeRateSet(msg.sender, oldFeeRate, newFeeRate);
    }

    function _setFeeRecipient(address _newFeeRecipient) internal {
        _requireNotZero(_newFeeRecipient);
        if (_newFeeRecipient == feeRecipient) revert SameRecipient();

        address oldFeeRecipient = feeRecipient;
        feeRecipient = _newFeeRecipient;
        emit FeeRecipientSet(msg.sender, oldFeeRecipient, _newFeeRecipient);
    }

    // ==================== Events ====================

    /**
     * @dev Emitted when the node operator fee is set.
     * @param sender the address of the sender
     * @param oldFeeRate The old node operator fee rate.
     * @param newFeeRate The new node operator fee rate.
     */
    event FeeRateSet(address indexed sender, uint256 oldFeeRate, uint256 newFeeRate);

    /**
     * @dev Emitted when the node operator fee is disbursed.
     * @param sender the address of the sender
     * @param fee the amount of disbursed fee.
     */
    event FeeDisbursed(address indexed sender, uint256 fee);

    /**
     * @dev Emitted when the node operator fee recipient is set.
     * @param sender the address of the sender who set the recipient
     * @param oldFeeRecipient the old node operator fee recipient
     * @param newFeeRecipient the new node operator fee recipient
     */
    event FeeRecipientSet(address indexed sender, address oldFeeRecipient, address newFeeRecipient);

    /**
     * @dev Emitted when the settled growth is set.
     * @param oldSettledGrowth the old settled growth
     * @param newSettledGrowth the new settled growth
     */
    event SettledGrowthSet(int128 oldSettledGrowth, int128 newSettledGrowth);

    /**
     * @dev Emitted when the settled growth is corrected.
     * @param timestamp new correction timestamp
     */
    event CorrectionTimestampUpdated(uint256 timestamp);

    // ==================== Errors ====================

    /**
     * @dev Error emitted when the combined feeBPs exceed 100%.
     */
    error FeeValueExceed100Percent();

    /**
     * @dev Error emitted when trying to disburse an abnormally high fee.
     */
    error AbnormallyHighFee();

    /**
     * @dev Error emitted when trying to set same value for recipient
     */
    error SameRecipient();

    /**
     * @dev Error emitted when trying to set same value for settled growth
     */
    error SameSettledGrowth();

    /**
     * @dev Error emitted when the settled growth does not match the expected value during connection.
     */
    error SettledGrowthMismatch();

    /**
     * @dev Error emitted when the report is stale.
     */
    error ReportStale();

    /**
     * @dev Error emitted when the correction is made after the report.
     */
    error CorrectionAfterReport();

    /**
     * @dev Error emitted when the settled growth does not match the expected value.
     */
    error UnexpectedSettledGrowth();

    /**
     * @dev Error emitted when the fee exemption amount does not match the expected value
     */
    error UnexpectedFeeExemptionAmount();

    /**
     * @dev Error emitted when the vault is quarantined.
     */
    error VaultQuarantined();
}

// SPDX-License-Identifier: GPL-3.0
// SPDX-FileCopyrightText: 2025 Lido <[email protected]>

// See contracts/COMPILERS.md
pragma solidity 0.8.25;

import {Clones} from "@openzeppelin/contracts-v5.2/proxy/Clones.sol";
import {AccessControlConfirmable} from "contracts/0.8.25/utils/AccessControlConfirmable.sol";
import {ILidoLocator} from "contracts/common/interfaces/ILidoLocator.sol";

import {IStakingVault} from "../interfaces/IStakingVault.sol";
import {OperatorGrid} from "../OperatorGrid.sol";
import {VaultHub} from "../VaultHub.sol";

/**
 * @title Permissions
 * @author Lido
 * @notice Provides granular permissions for StakingVault operations.
 */
abstract contract Permissions is AccessControlConfirmable {
    /**
     * @notice Struct containing an account and a role for granting/revoking roles.
     */
    struct RoleAssignment {
        address account;
        bytes32 role;
    }

    /**
     * @notice Permission for funding the StakingVault.
     */
    /// @dev 0x933b7d5c112a4d05b489cea0b2ced98acb27d3d0fc9827c92cdacb2d6c5559c2
    bytes32 public constant FUND_ROLE = keccak256("vaults.Permissions.Fund");

    /**
     * @notice Permission for withdrawing funds from the StakingVault.
     */
    /// @dev 0x355caf1c2580ed8185acb5ea3573b71f85186b41bdf69e3eb8f1fcd122a562df
    bytes32 public constant WITHDRAW_ROLE = keccak256("vaults.Permissions.Withdraw");

    /**
     * @notice Permission for minting stETH shares backed by the StakingVault.
     */
    /// @dev 0xe996ac9b332538bb1fa3cd6743aa47011623cdb94bd964a494ee9d371e4a27d3
    bytes32 public constant MINT_ROLE = keccak256("vaults.Permissions.Mint");

    /**
     * @notice Permission for burning stETH shares backed by the StakingVault.
     */
    /// @dev 0x689f0a569be0c9b6cd2c11c81cb0add722272abdae6b649fdb1e05f1d9bb8a2f
    bytes32 public constant BURN_ROLE = keccak256("vaults.Permissions.Burn");

    /**
     * @notice Permission for rebalancing the StakingVault.
     */
    /// @dev 0x3f82ecf462ddac43fc17ba11472c35f18b7760b4f5a5fc50b9625f9b5a22cf62
    bytes32 public constant REBALANCE_ROLE = keccak256("vaults.Permissions.Rebalance");

    /**
     * @notice Permission for pausing beacon chain deposits on the StakingVault.
     */
    /// @dev 0xa90c7030a27f389f9fc8ed21a0556f40c88130cc14a80db936bed68261819b2c
    bytes32 public constant PAUSE_BEACON_CHAIN_DEPOSITS_ROLE = keccak256("vaults.Permissions.PauseDeposits");

    /**
     * @notice Permission for resuming beacon chain deposits on the StakingVault.
     */
    /// @dev 0x59d005e32db662b94335d6bedfeb453fd2202b9f0cc7a6ed498d9098171744b0
    bytes32 public constant RESUME_BEACON_CHAIN_DEPOSITS_ROLE = keccak256("vaults.Permissions.ResumeDeposits");

    /**
     * @notice Permission for requesting validator exit from the StakingVault.
     */
    /// @dev 0x32d0d6546e21c13ff633616141dc9daad87d248d1d37c56bf493d06d627ecb7b
    bytes32 public constant REQUEST_VALIDATOR_EXIT_ROLE = keccak256("vaults.Permissions.RequestValidatorExit");

    /**
     * @notice Permission for triggering validator withdrawal from the StakingVault using EIP-7002 triggerable exit.
     */
    /// @dev 0xea19d3b23bd90fdd52445ad672f2b6fb1fef7230d49c6a827c1cd288d02994d5
    bytes32 public constant TRIGGER_VALIDATOR_WITHDRAWAL_ROLE =
        keccak256("vaults.Permissions.TriggerValidatorWithdrawal");

    /**
     * @notice Permission for voluntary disconnecting the StakingVault.
     */
    /// @dev 0x9586321ac05f110e4b4a0a42aba899709345af0ca78910e8832ddfd71fed2bf4
    bytes32 public constant VOLUNTARY_DISCONNECT_ROLE = keccak256("vaults.Permissions.VoluntaryDisconnect");

    /**
     * @dev Permission for vault configuration operations on the OperatorGrid (tier changes, tier sync, share limit updates).
     */
    /// @dev 0x25482e7dc9e29f6da5bd70b6d19d17bbf44021da51ba0664a9f430c94a09c674
    bytes32 public constant VAULT_CONFIGURATION_ROLE = keccak256("vaults.Permissions.VaultConfiguration");

    VaultHub public immutable VAULT_HUB;
    ILidoLocator public immutable LIDO_LOCATOR;

    /**
     * @notice Indicates whether the contract has been initialized
     */
    bool public initialized;

    constructor(address _vaultHub, address _lidoLocator) {
        _requireNotZero(_vaultHub);
        _requireNotZero(_lidoLocator);

        initialized = true;

        // @dev vaultHub is cached as immutable to save gas for main operations
        VAULT_HUB = VaultHub(payable(_vaultHub));
        LIDO_LOCATOR = ILidoLocator(_lidoLocator);
    }

    /**
     * @notice Modifier to prevent reinitialization of the contract.
     * @dev Extracted to modifier to avoid Slither warning.
     */
    modifier initializer() {
        if (initialized) revert AlreadyInitialized();

        initialized = true;
        _;

        emit Initialized();
    }

    /**
     * @dev Sets the ACL default admin and confirmation expiry time.
     * @param _defaultAdmin The address of the default admin
     * @param _confirmExpiry The confirmation expiry time in seconds
     */
    function _initialize(address _defaultAdmin, uint256 _confirmExpiry) internal initializer {
        _requireNotZero(_defaultAdmin);

        _grantRole(DEFAULT_ADMIN_ROLE, _defaultAdmin);
        _validateConfirmExpiry(_confirmExpiry);
        _setConfirmExpiry(_confirmExpiry);
    }

    /**
     * @notice Returns the address of the underlying StakingVault.
     * @return The address of the StakingVault.
     */
    function stakingVault() external view returns (IStakingVault) {
        return _stakingVault();
    }

    // ==================== Role Management Functions ====================

    /**
     * @notice Mass-grants multiple roles to multiple accounts.
     * @param _assignments An array of role assignments.
     * @dev Performs the role admin checks internally.
     * @dev If an account is already a member of a role, doesn't revert, emits no events.
     */
    function grantRoles(RoleAssignment[] calldata _assignments) external {
        _requireNotZero(_assignments.length);

        for (uint256 i = 0; i < _assignments.length; i++) {
            grantRole(_assignments[i].role, _assignments[i].account);
        }
    }

    /**
     * @notice Mass-revokes multiple roles from multiple accounts.
     * @param _assignments An array of role assignments.
     * @dev Performs the role admin checks internally.
     * @dev If an account is not a member of a role, doesn't revert, emits no events.
     */
    function revokeRoles(RoleAssignment[] calldata _assignments) external {
        _requireNotZero(_assignments.length);

        for (uint256 i = 0; i < _assignments.length; i++) {
            revokeRole(_assignments[i].role, _assignments[i].account);
        }
    }

    /**
     * @dev Returns an array of roles that need to confirm the calls that require confirmations
     * @return The roles that need to confirm the call.
     */
    function confirmingRoles() public pure virtual returns (bytes32[] memory);

    /**
     * @dev A custom modifier that checks if the caller has a role or the admin role for a given role.
     * @param _role The role to check.
     */
    modifier onlyRoleMemberOrAdmin(bytes32 _role) {
        if (!(hasRole(_role, msg.sender) || hasRole(getRoleAdmin(_role), msg.sender))) {
            revert AccessControlUnauthorizedAccount(msg.sender, _role);
        }
        _;
    }

    /**
     * @dev Checks the FUND_ROLE and funds the StakingVault.
     * @param _ether The amount of ether to fund the StakingVault with.
     */
    function _fund(uint256 _ether) internal onlyRoleMemberOrAdmin(FUND_ROLE) {
        VAULT_HUB.fund{value: _ether}(address(_stakingVault()));
    }

    /**
     * @dev Checks the WITHDRAW_ROLE and withdraws funds from the StakingVault.
     * @param _recipient The address to withdraw the funds to.
     * @param _ether The amount of ether to withdraw from the StakingVault.
     */
    function _withdraw(address _recipient, uint256 _ether) internal virtual onlyRoleMemberOrAdmin(WITHDRAW_ROLE) {
        VAULT_HUB.withdraw(address(_stakingVault()), _recipient, _ether);
    }

    /**
     * @dev Checks the MINT_ROLE and mints shares backed by the StakingVault.
     * @param _recipient The address to mint the shares to.
     * @param _shares The amount of shares to mint.
     */
    function _mintShares(address _recipient, uint256 _shares) internal onlyRoleMemberOrAdmin(MINT_ROLE) {
        VAULT_HUB.mintShares(address(_stakingVault()), _recipient, _shares);
    }

    /**
     * @dev Checks the BURN_ROLE and burns shares backed by the StakingVault.
     * @param _shares The amount of shares to burn.
     */
    function _burnShares(uint256 _shares) internal onlyRoleMemberOrAdmin(BURN_ROLE) {
        VAULT_HUB.burnShares(address(_stakingVault()), _shares);
    }

    /**
     * @dev Checks the REBALANCE_ROLE and rebalances the StakingVault.
     * @param _shares The amount of shares to rebalance the StakingVault with.
     */
    function _rebalanceVault(uint256 _shares) internal onlyRoleMemberOrAdmin(REBALANCE_ROLE) {
        VAULT_HUB.rebalance(address(_stakingVault()), _shares);
    }

    /**
     * @dev Checks the PAUSE_BEACON_CHAIN_DEPOSITS_ROLE and pauses beacon chain deposits on the StakingVault.
     */
    function _pauseBeaconChainDeposits() internal onlyRoleMemberOrAdmin(PAUSE_BEACON_CHAIN_DEPOSITS_ROLE) {
        VAULT_HUB.pauseBeaconChainDeposits(address(_stakingVault()));
    }

    /**
     * @dev Checks the RESUME_BEACON_CHAIN_DEPOSITS_ROLE and resumes beacon chain deposits on the StakingVault.
     */
    function _resumeBeaconChainDeposits() internal onlyRoleMemberOrAdmin(RESUME_BEACON_CHAIN_DEPOSITS_ROLE) {
        VAULT_HUB.resumeBeaconChainDeposits(address(_stakingVault()));
    }

    /**
     * @dev Checks the REQUEST_VALIDATOR_EXIT_ROLE and requests validator exit on the StakingVault.
     */
    function _requestValidatorExit(
        bytes calldata _pubkeys
    ) internal onlyRoleMemberOrAdmin(REQUEST_VALIDATOR_EXIT_ROLE) {
        VAULT_HUB.requestValidatorExit(address(_stakingVault()), _pubkeys);
    }

    /**
     * @dev Checks the TRIGGER_VALIDATOR_WITHDRAWAL_ROLE and triggers validator withdrawal on the StakingVault
     *      using EIP-7002 triggerable exit.
     */
    function _triggerValidatorWithdrawals(
        bytes calldata _pubkeys,
        uint64[] calldata _amountsInGwei,
        address _refundRecipient
    ) internal onlyRoleMemberOrAdmin(TRIGGER_VALIDATOR_WITHDRAWAL_ROLE) {
        VAULT_HUB.triggerValidatorWithdrawals{value: msg.value}(
            address(_stakingVault()),
            _pubkeys,
            _amountsInGwei,
            _refundRecipient
        );
    }

    /**
     * @dev Checks the VOLUNTARY_DISCONNECT_ROLE and voluntarily disconnects the StakingVault.
     */
    function _voluntaryDisconnect() internal onlyRoleMemberOrAdmin(VOLUNTARY_DISCONNECT_ROLE) {
        VAULT_HUB.voluntaryDisconnect(address(_stakingVault()));
    }

    /**
     * @dev Checks the DEFAULT_ADMIN_ROLE and transfers the StakingVault ownership.
     * @param _newOwner The address to transfer the ownership to.
     */
    function _transferOwnership(address _newOwner) internal onlyRole(DEFAULT_ADMIN_ROLE) {
        _stakingVault().transferOwnership(_newOwner);
    }

    /**
     * @dev Checks the DEFAULT_ADMIN_ROLE and accepts the StakingVault ownership.
     */
    function _acceptOwnership() internal onlyRole(DEFAULT_ADMIN_ROLE) {
        _stakingVault().acceptOwnership();
    }

    /**
     * @dev Checks the confirming roles and transfer the ownership of the vault without disconnecting it from the hub
     * @param _newOwner The address to set the owner to.
     */
    function _transferVaultOwnership(address _newOwner) internal returns (bool) {
        if (!_collectAndCheckConfirmations(msg.data, confirmingRoles())) return false;
        VAULT_HUB.transferVaultOwnership(address(_stakingVault()), _newOwner);
        return true;
    }

    /**
     * @dev Checks the VAULT_CONFIGURATION_ROLE and requests a change of the tier on the OperatorGrid.
     * @param _tierId The tier to change to.
     * @param _requestedShareLimit The requested share limit.
     * @return bool Whether the tier change was executed.
     */
    function _changeTier(
        uint256 _tierId,
        uint256 _requestedShareLimit
    ) internal onlyRoleMemberOrAdmin(VAULT_CONFIGURATION_ROLE) returns (bool) {
        return _operatorGrid().changeTier(address(_stakingVault()), _tierId, _requestedShareLimit);
    }

    /**
     * @dev Checks the VAULT_CONFIGURATION_ROLE and requests a sync of the tier on the OperatorGrid.
     * @return bool Whether the tier sync was executed.
     */
    function _syncTier() internal onlyRoleMemberOrAdmin(VAULT_CONFIGURATION_ROLE) returns (bool) {
        return _operatorGrid().syncTier(address(_stakingVault()));
    }

    /**
     * @dev Checks the VAULT_CONFIGURATION_ROLE and updates the share limit on the OperatorGrid.
     * @param _requestedShareLimit The requested share limit.
     * @return bool Whether the share limit update was executed.
     */
    function _updateVaultShareLimit(uint256 _requestedShareLimit) internal onlyRoleMemberOrAdmin(VAULT_CONFIGURATION_ROLE) returns (bool) {
        return _operatorGrid().updateVaultShareLimit(address(_stakingVault()), _requestedShareLimit);
    }

    /**
     * @dev Loads the address of the underlying StakingVault.
     * @return addr The address of the StakingVault.
     */
    function _stakingVault() internal view returns (IStakingVault) {
        bytes memory args = Clones.fetchCloneArgs(address(this));
        address stakingVaultAddress;
        assembly {
            stakingVaultAddress := mload(add(args, 32))
        }
        return IStakingVault(stakingVaultAddress);
    }

    function _operatorGrid() internal view returns (OperatorGrid) {
        return OperatorGrid(LIDO_LOCATOR.operatorGrid());
    }

    function _requireNotZero(uint256 _value) internal pure {
        if (_value == 0) revert ZeroArgument();
    }

    function _requireNotZero(address _address) internal pure {
        if (_address == address(0)) revert ZeroAddress();
    }

    /**
     * @notice Emitted when the contract is initialized
     */
    event Initialized();

    /**
     * @notice Error when the contract is already initialized.
     */
    error AlreadyInitialized();

    /**
     * @notice Error thrown for when a given value cannot be zero
     */
    error ZeroArgument();

    /**
     * @notice Error thrown for when a given address cannot be zero
     */
    error ZeroAddress();
}

File 28 of 50 : IPinnedBeaconProxy.sol
// SPDX-FileCopyrightText: 2025 Lido <[email protected]>
// SPDX-License-Identifier: GPL-3.0

// See contracts/COMPILERS.md
// solhint-disable-next-line lido/fixed-compiler-version
pragma solidity >=0.8.0;

/**
 * @title IPinnedBeaconProxy
 * @author Lido
 * @notice Interface for the `PinnedBeaconProxy` contract
 */
interface IPinnedBeaconProxy {
    function isOssified() external view returns (bool);
}

// SPDX-FileCopyrightText: 2025 Lido <[email protected]>
// SPDX-License-Identifier: GPL-3.0

// See contracts/COMPILERS.md
// solhint-disable-next-line lido/fixed-compiler-version
pragma solidity >=0.8.0;

import {IStakingVault} from "./IStakingVault.sol";

/**
 * @title IPredepositGuarantee
 * @author Lido
 * @notice Interface for the `PredepositGuarantee` contract
 */
interface IPredepositGuarantee {
    /**
     * @notice represents validator stages in PDG flow
     * @param NONE - initial stage
     * @param PREDEPOSITED - PREDEPOSIT_AMOUNT is deposited to this validator by the vault
     * @param PROVEN - validator is proven to be valid and can be used to deposit to beacon chain
     * @param ACTIVATED - validator is proven and the ACTIVATION_DEPOSIT_AMOUNT is deposited to this validator
     * @param COMPENSATED - disproven validator has its PREDEPOSIT_AMOUNT ether compensated to staking vault owner and validator cannot be used in PDG anymore
     */
    enum ValidatorStage {
        NONE,
        PREDEPOSITED,
        PROVEN,
        ACTIVATED,
        COMPENSATED
    }
    /**
     * @notice represents status of the validator in PDG
     * @param stage represents validator stage in PDG flow
     * @param stakingVault pins validator to specific StakingVault
     * @param nodeOperator pins validator to specific NO
     */
    struct ValidatorStatus {
        ValidatorStage stage;
        IStakingVault stakingVault;
        address nodeOperator;
    }

    /**
     * @notice user input for validator proof verification
     * @custom:proof array of merkle proofs from parent(pubkey,wc) node to Beacon block root
     * @custom:pubkey of validator to prove
     * @custom:validatorIndex of validator in CL state tree
     * @custom:childBlockTimestamp of EL block that has parent block beacon root in BEACON_ROOTS contract
     * @custom:slot of the beacon block for which the proof is generated
     * @custom:proposerIndex of the beacon block for which the proof is generated
     */
    struct ValidatorWitness {
        bytes32[] proof;
        bytes pubkey;
        uint256 validatorIndex;
        uint64 childBlockTimestamp;
        uint64 slot;
        uint64 proposerIndex;
    }

    function pendingActivations(IStakingVault _vault) external view returns (uint256);
    function validatorStatus(bytes calldata _pubkey) external view returns (ValidatorStatus memory);
    function proveUnknownValidator(ValidatorWitness calldata _witness, IStakingVault _stakingVault) external;
}

// SPDX-FileCopyrightText: 2025 Lido <[email protected]>
// SPDX-License-Identifier: GPL-3.0

// See contracts/COMPILERS.md
// solhint-disable-next-line lido/fixed-compiler-version
pragma solidity >=0.8.0;

import {IDepositContract} from "contracts/common/interfaces/IDepositContract.sol";

/**
 * @title IStakingVault
 * @author Lido
 * @notice Interface for the `StakingVault` contract
 */
interface IStakingVault {
    /**
     * @notice validator deposit from the `StakingVault` to the beacon chain
     * @dev withdrawal credentials are provided by the vault
     * @custom:pubkey The validator's BLS public key (48 bytes)
     * @custom:signature BLS signature of the deposit data (96 bytes)
     * @custom:amount Amount of ETH to deposit in wei (must be a multiple of 1 Gwei and minimum of 1 ETH)
     * @custom:depositDataRoot The root hash of the deposit data per ETH beacon spec
     */
    struct Deposit {
        bytes pubkey;
        bytes signature;
        uint256 amount;
        bytes32 depositDataRoot;
    }

    function DEPOSIT_CONTRACT() external view returns (IDepositContract);
    function initialize(address _owner, address _nodeOperator, address _depositor) external;
    function version() external pure returns (uint64);
    function getInitializedVersion() external view returns (uint64);
    function withdrawalCredentials() external view returns (bytes32);

    function owner() external view returns (address);
    function pendingOwner() external view returns (address);
    function acceptOwnership() external;
    function transferOwnership(address _newOwner) external;

    function nodeOperator() external view returns (address);
    function depositor() external view returns (address);
    function calculateValidatorWithdrawalFee(uint256 _keysCount) external view returns (uint256);
    function fund() external payable;
    function withdraw(address _recipient, uint256 _ether) external;

    function beaconChainDepositsPaused() external view returns (bool);
    function pauseBeaconChainDeposits() external;
    function resumeBeaconChainDeposits() external;
    function depositToBeaconChain(Deposit calldata _deposits) external;

    function requestValidatorExit(bytes calldata _pubkeys) external;
    function triggerValidatorWithdrawals(bytes calldata _pubkeys, uint64[] calldata _amountsInGwei, address _refundRecipient) external payable;
    function ejectValidators(bytes calldata _pubkeys, address _refundRecipient) external payable;
    function setDepositor(address _depositor) external;
    function ossify() external;
    function collectERC20(address _token, address _recipient, uint256 _amount) external;

    function availableBalance() external view returns (uint256);
    function stagedBalance() external view returns (uint256);
    function stage(uint256 _ether) external;
    function unstage(uint256 _ether) external;
    function depositFromStaged(Deposit calldata _deposit, uint256 _additionalAmount) external;
}

// SPDX-FileCopyrightText: 2025 Lido <[email protected]>
// SPDX-License-Identifier: GPL-3.0

// See contracts/COMPILERS.md
pragma solidity 0.8.25;

interface IVaultFactory {
    function deployedVaults(address _vault) external view returns (bool);
}

// SPDX-FileCopyrightText: 2025 Lido <[email protected]>
// SPDX-License-Identifier: GPL-3.0

// See contracts/COMPILERS.md
pragma solidity 0.8.25;

import {MerkleProof} from "@openzeppelin/contracts-v5.2/utils/cryptography/MerkleProof.sol";

import {AccessControlEnumerableUpgradeable} from "contracts/openzeppelin/5.2/upgradeable/access/extensions/AccessControlEnumerableUpgradeable.sol";

import {ILazyOracle} from "contracts/common/interfaces/ILazyOracle.sol";
import {ILidoLocator} from "contracts/common/interfaces/ILidoLocator.sol";
import {ILido} from "contracts/common/interfaces/ILido.sol";

import {VaultHub} from "./VaultHub.sol";
import {OperatorGrid} from "./OperatorGrid.sol";

import {IStakingVault} from "./interfaces/IStakingVault.sol";
import {IPredepositGuarantee} from "./interfaces/IPredepositGuarantee.sol";

import {DoubleRefSlotCache, DOUBLE_CACHE_LENGTH} from "./lib/RefSlotCache.sol";

contract LazyOracle is ILazyOracle, AccessControlEnumerableUpgradeable {
    using DoubleRefSlotCache for DoubleRefSlotCache.Int104WithCache[DOUBLE_CACHE_LENGTH];

    enum QuarantineState {
        NO_QUARANTINE,      // No active quarantine
        QUARANTINE_ACTIVE,  // Quarantine active, not expired
        QUARANTINE_EXPIRED  // Quarantine period has passed
    }

    /// @custom:storage-location erc7201:Lido.Vaults.LazyOracle
    struct Storage {
        /// @notice root of the vaults data tree
        bytes32 vaultsDataTreeRoot;
        /// @notice CID of the vaults data tree
        string vaultsDataReportCid;
        /// @notice timestamp of the vaults data
        uint64 vaultsDataTimestamp;
        /// @notice refSlot of the vaults data
        uint48 vaultsDataRefSlot;
        /// @notice total value increase quarantine period
        uint64 quarantinePeriod;
        /// @notice max reward ratio for refSlot-observed total value, basis points
        uint16 maxRewardRatioBP;
        /// @notice max Lido fee rate per second, in wei
        uint64 maxLidoFeeRatePerSecond;  // 64 bit is enough for up to 18 ETH/s
        /// @notice deposit quarantines for each vault
        mapping(address vault => Quarantine) vaultQuarantines;
    }

    /*
        A quarantine is a timelock applied to any sudden jump in a vault's reported total value
        that cannot be immediately confirmed on-chain (via the inOutDelta difference). If the
        reported total value exceeds the expected routine EL/CL rewards, the excess is pushed
        into a quarantine buffer for a predefined cooldown period. Only after this delay is the
        quarantined value released into VaultHub's total value.

        Normal top-ups — where the vault owner funds the contract directly using the `fund()`
        function — do not go through quarantine, as they can be verified on-chain via the
        inOutDelta value. These direct fundings are reflected immediately. In contrast,
        consolidations or deposits that bypass the vault's balance must sit in quarantine.

        Example flow:

        Time 0: Total Value = 100 ETH
        ┌────────────────────────────────────┐
        │            100 ETH Active          │
        └────────────────────────────────────┘

        Time 1: Sudden jump of +50 ETH → start quarantine for 50 ETH
        ┌────────────────────────────────────┐
        │            100 ETH Active          │
        │            50 ETH Quarantined      │
        └────────────────────────────────────┘

        Time 2: Another jump of +70 ETH → wait for current quarantine to expire
        ┌────────────────────────────────────┐
        │            100 ETH Active          │
        │            50 ETH Quarantined      │
        │            70 ETH Quarantine Queue │
        └────────────────────────────────────┘

        Time 3: First quarantine expires → add 50 ETH to active value, start new quarantine for 70 ETH
        ┌────────────────────────────────────┐
        │            150 ETH Active          │
        │            70 ETH Quarantined      │
        └────────────────────────────────────┘

        Time 4: Second quarantine expires → add 70 ETH to active value
        ┌────────────────────────────────────┐
        │            220 ETH Active          │
        └────────────────────────────────────┘
    */
    struct Quarantine {
        uint128 pendingTotalValueIncrease;
        uint64 startTimestamp;
    }

    struct QuarantineInfo {
        bool isActive;
        uint256 pendingTotalValueIncrease;
        uint256 startTimestamp;
        uint256 endTimestamp;
    }

    struct VaultInfo {
        address vault;
        uint256 aggregatedBalance; // includes availableBalance and stagedBalance
        int256 inOutDelta;
        bytes32 withdrawalCredentials;
        uint256 liabilityShares;
        uint256 maxLiabilityShares;
        uint256 mintableStETH;
        uint96 shareLimit;
        uint16 reserveRatioBP;
        uint16 forcedRebalanceThresholdBP;
        uint16 infraFeeBP;
        uint16 liquidityFeeBP;
        uint16 reservationFeeBP;
        bool pendingDisconnect;
    }

    // keccak256(abi.encode(uint256(keccak256("Lido.Vaults.LazyOracle")) - 1)) & ~bytes32(uint256(0xff))
    bytes32 private constant LAZY_ORACLE_STORAGE_LOCATION =
        0x73a2a247d4b1b6fe056fe90935e9bd3694e896bafdd08f046c2afe6ec2db2100;

    /// @dev 0x7baf7f4a9784fa74c97162de631a3eb567edeb85878cb6965945310f2c512c63
    bytes32 public constant UPDATE_SANITY_PARAMS_ROLE = keccak256("vaults.LazyOracle.UpdateSanityParams");

    ILidoLocator public immutable LIDO_LOCATOR;

    /// @dev basis points base
    uint256 private constant TOTAL_BASIS_POINTS = 100_00;
    uint256 private constant MAX_SANE_TOTAL_VALUE = type(uint96).max;
    uint256 public constant MAX_QUARANTINE_PERIOD = 30 days;
    /// @dev max value for reward ratio - it's about 650%
    uint256 public constant MAX_REWARD_RATIO = type(uint16).max;
    uint256 public constant MAX_LIDO_FEE_RATE_PER_SECOND = 10 ether;

    constructor(address _lidoLocator) {
        LIDO_LOCATOR = ILidoLocator(payable(_lidoLocator));

        _disableInitializers();
    }

    /// @notice Initializes the contract
    /// @param _admin Address of the admin
    /// @param _quarantinePeriod the quarantine period, seconds
    /// @param _maxRewardRatioBP the max reward ratio, basis points
    /// @param _maxLidoFeeRatePerSecond the max Lido fee rate per second
    function initialize(
        address _admin,
        uint256 _quarantinePeriod,
        uint256 _maxRewardRatioBP,
        uint256 _maxLidoFeeRatePerSecond
    ) external initializer {
        if (_admin == address(0)) revert AdminCannotBeZero();
        _grantRole(DEFAULT_ADMIN_ROLE, _admin);

        _updateSanityParams(_quarantinePeriod, _maxRewardRatioBP, _maxLidoFeeRatePerSecond);
    }

    /// @notice returns the latest report data
    /// @return timestamp of the report
    /// @return refSlot of the report
    /// @return treeRoot merkle root of the report
    /// @return reportCid IPFS CID for the report JSON file
    function latestReportData() external view returns (
        uint256 timestamp,
        uint256 refSlot,
        bytes32 treeRoot,
        string memory reportCid
    ) {
        Storage storage $ = _storage();
        return ($.vaultsDataTimestamp, $.vaultsDataRefSlot, $.vaultsDataTreeRoot, $.vaultsDataReportCid);
    }

    /// @notice returns the latest report timestamp
    function latestReportTimestamp() external view returns (uint256) {
        return _storage().vaultsDataTimestamp;
    }

    /// @notice returns the quarantine period
    function quarantinePeriod() external view returns (uint256) {
        return _storage().quarantinePeriod;
    }

    /// @notice returns the max reward ratio for refSlot total value, basis points
    function maxRewardRatioBP() external view returns (uint256) {
        return _storage().maxRewardRatioBP;
    }

    /// @notice returns the max Lido fee rate per second, in ether
    function maxLidoFeeRatePerSecond() external view returns (uint256) {
        return _storage().maxLidoFeeRatePerSecond;
    }

    /// @notice returns the quarantine info for the vault
    /// @param _vault the address of the vault
    /// @dev returns zeroed structure if there is no active quarantine
    function vaultQuarantine(address _vault) external view returns (QuarantineInfo memory) {
        Quarantine storage q = _storage().vaultQuarantines[_vault];
        if (q.pendingTotalValueIncrease == 0) {
            return QuarantineInfo(false, 0, 0, 0);
        }

        return QuarantineInfo({
            isActive: true,
            pendingTotalValueIncrease: q.pendingTotalValueIncrease,
            startTimestamp: q.startTimestamp,
            endTimestamp: q.startTimestamp + _storage().quarantinePeriod
        });
    }

    /// @notice returns the number of vaults connected to the VaultHub
    /// @return the number of vaults connected to the VaultHub
    function vaultsCount() external view returns (uint256) {
        return _vaultHub().vaultsCount();
    }

    /// @notice returns batch of vaults info
    /// @param _offset in the vaults list [0, vaultsCount)
    /// @param _limit maximum number of vaults to return
    /// @return batch of vaults info
    function batchVaultsInfo(uint256 _offset, uint256 _limit) external view returns (VaultInfo[] memory) {
        VaultHub vaultHub = _vaultHub();
        uint256 vaultCount = vaultHub.vaultsCount();
        uint256 batchSize;
        if (_offset > vaultCount) {
            batchSize = 0;
        } else {
            batchSize = _offset + _limit > vaultCount ? vaultCount - _offset : _limit;
        }

        VaultInfo[] memory batch = new VaultInfo[](batchSize);
        for (uint256 i = 0; i < batchSize; i++) {
            address vaultAddress = vaultHub.vaultByIndex(_offset + i + 1);
            batch[i] = _vaultInfo(vaultAddress, vaultHub);
        }
        return batch;
    }

    /// @notice returns the vault data info
    /// @param _vault the address of the vault
    /// @return the vault data info
    function vaultInfo(address _vault) external view returns (VaultInfo memory) {
        return _vaultInfo(_vault, _vaultHub());
    }

    /**
     * @notice batch method to mass check the validator stages in PredepositGuarantee contract
     * @param _pubkeys the array of validator's pubkeys to check
     */
    function batchValidatorStages(
        bytes[] calldata _pubkeys
    ) external view returns (IPredepositGuarantee.ValidatorStage[] memory batch) {
        batch = new IPredepositGuarantee.ValidatorStage[](_pubkeys.length);

        for (uint256 i = 0; i < _pubkeys.length; i++) {
            batch[i] = predepositGuarantee().validatorStatus(_pubkeys[i]).stage;
        }
    }

    /// @notice update the sanity parameters
    /// @param _quarantinePeriod the quarantine period
    /// @param _maxRewardRatioBP the max EL CL rewards
    /// @param _maxLidoFeeRatePerSecond the max Lido fee rate per second
    function updateSanityParams(
        uint256 _quarantinePeriod,
        uint256 _maxRewardRatioBP,
        uint256 _maxLidoFeeRatePerSecond
    ) external onlyRole(UPDATE_SANITY_PARAMS_ROLE) {
        _updateSanityParams(_quarantinePeriod, _maxRewardRatioBP, _maxLidoFeeRatePerSecond);
    }

    /// @notice Store the report root and its meta information
    /// @param _vaultsDataTimestamp the timestamp of the report
    /// @param _vaultsDataRefSlot the refSlot of the report
    /// @param _vaultsDataTreeRoot the root of the report
    /// @param _vaultsDataReportCid the CID of the report
    function updateReportData(
        uint256 _vaultsDataTimestamp,
        uint256 _vaultsDataRefSlot,
        bytes32 _vaultsDataTreeRoot,
        string memory _vaultsDataReportCid
    ) external override(ILazyOracle) {
        if (msg.sender != LIDO_LOCATOR.accountingOracle()) revert NotAuthorized();

        Storage storage $ = _storage();
        $.vaultsDataTimestamp = uint64(_vaultsDataTimestamp);
        $.vaultsDataRefSlot = uint48(_vaultsDataRefSlot);
        $.vaultsDataTreeRoot = _vaultsDataTreeRoot;
        $.vaultsDataReportCid = _vaultsDataReportCid;

        emit VaultsReportDataUpdated(
            _vaultsDataTimestamp,
            _vaultsDataRefSlot,
            _vaultsDataTreeRoot,
            _vaultsDataReportCid
        );
    }

    /// @notice Permissionless update of the vault data
    /// @param _vault the address of the vault
    /// @param _totalValue the total value of the vault
    /// @param _cumulativeLidoFees the cumulative Lido fees accrued on the vault (nominated in ether)
    /// @param _liabilityShares the liabilityShares value of the vault (on the vaultsDataRefSlot)
    /// @param _maxLiabilityShares the maxLiabilityShares value of the vault (on the vaultsDataRefSlot)
    /// @param _proof the proof of the reported data
    function updateVaultData(
        address _vault,
        uint256 _totalValue,
        uint256 _cumulativeLidoFees,
        uint256 _liabilityShares,
        uint256 _maxLiabilityShares,
        uint256 _slashingReserve,
        bytes32[] calldata _proof
    ) external {
        bytes32 leaf = keccak256(
            bytes.concat(
                keccak256(
                    abi.encode(
                        _vault,
                        _totalValue,
                        _cumulativeLidoFees,
                        _liabilityShares,
                        _maxLiabilityShares,
                        _slashingReserve
                    )
                )
            )
        );
        if (!MerkleProof.verify(_proof, _storage().vaultsDataTreeRoot, leaf)) revert InvalidProof();

        uint256 vaultsDataTimestamp = _storage().vaultsDataTimestamp;
        (uint256 checkedTotalValue, int256 inOutDelta) = _handleSanityChecks(
            _vault,
            _totalValue,
            _storage().vaultsDataRefSlot,
            vaultsDataTimestamp,
            _cumulativeLidoFees,
            _liabilityShares,
            _maxLiabilityShares
        );

        _vaultHub().applyVaultReport(
            _vault,
            vaultsDataTimestamp,
            checkedTotalValue,
            inOutDelta,
            _cumulativeLidoFees,
            _liabilityShares,
            _maxLiabilityShares,
            _slashingReserve
        );
    }

    /// @notice removes the quarantine for the vault
    /// @param _vault the address of the vault
    function removeVaultQuarantine(address _vault) external {
        if (msg.sender != LIDO_LOCATOR.vaultHub()) revert NotAuthorized();

        mapping(address => Quarantine) storage quarantines = _storage().vaultQuarantines;
        if (quarantines[_vault].pendingTotalValueIncrease > 0) {
            emit QuarantineRemoved(_vault);
        }
        delete quarantines[_vault];
    }

    function _vaultInfo(address _vault, VaultHub _vh) internal view returns (VaultInfo memory) {
        IStakingVault vault = IStakingVault(_vault);
        VaultHub.VaultConnection memory connection = _vh.vaultConnection(_vault);
        VaultHub.VaultRecord memory record = _vh.vaultRecord(_vault);
        return VaultInfo(
            _vault,
            vault.availableBalance() + vault.stagedBalance(),
            record.inOutDelta.currentValue(),
            vault.withdrawalCredentials(),
            record.liabilityShares,
            record.maxLiabilityShares,
            _mintableStETH(_vault, _vh),
            connection.shareLimit,
            connection.reserveRatioBP,
            connection.forcedRebalanceThresholdBP,
            connection.infraFeeBP,
            connection.liquidityFeeBP,
            connection.reservationFeeBP,
            _vh.isPendingDisconnect(_vault)
        );
    }

    /// @notice handle sanity checks for the vault lazy report data
    /// @param _vault the address of the vault
    /// @param _totalValue the total value of the vault in refSlot
    /// @param _reportRefSlot the refSlot of the report
    /// @param _reportTimestamp the timestamp of the report
    /// @param _cumulativeLidoFees the cumulative Lido fees accrued on the vault (nominated in ether)
    /// @param _liabilityShares the liabilityShares value of the vault (on the _reportRefSlot)
    /// @param _maxLiabilityShares the maxLiabilityShares value of the vault (on the _reportRefSlot)
    /// @return totalValueWithoutQuarantine the smoothed total value of the vault after sanity checks
    /// @return inOutDeltaOnRefSlot the inOutDelta in the refSlot
    function _handleSanityChecks(
        address _vault,
        uint256 _totalValue,
        uint256 _reportRefSlot,
        uint256 _reportTimestamp,
        uint256 _cumulativeLidoFees,
        uint256 _liabilityShares,
        uint256 _maxLiabilityShares
    ) internal returns (uint256 totalValueWithoutQuarantine, int256 inOutDeltaOnRefSlot) {
        VaultHub vaultHub = _vaultHub();
        VaultHub.VaultRecord memory record = vaultHub.vaultRecord(_vault);
        uint48 previousReportTs = record.report.timestamp;

        // 0. Check if the report is already fresh enough
        if (uint48(_reportTimestamp) <= previousReportTs) {
            revert VaultReportIsFreshEnough();
        }

        // 1. Calculate inOutDelta in the refSlot
        int256 currentInOutDelta = record.inOutDelta.currentValue();
        inOutDeltaOnRefSlot = record.inOutDelta.getValueForRefSlot(uint48(_reportRefSlot));

        // 2. Sanity check for total value increase
        totalValueWithoutQuarantine = _processTotalValue(
            _vault, _totalValue, inOutDeltaOnRefSlot, record, _reportTimestamp);

        // 3. Sanity check for dynamic total value underflow
        if (int256(totalValueWithoutQuarantine) + currentInOutDelta - inOutDeltaOnRefSlot < 0) {
            revert UnderflowInTotalValueCalculation();
        }

        // 4. Sanity check for cumulative Lido fees
        uint256 previousCumulativeLidoFees = record.cumulativeLidoFees;
        if (previousCumulativeLidoFees > _cumulativeLidoFees) {
            revert CumulativeLidoFeesTooLow(_cumulativeLidoFees, previousCumulativeLidoFees);
        }

        uint256 maxLidoFees = (_reportTimestamp - previousReportTs) * uint256(_storage().maxLidoFeeRatePerSecond);
        if (_cumulativeLidoFees - previousCumulativeLidoFees > maxLidoFees) {
            revert CumulativeLidoFeesTooLarge(_cumulativeLidoFees - previousCumulativeLidoFees, maxLidoFees);
        }

        // 5. _maxLiabilityShares is greater or equal than _liabilityShares and current `maxLiabilityShares`
        if (_maxLiabilityShares < _liabilityShares || _maxLiabilityShares < record.maxLiabilityShares) {
            revert InvalidMaxLiabilityShares();
        }
    }

    /*
        Quarantine State Diagram

        States:
        • NO_QUARANTINE: No active quarantine, all value is immediately available
        • QUARANTINE_ACTIVE: Total value increase is quarantined, waiting for expiration
        • QUARANTINE_EXPIRED: Quarantine period passed, quarantined value can be released

        ┌─────────────────┐                              ┌──────────────────┐
        │  NO_QUARANTINE  │ reported > threshold         │QUARANTINE_ACTIVE │
        │                 ├─────────────────────────────►│                  │
        │  quarantined=0  │                              │  quarantined>0   │
        │  startTime=0    │◄─────────────────────────────┤  startTime>0     │
        │                 |                              │  time<expiration |
        └─────────────────┘ reported ≤ threshold         └───┬──────────────┘
                ▲         (early release)                    │       ▲
                │                                            │       │  increase > quarantined + rewards
                │                          time ≥            │       │  (release old, start new)
                │                          quarantine period │       │
                │                                            ▼       │
                │                                      ┌─────────────┴────────┐
                │ reported ≤ threshold OR              │  QUARANTINE_EXPIRED  │
                │ increase ≤ quarantined + rewards     │                      │
                │                                      │  quarantined>0       │
                │                                      │  startTime>0         │
                └──────────────────────────────────────┤  time>=expiration    │
                                                       └──────────────────────┘

        Legend:
        • threshold = onchainTotalValue * (100% + maxRewardRatio)
        • increase = reportedTotalValue - onchainTotalValue
        • quarantined - total value increase that is currently quarantined
        • rewards - expected EL/CL rewards based on maxRewardRatio
        • time = block.timestamp
        • expiration = quarantine.startTimestamp + quarantinePeriod
    */
    function _processTotalValue(
        address _vault,
        uint256 _reportedTotalValue,
        int256 _inOutDeltaOnRefSlot,
        VaultHub.VaultRecord memory record,
        uint256 _reportTimestamp
    ) internal returns (uint256 totalValueWithoutQuarantine) {
        if (_reportedTotalValue > MAX_SANE_TOTAL_VALUE) {
            revert TotalValueTooLarge();
        }

        // Calculate base values for quarantine logic -------------------------
        // --------------------------------------------------------------------

        // 0. Read storage values
        Storage storage $ = _storage();
        Quarantine storage quarantine = $.vaultQuarantines[_vault];
        uint256 quarantinedValue = quarantine.pendingTotalValueIncrease;
        // 1. Onchain total value on refSlot, it does not include CL difference and EL rewards for the period
        uint256 onchainTotalValueOnRefSlot =
            uint256(int256(uint256(record.report.totalValue)) + _inOutDeltaOnRefSlot - record.report.inOutDelta);
        // 2. Some percentage of funds that haven’t passed through the vault’s balance is allowed for handling EL and CL rewards.
        // NB: allowed amount of rewards is not scaled by time here, because:
        // - if we set a small per-day percentage, honest vaults receiving unexpectedly high MEV would get quarantined;
        // - if we set a large per-day percentage, a vault that hasn’t reported for a long time could bypass quarantine;
        // As a result, we would need to impose very tiny limits for non-quarantine percentage — which would complicate the logic
        // without bringing meaningful improvements.
        uint256 quarantineThreshold =
            onchainTotalValueOnRefSlot * (TOTAL_BASIS_POINTS + $.maxRewardRatioBP) / TOTAL_BASIS_POINTS;
        // 3. Determine current quarantine state
        QuarantineState currentState = _determineQuarantineState(quarantine, quarantinedValue, _reportTimestamp);


        // Execute logic based on current state and conditions ----------------
        // --------------------------------------------------------------------

        if (currentState == QuarantineState.NO_QUARANTINE) {
            if (_reportedTotalValue <= quarantineThreshold) {
                // Transition: NO_QUARANTINE → NO_QUARANTINE (no change needed)
                return _reportedTotalValue;
            } else {
                // Transition: NO_QUARANTINE → QUARANTINE_ACTIVE (start new quarantine)
                _startNewQuarantine(
                    _vault,
                    quarantine,
                    _reportedTotalValue - onchainTotalValueOnRefSlot,
                    _reportTimestamp
                );
                return onchainTotalValueOnRefSlot;
            }
        } else if (currentState == QuarantineState.QUARANTINE_ACTIVE) {
            if (_reportedTotalValue <= quarantineThreshold) {
                // Transition: QUARANTINE_ACTIVE → NO_QUARANTINE (release quarantine early)
                delete $.vaultQuarantines[_vault];
                emit QuarantineReleased(_vault, 0);
                return _reportedTotalValue;
            } else {
                // Transition: QUARANTINE_ACTIVE → QUARANTINE_ACTIVE (maintain quarantine)
                return onchainTotalValueOnRefSlot;
            }
        } else { // QuarantineState.QUARANTINE_EXPIRED
            uint256 totalValueIncrease = _reportedTotalValue > onchainTotalValueOnRefSlot
                ? _reportedTotalValue - onchainTotalValueOnRefSlot
                : 0;
            uint256 quarantineThresholdWithRewards = quarantineThreshold + quarantinedValue
                * (TOTAL_BASIS_POINTS + $.maxRewardRatioBP) / TOTAL_BASIS_POINTS;

            if (_reportedTotalValue <= quarantineThresholdWithRewards) {
                // Transition: QUARANTINE_EXPIRED → NO_QUARANTINE (release and accept all)
                delete $.vaultQuarantines[_vault];
                emit QuarantineReleased(_vault, _reportedTotalValue <= quarantineThreshold ? 0 : totalValueIncrease);
                return _reportedTotalValue;
            } else {
                // Transition: QUARANTINE_EXPIRED → QUARANTINE_ACTIVE (release old, start new)
                emit QuarantineReleased(_vault, quarantinedValue);
                _startNewQuarantine(_vault, quarantine, totalValueIncrease - quarantinedValue, _reportTimestamp);
                return onchainTotalValueOnRefSlot + quarantinedValue;
            }
        }
    }

    function _determineQuarantineState(
        Quarantine storage _quarantine,
        uint256 _quarantinedValue,
        uint256 _vaultsDataTimestamp
    ) internal view returns (QuarantineState) {
        if (_quarantinedValue == 0) {
            return QuarantineState.NO_QUARANTINE;
        }

        bool isQuarantineExpired = (_vaultsDataTimestamp - _quarantine.startTimestamp) >= _storage().quarantinePeriod;
        return isQuarantineExpired ? QuarantineState.QUARANTINE_EXPIRED : QuarantineState.QUARANTINE_ACTIVE;
    }

    function _startNewQuarantine(
        address _vault,
        Quarantine storage _quarantine,
        uint256 _amountToQuarantine,
        uint256 _currentTimestamp
    ) internal {
        _quarantine.pendingTotalValueIncrease = uint128(_amountToQuarantine);
        _quarantine.startTimestamp = uint64(_currentTimestamp);
        emit QuarantineActivated(_vault, _amountToQuarantine);
    }

    function _updateSanityParams(uint256 _quarantinePeriod, uint256 _maxRewardRatioBP, uint256 _maxLidoFeeRatePerSecond) internal {
        if (_quarantinePeriod > MAX_QUARANTINE_PERIOD) revert QuarantinePeriodTooLarge(_quarantinePeriod, MAX_QUARANTINE_PERIOD);
        if (_maxRewardRatioBP > MAX_REWARD_RATIO) revert MaxRewardRatioTooLarge(_maxRewardRatioBP, MAX_REWARD_RATIO);
        if (_maxLidoFeeRatePerSecond > MAX_LIDO_FEE_RATE_PER_SECOND) revert MaxLidoFeeRatePerSecondTooLarge(_maxLidoFeeRatePerSecond, MAX_LIDO_FEE_RATE_PER_SECOND);

        Storage storage $ = _storage();
        $.quarantinePeriod = uint64(_quarantinePeriod);
        $.maxRewardRatioBP = uint16(_maxRewardRatioBP);
        $.maxLidoFeeRatePerSecond = uint64(_maxLidoFeeRatePerSecond);
        emit SanityParamsUpdated(_quarantinePeriod, _maxRewardRatioBP, _maxLidoFeeRatePerSecond);
    }

    function _mintableStETH(address _vault, VaultHub _vh) internal view returns (uint256) {
        uint256 mintableShares = _vh.totalMintingCapacityShares(_vault, 0 /* zero eth delta */);
        return _getPooledEthBySharesRoundUp(mintableShares);
    }

    function _storage() internal pure returns (Storage storage $) {
        assembly {
            $.slot := LAZY_ORACLE_STORAGE_LOCATION
        }
    }

    function predepositGuarantee() internal view returns (IPredepositGuarantee) {
        return IPredepositGuarantee(LIDO_LOCATOR.predepositGuarantee());
    }

    function _vaultHub() internal view returns (VaultHub) {
        return VaultHub(payable(LIDO_LOCATOR.vaultHub()));
    }

    function _operatorGrid() internal view returns (OperatorGrid) {
        return OperatorGrid(LIDO_LOCATOR.operatorGrid());
    }

    function _getPooledEthBySharesRoundUp(uint256 _shares) internal view returns (uint256) {
        return ILido(LIDO_LOCATOR.lido()).getPooledEthBySharesRoundUp(_shares);
    }

    event VaultsReportDataUpdated(uint256 indexed timestamp, uint256 indexed refSlot, bytes32 indexed root, string cid);
    event QuarantineActivated(address indexed vault, uint256 delta);
    event QuarantineReleased(address indexed vault, uint256 delta);
    event QuarantineRemoved(address indexed vault);
    event SanityParamsUpdated(uint256 quarantinePeriod, uint256 maxRewardRatioBP, uint256 maxLidoFeeRatePerSecond);

    error AdminCannotBeZero();
    error NotAuthorized();
    error InvalidProof();
    error UnderflowInTotalValueCalculation();
    error TotalValueTooLarge();
    error VaultReportIsFreshEnough();
    error CumulativeLidoFeesTooLow(uint256 reportingFees, uint256 previousFees);
    error CumulativeLidoFeesTooLarge(uint256 feeIncrease, uint256 maxFeeIncrease);
    error QuarantinePeriodTooLarge(uint256 quarantinePeriod, uint256 maxQuarantinePeriod);
    error MaxRewardRatioTooLarge(uint256 rewardRatio, uint256 maxRewardRatio);
    error MaxLidoFeeRatePerSecondTooLarge(uint256 feeRate, uint256 maxFeeRate);
    error InvalidMaxLiabilityShares();
}

// SPDX-FileCopyrightText: 2025 Lido <[email protected]>
// SPDX-License-Identifier: GPL-3.0

// See contracts/COMPILERS.md
pragma solidity 0.8.25;

import {SafeERC20} from "@openzeppelin/contracts-v5.2/token/ERC20/utils/SafeERC20.sol";
import {IERC20} from "@openzeppelin/contracts-v5.2/token/ERC20/IERC20.sol";

library RecoverTokens {
    /**
     * @notice ETH address convention per EIP-7528
     */
    address internal constant ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;

    /**
     * @notice Emitted when the ERC20 `token` or ether is recovered (i.e. transferred)
     * @param to The address of the recovery recipient
     * @param assetAddress The address of the recovered ERC20 token (0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee for ether)
     * @param amount The amount of the token recovered
     */
    event AssetsRecovered(address indexed to, address indexed assetAddress, uint256 amount);

    /**
     * @notice Error thrown when recovery of ETH fails on transfer to recipient
     * @param recipient Address of the recovery recipient
     * @param amount Amount of ETH attempted to recover
     */
    error EthTransferFailed(address recipient, uint256 amount);

    function _recoverEth(
        address _recipient,
        uint256 _amount
    ) internal {
        (bool success,) = payable(_recipient).call{value: _amount}("");
        if (!success) revert EthTransferFailed(_recipient, _amount);

        emit AssetsRecovered(_recipient, ETH, _amount);
    }

    function _recoverERC20(
        address _token,
        address _recipient,
        uint256 _amount
    ) internal {
        SafeERC20.safeTransfer(IERC20(_token), _recipient, _amount);

        emit AssetsRecovered(_recipient, _token, _amount);
    }
}

// SPDX-FileCopyrightText: 2025 Lido <[email protected]>
// SPDX-License-Identifier: GPL-3.0

// See contracts/COMPILERS.md
// solhint-disable one-contract-per-file
pragma solidity 0.8.25;

import {IHashConsensus} from "contracts/common/interfaces/IHashConsensus.sol";

uint256 constant DOUBLE_CACHE_LENGTH = 2;

// wrap external call in function to save bytecode
function _getCurrentRefSlot(IHashConsensus _consensus) view returns (uint256) {
    (uint256 refSlot, ) = _consensus.getCurrentFrame();
    return refSlot;
}

library RefSlotCache {
    struct Uint104WithCache {
        uint104 value;
        uint104 valueOnRefSlot;
        uint48 refSlot;
    }

    /// @notice Increases the value and caches the previous value for the current refSlot
    /// @param _storage The storage slot to update
    /// @param _consensus The consensus contract to get the current refSlot
    /// @param _increment increment the value by this amount
    /// @return the updated struct to be saved in storage
    function withValueIncrease(
        Uint104WithCache storage _storage,
        IHashConsensus _consensus,
        uint104 _increment
    ) internal view returns (Uint104WithCache memory) {
        uint256 refSlot = _getCurrentRefSlot(_consensus);

        Uint104WithCache memory newCache = _storage;

        if (newCache.refSlot != uint48(refSlot)) {
            newCache.valueOnRefSlot = _storage.value;
            newCache.refSlot = uint48(refSlot);
        }

        newCache.value += _increment;

        return newCache;
    }

    /// @notice Returns the value for the current refSlot
    /// @param _storage the storage pointer for the cached value
    /// @param _consensus the consensus contract to get the current refSlot
    /// @return the cached value if it's changed since the last refSlot, the current value otherwise
    function getValueForLastRefSlot(
        Uint104WithCache storage _storage,
        IHashConsensus _consensus
    ) internal view returns (uint104) {
        uint256 refSlot = _getCurrentRefSlot(_consensus);
        if (uint48(refSlot) != _storage.refSlot) {
            return _storage.value;
        } else {
            return _storage.valueOnRefSlot;
        }
    }
}

library DoubleRefSlotCache {
    struct Int104WithCache {
        int104 value;
        int104 valueOnRefSlot;
        uint48 refSlot;
    }

    /// @notice Initializes the cache with the given value
    /// @param _value the value to initialize the cache with
    /// @return the initialized cache
    function initializeInt104DoubleCache(
        int104 _value
    ) internal pure returns (Int104WithCache[DOUBLE_CACHE_LENGTH] memory) {
        return [
            Int104WithCache({
                value: _value,
                valueOnRefSlot: 0,
                refSlot: 0 // first cache slot is active by default (as >= used in _activeCacheIndex)
            }),
            Int104WithCache(0, 0, 0)
        ];
    }

    /// @notice Increases the value and caches the previous value for the current refSlot
    /// @param _storage The storage slot to update
    /// @param _consensus The consensus contract to get the current refSlot
    /// @param _increment increment the value by this amount
    /// @return the updated struct to be saved in storage
    function withValueIncrease(
        Int104WithCache[DOUBLE_CACHE_LENGTH] storage _storage,
        IHashConsensus _consensus,
        int104 _increment
    ) internal view returns (Int104WithCache[DOUBLE_CACHE_LENGTH] memory) {
        uint256 refSlot = _getCurrentRefSlot(_consensus);

        Int104WithCache[DOUBLE_CACHE_LENGTH] memory newCache = _storage;
        uint256 activeCacheIndex = _activeCacheIndex(newCache);

        if (newCache[activeCacheIndex].refSlot != uint48(refSlot)) {
            uint256 previousCacheIndex = activeCacheIndex;
            activeCacheIndex = 1 - activeCacheIndex;
            newCache[activeCacheIndex].value = newCache[previousCacheIndex].value;
            newCache[activeCacheIndex].valueOnRefSlot = newCache[previousCacheIndex].value;
            newCache[activeCacheIndex].refSlot = uint48(refSlot);
        }

        newCache[activeCacheIndex].value += _increment;

        return newCache;
    }

    /// @notice Returns the current value of the cache
    /// @param _cache the storage pointer for the array of cached values
    /// @return the current value of the cache
    function currentValue(Int104WithCache[DOUBLE_CACHE_LENGTH] memory _cache) internal pure returns (int104) {
        return _cache[_activeCacheIndex(_cache)].value;
    }

    /// @notice Returns the value for the refSlot
    /// @param _cache the storage pointer for the cached value
    /// @param _refSlot the refSlot to get the value for
    /// @return the cached value if it's changed since the last refSlot, the current value otherwise
    /// @dev reverts if the cache was overwritten after target refSlot
    function getValueForRefSlot(
        Int104WithCache[DOUBLE_CACHE_LENGTH] memory _cache,
        uint48 _refSlot
    ) internal pure returns (int104) {
        uint256 activeCacheIndex = _activeCacheIndex(_cache);

        // 1. refSlot is more than activeRefSlot
        if (_refSlot > _cache[activeCacheIndex].refSlot) {
            return _cache[activeCacheIndex].value;
        }

        uint256 previousCacheIndex = 1 - activeCacheIndex;
        // 2. refSlot is in (prevRefSlot, activeRefSlot]
        if (_refSlot > _cache[previousCacheIndex].refSlot) {
            return _cache[activeCacheIndex].valueOnRefSlot;
        }

        // 3. refSlot is equal to prevRefSlot
        if (_refSlot == _cache[previousCacheIndex].refSlot) {
            return _cache[previousCacheIndex].valueOnRefSlot;
        }

        // 4. refSlot is less than prevRefSlot
        revert InOutDeltaCacheIsOverwritten();
    }

    /// @dev There is a limitation on the refSlot value: it must be less than 2^48.
    /// If it exceeds this limit, the refSlot will be truncated to 48 bits.
    /// _activeCacheIndex may work incorrectly if one refSlot value is truncated and the other is not,
    /// because the non-truncated value will always be greater than the truncated one,
    /// causing incorrect activeIndex determination. However, 2^48 is a very large number,
    /// so if block time will be 1 second, it will take 8_925_512 years to reach this limit.
    function _activeCacheIndex(Int104WithCache[DOUBLE_CACHE_LENGTH] memory _cache) private pure returns (uint256) {
        return _cache[0].refSlot >= _cache[1].refSlot ? 0 : 1;
    }

    error InOutDeltaCacheIsOverwritten();
}

// SPDX-FileCopyrightText: 2025 Lido <[email protected]>
// SPDX-License-Identifier: GPL-3.0

// See contracts/COMPILERS.md
pragma solidity 0.8.25;

import {AccessControlEnumerableUpgradeable} from "contracts/openzeppelin/5.2/upgradeable/access/extensions/AccessControlEnumerableUpgradeable.sol";
import {SafeCast} from "@openzeppelin/contracts-v5.2/utils/math/SafeCast.sol";

import {Math256} from "contracts/common/lib/Math256.sol";
import {ILidoLocator} from "contracts/common/interfaces/ILidoLocator.sol";

import {Confirmable2Addresses} from "../utils/Confirmable2Addresses.sol";

import {IStakingVault} from "./interfaces/IStakingVault.sol";
import {VaultHub} from "./VaultHub.sol";


struct TierParams {
    uint256 shareLimit;
    uint256 reserveRatioBP;
    uint256 forcedRebalanceThresholdBP;
    uint256 infraFeeBP;
    uint256 liquidityFeeBP;
    uint256 reservationFeeBP;
}

/**
 * @title OperatorGrid
 * @author loga4
 * @notice
 * OperatorGrid is a contract that manages mint parameters for vaults when they are connected to the VaultHub.
 * These parameters include:
 * - shareLimit: maximum amount of shares that can be minted
 * - reserveRatioBP: reserve ratio in basis points
 * - forcedRebalanceThresholdBP: forced rebalance threshold in basis points
 * - infraFeeBP: infra fee in basis points
 * - liquidityFeeBP: liquidity fee in basis points
 * - reservationFeeBP: reservation fee in basis points
 *
 * These parameters are determined by the Tier in which the Vault is registered.
 *
 */
contract OperatorGrid is AccessControlEnumerableUpgradeable, Confirmable2Addresses {
    /*
      Key concepts:
      1. Default Registration:
         - All Vaults initially have default tier (DEFAULT_TIER_ID = 0)
         - The default tier has no group

         DEFAULT_TIER_ID = 0
        ┌──────────────────────┐
        │        Tier 1        │
        │  tierShareLimit = z  │
        │  Vault_1 ... Vault_m │
        └──────────────────────┘

       2. Tier Change Process:
         - To predefine vaults tier or modify the existing vault's connection parameters to VaultHub, a tier change must be requested
         - Both vault owner and node operator must confirm the change (doesn't matter who confirms first)
         - The confirmation has an expiry time (default 1 hour)

       3. Tier Reset:
         - When a vault is disconnected from VaultHub, its tier is automatically reset to the default tier (DEFAULT_TIER_ID)

       4. Tier Capacity:
         - Tiers are not limited by the number of vaults
         - Tiers are limited by the sum of vaults' liability shares
         - Administrative operations (like bad debt socialization) can bypass tier/group limits

        ┌──────────────────────────────────────────────────────┐
        │                 Group 1 = operator 1                 │
        │  ┌────────────────────────────────────────────────┐  │
        │  │  groupShareLimit = 1kk                         │  │
        │  └────────────────────────────────────────────────┘  │
        │  ┌──────────────────────┐  ┌──────────────────────┐  │
        │  │       Tier 1         │  │       Tier 2         │  │
        │  │  tierShareLimit = x  │  │  tierShareLimit = y  │  │
        │  │  Vault_2 ... Vault_k │  │                      │  │
        │  └──────────────────────┘  └──────────────────────┘  │
        └──────────────────────────────────────────────────────┘

        5. Jail Mechanism:
         - A vault can be "jailed" as a penalty mechanism for misbehavior or violations
         - When a vault is in jail, it cannot mint new stETH shares (normal minting operations are blocked)
         - Vaults can be jailed/unjailed by addresses with appropriate governance roles
         - Administrative operations (like bad debt socialization) can bypass jail restrictions
     */

    /// @dev 0xa495a3428837724c7f7648cda02eb83c9c4c778c8688d6f254c7f3f80c154d55
    bytes32 public constant REGISTRY_ROLE = keccak256("vaults.OperatorsGrid.Registry");

    /// @notice Lido Locator contract
    ILidoLocator public immutable LIDO_LOCATOR;

    uint256 public constant DEFAULT_TIER_ID = 0;

    // Special address to denote that default tier is not linked to any real operator
    address public constant DEFAULT_TIER_OPERATOR = address(uint160(type(uint160).max));

    /// @dev basis points base
    uint256 internal constant TOTAL_BASIS_POINTS = 100_00;
    /// @dev max value for fees in basis points - it's about 650%
    uint256 internal constant MAX_FEE_BP = type(uint16).max;
    /// @dev max value for reserve ratio in basis points - 9999
    uint256 internal constant MAX_RESERVE_RATIO_BP = 99_99;

    // -----------------------------
    //            STRUCTS
    // -----------------------------
    struct Group {
        address operator;
        uint96 shareLimit;
        uint96 liabilityShares;
        uint256[] tierIds;
    }

    struct Tier {
        address operator;
        uint96 shareLimit;
        uint96 liabilityShares;
        uint16 reserveRatioBP;
        uint16 forcedRebalanceThresholdBP;
        uint16 infraFeeBP;
        uint16 liquidityFeeBP;
        uint16 reservationFeeBP;
    }

    /**
     * @notice ERC-7201 storage namespace for the OperatorGrid
     * @dev ERC-7201 namespace is used to prevent upgrade collisions
     * @custom:storage-location erc7201:Lido.Vaults.OperatorGrid
     * @custom:tiers Tiers
     * @custom:vaultTier Vault tier
     * @custom:groups Groups
     * @custom:nodeOperators Node operators
     * @custom:isVaultInJail if true, vault is in jail and can't mint stETH
     */
    struct ERC7201Storage {
        Tier[] tiers;
        mapping(address vault => uint256 tierId) vaultTier;
        mapping(address nodeOperator => Group) groups;
        address[] nodeOperators;
        mapping(address vault => bool isInJail) isVaultInJail;
    }

    /**
     * @notice Storage offset slot for ERC-7201 namespace
     *         The storage namespace is used to prevent upgrade collisions
     *         keccak256(abi.encode(uint256(keccak256("Lido.Vaults.OperatorGrid")) - 1)) & ~bytes32(uint256(0xff))
     */
    bytes32 private constant OPERATOR_GRID_STORAGE_LOCATION =
        0x6b64617c951381e2c1eff2be939fe368ab6d76b7d335df2e47ba2309eba1c700;


    /// @notice Initializes the contract with a LidoLocator
    /// @param _locator LidoLocator contract
    constructor(ILidoLocator _locator) {
        LIDO_LOCATOR = _locator;

        _disableInitializers();
    }

    /// @notice Initializes the contract with an admin
    /// @param _admin Address of the admin
    /// @param _defaultTierParams Default tier params for the default tier
    function initialize(address _admin, TierParams calldata _defaultTierParams) external initializer {
        if (_admin == address(0)) revert ZeroArgument("_admin");

        __AccessControlEnumerable_init();
        __Confirmations_init();
        _grantRole(DEFAULT_ADMIN_ROLE, _admin);

        _validateParams(
            DEFAULT_TIER_ID,
            _defaultTierParams.reserveRatioBP,
            _defaultTierParams.forcedRebalanceThresholdBP,
            _defaultTierParams.infraFeeBP,
            _defaultTierParams.liquidityFeeBP,
            _defaultTierParams.reservationFeeBP
        );

        ERC7201Storage storage $ = _getStorage();

        //create default tier with default share limit
        $.tiers.push(
            Tier({
                operator: DEFAULT_TIER_OPERATOR,
                shareLimit: uint96(_defaultTierParams.shareLimit),
                reserveRatioBP: uint16(_defaultTierParams.reserveRatioBP),
                forcedRebalanceThresholdBP: uint16(_defaultTierParams.forcedRebalanceThresholdBP),
                infraFeeBP: uint16(_defaultTierParams.infraFeeBP),
                liquidityFeeBP: uint16(_defaultTierParams.liquidityFeeBP),
                reservationFeeBP: uint16(_defaultTierParams.reservationFeeBP),
                liabilityShares: 0
            })
        );
    }

    /// @notice Sets the confirmation expiry period
    /// @param _newConfirmExpiry The new confirmation expiry period in seconds
    function setConfirmExpiry(uint256 _newConfirmExpiry) external onlyRole(REGISTRY_ROLE) {
        _validateConfirmExpiry(_newConfirmExpiry);
        _setConfirmExpiry(_newConfirmExpiry);
    }

    /// @notice Registers a new group
    /// @param _nodeOperator address of the node operator
    /// @param _shareLimit Maximum share limit for the group
    function registerGroup(address _nodeOperator, uint256 _shareLimit) external onlyRole(REGISTRY_ROLE) {
        if (_nodeOperator == address(0)) revert ZeroArgument("_nodeOperator");

        ERC7201Storage storage $ = _getStorage();
        if ($.groups[_nodeOperator].operator != address(0)) revert GroupExists();

        $.groups[_nodeOperator] = Group({
            operator: _nodeOperator,
            shareLimit: SafeCast.toUint96(_shareLimit),
            liabilityShares: 0,
            tierIds: new uint256[](0)
        });
        $.nodeOperators.push(_nodeOperator);

        emit GroupAdded(_nodeOperator, _shareLimit);
    }

    /// @notice Updates the share limit of a group
    /// @param _nodeOperator address of the node operator
    /// @param _shareLimit New share limit value
    function updateGroupShareLimit(address _nodeOperator, uint256 _shareLimit) external onlyRole(REGISTRY_ROLE) {
        if (_nodeOperator == address(0)) revert ZeroArgument("_nodeOperator");

        ERC7201Storage storage $ = _getStorage();
        Group storage group_ = $.groups[_nodeOperator];
        if (group_.operator == address(0)) revert GroupNotExists();

        group_.shareLimit = SafeCast.toUint96(_shareLimit);

        emit GroupShareLimitUpdated(_nodeOperator, _shareLimit);
    }

    /// @notice Returns a group by node operator address
    /// @param _nodeOperator address of the node operator
    /// @return Group
    function group(address _nodeOperator) external view returns (Group memory) {
        return _getStorage().groups[_nodeOperator];
    }

    /// @notice Returns a node operator address by index
    /// @param _index index of the node operator
    /// @return Node operator address
    function nodeOperatorAddress(uint256 _index) external view returns (address) {
        ERC7201Storage storage $ = _getStorage();
        if (_index >= $.nodeOperators.length) revert NodeOperatorNotExists();
        return $.nodeOperators[_index];
    }

    /// @notice Returns a node operator count
    /// @return Node operator count
    function nodeOperatorCount() external view returns (uint256) {
        return _getStorage().nodeOperators.length;
    }

    /// @notice Registers a new tier
    /// @param _nodeOperator address of the node operator
    /// @param _tiers array of tiers to register
    function registerTiers(
        address _nodeOperator,
        TierParams[] calldata _tiers
    ) external onlyRole(REGISTRY_ROLE) {
        if (_nodeOperator == address(0)) revert ZeroArgument("_nodeOperator");

        ERC7201Storage storage $ = _getStorage();
        Group storage group_ = $.groups[_nodeOperator];
        if (group_.operator == address(0)) revert GroupNotExists();

        uint256 tierId = $.tiers.length;
        uint256 length = _tiers.length;
        for (uint256 i = 0; i < length; i++) {
            _validateParams(
                tierId,
                _tiers[i].reserveRatioBP,
                _tiers[i].forcedRebalanceThresholdBP,
                _tiers[i].infraFeeBP,
                _tiers[i].liquidityFeeBP,
                _tiers[i].reservationFeeBP
            );

            Tier memory tier_ = Tier({
                operator: _nodeOperator,
                shareLimit: uint96(_tiers[i].shareLimit),
                reserveRatioBP: uint16(_tiers[i].reserveRatioBP),
                forcedRebalanceThresholdBP: uint16(_tiers[i].forcedRebalanceThresholdBP),
                infraFeeBP: uint16(_tiers[i].infraFeeBP),
                liquidityFeeBP: uint16(_tiers[i].liquidityFeeBP),
                reservationFeeBP: uint16(_tiers[i].reservationFeeBP),
                liabilityShares: 0
            });
            $.tiers.push(tier_);
            group_.tierIds.push(tierId);

            emit TierAdded(
                _nodeOperator,
                tierId,
                uint96(tier_.shareLimit),
                uint16(tier_.reserveRatioBP),
                uint16(tier_.forcedRebalanceThresholdBP),
                uint16(tier_.infraFeeBP),
                uint16(tier_.liquidityFeeBP),
                uint16(tier_.reservationFeeBP)
            );

            tierId++;
        }
    }

    /// @notice Returns a tier by ID
    /// @param _tierId id of the tier
    /// @return Tier
    function tier(uint256 _tierId) external view returns (Tier memory) {
        ERC7201Storage storage $ = _getStorage();
        if (_tierId >= $.tiers.length) revert TierNotExists();
        return $.tiers[_tierId];
    }

    /// @notice Returns a tiers count
    /// @return Tiers count
    function tiersCount() external view returns (uint256) {
        return _getStorage().tiers.length;
    }

    /// @notice Alters multiple tiers
    /// @dev We do not enforce to update old vaults with the new tier params, only new ones.
    /// @param _tierIds array of tier ids to alter
    /// @param _tierParams array of new tier params
    function alterTiers(
        uint256[] calldata _tierIds,
        TierParams[] calldata _tierParams
    ) external onlyRole(REGISTRY_ROLE) {
        if (_tierIds.length != _tierParams.length) revert ArrayLengthMismatch();

        ERC7201Storage storage $ = _getStorage();
        uint256 length = _tierIds.length;
        uint256 tiersLength = $.tiers.length;

        for (uint256 i = 0; i < length; i++) {
            if (_tierIds[i] >= tiersLength) revert TierNotExists();

            _validateParams(
                _tierIds[i],
                _tierParams[i].reserveRatioBP,
                _tierParams[i].forcedRebalanceThresholdBP,
                _tierParams[i].infraFeeBP,
                _tierParams[i].liquidityFeeBP,
                _tierParams[i].reservationFeeBP
            );

            Tier storage tier_ = $.tiers[_tierIds[i]];

            tier_.shareLimit = uint96(_tierParams[i].shareLimit);
            tier_.reserveRatioBP = uint16(_tierParams[i].reserveRatioBP);
            tier_.forcedRebalanceThresholdBP = uint16(_tierParams[i].forcedRebalanceThresholdBP);
            tier_.infraFeeBP = uint16(_tierParams[i].infraFeeBP);
            tier_.liquidityFeeBP = uint16(_tierParams[i].liquidityFeeBP);
            tier_.reservationFeeBP = uint16(_tierParams[i].reservationFeeBP);

            emit TierUpdated(
                _tierIds[i],
                tier_.shareLimit,
                tier_.reserveRatioBP,
                tier_.forcedRebalanceThresholdBP,
                tier_.infraFeeBP,
                tier_.liquidityFeeBP,
                tier_.reservationFeeBP
            );
        }
    }

    /*

    Legend:
    V = Vault1.liabilityShares
    LS = liabilityShares

    Scheme1 - transfer Vault from default tier to Tier2

                                         ┌──────────────────────────────┐
                                         │           Group 1            │
                                         │                              │
    ┌────────────────────┐               │  ┌─────────┐  ┌───────────┐  │
    │  Tier 1 (default)  │   confirm     │  │ Tier 2  │  │ Tier 3    │  │
    │  LS: -V            │    ─────>     │  │ LS:+V   │  │           │  │
    └────────────────────┘               │  └─────────┘  └───────────┘  │
                                         │                              │
                                         │   Group1.liabilityShares: +V │
                                         └──────────────────────────────┘

    After confirmation:
    - Tier 1.liabilityShares   = -V
    - Tier 2.liabilityShares   = +V
    - Group1.liabilityShares   = +V

    --------------------------------------------------------------------------
    Scheme2 - transfer Vault from Tier2 to Tier3, no need to change group minted shares

    ┌────────────────────────────────┐     ┌────────────────────────────────┐
    │           Group 1              │     │           Group 2              │
    │                                │     │                                │
    │  ┌───────────┐  ┌───────────┐  │     │  ┌───────────┐                 │
    │  │ Tier 2    │  │ Tier 3    │  │     │  │ Tier 4    │                 │
    │  │ LS:-V     │  │ LS:+V     │  │     │  │           │                 │
    │  └───────────┘  └───────────┘  │     │  └───────────┘                 │
    │  operator1                     │     │  operator2                     │
    └────────────────────────────────┘     └────────────────────────────────┘

    After confirmation:
    - Tier 2.liabilityShares   = -V
    - Tier 3.liabilityShares   = +V

    NB: Cannot change from Tier2 to Tier1, because Tier1 has no group
    NB: Cannot change from Tier2 to Tier4, because Tier4 has different operator.

    */
    /// @notice Vault tier change with multi-role confirmation
    /// @param _vault address of the vault
    /// @param _requestedTierId id of the tier
    /// @param _requestedShareLimit share limit to set
    /// @return bool Whether the tier change was executed.
    /// @dev Requires vault to be connected to VaultHub to finalize tier change.
    /// @dev Both vault owner (via Dashboard) and node operator confirmations are required.
    function changeTier(
        address _vault,
        uint256 _requestedTierId,
        uint256 _requestedShareLimit
    ) external returns (bool) {
        if (_vault == address(0)) revert ZeroArgument("_vault");

        ERC7201Storage storage $ = _getStorage();
        if (_requestedTierId >= $.tiers.length) revert TierNotExists();
        if (_requestedTierId == DEFAULT_TIER_ID) revert CannotChangeToDefaultTier();

        VaultHub vaultHub = _vaultHub();
        bool isVaultConnected = vaultHub.isVaultConnected(_vault);

        address vaultOwner = isVaultConnected
            ? vaultHub.vaultConnection(_vault).owner
            : IStakingVault(_vault).owner();

        address nodeOperator = IStakingVault(_vault).nodeOperator();

        uint256 vaultTierId = $.vaultTier[_vault];
        if (vaultTierId == _requestedTierId) revert TierAlreadySet();

        Tier storage requestedTier = $.tiers[_requestedTierId];
        if (nodeOperator != requestedTier.operator) revert TierNotInOperatorGroup();
        if (_requestedShareLimit > requestedTier.shareLimit) {
            revert RequestedShareLimitTooHigh(_requestedShareLimit, requestedTier.shareLimit);
        }

        // store the caller's confirmation; only proceed if the required number of confirmations is met.
        if (!_collectAndCheckConfirmations(msg.data, vaultOwner, nodeOperator)) return false;
        uint256 vaultLiabilityShares = vaultHub.liabilityShares(_vault);

        // check if tier limit is exceeded
        if (requestedTier.liabilityShares + vaultLiabilityShares > requestedTier.shareLimit) revert TierLimitExceeded();

        // if the vault was in the default tier:
        // - that mean that the vault has no group, so we decrease only the minted shares of the default tier
        // - but need to check requested group limit exceeded
        if (vaultTierId == DEFAULT_TIER_ID) {
            Group storage requestedGroup = $.groups[nodeOperator];
            if (requestedGroup.liabilityShares + vaultLiabilityShares > requestedGroup.shareLimit) {
                revert GroupLimitExceeded();
            }
            requestedGroup.liabilityShares += uint96(vaultLiabilityShares);
        }

        Tier storage currentTier = $.tiers[vaultTierId];

        currentTier.liabilityShares -= uint96(vaultLiabilityShares);
        requestedTier.liabilityShares += uint96(vaultLiabilityShares);

        $.vaultTier[_vault] = _requestedTierId;

        vaultHub.updateConnection(
            _vault,
            _requestedShareLimit,
            requestedTier.reserveRatioBP,
            requestedTier.forcedRebalanceThresholdBP,
            requestedTier.infraFeeBP,
            requestedTier.liquidityFeeBP,
            requestedTier.reservationFeeBP
        );

        emit TierChanged(_vault, _requestedTierId, _requestedShareLimit);

        return true;
    }

    /// @notice Syncs vault tier with current tier params
    /// @param _vault address of the vault
    /// @return bool Whether the sync was executed.
    /// @dev Requires vault to be connected to VaultHub.
    /// @dev Both vault owner (via Dashboard) and node operator confirmations are required.
    function syncTier(address _vault) external returns (bool) {
        (VaultHub vaultHub, VaultHub.VaultConnection memory vaultConnection,
        address vaultOwner, address nodeOperator, uint256 vaultTierId) = _getVaultContextForConnectedVault(_vault);

        Tier storage tier_ = _getStorage().tiers[vaultTierId];

        if (
            vaultConnection.reserveRatioBP == tier_.reserveRatioBP &&
            vaultConnection.forcedRebalanceThresholdBP == tier_.forcedRebalanceThresholdBP &&
            vaultConnection.infraFeeBP == tier_.infraFeeBP &&
            vaultConnection.liquidityFeeBP == tier_.liquidityFeeBP &&
            vaultConnection.reservationFeeBP == tier_.reservationFeeBP
        ) {
            revert VaultAlreadySyncedWithTier();
        }

        // store the caller's confirmation; only proceed if the required number of confirmations is met.
        if (!_collectAndCheckConfirmations(msg.data, vaultOwner, nodeOperator)) return false;

        vaultHub.updateConnection(
            _vault,
            vaultConnection.shareLimit,
            tier_.reserveRatioBP,
            tier_.forcedRebalanceThresholdBP,
            tier_.infraFeeBP,
            tier_.liquidityFeeBP,
            tier_.reservationFeeBP
        );

        return true;
    }

    /// @notice Update vault share limit
    /// @param _vault address of the vault
    /// @param _requestedShareLimit share limit to set
    /// @return bool Whether the update was executed.
    /// @dev Requires vault to be connected to VaultHub.
    /// @dev Both vault owner (via Dashboard) and node operator confirmations are required.
    function updateVaultShareLimit(address _vault, uint256 _requestedShareLimit) external returns (bool) {
        (VaultHub vaultHub, VaultHub.VaultConnection memory vaultConnection,
        address vaultOwner, address nodeOperator, uint256 vaultTierId) = _getVaultContextForConnectedVault(_vault);

        uint256 tierShareLimit = _getStorage().tiers[vaultTierId].shareLimit;

        if (_requestedShareLimit > tierShareLimit) revert RequestedShareLimitTooHigh(_requestedShareLimit, tierShareLimit);
        if (_requestedShareLimit == vaultConnection.shareLimit) revert ShareLimitAlreadySet();

        // store the caller's confirmation; only proceed if the required number of confirmations is met.
        if (!_collectAndCheckConfirmations(msg.data, vaultOwner, nodeOperator)) return false;

        vaultHub.updateConnection(
            _vault,
            _requestedShareLimit,
            vaultConnection.reserveRatioBP,
            vaultConnection.forcedRebalanceThresholdBP,
            vaultConnection.infraFeeBP,
            vaultConnection.liquidityFeeBP,
            vaultConnection.reservationFeeBP
        );

        return true;
    }

    /// @notice Reset vault's tier to default
    /// @param _vault address of the vault
    /// @dev Requires vault's liabilityShares to be zero before resetting the tier
    function resetVaultTier(address _vault) external {
        if (msg.sender != LIDO_LOCATOR.vaultHub()) revert NotAuthorized("resetVaultTier", msg.sender);

        ERC7201Storage storage $ = _getStorage();

        if ($.vaultTier[_vault] != DEFAULT_TIER_ID) {
            $.vaultTier[_vault] = DEFAULT_TIER_ID;

            emit TierChanged(_vault, DEFAULT_TIER_ID, $.tiers[DEFAULT_TIER_ID].shareLimit);
        }
    }

    /// @notice updates fees for the vault
    /// @param _vault vault address
    /// @param _infraFeeBP new infra fee in basis points
    /// @param _liquidityFeeBP new liquidity fee in basis points
    /// @param _reservationFeeBP new reservation fee in basis points
    function updateVaultFees(
        address _vault,
        uint256 _infraFeeBP,
        uint256 _liquidityFeeBP,
        uint256 _reservationFeeBP
    ) external onlyRole(REGISTRY_ROLE) {
        if (_vault == address(0)) revert ZeroArgument("_vault");

        _requireLessOrEqToBP(_infraFeeBP, MAX_FEE_BP);
        _requireLessOrEqToBP(_liquidityFeeBP, MAX_FEE_BP);
        _requireLessOrEqToBP(_reservationFeeBP, MAX_FEE_BP);

        VaultHub vaultHub = _vaultHub();
        if (!vaultHub.isVaultConnected(_vault)) revert VaultNotConnected();

        VaultHub.VaultConnection memory vaultConnection = vaultHub.vaultConnection(_vault);
        vaultHub.updateConnection(
            _vault,
            vaultConnection.shareLimit,
            vaultConnection.reserveRatioBP,
            vaultConnection.forcedRebalanceThresholdBP,
            _infraFeeBP,
            _liquidityFeeBP,
            _reservationFeeBP
        );
    }

   // -----------------------------
   //     MINT / BURN
   // -----------------------------

    /// @notice Mint shares limit check
    /// @param _vault address of the vault
    /// @param _amount amount of shares will be minted
    /// @param _overrideLimits true if group and tier limits should not be checked
    function onMintedShares(
        address _vault,
        uint256 _amount,
        bool _overrideLimits
    ) external {
        if (msg.sender != LIDO_LOCATOR.vaultHub()) revert NotAuthorized("onMintedShares", msg.sender);

        ERC7201Storage storage $ = _getStorage();

        if (!_overrideLimits && $.isVaultInJail[_vault]) revert VaultInJail();

        uint256 tierId = $.vaultTier[_vault];
        Tier storage tier_ = $.tiers[tierId];

        uint96 tierLiabilityShares = tier_.liabilityShares;
        if (!_overrideLimits && tierLiabilityShares + _amount > tier_.shareLimit) {
            revert TierLimitExceeded();
        }

        tier_.liabilityShares = tierLiabilityShares + uint96(_amount);

        if (tierId != DEFAULT_TIER_ID) {
            Group storage group_ = $.groups[tier_.operator];
            uint96 groupMintedShares = group_.liabilityShares;
            if (!_overrideLimits && groupMintedShares + _amount > group_.shareLimit) {
                revert GroupLimitExceeded();
            }

            group_.liabilityShares = groupMintedShares + uint96(_amount);
        }
    }

    /// @notice Burn shares limit check
    /// @param _vault address of the vault
    /// @param _amount amount of shares to burn
    function onBurnedShares(
        address _vault,
        uint256 _amount
    ) external {
        if (msg.sender != LIDO_LOCATOR.vaultHub()) revert NotAuthorized("burnShares", msg.sender);

        ERC7201Storage storage $ = _getStorage();

        uint256 tierId = $.vaultTier[_vault];

        Tier storage tier_ = $.tiers[tierId];

        // we skip the check for minted shared underflow, because it's done in the VaultHub.burnShares()

        tier_.liabilityShares -= uint96(_amount);

        if (tierId != DEFAULT_TIER_ID) {
            Group storage group_ = $.groups[tier_.operator];
            group_.liabilityShares -= uint96(_amount);
        }
    }

    /// @notice Updates if the vault is in jail
    /// @param _vault vault address
    /// @param _isInJail true if the vault is in jail, false otherwise
    function setVaultJailStatus(address _vault, bool _isInJail) external onlyRole(REGISTRY_ROLE) {
        if (_vault == address(0)) revert ZeroArgument("_vault");

        ERC7201Storage storage $ = _getStorage();
        if ($.isVaultInJail[_vault] == _isInJail) revert VaultInJailAlreadySet();
        $.isVaultInJail[_vault] = _isInJail;

        emit VaultJailStatusUpdated(_vault, _isInJail);
    }

    /// @notice Get vault's tier limits
    /// @param _vault address of the vault
    /// @return nodeOperator node operator of the vault
    /// @return tierId tier id of the vault
    /// @return shareLimit share limit of the vault
    /// @return reserveRatioBP reserve ratio of the vault
    /// @return forcedRebalanceThresholdBP forced rebalance threshold of the vault
    /// @return infraFeeBP infra fee of the vault
    /// @return liquidityFeeBP liquidity fee of the vault
    /// @return reservationFeeBP reservation fee of the vault
    function vaultTierInfo(address _vault)
        external
        view
        returns (
            address nodeOperator,
            uint256 tierId,
            uint256 shareLimit,
            uint256 reserveRatioBP,
            uint256 forcedRebalanceThresholdBP,
            uint256 infraFeeBP,
            uint256 liquidityFeeBP,
            uint256 reservationFeeBP
        )
    {
        ERC7201Storage storage $ = _getStorage();

        tierId = $.vaultTier[_vault];

        Tier memory t = $.tiers[tierId];
        nodeOperator = t.operator;

        shareLimit = t.shareLimit;
        reserveRatioBP = t.reserveRatioBP;
        forcedRebalanceThresholdBP = t.forcedRebalanceThresholdBP;
        infraFeeBP = t.infraFeeBP;
        liquidityFeeBP = t.liquidityFeeBP;
        reservationFeeBP = t.reservationFeeBP;
    }

    /// @notice Returns the effective share limit of a vault according to the OperatorGrid and vault share limits
    /// @param _vault address of the vault
    /// @return shareLimit effective share limit of the vault
    function effectiveShareLimit(address _vault) public view returns (uint256) {
        VaultHub vaultHub = _vaultHub();
        uint256 shareLimit = vaultHub.vaultConnection(_vault).shareLimit;
        uint256 liabilityShares = vaultHub.liabilityShares(_vault);

        uint256 gridShareLimit = _gridRemainingShareLimit(_vault) + liabilityShares;
        return Math256.min(gridShareLimit, shareLimit);
    }

    /// @notice Returns true if the vault is in jail
    /// @param _vault address of the vault
    /// @return true if the vault is in jail
    function isVaultInJail(address _vault) external view returns (bool) {
        return _getStorage().isVaultInJail[_vault];
    }

    /// @notice Returns the remaining share limit in a given tier and group
    /// @param _vault address of the vault
    /// @return remaining share limit
    /// @dev remaining share limit inherits the limits of the vault tier and group,
    ///      and accounts liabilities of other vaults belonging to the same tier and group
    function _gridRemainingShareLimit(address _vault) internal view returns (uint256) {
        ERC7201Storage storage $ = _getStorage();
        uint256 tierId = $.vaultTier[_vault];
        Tier storage t = $.tiers[tierId];

        uint256 tierLimit = t.shareLimit;
        uint256 tierRemaining = tierLimit > t.liabilityShares ? tierLimit - t.liabilityShares : 0;

        if (tierId == DEFAULT_TIER_ID) return tierRemaining;

        Group storage g = $.groups[t.operator];
        uint256 groupLimit = g.shareLimit;
        uint256 groupRemaining = groupLimit > g.liabilityShares ? groupLimit - g.liabilityShares : 0;
        return Math256.min(tierRemaining, groupRemaining);
    }

    /// @notice Validates tier parameters
    /// @param _reserveRatioBP Reserve ratio
    /// @param _forcedRebalanceThresholdBP Forced rebalance threshold
    /// @param _infraFeeBP Infra fee
    /// @param _liquidityFeeBP Liquidity fee
    /// @param _reservationFeeBP Reservation fee
    function _validateParams(
      uint256 _tierId,
      uint256 _reserveRatioBP,
      uint256 _forcedRebalanceThresholdBP,
      uint256 _infraFeeBP,
      uint256 _liquidityFeeBP,
      uint256 _reservationFeeBP
    ) internal pure {
        if (_reserveRatioBP == 0) revert ZeroArgument("_reserveRatioBP");
        if (_reserveRatioBP > MAX_RESERVE_RATIO_BP)
            revert ReserveRatioTooHigh(_tierId, _reserveRatioBP, MAX_RESERVE_RATIO_BP);

        if (_forcedRebalanceThresholdBP == 0) revert ZeroArgument("_forcedRebalanceThresholdBP");
        if (_forcedRebalanceThresholdBP > _reserveRatioBP)
            revert ForcedRebalanceThresholdTooHigh(_tierId, _forcedRebalanceThresholdBP, _reserveRatioBP);

        if (_infraFeeBP > MAX_FEE_BP)
            revert InfraFeeTooHigh(_tierId, _infraFeeBP, MAX_FEE_BP);

        if (_liquidityFeeBP > MAX_FEE_BP)
            revert LiquidityFeeTooHigh(_tierId, _liquidityFeeBP, MAX_FEE_BP);

        if (_reservationFeeBP > MAX_FEE_BP)
            revert ReservationFeeTooHigh(_tierId, _reservationFeeBP, MAX_FEE_BP);
    }

    function _vaultHub() internal view returns (VaultHub) {
        return VaultHub(payable(LIDO_LOCATOR.vaultHub()));
    }

    function _getStorage() private pure returns (ERC7201Storage storage $) {
        assembly {
            $.slot := OPERATOR_GRID_STORAGE_LOCATION
        }
    }

    function _getVaultContextForConnectedVault(address _vault) internal view returns (
        VaultHub vaultHub,
        VaultHub.VaultConnection memory vaultConnection,
        address vaultOwner,
        address nodeOperator,
        uint256 vaultTierId
    ) {
        if (_vault == address(0)) revert ZeroArgument("_vault");

        vaultHub = _vaultHub();
        if (!vaultHub.isVaultConnected(_vault)) revert VaultNotConnected();

        vaultConnection = vaultHub.vaultConnection(_vault);
        vaultOwner = vaultConnection.owner;
        nodeOperator = IStakingVault(_vault).nodeOperator();

        vaultTierId = _getStorage().vaultTier[_vault];
    }

    function _requireLessOrEqToBP(uint256 _valueBP, uint256 _maxValueBP) internal pure {
        if (_valueBP > _maxValueBP) revert InvalidBasisPoints(_valueBP, _maxValueBP);
    }

    // -----------------------------
    //            EVENTS
    // -----------------------------
    event GroupAdded(address indexed nodeOperator, uint256 shareLimit);
    event GroupShareLimitUpdated(address indexed nodeOperator, uint256 shareLimit);
    event TierAdded(
        address indexed nodeOperator,
        uint256 indexed tierId,
        uint256 shareLimit,
        uint256 reserveRatioBP,
        uint256 forcedRebalanceThresholdBP,
        uint256 infraFeeBP,
        uint256 liquidityFeeBP,
        uint256 reservationFeeBP
    );
    event TierChanged(address indexed vault, uint256 indexed tierId, uint256 shareLimit);
    event TierUpdated(
      uint256 indexed tierId,
      uint256 shareLimit,
      uint256 reserveRatioBP,
      uint256 forcedRebalanceThresholdBP,
      uint256 infraFeeBP,
      uint256 liquidityFeeBP,
      uint256 reservationFeeBP
    );
    event VaultJailStatusUpdated(address indexed vault, bool isInJail);

    // -----------------------------
    //            ERRORS
    // -----------------------------
    error NotAuthorized(string operation, address sender);
    error ZeroArgument(string argument);
    error GroupExists();
    error GroupNotExists();
    error GroupLimitExceeded();
    error NodeOperatorNotExists();
    error TierLimitExceeded();
    error VaultInJailAlreadySet();
    error VaultInJail();

    error TierNotExists();
    error TierAlreadySet();
    error TierNotInOperatorGroup();
    error CannotChangeToDefaultTier();

    error ReserveRatioTooHigh(uint256 tierId, uint256 reserveRatioBP, uint256 maxReserveRatioBP);
    error ForcedRebalanceThresholdTooHigh(uint256 tierId, uint256 forcedRebalanceThresholdBP, uint256 reserveRatioBP);
    error InfraFeeTooHigh(uint256 tierId, uint256 infraFeeBP, uint256 maxInfraFeeBP);
    error LiquidityFeeTooHigh(uint256 tierId, uint256 liquidityFeeBP, uint256 maxLiquidityFeeBP);
    error ReservationFeeTooHigh(uint256 tierId, uint256 reservationFeeBP, uint256 maxReservationFeeBP);
    error ArrayLengthMismatch();
    error RequestedShareLimitTooHigh(uint256 requestedShareLimit, uint256 tierShareLimit);
    error VaultNotConnected();
    error VaultAlreadySyncedWithTier();
    error ShareLimitAlreadySet();
    error InvalidBasisPoints(uint256 valueBP, uint256 maxValueBP);
}

File 36 of 50 : VaultHub.sol
// SPDX-FileCopyrightText: 2025 Lido <[email protected]>
// SPDX-License-Identifier: GPL-3.0

// See contracts/COMPILERS.md
pragma solidity 0.8.25;

import {SafeCast} from "@openzeppelin/contracts-v5.2/utils/math/SafeCast.sol";

import {Math256} from "contracts/common/lib/Math256.sol";
import {ILidoLocator} from "contracts/common/interfaces/ILidoLocator.sol";
import {ILido} from "contracts/common/interfaces/ILido.sol";
import {IHashConsensus} from "contracts/common/interfaces/IHashConsensus.sol";

import {IStakingVault} from "./interfaces/IStakingVault.sol";
import {IPredepositGuarantee} from "./interfaces/IPredepositGuarantee.sol";
import {IPinnedBeaconProxy} from "./interfaces/IPinnedBeaconProxy.sol";
import {IVaultFactory} from "./interfaces/IVaultFactory.sol";

import {OperatorGrid} from "./OperatorGrid.sol";
import {LazyOracle} from "./LazyOracle.sol";

import {PausableUntilWithRoles} from "../utils/PausableUntilWithRoles.sol";
import {RefSlotCache, DoubleRefSlotCache, DOUBLE_CACHE_LENGTH} from "./lib/RefSlotCache.sol";

/// @notice VaultHub is a contract that manages StakingVaults connected to the Lido protocol
/// It allows to connect and disconnect vaults, mint and burn stETH using vaults as collateral
/// Also, it facilitates the individual per-vault reports from the lazy oracle to the vaults and charges Lido fees
/// @author folkyatina
contract VaultHub is PausableUntilWithRoles {
    using RefSlotCache for RefSlotCache.Uint104WithCache;
    using DoubleRefSlotCache for DoubleRefSlotCache.Int104WithCache[DOUBLE_CACHE_LENGTH];

    // -----------------------------
    //           STORAGE STRUCTS
    // -----------------------------
    /// @custom:storage-location erc7201:Lido.Vaults.VaultHub
    struct Storage {
        /// @notice accounting records for each vault
        mapping(address vault => VaultRecord) records;
        /// @notice connection parameters for each vault
        mapping(address vault => VaultConnection) connections;
        /// @notice 1-based array of vaults connected to the hub. index 0 is reserved for not connected vaults
        address[] vaults;
        /// @notice amount of bad debt that was internalized from the vault to become the protocol loss
        RefSlotCache.Uint104WithCache badDebtToInternalize;
    }

    struct VaultConnection {
        // ### 1st slot
        /// @notice address of the vault owner
        address owner;
        /// @notice maximum number of stETH shares that can be minted by vault owner
        uint96 shareLimit;
        // ### 2nd slot
        /// @notice index of the vault in the list of vaults. Indexes are not guaranteed to be stable.
        /// @dev vaultIndex is always greater than 0
        uint96 vaultIndex;
        /// @notice timestamp of the block when disconnection was initiated
        /// equal 0 if vault is disconnected and max(uint48) - for connected ,
        uint48 disconnectInitiatedTs;
        /// @notice share of ether that is locked on the vault as an additional reserve
        /// e.g RR=30% means that for 1stETH minted 1/(1-0.3)=1.428571428571428571 ETH is locked on the vault
        uint16 reserveRatioBP;
        /// @notice if vault's reserve decreases to this threshold, it should be force rebalanced
        uint16 forcedRebalanceThresholdBP;
        /// @notice infra fee in basis points
        uint16 infraFeeBP;
        /// @notice liquidity fee in basis points
        uint16 liquidityFeeBP;
        /// @notice reservation fee in basis points
        uint16 reservationFeeBP;
        /// @notice if true, vault owner intends to pause the beacon chain deposits
        bool beaconChainDepositsPauseIntent;
        /// 24 bits gap
    }

    struct VaultRecord {
        // ### 1st slot
        /// @notice latest report for the vault
        Report report;
        // ### 2nd slot
        /// @notice max number of shares that was minted by the vault in current Oracle period
        /// (used to calculate the locked value on the vault)
        uint96 maxLiabilityShares;
        /// @notice liability shares of the vault
        uint96 liabilityShares;
        // ### 3rd and 4th slots
        /// @notice inOutDelta of the vault (all deposits - all withdrawals)
        DoubleRefSlotCache.Int104WithCache[DOUBLE_CACHE_LENGTH] inOutDelta;
        // ### 5th slot
        /// @notice the minimal value that the reserve part of the locked can be
        uint128 minimalReserve;
        /// @notice part of liability shares reserved to be burnt as Lido core redemptions
        uint128 redemptionShares;
        // ### 6th slot
        /// @notice cumulative value for Lido fees that accrued on the vault
        uint128 cumulativeLidoFees;
        /// @notice cumulative value for Lido fees that were settled on the vault
        uint128 settledLidoFees;
    }

    struct Report {
        /// @notice total value of the vault
        uint104 totalValue;
        /// @notice inOutDelta of the report
        int104 inOutDelta;
        /// @notice timestamp (in seconds)
        uint48 timestamp;
    }

    // -----------------------------
    //           CONSTANTS
    // -----------------------------
    // some constants are immutables to save bytecode

    // keccak256(abi.encode(uint256(keccak256("Lido.Vaults.VaultHub")) - 1)) & ~bytes32(uint256(0xff))
    bytes32 private constant STORAGE_LOCATION = 0x9eb73ffa4c77d08d5d1746cf5a5e50a47018b610ea5d728ea9bd9e399b76e200;

    /// @notice role that allows to disconnect vaults from the hub
    /// @dev 0x479bc4a51d27fbdc8e51b5b1ebd3dcd58bd229090980bff226f8930587e69ce3
    bytes32 public immutable VAULT_MASTER_ROLE = keccak256("vaults.VaultHub.VaultMasterRole");
    /// @notice role that allows to accrue Lido Core redemptions on the vault
    /// @dev 0x44f007e8cc2a08047a03d8d9c295057454942eb49ee3ced9c87e9b9406f21174
    bytes32 public immutable REDEMPTION_MASTER_ROLE = keccak256("vaults.VaultHub.RedemptionMasterRole");
    /// @notice role that allows to trigger validator exits under extreme conditions
    /// @dev 0x2159c5943234d9f3a7225b9a743ea06e4a0d0ba5ed82889e867759a8a9eb7883
    bytes32 public immutable VALIDATOR_EXIT_ROLE = keccak256("vaults.VaultHub.ValidatorExitRole");
    /// @notice role that allows to bail out vaults with bad debt
    /// @dev 0xa85bab4b576ca359fa6ae02ab8744b5c85c7e7ed4d7e0bca7b5b64580ac5d17d
    bytes32 public immutable BAD_DEBT_MASTER_ROLE = keccak256("vaults.VaultHub.BadDebtMasterRole");

    /// @notice amount of ETH that is locked on the vault on connect and can be withdrawn on disconnect only
    uint256 public constant CONNECT_DEPOSIT = 1 ether;
    /// @notice The time delta for report freshness check
    uint256 public constant REPORT_FRESHNESS_DELTA = 2 days;

    /// @dev basis points base
    uint256 internal constant TOTAL_BASIS_POINTS = 100_00;
    /// @dev special value for `disconnectTimestamp` storage means the vault is not marked for disconnect
    uint48 internal constant DISCONNECT_NOT_INITIATED = type(uint48).max;
    /// @notice minimum amount of ether that is required for the beacon chain deposit
    /// @dev used as a threshold for the beacon chain deposits pause
    uint256 internal constant MIN_BEACON_DEPOSIT = 1 ether;
    /// @dev amount of ether required to activate a validator after PDG
    uint256 internal constant PDG_ACTIVATION_DEPOSIT = 31 ether;

    // -----------------------------
    //           IMMUTABLES
    // -----------------------------

    /// @notice limit for a single vault share limit relative to Lido TVL in basis points
    uint256 public immutable MAX_RELATIVE_SHARE_LIMIT_BP;

    ILido public immutable LIDO;
    ILidoLocator public immutable LIDO_LOCATOR;
    /// @dev it's cached as immutable to save the gas, but it's add some rigidity to the contract structure
    /// and will require update of the VaultHub if HashConsensus changes
    IHashConsensus public immutable CONSENSUS_CONTRACT;

    /// @param _locator Lido Locator contract
    /// @param _lido Lido stETH contract
    /// @param _consensusContract Hash consensus contract
    /// @param _maxRelativeShareLimitBP Maximum share limit relative to TVL in basis points
    constructor(ILidoLocator _locator, ILido _lido, IHashConsensus _consensusContract, uint256 _maxRelativeShareLimitBP) {
        _requireNotZero(_maxRelativeShareLimitBP);
        if (_maxRelativeShareLimitBP > TOTAL_BASIS_POINTS) revert InvalidBasisPoints(_maxRelativeShareLimitBP, TOTAL_BASIS_POINTS);

        MAX_RELATIVE_SHARE_LIMIT_BP = _maxRelativeShareLimitBP;

        LIDO_LOCATOR = _locator;
        LIDO = _lido;
        CONSENSUS_CONTRACT = _consensusContract;

        _disableInitializers();
    }

    /// @dev used to perform rebalance operations
    receive() external payable {}

    /// @notice initialize the vault hub
    /// @param _admin default admin address
    function initialize(address _admin) external initializer {
        _requireNotZero(_admin);

        __AccessControlEnumerable_init();

        // the stone in the elevator. index 0 is reserved for not connected vaults
        _storage().vaults.push(address(0));

        _grantRole(DEFAULT_ADMIN_ROLE, _admin);
    }

    /// @notice returns the number of vaults connected to the hub
    /// @dev since index 0 is reserved for not connected vaults, it's always 1 less than the vaults array length
    function vaultsCount() external view returns (uint256) {
        return _storage().vaults.length - 1;
    }

    /// @notice returns the vault address by its index
    /// @param _index index of the vault in the 1-based list of vaults. possible range [1, vaultsCount()]
    /// @dev Indexes are guaranteed to be stable only in one transaction.
    function vaultByIndex(uint256 _index) external view returns (address) {
        _requireNotZero(_index);
        return _storage().vaults[_index];
    }

    /// @return connection parameters struct for the given vault
    /// @dev it returns empty struct if the vault is not connected to the hub
    /// @dev it may return connection even if it's pending to be disconnected
    function vaultConnection(address _vault) external view returns (VaultConnection memory) {
        return _vaultConnection(_vault);
    }

    /// @return the accounting record struct for the given vault
    /// @dev it returns empty struct if the vault is not connected to the hub
    function vaultRecord(address _vault) external view returns (VaultRecord memory) {
        return _vaultRecord(_vault);
    }

    /// @return true if the vault is connected to the hub or pending to be disconnected
    function isVaultConnected(address _vault) external view returns (bool) {
        return _vaultConnection(_vault).vaultIndex != 0;
    }

    /// @return true if vault is pending for disconnect, false if vault is connected or disconnected
    /// @dev disconnect can be performed by applying the report for the period when it was initiated
    function isPendingDisconnect(address _vault) external view returns (bool) {
        return _isPendingDisconnect(_vaultConnection(_vault));
    }

    /// @return total value of the vault
    /// @dev returns 0 if the vault is not connected
    function totalValue(address _vault) external view returns (uint256) {
        return _totalValue(_vaultRecord(_vault));
    }

    /// @return liability shares of the vault
    /// @dev returns 0 if the vault is not connected
    function liabilityShares(address _vault) external view returns (uint256) {
        return _vaultRecord(_vault).liabilityShares;
    }

    /// @return locked amount of ether for the vault
    /// @dev returns 0 if the vault is not connected
    function locked(address _vault) external view returns (uint256) {
        return _locked(_vaultConnection(_vault), _vaultRecord(_vault));
    }

    /// @return the amount of ether that can be locked in the vault given the current total value
    /// @dev returns 0 if the vault is not connected
    function maxLockableValue(address _vault) external view returns (uint256) {
        return _maxLockableValue(_vaultRecord(_vault));
    }

    /// @notice Calculates the total number of shares that is possible to mint on the vault
    /// @param _vault The address of the vault
    /// @param _deltaValue The delta value to apply to the total value of the vault (may be negative)
    /// @return the number of shares that can be minted
    /// @dev returns 0 if the vault is not connected
    function totalMintingCapacityShares(address _vault, int256 _deltaValue) external view returns (uint256) {
        return _totalMintingCapacityShares(_vault, _deltaValue);
    }

    /// @return the amount of ether that can be instantly withdrawn from the staking vault
    /// @dev returns 0 if the vault is not connected or disconnect pending
    function withdrawableValue(address _vault) external view returns (uint256) {
        VaultConnection storage connection = _vaultConnection(_vault);
        if (_isPendingDisconnect(connection)) return 0;

        return _withdrawableValue(_vault, connection, _vaultRecord(_vault));
    }

    /// @return latest report for the vault
    /// @dev returns empty struct if the vault is not connected
    function latestReport(address _vault) external view returns (Report memory) {
        return _vaultRecord(_vault).report;
    }

    /// @return true if the report for the vault is fresh, false otherwise
    /// @dev returns false if the vault is not connected
    function isReportFresh(address _vault) external view returns (bool) {
        return _isReportFresh(_vaultRecord(_vault));
    }

    /// @notice checks if the vault is healthy by comparing its total value after applying forced rebalance threshold
    ///         against current liability shares
    /// @param _vault vault address
    /// @return true if vault is healthy, false otherwise
    /// @dev returns true if the vault is not connected
    function isVaultHealthy(address _vault) external view returns (bool) {
        return _isVaultHealthy(_vaultConnection(_vault), _vaultRecord(_vault));
    }

    /// @notice calculate shares amount to make the vault healthy using rebalance
    /// @param _vault vault address
    /// @return shares amount or UINT256_MAX if it's impossible to make the vault healthy using rebalance
    /// @dev returns 0 if the vault is not connected
    function healthShortfallShares(address _vault) external view returns (uint256) {
        return _healthShortfallShares(_vaultConnection(_vault), _vaultRecord(_vault));
    }

    /// @notice calculate ether amount required to cover obligations shortfall of the vault
    /// @param _vault vault address
    /// @return ether amount or UINT256_MAX if it's impossible to cover obligations shortfall
    /// @dev returns 0 if the vault is not connected
    function obligationsShortfallValue(address _vault) external view returns (uint256) {
        VaultConnection storage connection = _vaultConnection(_vault);
        if (connection.vaultIndex == 0) return 0;

        return _obligationsShortfallValue(_vault, connection, _vaultRecord(_vault));
    }

    /// @notice returns the vault's current obligations toward the protocol
    ///
    /// Obligations are amounts the vault must cover, in the following priority:
    /// 1) Maintain healthiness - burn/rebalance liability shares until the health ratio is restored
    /// 2) Cover redemptions - burn/rebalance part of the liability shares marked as `redemptionShares`
    /// 3) Pay Lido fees - settle accrued but unsettled fees
    ///
    /// Effects:
    /// - Withdrawals from the vault are limited by the amount required to cover the obligations
    /// - Beacon chain deposits are auto-paused while the vault is unhealthy, has redemptions to cover, or has
    ///   unsettled fees ≥ `MIN_BEACON_DEPOSIT` (1 ETH)
    ///
    /// How to settle:
    /// - Anyone can:
    ///   - Rebalance shares permissionlessly when there are funds via `forceRebalance` (restores health / covers redemptions)
    ///   - Settle fees permissionlessly when there are funds via `settleLidoFees`
    /// - The owner (or a trusted role) can trigger validator exits / withdrawals to source ETH when needed
    ///
    /// @param _vault vault address
    /// @return sharesToBurn amount of shares to burn / rebalance
    /// @return feesToSettle amount of Lido fees to settle
    /// @dev if the vault has bad debt (i.e. not fixable by rebalance), returns `type(uint256).max` for `sharesToBurn`
    /// @dev returns (0, 0) if the vault is not connected
    function obligations(address _vault) external view returns (uint256 sharesToBurn, uint256 feesToSettle) {
        VaultConnection storage connection = _vaultConnection(_vault);
        VaultRecord storage record = _vaultRecord(_vault);

        return (
            _obligationsShares(connection, record),
            _unsettledLidoFeesValue(record)
        );
    }

    /// @return the amount of Lido fees that currently can be settled. Even if vault's balance is sufficient to cover
    ///         the fees, some amount may be blocked for redemptions, or locked ether
    /// @dev returns 0 if the vault is not connected
    function settleableLidoFeesValue(address _vault) external view returns (uint256) {
        VaultRecord storage record = _vaultRecord(_vault);
        return _settleableLidoFeesValue(_vault, _vaultConnection(_vault), record, _unsettledLidoFeesValue(record));
    }

    /// @notice amount of bad debt to be internalized to become the protocol loss
    /// @return the number of shares to internalize as bad debt during the oracle report
    /// @dev the value is lagging increases that was done after the current refSlot to the next one
    function badDebtToInternalize() external view returns (uint256) {
        return _storage().badDebtToInternalize.getValueForLastRefSlot(CONSENSUS_CONTRACT);
    }

    /// @notice connects a vault to the hub in permissionless way, get limits from the Operator Grid
    /// @param _vault vault address
    /// @dev vault should have transferred ownership to the VaultHub contract
    function connectVault(address _vault) external whenResumed {
        _requireNotZero(_vault);

        if (!IVaultFactory(LIDO_LOCATOR.vaultFactory()).deployedVaults(_vault)) revert VaultNotFactoryDeployed(_vault);
        IStakingVault vault_ = IStakingVault(_vault);
        _requireSender(vault_.owner());
        if (vault_.pendingOwner() != address(this)) revert VaultHubNotPendingOwner(_vault);
        if (IPinnedBeaconProxy(address(vault_)).isOssified()) revert VaultOssified(_vault);
        if (vault_.depositor() != address(_predepositGuarantee())) revert PDGNotDepositor(_vault);
        // we need vault to match staged balance with pendingActivations
        if (vault_.stagedBalance() != _predepositGuarantee().pendingActivations(vault_) * PDG_ACTIVATION_DEPOSIT) {
            revert InsufficientStagedBalance(_vault);
        }

        (
            , // nodeOperatorInTier
            , // tierId
            uint256 shareLimit,
            uint256 reserveRatioBP,
            uint256 forcedRebalanceThresholdBP,
            uint256 infraFeeBP,
            uint256 liquidityFeeBP,
            uint256 reservationFeeBP
        ) = _operatorGrid().vaultTierInfo(_vault);

        _connectVault(_vault,
            shareLimit,
            reserveRatioBP,
            forcedRebalanceThresholdBP,
            infraFeeBP,
            liquidityFeeBP,
            reservationFeeBP
        );

        IStakingVault(_vault).acceptOwnership();

        emit VaultConnected({
            vault: _vault,
            shareLimit: shareLimit,
            reserveRatioBP: reserveRatioBP,
            forcedRebalanceThresholdBP: forcedRebalanceThresholdBP,
            infraFeeBP: infraFeeBP,
            liquidityFeeBP: liquidityFeeBP,
            reservationFeeBP: reservationFeeBP
        });
    }

    /// @notice updates a redemption shares on the vault
    /// @param _vault The address of the vault
    /// @param _liabilitySharesTarget maximum amount of liabilityShares that will be preserved, the rest will be
    ///         marked as redemptionShares. If value is greater than liabilityShares, redemptionShares are set to 0
    /// @dev NB: Mechanism to be triggered when Lido Core TVL <= stVaults TVL
    function setLiabilitySharesTarget(address _vault, uint256 _liabilitySharesTarget) external onlyRole(REDEMPTION_MASTER_ROLE) {
        VaultConnection storage connection = _checkConnection(_vault);
        VaultRecord storage record = _vaultRecord(_vault);

        uint256 liabilityShares_ = record.liabilityShares;
        uint256 redemptionShares = liabilityShares_ > _liabilitySharesTarget ? liabilityShares_ - _liabilitySharesTarget : 0;
        record.redemptionShares = uint128(redemptionShares);

        _updateBeaconChainDepositsPause(_vault, record, connection);

        emit VaultRedemptionSharesUpdated(_vault, record.redemptionShares);
    }

    /// @notice updates the vault's connection parameters
    /// @dev Reverts if the vault is not healthy as of latest report
    /// @param _vault vault address
    /// @param _shareLimit new share limit
    /// @param _reserveRatioBP new reserve ratio
    /// @param _forcedRebalanceThresholdBP new forced rebalance threshold
    /// @param _infraFeeBP new infra fee
    /// @param _liquidityFeeBP new liquidity fee
    /// @param _reservationFeeBP new reservation fee
    /// @dev requires the fresh report
    function updateConnection(
        address _vault,
        uint256 _shareLimit,
        uint256 _reserveRatioBP,
        uint256 _forcedRebalanceThresholdBP,
        uint256 _infraFeeBP,
        uint256 _liquidityFeeBP,
        uint256 _reservationFeeBP
    ) external {
        _requireSender(address(_operatorGrid()));
        _requireSaneShareLimit(_shareLimit);

        VaultConnection storage connection = _checkConnection(_vault);
        VaultRecord storage record = _vaultRecord(_vault);

        _requireFreshReport(_vault, record);

        uint256 totalValue_ = _totalValue(record);
        uint256 liabilityShares_ = record.liabilityShares;

        if (_isThresholdBreached(totalValue_, liabilityShares_, _reserveRatioBP)) {
            revert VaultMintingCapacityExceeded(_vault, totalValue_, liabilityShares_, _reserveRatioBP);
        }

        // special event for the Oracle to track fee calculation
        emit VaultFeesUpdated({
            vault: _vault,
            preInfraFeeBP: connection.infraFeeBP,
            preLiquidityFeeBP: connection.liquidityFeeBP,
            preReservationFeeBP: connection.reservationFeeBP,
            infraFeeBP: _infraFeeBP,
            liquidityFeeBP: _liquidityFeeBP,
            reservationFeeBP: _reservationFeeBP
        });

        connection.shareLimit = uint96(_shareLimit);
        connection.reserveRatioBP = uint16(_reserveRatioBP);
        connection.forcedRebalanceThresholdBP = uint16(_forcedRebalanceThresholdBP);
        connection.infraFeeBP = uint16(_infraFeeBP);
        connection.liquidityFeeBP = uint16(_liquidityFeeBP);
        connection.reservationFeeBP = uint16(_reservationFeeBP);

        emit VaultConnectionUpdated({
            vault: _vault,
            nodeOperator: _nodeOperator(_vault),
            shareLimit: _shareLimit,
            reserveRatioBP: _reserveRatioBP,
            forcedRebalanceThresholdBP: _forcedRebalanceThresholdBP
        });
    }

    /// @notice disconnect a vault from the hub
    /// @param _vault vault address
    /// @dev msg.sender must have VAULT_MASTER_ROLE
    /// @dev vault's `liabilityShares` should be zero
    /// @dev requires the fresh report (see _initiateDisconnection)
    function disconnect(address _vault) external onlyRole(VAULT_MASTER_ROLE) {
        _initiateDisconnection(_vault, _checkConnection(_vault), _vaultRecord(_vault), false);

        emit VaultDisconnectInitiated(_vault);
    }

    /// @notice update of the vault data by the lazy oracle report
    /// @param _vault the address of the vault
    /// @param _reportTimestamp the timestamp of the report (last 32 bits of it)
    /// @param _reportTotalValue the total value of the vault
    /// @param _reportInOutDelta the inOutDelta of the vault
    /// @param _reportCumulativeLidoFees the cumulative Lido fees of the vault
    /// @param _reportLiabilityShares the liabilityShares of the vault on refSlot
    /// @param _reportMaxLiabilityShares the maxLiabilityShares of the vault on refSlot
    /// @param _reportSlashingReserve the slashingReserve of the vault
    /// @dev NB: LazyOracle sanity checks already verify that the fee can only increase
    function applyVaultReport(
        address _vault,
        uint256 _reportTimestamp,
        uint256 _reportTotalValue,
        int256 _reportInOutDelta,
        uint256 _reportCumulativeLidoFees,
        uint256 _reportLiabilityShares,
        uint256 _reportMaxLiabilityShares,
        uint256 _reportSlashingReserve
    ) external whenResumed {
        _requireSender(address(_lazyOracle()));

        VaultConnection storage connection = _vaultConnection(_vault);
        _requireConnected(connection, _vault);

        VaultRecord storage record = _vaultRecord(_vault);

        if (connection.disconnectInitiatedTs <= _reportTimestamp) {
            if (_reportSlashingReserve == 0 && record.liabilityShares == 0) {
                // liabilityShares can increase if badDebt was socialized to this vault
                IStakingVault(_vault).transferOwnership(connection.owner);
                _deleteVault(_vault, connection);

                emit VaultDisconnectCompleted(_vault);
                return;
            } else {
                // we abort the disconnect process as there is a slashing conflict yet to be resolved
                connection.disconnectInitiatedTs = DISCONNECT_NOT_INITIATED;
                emit VaultDisconnectAborted(_vault, _reportSlashingReserve);
            }
        }

        _applyVaultReport(
            record,
            _reportTimestamp,
            _reportTotalValue,
            _reportInOutDelta,
            _reportCumulativeLidoFees,
            _reportLiabilityShares,
            _reportMaxLiabilityShares,
            _reportSlashingReserve
        );

        emit VaultReportApplied({
            vault: _vault,
            reportTimestamp: _reportTimestamp,
            reportTotalValue: _reportTotalValue,
            reportInOutDelta: _reportInOutDelta,
            reportCumulativeLidoFees: _reportCumulativeLidoFees,
            reportLiabilityShares: _reportLiabilityShares,
            reportMaxLiabilityShares: _reportMaxLiabilityShares,
            reportSlashingReserve: _reportSlashingReserve
        });

        _updateBeaconChainDepositsPause(_vault, record, connection);
    }

    /// @notice Transfer the bad debt from the donor vault to the acceptor vault
    /// @param _badDebtVault address of the vault that has the bad debt
    /// @param _vaultAcceptor address of the vault that will accept the bad debt
    /// @param _maxSharesToSocialize maximum amount of shares to socialize
    /// @return number of shares that was socialized
    ///         (it's limited by acceptor vault capacity and bad debt actual size)
    /// @dev msg.sender must have BAD_DEBT_MASTER_ROLE
    /// @dev requires the fresh report for both bad debt and acceptor vaults
    function socializeBadDebt(
        address _badDebtVault,
        address _vaultAcceptor,
        uint256 _maxSharesToSocialize
    ) external onlyRole(BAD_DEBT_MASTER_ROLE) returns (uint256) {
        _requireNotZero(_badDebtVault);
        _requireNotZero(_vaultAcceptor);
        _requireNotZero(_maxSharesToSocialize);
        if (_nodeOperator(_vaultAcceptor) != _nodeOperator(_badDebtVault)) {
            revert BadDebtSocializationNotAllowed();
        }

        VaultConnection storage badDebtConnection = _vaultConnection(_badDebtVault);
        VaultRecord storage badDebtRecord = _vaultRecord(_badDebtVault);
        VaultConnection storage acceptorConnection = _vaultConnection(_vaultAcceptor);
        VaultRecord storage acceptorRecord = _vaultRecord(_vaultAcceptor);

        _requireConnected(badDebtConnection, _badDebtVault);
        _requireConnected(acceptorConnection, _vaultAcceptor);
        _requireFreshReport(_badDebtVault, badDebtRecord);
        _requireFreshReport(_vaultAcceptor, acceptorRecord);

        uint256 badDebtShares = _badDebtShares(badDebtRecord);
        uint256 badDebtToSocialize = Math256.min(badDebtShares, _maxSharesToSocialize);

        uint256 acceptorTotalValueShares = _getSharesByPooledEth(_totalValue(acceptorRecord));
        uint256 acceptorLiabilityShares = acceptorRecord.liabilityShares;

        // it's possible to socialize up to bad debt:
        uint256 acceptorCapacity = acceptorTotalValueShares < acceptorLiabilityShares ? 0
            : acceptorTotalValueShares - acceptorLiabilityShares;

        uint256 badDebtSharesToAccept = Math256.min(badDebtToSocialize, acceptorCapacity);

        if (badDebtSharesToAccept > 0) {
            _decreaseLiability(_badDebtVault, badDebtRecord, badDebtSharesToAccept);
            _increaseLiability({
                _vault: _vaultAcceptor,
                _record: acceptorRecord,
                _amountOfShares: badDebtSharesToAccept,
                _reserveRatioBP: acceptorConnection.reserveRatioBP,
                // don't check any limits
                _lockableValueLimit: type(uint256).max,
                _shareLimit: type(uint256).max,
                _overrideOperatorLimits: true
            });

            _updateBeaconChainDepositsPause(_vaultAcceptor, acceptorRecord, acceptorConnection);

            emit BadDebtSocialized(_badDebtVault, _vaultAcceptor, badDebtSharesToAccept);
        }

        return badDebtSharesToAccept;
    }

    /// @notice Internalize the bad debt to the protocol
    /// @param _badDebtVault address of the vault that has the bad debt
    /// @param _maxSharesToInternalize maximum amount of shares to internalize
    /// @return number of shares that was internalized (limited by actual size of the bad debt)
    /// @dev msg.sender must have BAD_DEBT_MASTER_ROLE
    /// @dev requires the fresh report
    function internalizeBadDebt(
        address _badDebtVault,
        uint256 _maxSharesToInternalize
    ) external onlyRole(BAD_DEBT_MASTER_ROLE) returns (uint256) {
        _requireNotZero(_badDebtVault);
        _requireNotZero(_maxSharesToInternalize);

        VaultConnection storage badDebtConnection = _vaultConnection(_badDebtVault);
        VaultRecord storage badDebtRecord = _vaultRecord(_badDebtVault);
        _requireConnected(badDebtConnection, _badDebtVault);
        _requireFreshReport(_badDebtVault, badDebtRecord);

        uint256 badDebtShares = _badDebtShares(badDebtRecord);
        uint256 badDebtToInternalize_ = Math256.min(badDebtShares, _maxSharesToInternalize);

        if (badDebtToInternalize_ > 0) {
            _decreaseLiability(_badDebtVault, badDebtRecord, badDebtToInternalize_);

            // store internalization in a separate counter that will be settled
            // by the Accounting Oracle during the report
            _storage().badDebtToInternalize = _storage().badDebtToInternalize.withValueIncrease({
                _consensus: CONSENSUS_CONTRACT,
                _increment: SafeCast.toUint104(badDebtToInternalize_)
            });

            emit BadDebtWrittenOffToBeInternalized(_badDebtVault, badDebtToInternalize_);
        }

        return badDebtToInternalize_;
    }

    /// @notice Reset the internalized bad debt to zero
    /// @dev msg.sender must be the accounting contract
    function decreaseInternalizedBadDebt(uint256 _amountOfShares) external {
        _requireSender(LIDO_LOCATOR.accounting());

        // don't cache previous value, we don't need it for sure
        _storage().badDebtToInternalize.value -= uint104(_amountOfShares);
    }

    /// @notice transfer the ownership of the vault to a new owner without disconnecting it from the hub
    /// @param _vault vault address
    /// @param _newOwner new owner address
    /// @dev msg.sender should be vault's owner
    function transferVaultOwnership(address _vault, address _newOwner) external {
        _requireNotZero(_newOwner);
        VaultConnection storage connection = _checkConnection(_vault);
        address oldOwner = connection.owner;

        _requireSender(oldOwner);

        connection.owner = _newOwner;

        emit VaultOwnershipTransferred({
            vault: _vault,
            newOwner: _newOwner,
            oldOwner: oldOwner
        });
    }

    /// @notice disconnects a vault from the hub
    /// @param _vault vault address
    /// @dev msg.sender should be vault's owner
    /// @dev vault's `liabilityShares` should be zero
    /// @dev requires the fresh report (see _initiateDisconnection)
    function voluntaryDisconnect(address _vault) external whenResumed {
        VaultConnection storage connection = _checkConnectionAndOwner(_vault);

        _initiateDisconnection(_vault, connection, _vaultRecord(_vault), true);

        emit VaultDisconnectInitiated(_vault);
    }

    /// @notice funds the vault passing ether as msg.value
    /// @param _vault vault address
    /// @dev msg.sender should be vault's owner
    function fund(address _vault) external payable whenResumed {
        _requireNotZero(_vault);
        VaultConnection storage connection = _vaultConnection(_vault);
        _requireConnected(connection, _vault);
        _requireSender(connection.owner);

        _updateInOutDelta(_vault, _vaultRecord(_vault), int104(int256(msg.value)));

        IStakingVault(_vault).fund{value: msg.value}();
    }

    /// @notice withdraws ether from the vault to the recipient address
    /// @param _vault vault address
    /// @param _recipient recipient address
    /// @param _ether amount of ether to withdraw
    /// @dev msg.sender should be vault's owner
    /// @dev requires the fresh report
    function withdraw(address _vault, address _recipient, uint256 _ether) external whenResumed {
        VaultConnection storage connection = _checkConnectionAndOwner(_vault);
        VaultRecord storage record = _vaultRecord(_vault);
        _requireFreshReport(_vault, record);

        uint256 withdrawable = _withdrawableValue(_vault, connection, record);
        if (_ether > withdrawable) {
            revert AmountExceedsWithdrawableValue(_vault, withdrawable, _ether);
        }

        _withdraw(_vault, record, _recipient, _ether);
    }

    /// @notice Rebalances StakingVault by withdrawing ether to VaultHub
    /// @param _vault vault address
    /// @param _shares amount of shares to rebalance
    /// @dev msg.sender should be vault's owner
    /// @dev requires the fresh report
    function rebalance(address _vault, uint256 _shares) external whenResumed {
        _requireNotZero(_shares);
        _checkConnectionAndOwner(_vault);

        VaultRecord storage record = _vaultRecord(_vault);
        _requireFreshReport(_vault, record);

        _rebalance(_vault, record, _shares);
    }

    /// @notice mint StETH shares backed by vault external balance to the receiver address
    /// @param _vault vault address
    /// @param _recipient address of the receiver
    /// @param _amountOfShares amount of stETH shares to mint
    /// @dev requires the fresh report
    function mintShares(address _vault, address _recipient, uint256 _amountOfShares) external whenResumed {
        _requireNotZero(_recipient);
        _requireNotZero(_amountOfShares);

        VaultConnection storage connection = _checkConnectionAndOwner(_vault);
        VaultRecord storage record = _vaultRecord(_vault);

        _requireFreshReport(_vault, record);

        _increaseLiability({
            _vault: _vault,
            _record: record,
            _amountOfShares: _amountOfShares,
            _reserveRatioBP: connection.reserveRatioBP,
            _lockableValueLimit: _maxLockableValue(record),
            _shareLimit: connection.shareLimit,
            _overrideOperatorLimits: false
        });

        LIDO.mintExternalShares(_recipient, _amountOfShares);

        emit MintedSharesOnVault(_vault, _amountOfShares, _locked(connection, record));
    }

    /// @notice burn steth shares from the balance of the VaultHub contract
    /// @param _vault vault address
    /// @param _amountOfShares amount of shares to burn
    /// @dev msg.sender should be vault's owner
    /// @dev this function is designed to be used by the smart contract, for EOA see `transferAndBurnShares`
    function burnShares(address _vault, uint256 _amountOfShares) public whenResumed {
        _requireNotZero(_amountOfShares);
        _checkConnectionAndOwner(_vault);

        VaultRecord storage record = _vaultRecord(_vault);

        _decreaseLiability(_vault, record, _amountOfShares);

        LIDO.burnExternalShares(_amountOfShares);

        _updateBeaconChainDepositsPause(_vault, record, _vaultConnection(_vault));

        emit BurnedSharesOnVault(_vault, _amountOfShares);
    }

    /// @notice separate burn function for EOA vault owners; requires vaultHub to be approved to transfer stETH
    /// @param _vault vault address
    /// @param _amountOfShares amount of shares to transfer and burn
    /// @dev msg.sender should be vault's owner
    function transferAndBurnShares(address _vault, uint256 _amountOfShares) external {
        LIDO.transferSharesFrom(msg.sender, address(this), _amountOfShares);

        burnShares(_vault, _amountOfShares);
    }

    /// @notice pauses beacon chain deposits for the vault
    /// @param _vault vault address
    /// @dev msg.sender should be vault's owner
    function pauseBeaconChainDeposits(address _vault) external {
        VaultConnection storage connection = _checkConnectionAndOwner(_vault);
        if (connection.beaconChainDepositsPauseIntent) revert PauseIntentAlreadySet();

        connection.beaconChainDepositsPauseIntent = true;
        emit BeaconChainDepositsPauseIntentSet(_vault, true);

        _pauseBeaconChainDepositsIfNotAlready(IStakingVault(_vault));
    }

    /// @notice resumes beacon chain deposits for the vault
    /// @param _vault vault address
    /// @dev msg.sender should be vault's owner
    /// @dev requires the fresh report
    /// @dev NB: if the vault has outstanding obligations, this call will clear the manual pause flag but deposits will
    ///         remain paused until the obligations are covered. Once covered, deposits will resume automatically
    function resumeBeaconChainDeposits(address _vault) external {
        VaultConnection storage connection = _checkConnectionAndOwner(_vault);
        if (!connection.beaconChainDepositsPauseIntent) revert PauseIntentAlreadyUnset();

        VaultRecord storage record = _vaultRecord(_vault);
        _requireFreshReport(_vault, record);

        connection.beaconChainDepositsPauseIntent = false;
        emit BeaconChainDepositsPauseIntentSet(_vault, false);

        _updateBeaconChainDepositsPause(_vault, record, connection);
    }

    /// @notice Emits a request event for the node operator to perform validator exit
    /// @param _vault vault address
    /// @param _pubkeys array of public keys of the validators to exit
    /// @dev msg.sender should be vault's owner
    function requestValidatorExit(address _vault, bytes calldata _pubkeys) external {
        _checkConnectionAndOwner(_vault);

        IStakingVault(_vault).requestValidatorExit(_pubkeys);
    }

    /// @notice Triggers validator withdrawals for the vault using EIP-7002
    /// @param _vault vault address
    /// @param _pubkeys array of public keys of the validators to withdraw from
    /// @param _amountsInGwei array of amounts to withdraw from each validator (0 for full withdrawal)
    /// @param _refundRecipient address that will receive the refund for transaction costs
    /// @dev msg.sender should be vault's owner
    /// @dev requires the fresh report (in case of partial withdrawals)
    /// @dev A withdrawal fee must be paid via msg.value.
    ///      `StakingVault.calculateValidatorWithdrawalFee()` can be used to calculate the approximate fee amount but
    ///      it's accurate only for the current block. The fee may change when the tx is included, so it's recommended
    ///      to send some surplus. The exact amount required will be paid and the excess will be refunded to the
    ///      `_refundRecipient` address. The fee required can grow exponentially, so limit msg.value wisely to avoid
    ///      overspending.
    function triggerValidatorWithdrawals(
        address _vault,
        bytes calldata _pubkeys,
        uint64[] calldata _amountsInGwei,
        address _refundRecipient
    ) external payable {
        VaultConnection storage connection = _checkConnectionAndOwner(_vault);
        VaultRecord storage record = _vaultRecord(_vault);

        uint256 minPartialAmountInGwei = type(uint256).max;
        for (uint256 i = 0; i < _amountsInGwei.length; i++) {
            if (_amountsInGwei[i] > 0 && _amountsInGwei[i] < minPartialAmountInGwei) {
                minPartialAmountInGwei = _amountsInGwei[i];
            }
        }

        if (minPartialAmountInGwei < type(uint256).max) {
            _requireFreshReport(_vault, record);

            /// @dev NB: Disallow partial withdrawals when the vault has obligations shortfall in order to prevent the
            ///      vault owner from clogging the consensus layer withdrawal queue by front-running and delaying the
            ///      forceful validator exits required for rebalancing the vault. Partial withdrawals only allowed if
            ///      the requested amount of withdrawals is enough to cover the uncovered obligations.
            uint256 obligationsShortfallAmount = _obligationsShortfallValue(_vault, connection, record);
            if (obligationsShortfallAmount > 0 && minPartialAmountInGwei * 1e9 < obligationsShortfallAmount) {
                revert PartialValidatorWithdrawalNotAllowed();
            }
        }

        _triggerVaultValidatorWithdrawals(_vault, msg.value, _pubkeys, _amountsInGwei, _refundRecipient);
    }

    /// @notice Triggers validator full withdrawals for the vault using EIP-7002 if the vault has obligations shortfall
    /// @param _vault address of the vault to exit validators from
    /// @param _pubkeys array of public keys of the validators to exit
    /// @param _refundRecipient address that will receive the refund for transaction costs
    /// @dev In case the vault has obligations shortfall, trusted actor with the role can force its validators to
    ///      exit the beacon chain. This returns the vault's deposited ETH back to vault's balance and allows to
    ///      rebalance the vault
    /// @dev requires the fresh report
    /// @dev A withdrawal fee must be paid via msg.value.
    ///      `StakingVault.calculateValidatorWithdrawalFee()` can be used to calculate the approximate fee amount but
    ///      it's accurate only for the current block. The fee may change when the tx is included, so it's recommended
    ///      to send some surplus. The exact amount required will be paid and the excess will be refunded to the
    ///      `_refundRecipient` address. The fee required can grow exponentially, so limit msg.value wisely to avoid
    ///      overspending.
    function forceValidatorExit(
        address _vault,
        bytes calldata _pubkeys,
        address _refundRecipient
    ) external payable onlyRole(VALIDATOR_EXIT_ROLE) {
        VaultConnection storage connection = _checkConnection(_vault);
        VaultRecord storage record = _vaultRecord(_vault);
        _requireFreshReport(_vault, record);

        uint256 obligationsShortfallAmount = _obligationsShortfallValue(_vault, connection, record);
        if (obligationsShortfallAmount == 0) revert ForcedValidatorExitNotAllowed();

        uint64[] memory amountsInGwei = new uint64[](0);
        _triggerVaultValidatorWithdrawals(_vault, msg.value, _pubkeys, amountsInGwei, _refundRecipient);

        emit ForcedValidatorExitTriggered(_vault, _pubkeys, _refundRecipient);
    }

    /// @notice allows anyone to rebalance a vault with an obligations shortfall
    /// @param _vault vault address
    /// @dev uses all available ether in the vault to cover outstanding obligations and restore vault health; this
    ///      operation does not settle Lido fees
    /// @dev requires the fresh report
    function forceRebalance(address _vault) external {
        VaultConnection storage connection = _checkConnection(_vault);
        VaultRecord storage record = _vaultRecord(_vault);
        _requireFreshReport(_vault, record);

        uint256 availableBalance = Math256.min(_availableBalance(_vault), _totalValue(record));
        if (availableBalance == 0) revert NoFundsForForceRebalance(_vault);

        uint256 sharesToForceRebalance = Math256.min(
            _obligationsShares(connection, record),
            _getSharesByPooledEth(availableBalance)
        );

        if (sharesToForceRebalance == 0) revert NoReasonForForceRebalance(_vault);

        _rebalance(_vault, record, sharesToForceRebalance);
    }

    /// @notice allows anyone to settle any outstanding Lido fees for a vault, sending them to the treasury
    /// @param _vault vault address
    /// @dev requires the fresh report
    function settleLidoFees(address _vault) external {
        VaultConnection storage connection = _checkConnection(_vault);
        VaultRecord storage record = _vaultRecord(_vault);
        _requireFreshReport(_vault, record);

        uint256 unsettledLidoFees = _unsettledLidoFeesValue(record);
        if (unsettledLidoFees == 0) revert NoUnsettledLidoFeesToSettle(_vault);

        uint256 valueToSettle = _settleableLidoFeesValue(_vault, connection, record, unsettledLidoFees);
        if (valueToSettle == 0) revert NoFundsToSettleLidoFees(_vault, unsettledLidoFees);

        _settleLidoFees(_vault, record, connection, valueToSettle);
    }

    /// @notice Proves that validators unknown to PDG have correct WC to participate in the vault
    /// @param _vault vault address
    /// @param _witness ValidatorWitness struct proving validator WC belonging to staking vault
    function proveUnknownValidatorToPDG(
        address _vault,
        IPredepositGuarantee.ValidatorWitness calldata _witness
    ) external {
        _checkConnectionAndOwner(_vault);

        _predepositGuarantee().proveUnknownValidator(_witness, IStakingVault(_vault));
    }

    /// @notice collects ERC20 tokens from vault
    /// @param _vault vault address
    /// @param _token address of the ERC20 token to collect
    /// @param _recipient address to send collected tokens to
    /// @param _amount amount of tokens to collect
    /// @dev will revert with ZeroArgument() if _token, _recipient or _amount is zero
    /// @dev will revert with EthCollectionNotAllowed() if _token is ETH (via EIP-7528 address)
    function collectERC20FromVault(
        address _vault,
        address _token,
        address _recipient,
        uint256 _amount
    ) external {
         _checkConnectionAndOwner(_vault);
         IStakingVault(_vault).collectERC20(_token, _recipient, _amount);
    }

    function _connectVault(
        address _vault,
        uint256 _shareLimit,
        uint256 _reserveRatioBP,
        uint256 _forcedRebalanceThresholdBP,
        uint256 _infraFeeBP,
        uint256 _liquidityFeeBP,
        uint256 _reservationFeeBP
    ) internal {
        _requireSaneShareLimit(_shareLimit);

        VaultConnection memory connection = _vaultConnection(_vault);
        if (connection.vaultIndex != 0) revert AlreadyConnected(_vault, connection.vaultIndex);

        uint256 vaultBalance = _availableBalance(_vault);
        if (vaultBalance < CONNECT_DEPOSIT) revert VaultInsufficientBalance(_vault, vaultBalance, CONNECT_DEPOSIT);

        IStakingVault vault = IStakingVault(_vault);

        // Connecting a new vault with totalValue == balance
        VaultRecord memory record = VaultRecord({
            report: Report({
                totalValue: uint104(vaultBalance),
                inOutDelta: int104(int256(vaultBalance)),
                timestamp: uint48(block.timestamp)
            }),
            maxLiabilityShares: 0,
            liabilityShares: 0,
            inOutDelta: DoubleRefSlotCache.initializeInt104DoubleCache(int104(int256(vaultBalance))),
            minimalReserve: uint128(CONNECT_DEPOSIT),
            redemptionShares: 0,
            cumulativeLidoFees: 0,
            settledLidoFees: 0
        });

        connection = VaultConnection({
            owner: vault.owner(),
            shareLimit: uint96(_shareLimit),
            vaultIndex: uint96(_storage().vaults.length),
            disconnectInitiatedTs: DISCONNECT_NOT_INITIATED,
            reserveRatioBP: uint16(_reserveRatioBP),
            forcedRebalanceThresholdBP: uint16(_forcedRebalanceThresholdBP),
            infraFeeBP: uint16(_infraFeeBP),
            liquidityFeeBP: uint16(_liquidityFeeBP),
            reservationFeeBP: uint16(_reservationFeeBP),
            beaconChainDepositsPauseIntent: vault.beaconChainDepositsPaused()
        });

        _addVault(_vault, connection, record);
    }

    function _initiateDisconnection(
        address _vault,
        VaultConnection storage _connection,
        VaultRecord storage _record,
        bool _forceFullFeesSettlement
    ) internal {
        _requireFreshReport(_vault, _record);

        uint256 liabilityShares_ = _record.liabilityShares;
        if (liabilityShares_ > 0) revert NoLiabilitySharesShouldBeLeft(_vault, liabilityShares_);

        uint256 unsettledLidoFees = _unsettledLidoFeesValue(_record);
        if (unsettledLidoFees > 0) {
            uint256 balance = Math256.min(_availableBalance(_vault), _totalValue(_record));
            if (_forceFullFeesSettlement) {
                if (balance < unsettledLidoFees) revert NoUnsettledLidoFeesShouldBeLeft(_vault, unsettledLidoFees);

                _settleLidoFees(_vault, _record, _connection, unsettledLidoFees);
            } else {
                uint256 withdrawable = Math256.min(balance, unsettledLidoFees);
                if (withdrawable > 0) {
                    _settleLidoFees(_vault, _record, _connection, withdrawable);
                }
            }
        }

        _connection.disconnectInitiatedTs = uint48(block.timestamp);
    }

    function _applyVaultReport(
        VaultRecord storage _record,
        uint256 _reportTimestamp,
        uint256 _reportTotalValue,
        int256 _reportInOutDelta,
        uint256 _reportCumulativeLidoFees,
        uint256 _reportLiabilityShares,
        uint256 _reportMaxLiabilityShares,
        uint256 _reportSlashingReserve
    ) internal {
        _record.cumulativeLidoFees = uint128(_reportCumulativeLidoFees);
        _record.minimalReserve = uint128(Math256.max(CONNECT_DEPOSIT, _reportSlashingReserve));

        // We want to prevent 1 tx looping here:
        // 1. bring ETH (TV+)
        // 2. mint stETH (locked+)
        // 3. burn stETH
        // 4. bring the last report (locked-)
        // 5. withdraw ETH(TV-)

        // current maxLiabilityShares will be greater than the report one
        // if any stETH is minted on funds added after the refslot
        // in that case we don't update it (preventing unlock)
        if (_record.maxLiabilityShares == _reportMaxLiabilityShares) {
            _record.maxLiabilityShares = uint96(Math256.max(_record.liabilityShares, _reportLiabilityShares));
        }
        _record.report = Report({
            totalValue: uint104(_reportTotalValue),
            inOutDelta: int104(_reportInOutDelta),
            timestamp: uint48(_reportTimestamp)
        });
    }

    function _rebalance(address _vault, VaultRecord storage _record, uint256 _shares) internal {
        uint256 valueToRebalance = _getPooledEthBySharesRoundUp(_shares);

        _decreaseLiability(_vault, _record, _shares);
        _withdraw(_vault, _record, address(this), valueToRebalance);
        _rebalanceExternalEtherToInternal(valueToRebalance, _shares);

        _updateBeaconChainDepositsPause(_vault, _record, _vaultConnection(_vault));

        emit VaultRebalanced(_vault, _shares, valueToRebalance);
    }

    function _withdraw(address _vault, VaultRecord storage _record, address _recipient, uint256 _amount) internal {
        uint256 totalValue_ = _totalValue(_record);
        if (_amount > totalValue_) {
            revert AmountExceedsTotalValue(_vault, totalValue_, _amount);
        }

        _updateInOutDelta(_vault, _record, -int104(int256(_amount)));
        _withdrawFromVault(_vault, _recipient, _amount);
    }

    /// @dev Increases liabilityShares of the vault and updates the locked amount
    function _increaseLiability(
        address _vault,
        VaultRecord storage _record,
        uint256 _amountOfShares,
        uint256 _reserveRatioBP,
        uint256 _lockableValueLimit,
        uint256 _shareLimit,
        bool _overrideOperatorLimits
    ) internal {
        uint256 sharesAfterMint = _record.liabilityShares + _amountOfShares;
        if (sharesAfterMint > _shareLimit) {
            revert ShareLimitExceeded(_vault, sharesAfterMint, _shareLimit);
        }

        // Calculate the minimum ETH that needs to be locked in the vault to maintain the reserve ratio
        uint256 etherToLock = _locked(sharesAfterMint, _record.minimalReserve, _reserveRatioBP);
        if (etherToLock > _lockableValueLimit) {
            revert InsufficientValue(_vault, etherToLock, _lockableValueLimit);
        }

        if (sharesAfterMint > _record.maxLiabilityShares) {
            _record.maxLiabilityShares = uint96(sharesAfterMint);
        }

        _record.liabilityShares = uint96(sharesAfterMint);

        _operatorGrid().onMintedShares(_vault, _amountOfShares, _overrideOperatorLimits);
    }

    function _decreaseLiability(address _vault, VaultRecord storage _record, uint256 _amountOfShares) internal {
        uint256 liabilityShares_ = _record.liabilityShares;
        if (liabilityShares_ < _amountOfShares) revert InsufficientSharesToBurn(_vault, liabilityShares_);

        _record.liabilityShares = uint96(liabilityShares_ - _amountOfShares);

        uint256 redemptionShares = _record.redemptionShares;
        if (_amountOfShares > 0 && redemptionShares > 0) {
            uint256 decreasedRedemptionShares = redemptionShares - Math256.min(redemptionShares, _amountOfShares);
            _record.redemptionShares = uint128(decreasedRedemptionShares);

            emit VaultRedemptionSharesUpdated(_vault, decreasedRedemptionShares);
        }

        _operatorGrid().onBurnedShares(_vault, _amountOfShares);
    }

    function _badDebtShares(VaultRecord storage _record) internal view returns (uint256) {
        uint256 liabilityShares_ = _record.liabilityShares;
        uint256 totalValueShares = _getSharesByPooledEth(_totalValue(_record));

        if (totalValueShares > liabilityShares_) {
            return 0;
        }

        return liabilityShares_ - totalValueShares;
    }

    function _healthShortfallShares(
        VaultConnection storage _connection,
        VaultRecord storage _record
    ) internal view returns (uint256) {
        uint256 totalValue_ = _totalValue(_record);
        uint256 liabilityShares_ = _record.liabilityShares;

        bool isHealthy = !_isThresholdBreached(
            totalValue_,
            liabilityShares_,
            _connection.forcedRebalanceThresholdBP
        );

        // Health vault do not need to rebalance
        if (isHealthy) {
            return 0;
        }

        uint256 reserveRatioBP = _connection.reserveRatioBP;
        uint256 maxMintableRatio = (TOTAL_BASIS_POINTS - reserveRatioBP);
        uint256 liability = _getPooledEthBySharesRoundUp(liabilityShares_);

        // Impossible to rebalance a vault with bad debt
        if (liability > totalValue_) {
            return type(uint256).max;
        }

        // Solve the equation for X:
        // L - liability, TV - totalValue
        // MR - maxMintableRatio, 100 - TOTAL_BASIS_POINTS, RR - reserveRatio
        // X - amount of shares that should be withdrawn (TV - X) and used to repay the debt (LS - X)
        // to reduce the L/TV ratio back to MR
        // (L - X) / (TV - X) = MR / 100
        // (L - X) * 100 = (TV - X) * MR
        // L * 100 - X * 100 = TV * MR - X * MR
        // X * MR - X * 100 = TV * MR - L * 100
        // X * (MR - 100) = TV * MR - L * 100
        // X = (TV * MR - L * 100) / (MR - 100)
        // X = (L * 100 - TV * MR) / (100 - MR)
        // RR = 100 - MR
        // X = (L * 100 - TV * MR) / RR
        uint256 shortfallEth = (liability * TOTAL_BASIS_POINTS - totalValue_ * maxMintableRatio) / reserveRatioBP;

        // Add 10 extra shares to avoid dealing with rounding/precision issues
        uint256 shortfallShares = _getSharesByPooledEth(shortfallEth) + 10;

        return Math256.min(shortfallShares, liabilityShares_);
    }

    function _totalValue(VaultRecord storage _record) internal view returns (uint256) {
        Report memory report = _record.report;
        DoubleRefSlotCache.Int104WithCache[DOUBLE_CACHE_LENGTH] memory inOutDelta = _record.inOutDelta;
        return SafeCast.toUint256(int256(uint256(report.totalValue)) + inOutDelta.currentValue() - report.inOutDelta);
    }

    function _locked(
        VaultConnection storage _connection,
        VaultRecord storage _record
    ) internal view returns (uint256) {
        return _locked(_record.maxLiabilityShares, _record.minimalReserve, _connection.reserveRatioBP);
    }

    /// @param _liabilityShares amount of shares that the vault is minted
    /// @param _minimalReserve minimal amount of additional reserve to be locked
    /// @param _reserveRatioBP the reserve ratio of the vault
    /// @return the amount of collateral to be locked on the vault
    function _locked(
        uint256 _liabilityShares,
        uint256 _minimalReserve,
        uint256 _reserveRatioBP
    ) internal view returns (uint256) {
        uint256 liability = _getPooledEthBySharesRoundUp(_liabilityShares);

        // uint256 reserve = liability * TOTAL_BASIS_POINTS / (TOTAL_BASIS_POINTS - _reserveRatioBP) - liability;
        // simplified to:
        uint256 reserve = Math256.ceilDiv(liability * _reserveRatioBP, TOTAL_BASIS_POINTS - _reserveRatioBP);

        return liability + Math256.max(reserve, _minimalReserve);
    }

    function _isReportFresh(VaultRecord storage _record) internal view returns (bool) {
        uint256 latestReportTimestamp = _lazyOracle().latestReportTimestamp();
        return
            // check if AccountingOracle brought fresh report
            uint48(latestReportTimestamp) <= _record.report.timestamp &&
            // if Accounting Oracle stop bringing the report, last report is fresh during this time
            block.timestamp - latestReportTimestamp < REPORT_FRESHNESS_DELTA;
    }

    function _isVaultHealthy(
        VaultConnection storage _connection,
        VaultRecord storage _record
    ) internal view returns (bool) {
        return !_isThresholdBreached(
            _totalValue(_record),
            _record.liabilityShares,
            _connection.forcedRebalanceThresholdBP
        );
    }

    /// @dev Returns true if the vault liability breached the given threshold (inverted)
    function _isThresholdBreached(
        uint256 _vaultTotalValue,
        uint256 _vaultLiabilityShares,
        uint256 _thresholdBP
    ) internal view returns (bool) {
        uint256 liability = _getPooledEthBySharesRoundUp(_vaultLiabilityShares);
        return liability > _vaultTotalValue * (TOTAL_BASIS_POINTS - _thresholdBP) / TOTAL_BASIS_POINTS;
    }

    /// @return the total amount of ether needed to fully cover all outstanding obligations of the vault, including:
    ///         - shares to burn required to restore vault healthiness or cover redemptions
    ///         - unsettled Lido fees (if above the minimum beacon deposit)
    function _obligationsAmount(
        VaultConnection storage _connection,
        VaultRecord storage _record
    ) internal view returns (uint256) {
        uint256 sharesToBurn = _obligationsShares(_connection, _record);
        if (sharesToBurn == type(uint256).max) return type(uint256).max;

        // no need to cover fees if they are less than the minimum beacon deposit
        uint256 unsettledLidoFees = _unsettledLidoFeesValue(_record);
        uint256 feesToSettle = unsettledLidoFees < MIN_BEACON_DEPOSIT ? 0 : unsettledLidoFees;

        return _getPooledEthBySharesRoundUp(sharesToBurn) + feesToSettle;
    }

    /// @return the ether shortfall required to fully cover all outstanding obligations amount of the vault
    function _obligationsShortfallValue(
        address _vault,
        VaultConnection storage _connection,
        VaultRecord storage _record
    ) internal view returns (uint256) {
        uint256 obligationsAmount_ = _obligationsAmount(_connection, _record);
        if (obligationsAmount_ == type(uint256).max) return type(uint256).max;

        uint256 balance = _availableBalance(_vault);

        return obligationsAmount_ > balance ? obligationsAmount_ - balance : 0;
    }

    function _addVault(address _vault, VaultConnection memory _connection, VaultRecord memory _record) internal {
        Storage storage $ = _storage();
        $.vaults.push(_vault);

        $.connections[_vault] = _connection;
        $.records[_vault] = _record;
    }

    function _deleteVault(address _vault, VaultConnection storage _connection) internal {
        Storage storage $ = _storage();
        uint96 vaultIndex = _connection.vaultIndex;

        address lastVault = $.vaults[$.vaults.length - 1];
        $.connections[lastVault].vaultIndex = vaultIndex;
        $.vaults[vaultIndex] = lastVault;
        $.vaults.pop();

        delete $.connections[_vault];
        delete $.records[_vault];

        _lazyOracle().removeVaultQuarantine(_vault);
        _operatorGrid().resetVaultTier(_vault);
    }

    function _checkConnectionAndOwner(address _vault) internal view returns (VaultConnection storage connection) {
        connection = _checkConnection(_vault);
        _requireSender(connection.owner);
    }

    function _isPendingDisconnect(VaultConnection storage _connection) internal view returns (bool) {
        uint256 disconnectionTs = _connection.disconnectInitiatedTs;
        return disconnectionTs != 0 // vault is disconnected
            && disconnectionTs != DISCONNECT_NOT_INITIATED; // vault in connected but not pending for disconnect
    }

    function _checkConnection(address _vault) internal view returns (VaultConnection storage) {
        _requireNotZero(_vault);

        VaultConnection storage connection = _vaultConnection(_vault);
        _requireConnected(connection, _vault);
        if (_isPendingDisconnect(connection)) revert VaultIsDisconnecting(_vault);

        return connection;
    }

    /// @dev Caches the inOutDelta of the latest refSlot and updates the value
    function _updateInOutDelta(address _vault, VaultRecord storage _record, int104 _increment) internal {
        DoubleRefSlotCache.Int104WithCache[DOUBLE_CACHE_LENGTH] memory inOutDelta = _record.inOutDelta.withValueIncrease({
            _consensus: CONSENSUS_CONTRACT,
            _increment: _increment
        });
        _record.inOutDelta = inOutDelta;
        emit VaultInOutDeltaUpdated(_vault, inOutDelta.currentValue());
    }

    function _updateBeaconChainDepositsPause(
        address _vault,
        VaultRecord storage _record,
        VaultConnection storage _connection
    ) internal {
        IStakingVault vault_ = IStakingVault(_vault);
        uint256 obligationsAmount_ = _obligationsAmount(_connection, _record);
        if (obligationsAmount_ > 0) {
            _pauseBeaconChainDepositsIfNotAlready(vault_);
        } else if (!_connection.beaconChainDepositsPauseIntent) {
            _resumeBeaconChainDepositsIfNotAlready(vault_);
        }
    }

    function _settleLidoFees(
        address _vault,
        VaultRecord storage _record,
        VaultConnection storage _connection,
        uint256 _valueToSettle
    ) internal {
        uint256 settledLidoFees = _record.settledLidoFees + _valueToSettle;
        _record.settledLidoFees = uint128(settledLidoFees);

        _withdraw(_vault, _record, LIDO_LOCATOR.treasury(), _valueToSettle);
        _updateBeaconChainDepositsPause(_vault, _record, _connection);

        emit LidoFeesSettled({
            vault: _vault,
            transferred: _valueToSettle,
            cumulativeLidoFees: _record.cumulativeLidoFees,
            settledLidoFees: settledLidoFees
        });
    }

    /// @notice the amount of ether that can be withdrawn from the vault based on the available balance,
    ///         locked value, vault redemption shares (does not include Lido fees)
    function _withdrawableValueFeesIncluded(
        address _vault,
        VaultConnection storage _connection,
        VaultRecord storage _record
    ) internal view returns (uint256) {
        uint256 availableBalance = Math256.min(_availableBalance(_vault), _totalValue(_record));

        // We can't withdraw funds that can be used to cover redemptions
        uint256 redemptionValue = _getPooledEthBySharesRoundUp(_record.redemptionShares);
        if (redemptionValue > availableBalance) return 0;
        availableBalance -= redemptionValue;

        // We must account vaults locked value when calculating the withdrawable amount
        return Math256.min(availableBalance, _unlocked(_connection, _record));
    }

    /// @notice the amount of lido fees that can be settled on the vault based on the withdrawable value
    function _settleableLidoFeesValue(
        address _vault,
        VaultConnection storage _connection,
        VaultRecord storage _record,
        uint256 _feesToSettle
    ) internal view returns (uint256) {
        return Math256.min(_withdrawableValueFeesIncluded(_vault, _connection, _record), _feesToSettle);
    }

    /// @notice the amount of ether that can be instantly withdrawn from the vault based on the available balance,
    ///         locked value, vault redemption shares and unsettled Lido fees accrued on the vault
    function _withdrawableValue(
        address _vault,
        VaultConnection storage _connection,
        VaultRecord storage _record
    ) internal view returns (uint256) {
        uint256 withdrawable = _withdrawableValueFeesIncluded(_vault, _connection, _record);
        uint256 feesValue = _unsettledLidoFeesValue(_record);
        return withdrawable > feesValue ? withdrawable - feesValue : 0;
    }

    /// @notice Calculates the max lockable value of the vault
    /// @param _record The record of the vault
    /// @return the max lockable value of the vault
    function _maxLockableValue(VaultRecord storage _record) internal view returns (uint256) {
        uint256 totalValue_ = _totalValue(_record);
        uint256 unsettledLidoFees_ = _unsettledLidoFeesValue(_record);
        return totalValue_ > unsettledLidoFees_ ? totalValue_ - unsettledLidoFees_ : 0;
    }

    /// @notice Calculates the total number of shares that is possible to mint on the vault taking into account
    ///         minimal reserve, reserve ratio and the operator grid share limit
    /// @param _vault The address of the vault
    /// @param _deltaValue The delta value to apply to the total value of the vault (may be negative)
    /// @return the number of shares that can be minted
    /// @dev returns 0 if the vault is not connected
    function _totalMintingCapacityShares(address _vault, int256 _deltaValue) internal view returns (uint256) {
        VaultRecord storage record = _vaultRecord(_vault);
        VaultConnection storage connection = _vaultConnection(_vault);

        uint256 maxLockableValue_ = _maxLockableValue(record);
        if (_deltaValue >= 0) {
            maxLockableValue_ += uint256(_deltaValue);
        } else {
            uint256 negDeltaValue = uint256(-_deltaValue);
            if (maxLockableValue_ < negDeltaValue) return 0;
            maxLockableValue_ -= negDeltaValue;
        }

        uint256 minimalReserve_ = record.minimalReserve;
        if (maxLockableValue_ <= minimalReserve_) return 0;

        uint256 reserve = Math256.ceilDiv(maxLockableValue_ * connection.reserveRatioBP, TOTAL_BASIS_POINTS);

        uint256 capacityShares = _getSharesByPooledEth(maxLockableValue_ - Math256.max(reserve, minimalReserve_));
        return Math256.min(capacityShares, _operatorGrid().effectiveShareLimit(_vault));
    }

    function _unlocked(
        VaultConnection storage _connection,
        VaultRecord storage _record
    ) internal view returns (uint256) {
        uint256 totalValue_ = _totalValue(_record);
        uint256 locked_ = _locked(_connection, _record);
        return totalValue_ > locked_ ? totalValue_ - locked_ : 0;
    }

    function _unsettledLidoFeesValue(VaultRecord storage _record) internal view returns (uint256) {
        return _record.cumulativeLidoFees - _record.settledLidoFees;
    }

    function _obligationsShares(
        VaultConnection storage _connection,
        VaultRecord storage _record
    ) internal view returns (uint256) {
        return Math256.max(_healthShortfallShares(_connection, _record), _record.redemptionShares);
    }

    function _storage() internal pure returns (Storage storage $) {
        assembly {
            $.slot := STORAGE_LOCATION
        }
    }

    function _vaultConnection(address _vault) internal view returns (VaultConnection storage) {
        return _storage().connections[_vault];
    }

    function _vaultRecord(address _vault) internal view returns (VaultRecord storage) {
        return _storage().records[_vault];
    }

    // -----------------------------
    //          EXTERNAL CALLS
    // -----------------------------
    // All external calls that is used more than once is wrapped in internal function to save bytecode

    function _operatorGrid() internal view returns (OperatorGrid) {
        return OperatorGrid(LIDO_LOCATOR.operatorGrid());
    }

    function _lazyOracle() internal view returns (LazyOracle) {
        return LazyOracle(LIDO_LOCATOR.lazyOracle());
    }

    function _predepositGuarantee() internal view returns (IPredepositGuarantee) {
        return IPredepositGuarantee(LIDO_LOCATOR.predepositGuarantee());
    }

    function _getSharesByPooledEth(uint256 _ether) internal view returns (uint256) {
        return LIDO.getSharesByPooledEth(_ether);
    }

    function _getPooledEthBySharesRoundUp(uint256 _shares) internal view returns (uint256) {
        return LIDO.getPooledEthBySharesRoundUp(_shares);
    }

    function _rebalanceExternalEtherToInternal(uint256 _ether, uint256 _amountOfShares) internal {
        LIDO.rebalanceExternalEtherToInternal{value: _ether}(_amountOfShares);
    }

    function _triggerVaultValidatorWithdrawals(
        address _vault,
        uint256 _value,
        bytes calldata _pubkeys,
        uint64[] memory _amountsInGwei,
        address _refundRecipient
    ) internal {
        IStakingVault(_vault).triggerValidatorWithdrawals{value: _value}(_pubkeys, _amountsInGwei, _refundRecipient);
    }

    function _withdrawFromVault(address _vault, address _recipient, uint256 _amount) internal {
        IStakingVault(_vault).withdraw(_recipient, _amount);
    }

    function _nodeOperator(address _vault) internal view returns (address) {
        return IStakingVault(_vault).nodeOperator();
    }

    function _availableBalance(address _vault) internal view returns (uint256) {
        return IStakingVault(_vault).availableBalance();
    }

    function _requireNotZero(uint256 _value) internal pure {
        if (_value == 0) revert ZeroArgument();
    }

    function _requireNotZero(address _address) internal pure {
        if (_address == address(0)) revert ZeroAddress();
    }

    function _requireSender(address _sender) internal view {
        if (msg.sender != _sender) revert NotAuthorized();
    }

    function _requireSaneShareLimit(uint256 _shareLimit) internal view {
        uint256 maxSaneShareLimit = (LIDO.getTotalShares() * MAX_RELATIVE_SHARE_LIMIT_BP) / TOTAL_BASIS_POINTS;
        if (_shareLimit > maxSaneShareLimit) revert ShareLimitTooHigh(_shareLimit, maxSaneShareLimit);
    }

    function _requireConnected(VaultConnection storage _connection, address _vault) internal view {
        if (_connection.vaultIndex == 0) revert NotConnectedToHub(_vault);
    }

    function _requireFreshReport(address _vault, VaultRecord storage _record) internal view {
        if (!_isReportFresh(_record)) revert VaultReportStale(_vault);
    }

    function _isBeaconChainDepositsPaused(IStakingVault _vault) internal view returns (bool) {
        return _vault.beaconChainDepositsPaused();
    }

    function _pauseBeaconChainDepositsIfNotAlready(IStakingVault _vault) internal {
        if (!_isBeaconChainDepositsPaused(_vault)) {
            _vault.pauseBeaconChainDeposits();
        }
    }

    function _resumeBeaconChainDepositsIfNotAlready(IStakingVault _vault) internal {
        if (_isBeaconChainDepositsPaused(_vault)) {
            _vault.resumeBeaconChainDeposits();
        }
    }

    // -----------------------------
    //           EVENTS
    // -----------------------------

    /// @dev Warning! used by Accounting Oracle to calculate fees
    event VaultConnected(
        address indexed vault,
        uint256 shareLimit,
        uint256 reserveRatioBP,
        uint256 forcedRebalanceThresholdBP,
        uint256 infraFeeBP,
        uint256 liquidityFeeBP,
        uint256 reservationFeeBP
    );

    event VaultConnectionUpdated(
        address indexed vault,
        address indexed nodeOperator,
        uint256 shareLimit,
        uint256 reserveRatioBP,
        uint256 forcedRebalanceThresholdBP
    );

    /// @dev Warning! used by Accounting Oracle to calculate fees
    event VaultFeesUpdated(
        address indexed vault,
        uint256 preInfraFeeBP,
        uint256 preLiquidityFeeBP,
        uint256 preReservationFeeBP,
        uint256 infraFeeBP,
        uint256 liquidityFeeBP,
        uint256 reservationFeeBP
    );
    event VaultDisconnectInitiated(address indexed vault);
    event VaultDisconnectCompleted(address indexed vault);
    event VaultDisconnectAborted(address indexed vault, uint256 slashingReserve);
    event VaultReportApplied(
        address indexed vault,
        uint256 reportTimestamp,
        uint256 reportTotalValue,
        int256 reportInOutDelta,
        uint256 reportCumulativeLidoFees,
        uint256 reportLiabilityShares,
        uint256 reportMaxLiabilityShares,
        uint256 reportSlashingReserve
    );

    /// @dev Warning! used by Accounting Oracle to calculate fees
    event MintedSharesOnVault(address indexed vault, uint256 amountOfShares, uint256 lockedAmount);
    /// @dev Warning! used by Accounting Oracle to calculate fees
    event BurnedSharesOnVault(address indexed vault, uint256 amountOfShares);
    /// @dev Warning! used by Accounting Oracle to calculate fees
    event VaultRebalanced(address indexed vault, uint256 sharesBurned, uint256 etherWithdrawn);
    event VaultInOutDeltaUpdated(address indexed vault, int256 inOutDelta);
    event ForcedValidatorExitTriggered(address indexed vault, bytes pubkeys, address refundRecipient);

    /**
     * @notice Emitted when the vault ownership is changed
     * @param vault The address of the vault
     * @param newOwner The address of the new owner
     * @param oldOwner The address of the old owner
     */
    event VaultOwnershipTransferred(address indexed vault, address indexed newOwner, address indexed oldOwner);

    event LidoFeesSettled(address indexed vault, uint256 transferred, uint256 cumulativeLidoFees, uint256 settledLidoFees);
    event VaultRedemptionSharesUpdated(address indexed vault, uint256 redemptionShares);

    event BeaconChainDepositsPauseIntentSet(address indexed vault, bool pauseIntent);

    /// @dev Warning! used by Accounting Oracle to calculate fees
    event BadDebtSocialized(address indexed vaultDonor, address indexed vaultAcceptor, uint256 badDebtShares);
    /// @dev Warning! used by Accounting Oracle to calculate fees
    event BadDebtWrittenOffToBeInternalized(address indexed vault, uint256 badDebtShares);

    // -----------------------------
    //           ERRORS
    // -----------------------------

    error PauseIntentAlreadySet();
    error PauseIntentAlreadyUnset();

    error AmountExceedsTotalValue(address vault, uint256 totalValue, uint256 withdrawAmount);
    error AmountExceedsWithdrawableValue(address vault, uint256 withdrawable, uint256 requested);

    error NoFundsForForceRebalance(address vault);
    error NoReasonForForceRebalance(address vault);

    error NoUnsettledLidoFeesToSettle(address vault);
    error NoFundsToSettleLidoFees(address vault, uint256 unsettledLidoFees);

    error VaultMintingCapacityExceeded(
        address vault,
        uint256 totalValue,
        uint256 liabilityShares,
        uint256 newRebalanceThresholdBP
    );
    error InsufficientSharesToBurn(address vault, uint256 amount);
    error ShareLimitExceeded(address vault, uint256 expectedSharesAfterMint, uint256 shareLimit);
    error AlreadyConnected(address vault, uint256 index);
    error InsufficientStagedBalance(address vault);
    error NotConnectedToHub(address vault);
    error NotAuthorized();
    error ZeroAddress();
    error ZeroArgument();
    error InvalidBasisPoints(uint256 valueBP, uint256 maxValueBP);
    error ShareLimitTooHigh(uint256 shareLimit, uint256 maxShareLimit);
    error InsufficientValue(address vault, uint256 etherToLock, uint256 maxLockableValue);
    error NoLiabilitySharesShouldBeLeft(address vault, uint256 liabilityShares);
    error NoUnsettledLidoFeesShouldBeLeft(address vault, uint256 unsettledLidoFees);
    error VaultOssified(address vault);
    error VaultInsufficientBalance(address vault, uint256 currentBalance, uint256 expectedBalance);
    error VaultReportStale(address vault);
    error PDGNotDepositor(address vault);
    error VaultHubNotPendingOwner(address vault);
    error VaultIsDisconnecting(address vault);
    error PartialValidatorWithdrawalNotAllowed();
    error ForcedValidatorExitNotAllowed();
    error BadDebtSocializationNotAllowed();
    error VaultNotFactoryDeployed(address vault);
}

// SPDX-FileCopyrightText: 2025 Lido <[email protected]>
// SPDX-License-Identifier: GPL-3.0

// See contracts/COMPILERS.md
// solhint-disable-next-line lido/fixed-compiler-version
pragma solidity >=0.5.0;

interface IDepositContract {
    function get_deposit_root() external view returns (bytes32 rootHash);

    function deposit(
        bytes calldata pubkey, // 48 bytes
        bytes calldata withdrawal_credentials, // 32 bytes
        bytes calldata signature, // 96 bytes
        bytes32 deposit_data_root
    ) external payable;
}

// SPDX-FileCopyrightText: 2025 Lido <[email protected]>
// SPDX-License-Identifier: GPL-3.0

// See contracts/COMPILERS.md
// solhint-disable-next-line lido/fixed-compiler-version
pragma solidity >=0.5.0;


interface IHashConsensus {
    function getIsMember(address addr) external view returns (bool);

    function getCurrentFrame() external view returns (
        uint256 refSlot,
        uint256 reportProcessingDeadlineSlot
    );

    function getChainConfig() external view returns (
        uint256 slotsPerEpoch,
        uint256 secondsPerSlot,
        uint256 genesisTime
    );

    function getFrameConfig() external view returns (uint256 initialEpoch, uint256 epochsPerFrame);

    function getInitialRefSlot() external view returns (uint256);
}

File 39 of 50 : ILazyOracle.sol
// SPDX-FileCopyrightText: 2025 Lido <[email protected]>
// SPDX-License-Identifier: GPL-3.0

// See contracts/COMPILERS.md
// solhint-disable-next-line lido/fixed-compiler-version
pragma solidity >=0.5.0;

/**
 * Interface to connect AccountingOracle with LazyOracle and force type consistency
 */
interface ILazyOracle {
    function updateReportData(
        uint256 _vaultsDataTimestamp,
        uint256 _vaultsDataRefSlot,
        bytes32 _vaultsDataTreeRoot,
        string memory _vaultsDataReportCid
    ) external;
}

// SPDX-FileCopyrightText: 2025 Lido <[email protected]>
// SPDX-License-Identifier: GPL-3.0

// See contracts/COMPILERS.md
// solhint-disable-next-line lido/fixed-compiler-version
pragma solidity >=0.8.0;

import {IERC20} from "@openzeppelin/contracts-v4.4/token/ERC20/IERC20.sol";

import {IVersioned} from "contracts/common/interfaces/IVersioned.sol";

interface ILido is IERC20, IVersioned {
    function sharesOf(address) external view returns (uint256);

    function getSharesByPooledEth(uint256) external view returns (uint256);

    function getPooledEthByShares(uint256) external view returns (uint256);

    function getPooledEthBySharesRoundUp(uint256) external view returns (uint256);

    function transferSharesFrom(address, address, uint256) external returns (uint256);

    function transferShares(address, uint256) external returns (uint256);

    function rebalanceExternalEtherToInternal(uint256 _amountOfShares) external payable;

    function getTotalPooledEther() external view returns (uint256);

    function getExternalEther() external view returns (uint256);

    function getExternalShares() external view returns (uint256);

    function mintExternalShares(address, uint256) external;

    function burnExternalShares(uint256) external;

    function getTotalShares() external view returns (uint256);

    function getBeaconStat()
        external
        view
        returns (uint256 depositedValidators, uint256 beaconValidators, uint256 beaconBalance);

    function processClStateUpdate(
        uint256 _reportTimestamp,
        uint256 _preClValidators,
        uint256 _reportClValidators,
        uint256 _reportClBalance
    ) external;

    function collectRewardsAndProcessWithdrawals(
        uint256 _reportTimestamp,
        uint256 _reportClBalance,
        uint256 _adjustedPreCLBalance,
        uint256 _withdrawalsToWithdraw,
        uint256 _elRewardsToWithdraw,
        uint256 _lastWithdrawalRequestToFinalize,
        uint256 _simulatedShareRate,
        uint256 _etherToLockOnWithdrawalQueue
    ) external;

    function emitTokenRebase(
        uint256 _reportTimestamp,
        uint256 _timeElapsed,
        uint256 _preTotalShares,
        uint256 _preTotalEther,
        uint256 _postTotalShares,
        uint256 _postTotalEther,
        uint256 _postInternalShares,
        uint256 _postInternalEther,
        uint256 _sharesMintedAsFees
    ) external;

    function mintShares(address _recipient, uint256 _sharesAmount) external;

    function internalizeExternalBadDebt(uint256 _amountOfShares) external;
}

// SPDX-FileCopyrightText: 2025 Lido <[email protected]>
// SPDX-License-Identifier: GPL-3.0

// See contracts/COMPILERS.md
// solhint-disable-next-line lido/fixed-compiler-version
pragma solidity >=0.4.24 <0.9.0;

interface ILidoLocator {
    function accountingOracle() external view returns(address);
    function depositSecurityModule() external view returns(address);
    function elRewardsVault() external view returns(address);
    function lido() external view returns(address);
    function oracleReportSanityChecker() external view returns(address);
    function burner() external view returns(address);
    function stakingRouter() external view returns(address);
    function treasury() external view returns(address);
    function validatorsExitBusOracle() external view returns(address);
    function withdrawalQueue() external view returns(address);
    function withdrawalVault() external view returns(address);
    function postTokenRebaseReceiver() external view returns(address);
    function oracleDaemonConfig() external view returns(address);
    function accounting() external view returns (address);
    function predepositGuarantee() external view returns (address);
    function wstETH() external view returns (address);
    function vaultHub() external view returns (address);
    function vaultFactory() external view returns (address);
    function lazyOracle() external view returns (address);
    function operatorGrid() external view returns (address);

    /// @notice Returns core Lido protocol component addresses in a single call
    /// @dev This function provides a gas-efficient way to fetch multiple component addresses in a single call
    function coreComponents() external view returns(
        address elRewardsVault,
        address oracleReportSanityChecker,
        address stakingRouter,
        address treasury,
        address withdrawalQueue,
        address withdrawalVault
    );

    /// @notice Returns addresses of components involved in processing oracle reports in the Lido contract
    /// @dev This function provides a gas-efficient way to fetch multiple component addresses in a single call
    function oracleReportComponents() external view returns(
        address accountingOracle,
        address oracleReportSanityChecker,
        address burner,
        address withdrawalQueue,
        address postTokenRebaseReceiver,
        address stakingRouter,
        address vaultHub
    );
}

// SPDX-FileCopyrightText: 2025 Lido <[email protected]>
// SPDX-License-Identifier: GPL-3.0

// See contracts/COMPILERS.md
// solhint-disable-next-line
pragma solidity >=0.4.24;

interface IVersioned {
    /// @notice Returns the current contract version.
    function getContractVersion() external view returns (uint256);
}

// SPDX-FileCopyrightText: 2023 Lido <[email protected]>
// SPDX-License-Identifier: MIT

// Copied from: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/0457042d93d9dfd760dbaa06a4d2f1216fdbe297/contracts/utils/math/Math.sol

// See contracts/COMPILERS.md
// solhint-disable-next-line
pragma solidity >=0.4.24 <0.9.0;

library Math256 {
    /// @dev Returns the largest of two numbers.
    function max(uint256 a, uint256 b) internal pure returns (uint256) {
        return a > b ? a : b;
    }

    /// @dev Returns the smallest of two numbers.
    function min(uint256 a, uint256 b) internal pure returns (uint256) {
        return a < b ? a : b;
    }

    /// @dev Returns the largest of two numbers.
    function max(int256 a, int256 b) internal pure returns (int256) {
        return a > b ? a : b;
    }

    /// @dev Returns the smallest of two numbers.
    function min(int256 a, int256 b) internal pure returns (int256) {
        return a < b ? a : b;
    }

    /// @dev Returns the ceiling of the division of two numbers.
    ///
    /// This differs from standard division with `/` in that it rounds up instead
    /// of rounding down.
    function ceilDiv(uint256 a, uint256 b) internal pure returns (uint256) {
        // (a + b - 1) / b can overflow on addition, so we distribute.
        return a == 0 ? 0 : (a - 1) / b + 1;
    }

    /// @dev Returns absolute difference of two numbers.
    function absDiff(uint256 a, uint256 b) internal pure returns (uint256) {
        return a > b ? a - b : b - a;
    }
}

// SPDX-FileCopyrightText: 2023 Lido <[email protected]>, Aragon
// SPDX-License-Identifier: MIT

// solhint-disable-next-line lido/fixed-compiler-version
pragma solidity ^0.8.9;

library UnstructuredStorage {
    function getStorageBool(bytes32 position) internal view returns (bool data) {
        assembly { data := sload(position) }
    }

    function getStorageAddress(bytes32 position) internal view returns (address data) {
        assembly { data := sload(position) }
    }

    function getStorageBytes32(bytes32 position) internal view returns (bytes32 data) {
        assembly { data := sload(position) }
    }

    function getStorageUint256(bytes32 position) internal view returns (uint256 data) {
        assembly { data := sload(position) }
    }

    function setStorageBool(bytes32 position, bool data) internal {
        assembly { sstore(position, data) }
    }

    function setStorageAddress(bytes32 position, address data) internal {
        assembly { sstore(position, data) }
    }

    function setStorageBytes32(bytes32 position, bytes32 data) internal {
        assembly { sstore(position, data) }
    }

    function setStorageUint256(bytes32 position, uint256 data) internal {
        assembly { sstore(position, data) }
    }
}

// SPDX-FileCopyrightText: 2025 Lido <[email protected]>
// SPDX-License-Identifier: GPL-3.0
// solhint-disable-next-line lido/fixed-compiler-version
pragma solidity ^0.8.9;

import {UnstructuredStorage} from "contracts/common/lib/UnstructuredStorage.sol";

/**
 * @title PausableUntil
 * @notice allows to pause the contract for a specific duration or indefinitely
 */
abstract contract PausableUntil {
    using UnstructuredStorage for bytes32;

    /// Contract resume/pause control storage slot
    bytes32 internal constant RESUME_SINCE_TIMESTAMP_POSITION = keccak256("lido.PausableUntil.resumeSinceTimestamp");
    /// Special value for the infinite pause
    uint256 public constant PAUSE_INFINITELY = type(uint256).max;

    /// @notice Emitted when paused by the `pauseFor` or `pauseUntil` call
    event Paused(uint256 duration);
    /// @notice Emitted when resumed by the `resume` call
    event Resumed();

    error ZeroPauseDuration();
    error PausedExpected();
    error ResumedExpected();
    error PauseUntilMustBeInFuture();

    /// @notice Reverts if paused
    modifier whenResumed() {
        _checkResumed();
        _;
    }

    /// @notice Returns whether the contract is paused
    function isPaused() public view returns (bool) {
        return block.timestamp < RESUME_SINCE_TIMESTAMP_POSITION.getStorageUint256();
    }

    /// @notice Returns one of:
    ///  - PAUSE_INFINITELY if paused infinitely returns
    ///  - the timestamp when the contract get resumed if paused for specific duration
    ///  - some timestamp in past if not paused
    function getResumeSinceTimestamp() external view returns (uint256) {
        return RESUME_SINCE_TIMESTAMP_POSITION.getStorageUint256();
    }

    function _checkPaused() internal view {
        if (!isPaused()) {
            revert PausedExpected();
        }
    }

    function _checkResumed() internal view {
        if (isPaused()) {
            revert ResumedExpected();
        }
    }

    function _resume() internal {
        _checkPaused();
        RESUME_SINCE_TIMESTAMP_POSITION.setStorageUint256(block.timestamp);
        emit Resumed();
    }

    function _pauseFor(uint256 _duration) internal {
        _checkResumed();
        if (_duration == 0) revert ZeroPauseDuration();

        uint256 resumeSince;
        if (_duration == PAUSE_INFINITELY) {
            resumeSince = PAUSE_INFINITELY;
        } else {
            resumeSince = block.timestamp + _duration;
        }
        _setPausedState(resumeSince);
    }

    function _pauseUntil(uint256 _pauseUntilInclusive) internal {
        _checkResumed();
        if (_pauseUntilInclusive < block.timestamp) revert PauseUntilMustBeInFuture();

        uint256 resumeSince;
        if (_pauseUntilInclusive != PAUSE_INFINITELY) {
            resumeSince = _pauseUntilInclusive + 1;
        } else {
            resumeSince = PAUSE_INFINITELY;
        }
        _setPausedState(resumeSince);
    }

    function _setPausedState(uint256 _resumeSince) internal {
        RESUME_SINCE_TIMESTAMP_POSITION.setStorageUint256(_resumeSince);
        if (_resumeSince == PAUSE_INFINITELY) {
            emit Paused(PAUSE_INFINITELY);
        } else {
            emit Paused(_resumeSince - block.timestamp);
        }
    }
}

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (access/AccessControl.sol)

pragma solidity ^0.8.20;

import {IAccessControl} from "@openzeppelin/contracts-v5.2/access/IAccessControl.sol";
import {ContextUpgradeable} from "../utils/ContextUpgradeable.sol";
import {ERC165Upgradeable} from "../utils/introspection/ERC165Upgradeable.sol";
import {Initializable} from "../proxy/utils/Initializable.sol";

/**
 * @dev Contract module that allows children to implement role-based access
 * control mechanisms. This is a lightweight version that doesn't allow enumerating role
 * members except through off-chain means by accessing the contract event logs. Some
 * applications may benefit from on-chain enumerability, for those cases see
 * {AccessControlEnumerable}.
 *
 * Roles are referred to by their `bytes32` identifier. These should be exposed
 * in the external API and be unique. The best way to achieve this is by
 * using `public constant` hash digests:
 *
 * ```solidity
 * bytes32 public constant MY_ROLE = keccak256("MY_ROLE");
 * ```
 *
 * Roles can be used to represent a set of permissions. To restrict access to a
 * function call, use {hasRole}:
 *
 * ```solidity
 * function foo() public {
 *     require(hasRole(MY_ROLE, msg.sender));
 *     ...
 * }
 * ```
 *
 * Roles can be granted and revoked dynamically via the {grantRole} and
 * {revokeRole} functions. Each role has an associated admin role, and only
 * accounts that have a role's admin role can call {grantRole} and {revokeRole}.
 *
 * By default, the admin role for all roles is `DEFAULT_ADMIN_ROLE`, which means
 * that only accounts with this role will be able to grant or revoke other
 * roles. More complex role relationships can be created by using
 * {_setRoleAdmin}.
 *
 * WARNING: The `DEFAULT_ADMIN_ROLE` is also its own admin: it has permission to
 * grant and revoke this role. Extra precautions should be taken to secure
 * accounts that have been granted it. We recommend using {AccessControlDefaultAdminRules}
 * to enforce additional security measures for this role.
 */
abstract contract AccessControlUpgradeable is Initializable, ContextUpgradeable, IAccessControl, ERC165Upgradeable {
    struct RoleData {
        mapping(address account => bool) hasRole;
        bytes32 adminRole;
    }

    bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00;


    /// @custom:storage-location erc7201:openzeppelin.storage.AccessControl
    struct AccessControlStorage {
        mapping(bytes32 role => RoleData) _roles;
    }

    // keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.AccessControl")) - 1)) & ~bytes32(uint256(0xff))
    bytes32 private constant AccessControlStorageLocation = 0x02dd7bc7dec4dceedda775e58dd541e08a116c6c53815c0bd028192f7b626800;

    function _getAccessControlStorage() private pure returns (AccessControlStorage storage $) {
        assembly {
            $.slot := AccessControlStorageLocation
        }
    }

    /**
     * @dev Modifier that checks that an account has a specific role. Reverts
     * with an {AccessControlUnauthorizedAccount} error including the required role.
     */
    modifier onlyRole(bytes32 role) {
        _checkRole(role);
        _;
    }

    function __AccessControl_init() internal onlyInitializing {
    }

    function __AccessControl_init_unchained() internal onlyInitializing {
    }
    /**
     * @dev See {IERC165-supportsInterface}.
     */
    function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
        return interfaceId == type(IAccessControl).interfaceId || super.supportsInterface(interfaceId);
    }

    /**
     * @dev Returns `true` if `account` has been granted `role`.
     */
    function hasRole(bytes32 role, address account) public view virtual returns (bool) {
        AccessControlStorage storage $ = _getAccessControlStorage();
        return $._roles[role].hasRole[account];
    }

    /**
     * @dev Reverts with an {AccessControlUnauthorizedAccount} error if `_msgSender()`
     * is missing `role`. Overriding this function changes the behavior of the {onlyRole} modifier.
     */
    function _checkRole(bytes32 role) internal view virtual {
        _checkRole(role, _msgSender());
    }

    /**
     * @dev Reverts with an {AccessControlUnauthorizedAccount} error if `account`
     * is missing `role`.
     */
    function _checkRole(bytes32 role, address account) internal view virtual {
        if (!hasRole(role, account)) {
            revert AccessControlUnauthorizedAccount(account, role);
        }
    }

    /**
     * @dev Returns the admin role that controls `role`. See {grantRole} and
     * {revokeRole}.
     *
     * To change a role's admin, use {_setRoleAdmin}.
     */
    function getRoleAdmin(bytes32 role) public view virtual returns (bytes32) {
        AccessControlStorage storage $ = _getAccessControlStorage();
        return $._roles[role].adminRole;
    }

    /**
     * @dev Grants `role` to `account`.
     *
     * If `account` had not been already granted `role`, emits a {RoleGranted}
     * event.
     *
     * Requirements:
     *
     * - the caller must have ``role``'s admin role.
     *
     * May emit a {RoleGranted} event.
     */
    function grantRole(bytes32 role, address account) public virtual onlyRole(getRoleAdmin(role)) {
        _grantRole(role, account);
    }

    /**
     * @dev Revokes `role` from `account`.
     *
     * If `account` had been granted `role`, emits a {RoleRevoked} event.
     *
     * Requirements:
     *
     * - the caller must have ``role``'s admin role.
     *
     * May emit a {RoleRevoked} event.
     */
    function revokeRole(bytes32 role, address account) public virtual onlyRole(getRoleAdmin(role)) {
        _revokeRole(role, account);
    }

    /**
     * @dev Revokes `role` from the calling account.
     *
     * Roles are often managed via {grantRole} and {revokeRole}: this function's
     * purpose is to provide a mechanism for accounts to lose their privileges
     * if they are compromised (such as when a trusted device is misplaced).
     *
     * If the calling account had been revoked `role`, emits a {RoleRevoked}
     * event.
     *
     * Requirements:
     *
     * - the caller must be `callerConfirmation`.
     *
     * May emit a {RoleRevoked} event.
     */
    function renounceRole(bytes32 role, address callerConfirmation) public virtual {
        if (callerConfirmation != _msgSender()) {
            revert AccessControlBadConfirmation();
        }

        _revokeRole(role, callerConfirmation);
    }

    /**
     * @dev Sets `adminRole` as ``role``'s admin role.
     *
     * Emits a {RoleAdminChanged} event.
     */
    function _setRoleAdmin(bytes32 role, bytes32 adminRole) internal virtual {
        AccessControlStorage storage $ = _getAccessControlStorage();
        bytes32 previousAdminRole = getRoleAdmin(role);
        $._roles[role].adminRole = adminRole;
        emit RoleAdminChanged(role, previousAdminRole, adminRole);
    }

    /**
     * @dev Attempts to grant `role` to `account` and returns a boolean indicating if `role` was granted.
     *
     * Internal function without access restriction.
     *
     * May emit a {RoleGranted} event.
     */
    function _grantRole(bytes32 role, address account) internal virtual returns (bool) {
        AccessControlStorage storage $ = _getAccessControlStorage();
        if (!hasRole(role, account)) {
            $._roles[role].hasRole[account] = true;
            emit RoleGranted(role, account, _msgSender());
            return true;
        } else {
            return false;
        }
    }

    /**
     * @dev Attempts to revoke `role` from `account` and returns a boolean indicating if `role` was revoked.
     *
     * Internal function without access restriction.
     *
     * May emit a {RoleRevoked} event.
     */
    function _revokeRole(bytes32 role, address account) internal virtual returns (bool) {
        AccessControlStorage storage $ = _getAccessControlStorage();
        if (hasRole(role, account)) {
            $._roles[role].hasRole[account] = false;
            emit RoleRevoked(role, account, _msgSender());
            return true;
        } else {
            return false;
        }
    }
}

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (access/extensions/AccessControlEnumerable.sol)

pragma solidity ^0.8.20;

import {IAccessControlEnumerable} from "@openzeppelin/contracts-v5.2/access/extensions/IAccessControlEnumerable.sol";
import {AccessControlUpgradeable} from "../AccessControlUpgradeable.sol";
import {EnumerableSet} from "@openzeppelin/contracts-v5.2/utils/structs/EnumerableSet.sol";
import {Initializable} from "../../proxy/utils/Initializable.sol";

/**
 * @dev Extension of {AccessControl} that allows enumerating the members of each role.
 */
abstract contract AccessControlEnumerableUpgradeable is Initializable, IAccessControlEnumerable, AccessControlUpgradeable {
    using EnumerableSet for EnumerableSet.AddressSet;

    /// @custom:storage-location erc7201:openzeppelin.storage.AccessControlEnumerable
    struct AccessControlEnumerableStorage {
        mapping(bytes32 role => EnumerableSet.AddressSet) _roleMembers;
    }

    // keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.AccessControlEnumerable")) - 1)) & ~bytes32(uint256(0xff))
    bytes32 private constant AccessControlEnumerableStorageLocation = 0xc1f6fe24621ce81ec5827caf0253cadb74709b061630e6b55e82371705932000;

    function _getAccessControlEnumerableStorage() private pure returns (AccessControlEnumerableStorage storage $) {
        assembly {
            $.slot := AccessControlEnumerableStorageLocation
        }
    }

    function __AccessControlEnumerable_init() internal onlyInitializing {
    }

    function __AccessControlEnumerable_init_unchained() internal onlyInitializing {
    }
    /**
     * @dev See {IERC165-supportsInterface}.
     */
    function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
        return interfaceId == type(IAccessControlEnumerable).interfaceId || super.supportsInterface(interfaceId);
    }

    /**
     * @dev Returns one of the accounts that have `role`. `index` must be a
     * value between 0 and {getRoleMemberCount}, non-inclusive.
     *
     * Role bearers are not sorted in any particular way, and their ordering may
     * change at any point.
     *
     * WARNING: When using {getRoleMember} and {getRoleMemberCount}, make sure
     * you perform all queries on the same block. See the following
     * https://forum.openzeppelin.com/t/iterating-over-elements-on-enumerableset-in-openzeppelin-contracts/2296[forum post]
     * for more information.
     */
    function getRoleMember(bytes32 role, uint256 index) public view virtual returns (address) {
        AccessControlEnumerableStorage storage $ = _getAccessControlEnumerableStorage();
        return $._roleMembers[role].at(index);
    }

    /**
     * @dev Returns the number of accounts that have `role`. Can be used
     * together with {getRoleMember} to enumerate all bearers of a role.
     */
    function getRoleMemberCount(bytes32 role) public view virtual returns (uint256) {
        AccessControlEnumerableStorage storage $ = _getAccessControlEnumerableStorage();
        return $._roleMembers[role].length();
    }

    /**
     * @dev Return all accounts that have `role`
     *
     * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed
     * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that
     * this function has an unbounded cost, and using it as part of a state-changing function may render the function
     * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.
     */
    function getRoleMembers(bytes32 role) public view virtual returns (address[] memory) {
        AccessControlEnumerableStorage storage $ = _getAccessControlEnumerableStorage();
        return $._roleMembers[role].values();
    }

    /**
     * @dev Overload {AccessControl-_grantRole} to track enumerable memberships
     */
    function _grantRole(bytes32 role, address account) internal virtual override returns (bool) {
        AccessControlEnumerableStorage storage $ = _getAccessControlEnumerableStorage();
        bool granted = super._grantRole(role, account);
        if (granted) {
            $._roleMembers[role].add(account);
        }
        return granted;
    }

    /**
     * @dev Overload {AccessControl-_revokeRole} to track enumerable memberships
     */
    function _revokeRole(bytes32 role, address account) internal virtual override returns (bool) {
        AccessControlEnumerableStorage storage $ = _getAccessControlEnumerableStorage();
        bool revoked = super._revokeRole(role, account);
        if (revoked) {
            $._roleMembers[role].remove(account);
        }
        return revoked;
    }
}

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (proxy/utils/Initializable.sol)

pragma solidity ^0.8.20;

/**
 * @dev This is a base contract to aid in writing upgradeable contracts, or any kind of contract that will be deployed
 * behind a proxy. Since proxied contracts do not make use of a constructor, it's common to move constructor logic to an
 * external initializer function, usually called `initialize`. It then becomes necessary to protect this initializer
 * function so it can only be called once. The {initializer} modifier provided by this contract will have this effect.
 *
 * The initialization functions use a version number. Once a version number is used, it is consumed and cannot be
 * reused. This mechanism prevents re-execution of each "step" but allows the creation of new initialization steps in
 * case an upgrade adds a module that needs to be initialized.
 *
 * For example:
 *
 * [.hljs-theme-light.nopadding]
 * ```solidity
 * contract MyToken is ERC20Upgradeable {
 *     function initialize() initializer public {
 *         __ERC20_init("MyToken", "MTK");
 *     }
 * }
 *
 * contract MyTokenV2 is MyToken, ERC20PermitUpgradeable {
 *     function initializeV2() reinitializer(2) public {
 *         __ERC20Permit_init("MyToken");
 *     }
 * }
 * ```
 *
 * TIP: To avoid leaving the proxy in an uninitialized state, the initializer function should be called as early as
 * possible by providing the encoded function call as the `_data` argument to {ERC1967Proxy-constructor}.
 *
 * CAUTION: When used with inheritance, manual care must be taken to not invoke a parent initializer twice, or to ensure
 * that all initializers are idempotent. This is not verified automatically as constructors are by Solidity.
 *
 * [CAUTION]
 * ====
 * Avoid leaving a contract uninitialized.
 *
 * An uninitialized contract can be taken over by an attacker. This applies to both a proxy and its implementation
 * contract, which may impact the proxy. To prevent the implementation contract from being used, you should invoke
 * the {_disableInitializers} function in the constructor to automatically lock it when it is deployed:
 *
 * [.hljs-theme-light.nopadding]
 * ```
 * /// @custom:oz-upgrades-unsafe-allow constructor
 * constructor() {
 *     _disableInitializers();
 * }
 * ```
 * ====
 */
abstract contract Initializable {
    /**
     * @dev Storage of the initializable contract.
     *
     * It's implemented on a custom ERC-7201 namespace to reduce the risk of storage collisions
     * when using with upgradeable contracts.
     *
     * @custom:storage-location erc7201:openzeppelin.storage.Initializable
     */
    struct InitializableStorage {
        /**
         * @dev Indicates that the contract has been initialized.
         */
        uint64 _initialized;
        /**
         * @dev Indicates that the contract is in the process of being initialized.
         */
        bool _initializing;
    }

    // keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.Initializable")) - 1)) & ~bytes32(uint256(0xff))
    bytes32 private constant INITIALIZABLE_STORAGE = 0xf0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00;

    /**
     * @dev The contract is already initialized.
     */
    error InvalidInitialization();

    /**
     * @dev The contract is not initializing.
     */
    error NotInitializing();

    /**
     * @dev Triggered when the contract has been initialized or reinitialized.
     */
    event Initialized(uint64 version);

    /**
     * @dev A modifier that defines a protected initializer function that can be invoked at most once. In its scope,
     * `onlyInitializing` functions can be used to initialize parent contracts.
     *
     * Similar to `reinitializer(1)`, except that in the context of a constructor an `initializer` may be invoked any
     * number of times. This behavior in the constructor can be useful during testing and is not expected to be used in
     * production.
     *
     * Emits an {Initialized} event.
     */
    modifier initializer() {
        // solhint-disable-next-line var-name-mixedcase
        InitializableStorage storage $ = _getInitializableStorage();

        // Cache values to avoid duplicated sloads
        bool isTopLevelCall = !$._initializing;
        uint64 initialized = $._initialized;

        // Allowed calls:
        // - initialSetup: the contract is not in the initializing state and no previous version was
        //                 initialized
        // - construction: the contract is initialized at version 1 (no reininitialization) and the
        //                 current contract is just being deployed
        bool initialSetup = initialized == 0 && isTopLevelCall;
        bool construction = initialized == 1 && address(this).code.length == 0;

        if (!initialSetup && !construction) {
            revert InvalidInitialization();
        }
        $._initialized = 1;
        if (isTopLevelCall) {
            $._initializing = true;
        }
        _;
        if (isTopLevelCall) {
            $._initializing = false;
            emit Initialized(1);
        }
    }

    /**
     * @dev A modifier that defines a protected reinitializer function that can be invoked at most once, and only if the
     * contract hasn't been initialized to a greater version before. In its scope, `onlyInitializing` functions can be
     * used to initialize parent contracts.
     *
     * A reinitializer may be used after the original initialization step. This is essential to configure modules that
     * are added through upgrades and that require initialization.
     *
     * When `version` is 1, this modifier is similar to `initializer`, except that functions marked with `reinitializer`
     * cannot be nested. If one is invoked in the context of another, execution will revert.
     *
     * Note that versions can jump in increments greater than 1; this implies that if multiple reinitializers coexist in
     * a contract, executing them in the right order is up to the developer or operator.
     *
     * WARNING: Setting the version to 2**64 - 1 will prevent any future reinitialization.
     *
     * Emits an {Initialized} event.
     */
    modifier reinitializer(uint64 version) {
        // solhint-disable-next-line var-name-mixedcase
        InitializableStorage storage $ = _getInitializableStorage();

        if ($._initializing || $._initialized >= version) {
            revert InvalidInitialization();
        }
        $._initialized = version;
        $._initializing = true;
        _;
        $._initializing = false;
        emit Initialized(version);
    }

    /**
     * @dev Modifier to protect an initialization function so that it can only be invoked by functions with the
     * {initializer} and {reinitializer} modifiers, directly or indirectly.
     */
    modifier onlyInitializing() {
        _checkInitializing();
        _;
    }

    /**
     * @dev Reverts if the contract is not in an initializing state. See {onlyInitializing}.
     */
    function _checkInitializing() internal view virtual {
        if (!_isInitializing()) {
            revert NotInitializing();
        }
    }

    /**
     * @dev Locks the contract, preventing any future reinitialization. This cannot be part of an initializer call.
     * Calling this in the constructor of a contract will prevent that contract from being initialized or reinitialized
     * to any version. It is recommended to use this to lock implementation contracts that are designed to be called
     * through proxies.
     *
     * Emits an {Initialized} event the first time it is successfully executed.
     */
    function _disableInitializers() internal virtual {
        // solhint-disable-next-line var-name-mixedcase
        InitializableStorage storage $ = _getInitializableStorage();

        if ($._initializing) {
            revert InvalidInitialization();
        }
        if ($._initialized != type(uint64).max) {
            $._initialized = type(uint64).max;
            emit Initialized(type(uint64).max);
        }
    }

    /**
     * @dev Returns the highest version that has been initialized. See {reinitializer}.
     */
    function _getInitializedVersion() internal view returns (uint64) {
        return _getInitializableStorage()._initialized;
    }

    /**
     * @dev Returns `true` if the contract is currently initializing. See {onlyInitializing}.
     */
    function _isInitializing() internal view returns (bool) {
        return _getInitializableStorage()._initializing;
    }

    /**
     * @dev Returns a pointer to the storage namespace.
     */
    // solhint-disable-next-line var-name-mixedcase
    function _getInitializableStorage() private pure returns (InitializableStorage storage $) {
        assembly {
            $.slot := INITIALIZABLE_STORAGE
        }
    }
}

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.1) (utils/Context.sol)

pragma solidity ^0.8.20;
import {Initializable} from "../proxy/utils/Initializable.sol";

/**
 * @dev Provides information about the current execution context, including the
 * sender of the transaction and its data. While these are generally available
 * via msg.sender and msg.data, they should not be accessed in such a direct
 * manner, since when dealing with meta-transactions the account sending and
 * paying for execution may not be the actual sender (as far as an application
 * is concerned).
 *
 * This contract is only required for intermediate, library-like contracts.
 */
abstract contract ContextUpgradeable is Initializable {
    function __Context_init() internal onlyInitializing {
    }

    function __Context_init_unchained() internal onlyInitializing {
    }
    function _msgSender() internal view virtual returns (address) {
        return msg.sender;
    }

    function _msgData() internal view virtual returns (bytes calldata) {
        return msg.data;
    }

    function _contextSuffixLength() internal view virtual returns (uint256) {
        return 0;
    }
}

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (utils/introspection/ERC165.sol)

pragma solidity ^0.8.20;

import {IERC165} from "@openzeppelin/contracts-v5.2/utils/introspection/IERC165.sol";
import {Initializable} from "../../proxy/utils/Initializable.sol";

/**
 * @dev Implementation of the {IERC165} interface.
 *
 * Contracts that want to implement ERC-165 should inherit from this contract and override {supportsInterface} to check
 * for the additional interface id that will be supported. For example:
 *
 * ```solidity
 * function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
 *     return interfaceId == type(MyInterface).interfaceId || super.supportsInterface(interfaceId);
 * }
 * ```
 */
abstract contract ERC165Upgradeable is Initializable, IERC165 {
    function __ERC165_init() internal onlyInitializing {
    }

    function __ERC165_init_unchained() internal onlyInitializing {
    }
    /**
     * @dev See {IERC165-supportsInterface}.
     */
    function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) {
        return interfaceId == type(IERC165).interfaceId;
    }
}

Settings
{
  "optimizer": {
    "enabled": true,
    "runs": 200
  },
  "viaIR": true,
  "evmVersion": "cancun",
  "outputSelection": {
    "*": {
      "*": [
        "evm.bytecode",
        "evm.deployedBytecode",
        "devdoc",
        "userdoc",
        "metadata",
        "abi"
      ]
    }
  }
}

Contract ABI

API
[{"inputs":[{"internalType":"address","name":"_stETH","type":"address"},{"internalType":"address","name":"_wstETH","type":"address"},{"internalType":"address","name":"_vaultHub","type":"address"},{"internalType":"address","name":"_lidoLocator","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"AbnormallyHighFee","type":"error"},{"inputs":[],"name":"AccessControlBadConfirmation","type":"error"},{"inputs":[{"internalType":"address","name":"account","type":"address"},{"internalType":"bytes32","name":"neededRole","type":"bytes32"}],"name":"AccessControlUnauthorizedAccount","type":"error"},{"inputs":[],"name":"AlreadyInitialized","type":"error"},{"inputs":[],"name":"ConfirmExpiryOutOfBounds","type":"error"},{"inputs":[],"name":"ConnectedToVaultHub","type":"error"},{"inputs":[],"name":"CorrectionAfterReport","type":"error"},{"inputs":[],"name":"DashboardNotAllowed","type":"error"},{"inputs":[{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"EthTransferFailed","type":"error"},{"inputs":[{"internalType":"uint256","name":"requestedShares","type":"uint256"},{"internalType":"uint256","name":"remainingShares","type":"uint256"}],"name":"ExceedsMintingCapacity","type":"error"},{"inputs":[{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"uint256","name":"withdrawableValue","type":"uint256"}],"name":"ExceedsWithdrawable","type":"error"},{"inputs":[],"name":"FeeValueExceed100Percent","type":"error"},{"inputs":[],"name":"ForbiddenByPDGPolicy","type":"error"},{"inputs":[],"name":"PDGPolicyAlreadyActive","type":"error"},{"inputs":[],"name":"ReportStale","type":"error"},{"inputs":[{"internalType":"uint8","name":"bits","type":"uint8"},{"internalType":"int256","name":"value","type":"int256"}],"name":"SafeCastOverflowedIntDowncast","type":"error"},{"inputs":[{"internalType":"address","name":"token","type":"address"}],"name":"SafeERC20FailedOperation","type":"error"},{"inputs":[],"name":"SameRecipient","type":"error"},{"inputs":[],"name":"SameSettledGrowth","type":"error"},{"inputs":[],"name":"SenderNotMember","type":"error"},{"inputs":[],"name":"SettledGrowthMismatch","type":"error"},{"inputs":[],"name":"TierChangeNotConfirmed","type":"error"},{"inputs":[],"name":"UnexpectedFeeExemptionAmount","type":"error"},{"inputs":[],"name":"UnexpectedSettledGrowth","type":"error"},{"inputs":[],"name":"VaultQuarantined","type":"error"},{"inputs":[],"name":"ZeroAddress","type":"error"},{"inputs":[],"name":"ZeroArgument","type":"error"},{"inputs":[],"name":"ZeroConfirmingRoles","type":"error"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":true,"internalType":"address","name":"assetAddress","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"AssetsRecovered","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"sender","type":"address"},{"indexed":false,"internalType":"uint256","name":"oldConfirmExpiry","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"newConfirmExpiry","type":"uint256"}],"name":"ConfirmExpirySet","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"timestamp","type":"uint256"}],"name":"CorrectionTimestampUpdated","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"sender","type":"address"},{"indexed":false,"internalType":"uint256","name":"fee","type":"uint256"}],"name":"FeeDisbursed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"sender","type":"address"},{"indexed":false,"internalType":"uint256","name":"oldFeeRate","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"newFeeRate","type":"uint256"}],"name":"FeeRateSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"sender","type":"address"},{"indexed":false,"internalType":"address","name":"oldFeeRecipient","type":"address"},{"indexed":false,"internalType":"address","name":"newFeeRecipient","type":"address"}],"name":"FeeRecipientSet","type":"event"},{"anonymous":false,"inputs":[],"name":"Initialized","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"enum Dashboard.PDGPolicy","name":"pdgPolicy","type":"uint8"}],"name":"PDGPolicyEnacted","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"role","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"previousAdminRole","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"newAdminRole","type":"bytes32"}],"name":"RoleAdminChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"role","type":"bytes32"},{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":true,"internalType":"address","name":"sender","type":"address"}],"name":"RoleGranted","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"member","type":"address"},{"indexed":true,"internalType":"bytes32","name":"roleOrAddress","type":"bytes32"},{"indexed":false,"internalType":"uint256","name":"confirmTimestamp","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"expiryTimestamp","type":"uint256"},{"indexed":false,"internalType":"bytes","name":"data","type":"bytes"}],"name":"RoleMemberConfirmed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"role","type":"bytes32"},{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":true,"internalType":"address","name":"sender","type":"address"}],"name":"RoleRevoked","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"int128","name":"oldSettledGrowth","type":"int128"},{"indexed":false,"internalType":"int128","name":"newSettledGrowth","type":"int128"}],"name":"SettledGrowthSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"stakingVault","type":"address"},{"indexed":false,"internalType":"uint256","name":"deposits","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"totalAmount","type":"uint256"}],"name":"UnguaranteedDeposits","type":"event"},{"inputs":[],"name":"BURN_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"COLLECT_VAULT_ERC20_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"DEFAULT_ADMIN_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"FUND_ON_RECEIVE_FLAG_SLOT","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"FUND_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"LIDO_LOCATOR","outputs":[{"internalType":"contract ILidoLocator","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MAX_CONFIRM_EXPIRY","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MINT_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MIN_CONFIRM_EXPIRY","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"NODE_OPERATOR_FEE_EXEMPT_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"NODE_OPERATOR_MANAGER_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"NODE_OPERATOR_PROVE_UNKNOWN_VALIDATOR_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"NODE_OPERATOR_UNGUARANTEED_DEPOSIT_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"PAUSE_BEACON_CHAIN_DEPOSITS_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"REBALANCE_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"REQUEST_VALIDATOR_EXIT_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"RESUME_BEACON_CHAIN_DEPOSITS_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"STETH","outputs":[{"internalType":"contract ILido","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"TRIGGER_VALIDATOR_WITHDRAWAL_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"VAULT_CONFIGURATION_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"VAULT_HUB","outputs":[{"internalType":"contract VaultHub","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"VOLUNTARY_DISCONNECT_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"WITHDRAW_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"WSTETH","outputs":[{"internalType":"contract IWstETH","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_newOwner","type":"address"}],"name":"abandonDashboard","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"accruedFee","outputs":[{"internalType":"uint256","name":"fee","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_exemptedAmount","type":"uint256"}],"name":"addFeeExemption","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_amountOfShares","type":"uint256"}],"name":"burnShares","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_amountOfStETH","type":"uint256"}],"name":"burnStETH","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_amountOfWstETH","type":"uint256"}],"name":"burnWstETH","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_tierId","type":"uint256"},{"internalType":"uint256","name":"_requestedShareLimit","type":"uint256"}],"name":"changeTier","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"},{"internalType":"address","name":"_recipient","type":"address"},{"internalType":"uint256","name":"_amount","type":"uint256"}],"name":"collectERC20FromVault","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes","name":"_callData","type":"bytes"},{"internalType":"bytes32","name":"_role","type":"bytes32"}],"name":"confirmation","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"confirmingRoles","outputs":[{"internalType":"bytes32[]","name":"roles","type":"bytes32[]"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"_tierId","type":"uint256"},{"internalType":"uint256","name":"_requestedShareLimit","type":"uint256"},{"internalType":"uint256","name":"_currentSettledGrowth","type":"uint256"}],"name":"connectAndAcceptTier","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_currentSettledGrowth","type":"uint256"}],"name":"connectToVaultHub","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"int256","name":"_newSettledGrowth","type":"int256"},{"internalType":"int256","name":"_expectedSettledGrowth","type":"int256"}],"name":"correctSettledGrowth","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"disburseAbnormallyHighFee","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"disburseFee","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"feeRate","outputs":[{"internalType":"uint16","name":"","type":"uint16"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"feeRecipient","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"fund","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"getConfirmExpiry","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"}],"name":"getRoleAdmin","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"uint256","name":"index","type":"uint256"}],"name":"getRoleMember","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"}],"name":"getRoleMemberCount","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"}],"name":"getRoleMembers","outputs":[{"internalType":"address[]","name":"","type":"address[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"grantRole","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"account","type":"address"},{"internalType":"bytes32","name":"role","type":"bytes32"}],"internalType":"struct Permissions.RoleAssignment[]","name":"_assignments","type":"tuple[]"}],"name":"grantRoles","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"hasRole","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"healthShortfallShares","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_defaultAdmin","type":"address"},{"internalType":"address","name":"_nodeOperatorManager","type":"address"},{"internalType":"address","name":"_nodeOperatorFeeRecipient","type":"address"},{"internalType":"uint256","name":"_nodeOperatorFeeBP","type":"uint256"},{"internalType":"uint256","name":"_confirmExpiry","type":"uint256"}],"name":"initialize","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"initialized","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"latestCorrectionTimestamp","outputs":[{"internalType":"uint64","name":"","type":"uint64"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"latestReport","outputs":[{"components":[{"internalType":"uint104","name":"totalValue","type":"uint104"},{"internalType":"int104","name":"inOutDelta","type":"int104"},{"internalType":"uint48","name":"timestamp","type":"uint48"}],"internalType":"struct VaultHub.Report","name":"","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"liabilityShares","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"locked","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"maxLockableValue","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"minimalReserve","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_recipient","type":"address"},{"internalType":"uint256","name":"_amountOfShares","type":"uint256"}],"name":"mintShares","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"_recipient","type":"address"},{"internalType":"uint256","name":"_amountOfStETH","type":"uint256"}],"name":"mintStETH","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"_recipient","type":"address"},{"internalType":"uint256","name":"_amountOfWstETH","type":"uint256"}],"name":"mintWstETH","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"obligations","outputs":[{"internalType":"uint256","name":"sharesToBurn","type":"uint256"},{"internalType":"uint256","name":"feesToSettle","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"obligationsShortfallValue","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"pauseBeaconChainDeposits","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"pdgPolicy","outputs":[{"internalType":"enum Dashboard.PDGPolicy","name":"","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[{"components":[{"internalType":"bytes32[]","name":"proof","type":"bytes32[]"},{"internalType":"bytes","name":"pubkey","type":"bytes"},{"internalType":"uint256","name":"validatorIndex","type":"uint256"},{"internalType":"uint64","name":"childBlockTimestamp","type":"uint64"},{"internalType":"uint64","name":"slot","type":"uint64"},{"internalType":"uint64","name":"proposerIndex","type":"uint64"}],"internalType":"struct IPredepositGuarantee.ValidatorWitness[]","name":"_witnesses","type":"tuple[]"}],"name":"proveUnknownValidatorsToPDG","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_ether","type":"uint256"}],"name":"rebalanceVaultWithEther","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_shares","type":"uint256"}],"name":"rebalanceVaultWithShares","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_currentSettledGrowth","type":"uint256"}],"name":"reconnectToVaultHub","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"},{"internalType":"address","name":"_recipient","type":"address"},{"internalType":"uint256","name":"_amount","type":"uint256"}],"name":"recoverERC20","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_etherToFund","type":"uint256"}],"name":"remainingMintingCapacityShares","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"callerConfirmation","type":"address"}],"name":"renounceRole","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes","name":"_pubkeys","type":"bytes"}],"name":"requestValidatorExit","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"resumeBeaconChainDeposits","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"revokeRole","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"account","type":"address"},{"internalType":"bytes32","name":"role","type":"bytes32"}],"internalType":"struct Permissions.RoleAssignment[]","name":"_assignments","type":"tuple[]"}],"name":"revokeRoles","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_newConfirmExpiry","type":"uint256"}],"name":"setConfirmExpiry","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_newFeeRate","type":"uint256"}],"name":"setFeeRate","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_newFeeRecipient","type":"address"}],"name":"setFeeRecipient","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"enum Dashboard.PDGPolicy","name":"_pdgPolicy","type":"uint8"}],"name":"setPDGPolicy","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"settledGrowth","outputs":[{"internalType":"int128","name":"","type":"int128"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"stakingVault","outputs":[{"internalType":"contract IStakingVault","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes4","name":"interfaceId","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"syncTier","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"totalMintingCapacityShares","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalValue","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_newOwner","type":"address"}],"name":"transferVaultOwnership","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes","name":"_pubkeys","type":"bytes"},{"internalType":"uint64[]","name":"_amountsInGwei","type":"uint64[]"},{"internalType":"address","name":"_refundRecipient","type":"address"}],"name":"triggerValidatorWithdrawals","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"components":[{"internalType":"bytes","name":"pubkey","type":"bytes"},{"internalType":"bytes","name":"signature","type":"bytes"},{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"bytes32","name":"depositDataRoot","type":"bytes32"}],"internalType":"struct IStakingVault.Deposit[]","name":"_deposits","type":"tuple[]"}],"name":"unguaranteedDepositToBeaconChain","outputs":[{"internalType":"uint256","name":"totalAmount","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_requestedShareLimit","type":"uint256"}],"name":"updateShareLimit","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"vaultConnection","outputs":[{"components":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"uint96","name":"shareLimit","type":"uint96"},{"internalType":"uint96","name":"vaultIndex","type":"uint96"},{"internalType":"uint48","name":"disconnectInitiatedTs","type":"uint48"},{"internalType":"uint16","name":"reserveRatioBP","type":"uint16"},{"internalType":"uint16","name":"forcedRebalanceThresholdBP","type":"uint16"},{"internalType":"uint16","name":"infraFeeBP","type":"uint16"},{"internalType":"uint16","name":"liquidityFeeBP","type":"uint16"},{"internalType":"uint16","name":"reservationFeeBP","type":"uint16"},{"internalType":"bool","name":"beaconChainDepositsPauseIntent","type":"bool"}],"internalType":"struct VaultHub.VaultConnection","name":"","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"voluntaryDisconnect","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_recipient","type":"address"},{"internalType":"uint256","name":"_ether","type":"uint256"}],"name":"withdraw","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"withdrawableValue","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"stateMutability":"payable","type":"receive"}]

Block Uncle Number Difficulty Gas Used Reward
View All Uncles
Loading...
Loading
0xC0F6BaAABA3397FDeD43a0f4e421D2F4574dfc04
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.