From 13d7c37d73b7a780502189ef1f7337ca69c03948 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Thu, 28 May 2026 20:12:34 +1000 Subject: [PATCH 01/29] New CompoundingStakingStrategy without SSV functions. Refactored CompoundingStakingSSVStrategy to inherit CompoundingStakingStrategy. Removed ConsolidationController. Removed migrateClusterToETH. --- contracts/contracts/proxies/Proxies.sol | 9 + .../CompoundingStakingSSVStrategy.sol | 258 ++- ...ger.sol => CompoundingStakingStrategy.sol} | 482 +++--- .../NativeStaking/CompoundingStakingView.sol | 10 +- .../NativeStaking/ConsolidationController.sol | 499 ------ .../strategies/NativeStaking/README.md | 6 - ...197_deploy_compounding_staking_strategy.js | 99 ++ contracts/test/_fixture.js | 85 +- .../test/strategies/compoundingStaking.js | 115 ++ .../stakingConsolidation.mainnet.fork-test.js | 1408 ----------------- 10 files changed, 692 insertions(+), 2279 deletions(-) rename contracts/contracts/strategies/NativeStaking/{CompoundingValidatorManager.sol => CompoundingStakingStrategy.sol} (83%) delete mode 100644 contracts/contracts/strategies/NativeStaking/ConsolidationController.sol create mode 100644 contracts/deploy/mainnet/197_deploy_compounding_staking_strategy.js create mode 100644 contracts/test/strategies/compoundingStaking.js delete mode 100644 contracts/test/strategies/stakingConsolidation.mainnet.fork-test.js diff --git a/contracts/contracts/proxies/Proxies.sol b/contracts/contracts/proxies/Proxies.sol index 0677a7dbcd..1190afac45 100644 --- a/contracts/contracts/proxies/Proxies.sol +++ b/contracts/contracts/proxies/Proxies.sol @@ -236,6 +236,15 @@ contract CompoundingStakingSSVStrategyProxy is } +/** + * @notice CompoundingStakingStrategyProxy delegates calls to a CompoundingStakingStrategy implementation + */ +contract CompoundingStakingStrategyProxy is + InitializeGovernedUpgradeabilityProxy +{ + +} + /** * @notice OUSDMorphoV2StrategyProxy delegates calls to a Generalized4626Strategy implementation */ diff --git a/contracts/contracts/strategies/NativeStaking/CompoundingStakingSSVStrategy.sol b/contracts/contracts/strategies/NativeStaking/CompoundingStakingSSVStrategy.sol index 3ee62da8af..11a0a29089 100644 --- a/contracts/contracts/strategies/NativeStaking/CompoundingStakingSSVStrategy.sol +++ b/contracts/contracts/strategies/NativeStaking/CompoundingStakingSSVStrategy.sol @@ -1,22 +1,25 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.0; -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - -import { InitializableAbstractStrategy } from "../../utils/InitializableAbstractStrategy.sol"; -import { IWETH9 } from "../../interfaces/IWETH9.sol"; -import { CompoundingValidatorManager } from "./CompoundingValidatorManager.sol"; +import { ISSVNetwork, Cluster } from "../../interfaces/ISSVNetwork.sol"; +import { CompoundingStakingStrategy } from "./CompoundingStakingStrategy.sol"; /// @title Compounding Staking SSV Strategy /// @notice Strategy to deploy funds into DVT validators powered by the SSV Network /// @author Origin Protocol Inc -contract CompoundingStakingSSVStrategy is - CompoundingValidatorManager, - InitializableAbstractStrategy -{ +contract CompoundingStakingSSVStrategy is CompoundingStakingStrategy { + /// @notice The address of the SSV Network contract used to interface with + address internal immutable SSV_NETWORK; + // For future use uint256[50] private __gap; + event SSVValidatorRegistered( + bytes32 indexed pubKeyHash, + uint64[] operatorIds + ); + event SSVValidatorRemoved(bytes32 indexed pubKeyHash, uint64[] operatorIds); + /// @param _baseConfig Base strategy config with /// `platformAddress` not used so empty address /// `vaultAddress` the address of the OETH Vault contract @@ -33,187 +36,110 @@ contract CompoundingStakingSSVStrategy is address _beaconProofs, uint64 _beaconGenesisTimestamp ) - InitializableAbstractStrategy(_baseConfig) - CompoundingValidatorManager( + CompoundingStakingStrategy( + _baseConfig, _wethAddress, - _baseConfig.vaultAddress, _beaconChainDepositContract, - _ssvNetwork, _beaconProofs, _beaconGenesisTimestamp ) { - // Make sure nobody owns the implementation contract - _setGovernor(address(0)); - } - - /// @notice Set up initial internal state including - /// 1. approving the SSVNetwork to transfer SSV tokens from this strategy contract - /// @param _rewardTokenAddresses Not used so empty array - /// @param _assets Not used so empty array - /// @param _pTokens Not used so empty array - /// @param _initialDepositAmountWei The amount of ETH required for the first deposit to a new validator. - function initialize( - address[] memory _rewardTokenAddresses, - address[] memory _assets, - address[] memory _pTokens, - uint256 _initialDepositAmountWei - ) external onlyGovernor initializer { - InitializableAbstractStrategy._initialize( - _rewardTokenAddresses, - _assets, - _pTokens - ); - _setInitialDepositAmountWei(_initialDepositAmountWei); + SSV_NETWORK = _ssvNetwork; } - /// @notice Unlike other strategies, this does not deposit assets into the underlying platform. - /// It just checks the asset is WETH and emits the Deposit event. - /// To deposit WETH into validators, `registerSsvValidator` and `stakeEth` must be used. - /// @param _asset Address of the WETH token. - /// @param _amount Amount of WETH that was transferred to the strategy by the vault. - function deposit(address _asset, uint256 _amount) - external - override - onlyVault - nonReentrant - { - require(_asset == WETH, "Unsupported asset"); - require(_amount > 0, "Must deposit something"); - - // Account for the new WETH - depositedWethAccountedFor += _amount; + /** + * + * Validator Management + * + */ - emit Deposit(_asset, address(0), _amount); - } + /// @notice Registers a single validator in a SSV Cluster. + /// Only the Registrator can call this function. + /// @param publicKey The public key of the validator + /// @param operatorIds The operator IDs of the SSV Cluster + /// @param sharesData The shares data for the validator + /// @param cluster The SSV cluster details including the validator count and SSV balance + // slither-disable-start reentrancy-no-eth + function registerSsvValidator( + bytes calldata publicKey, + uint64[] calldata operatorIds, + bytes calldata sharesData, + Cluster calldata cluster + ) external payable onlyRegistrator whenNotPaused { + // Hash the public key using the Beacon Chain's format + bytes32 pubKeyHash = _hashPubKey(publicKey); + // Check each public key has not already been used + require( + validator[pubKeyHash].state == ValidatorState.NON_REGISTERED, + "Validator already registered" + ); - /// @notice Unlike other strategies, this does not deposit assets into the underlying platform. - /// It just emits the Deposit event. - /// To deposit WETH into validators `registerSsvValidator` and `stakeEth` must be used. - function depositAll() external override onlyVault nonReentrant { - uint256 wethBalance = IERC20(WETH).balanceOf(address(this)); - uint256 newWeth = wethBalance - depositedWethAccountedFor; + // Store the validator state as registered + validator[pubKeyHash].state = ValidatorState.REGISTERED; - if (newWeth > 0) { - // Account for the new WETH - depositedWethAccountedFor = wethBalance; + ISSVNetwork(SSV_NETWORK).registerValidator{ value: msg.value }( + publicKey, + operatorIds, + sharesData, + cluster + ); - emit Deposit(WETH, address(0), newWeth); - } + emit SSVValidatorRegistered(pubKeyHash, operatorIds); } - /// @notice Withdraw ETH and WETH from this strategy contract. - /// @param _recipient Address to receive withdrawn assets. - /// @param _asset Address of the WETH token. - /// @param _amount Amount of WETH to withdraw. - function withdraw( - address _recipient, - address _asset, - uint256 _amount - ) external override nonReentrant { - require(_asset == WETH, "Unsupported asset"); + /// @notice Remove the validator from the SSV Cluster after: + /// - the validator has been exited from `validatorWithdrawal` or slashed + /// - the validator has incorrectly registered and can not be staked to + /// - the initial deposit was front-run and the withdrawal address is not this strategy's address. + /// Make sure `validatorWithdrawal` is called with a zero amount and the validator has exited the Beacon chain. + /// If removed before the validator has exited the beacon chain will result in the validator being slashed. + /// Only the registrator can call this function. + /// @param publicKey The public key of the validator + /// @param operatorIds The operator IDs of the SSV Cluster + /// @param cluster The SSV cluster details including the validator count and SSV balance + function removeSsvValidator( + bytes calldata publicKey, + uint64[] calldata operatorIds, + Cluster calldata cluster + ) external onlyRegistrator { + // Hash the public key using the Beacon Chain's format + bytes32 pubKeyHash = _hashPubKey(publicKey); + ValidatorState currentState = validator[pubKeyHash].state; + // Can remove SSV validators that were incorrectly registered and can not be deposited to. require( - msg.sender == vaultAddress || msg.sender == validatorRegistrator, - "Caller not Vault or Registrator" + currentState == ValidatorState.REGISTERED || + currentState == ValidatorState.EXITED || + currentState == ValidatorState.INVALID, + "Validator not regd or exited" ); - _withdraw(_recipient, _amount, address(this).balance); - } - - function _withdraw( - address _recipient, - uint256 _withdrawAmount, - uint256 _ethBalance - ) internal { - require(_withdrawAmount > 0, "Must withdraw something"); - require(_recipient == vaultAddress, "Recipient not Vault"); - - // Convert any ETH from validator partial withdrawals, exits - // or execution rewards to WETH and do the necessary accounting. - if (_ethBalance > 0) _convertEthToWeth(_ethBalance); + validator[pubKeyHash].state = ValidatorState.REMOVED; - // Transfer WETH to the recipient and do the necessary accounting. - _transferWeth(_withdrawAmount, _recipient); + ISSVNetwork(SSV_NETWORK).removeValidator( + publicKey, + operatorIds, + cluster + ); - emit Withdrawal(WETH, address(0), _withdrawAmount); + emit SSVValidatorRemoved(pubKeyHash, operatorIds); } - /// @notice Transfer all WETH deposits, ETH from validator withdrawals and ETH from - /// execution rewards in this strategy to the vault. - /// This does not withdraw from the validators. That has to be done separately with the - /// `validatorWithdrawal` operation. - function withdrawAll() external override onlyVaultOrGovernor nonReentrant { - uint256 ethBalance = address(this).balance; - uint256 withdrawAmount = IERC20(WETH).balanceOf(address(this)) + - ethBalance; - - if (withdrawAmount > 0) { - _withdraw(vaultAddress, withdrawAmount, ethBalance); - } - } + // slither-disable-end reentrancy-no-eth - /// @notice Accounts for all the assets managed by this strategy which includes: - /// 1. The current WETH in this strategy contract - /// 2. The last verified ETH balance, total deposits and total validator balances - /// @param _asset Address of WETH asset. - /// @return balance Total value in ETH - function checkBalance(address _asset) - external - view + function _admitStake(bytes32 pubKeyHash, uint256 depositAmountWei) + internal override - returns (uint256 balance) { - require(_asset == WETH, "Unsupported asset"); - - // Load the last verified balance from the storage - // and add to the latest WETH balance of this strategy. - balance = - lastVerifiedEthBalance + - IWETH9(WETH).balanceOf(address(this)); - } - - /// @notice Returns bool indicating whether asset is supported by the strategy. - /// @param _asset The address of the WETH token. - function supportsAsset(address _asset) public view override returns (bool) { - return _asset == WETH; - } - - /// @notice Does nothing but needed as this function is abstract on InitializableAbstractStrategy - /// @dev Use to be used to approve SSV tokens but that is no longer used by the SSV Network. - function safeApproveAllTokens() public override {} - - /** - * @notice We can accept ETH directly to this contract from anyone as it does not impact our accounting - * like it did in the legacy NativeStakingStrategy. - * The new ETH will be accounted for in `checkBalance` after the next snapBalances and verifyBalances txs. - */ - receive() external payable {} - - /*************************************** - Internal functions - ****************************************/ - - /// @notice is not supported for this strategy as there is no platform token. - function setPTokenAddress(address, address) external pure override { - revert("Unsupported function"); - } - - /// @notice is not supported for this strategy as there is no platform token. - function removePToken(uint256) external pure override { - revert("Unsupported function"); - } + ValidatorState currentState = validator[pubKeyHash].state; + require( + currentState == ValidatorState.REGISTERED || + currentState == ValidatorState.VERIFIED || + currentState == ValidatorState.ACTIVE, + "Not registered or verified" + ); - /// @dev This strategy does not use a platform token like the old Aave and Compound strategies. - function _abstractSetPToken(address _asset, address) internal override {} - - /// @dev Consensus rewards are compounded to the validator's balance instead of being - /// swept to this strategy contract. - /// Execution rewards from MEV and tx priority accumulate as ETH in this strategy contract. - /// Withdrawals from validators also accumulate as ETH in this strategy contract. - /// It's too complex to separate the rewards from withdrawals so this function is not implemented. - /// Besides, ETH rewards are not sent to the Dripper any more. The Vault can now regulate - /// the increase in assets. - function _collectRewardTokens() internal pure override { - revert("Unsupported function"); + if (currentState == ValidatorState.REGISTERED) { + _recordFirstDeposit(pubKeyHash, depositAmountWei); + } } } diff --git a/contracts/contracts/strategies/NativeStaking/CompoundingValidatorManager.sol b/contracts/contracts/strategies/NativeStaking/CompoundingStakingStrategy.sol similarity index 83% rename from contracts/contracts/strategies/NativeStaking/CompoundingValidatorManager.sol rename to contracts/contracts/strategies/NativeStaking/CompoundingStakingStrategy.sol index 3ed478d156..0f34825d05 100644 --- a/contracts/contracts/strategies/NativeStaking/CompoundingValidatorManager.sol +++ b/contracts/contracts/strategies/NativeStaking/CompoundingStakingStrategy.sol @@ -9,20 +9,18 @@ import { Pausable } from "@openzeppelin/contracts/security/Pausable.sol"; import { Governable } from "../../governance/Governable.sol"; import { IDepositContract } from "../../interfaces/IDepositContract.sol"; import { IWETH9 } from "../../interfaces/IWETH9.sol"; -import { ISSVNetwork, Cluster } from "../../interfaces/ISSVNetwork.sol"; import { BeaconRoots } from "../../beacon/BeaconRoots.sol"; import { PartialWithdrawal } from "../../beacon/PartialWithdrawal.sol"; import { IBeaconProofs } from "../../interfaces/IBeaconProofs.sol"; +import { InitializableAbstractStrategy } from "../../utils/InitializableAbstractStrategy.sol"; /** - * @title Validator lifecycle management contract + * @title Compounding validator strategy * @notice This contract implements all the required functionality to - * register, deposit, withdraw, exit and remove validators. + * deposit, withdraw and account for vanilla compounding validators. * @author Origin Protocol Inc */ -abstract contract CompoundingValidatorManager is Governable, Pausable { - using SafeERC20 for IERC20; - +abstract contract CompoundingValidatorStorage is Governable, Pausable { /// @dev Validator balances over this amount will eventually become active on the beacon chain. /// Due to hysteresis, if the effective balance is 31 ETH, the actual balance /// must rise to 32.25 ETH to trigger an effective balance update to 32 ETH. @@ -52,8 +50,6 @@ abstract contract CompoundingValidatorManager is Governable, Pausable { address internal immutable WETH; /// @notice The address of the beacon chain deposit contract address internal immutable BEACON_CHAIN_DEPOSIT_CONTRACT; - /// @notice The address of the SSV Network contract used to interface with - address internal immutable SSV_NETWORK; /// @notice Address of the OETH Vault proxy contract address internal immutable VAULT_ADDRESS; /// @notice Address of the Beacon Proofs contract that verifies beacon chain data @@ -118,14 +114,14 @@ abstract contract CompoundingValidatorManager is Governable, Pausable { bytes32[] public depositList; enum ValidatorState { - NON_REGISTERED, // validator is not registered on the SSV network - REGISTERED, // validator is registered on the SSV network + NON_REGISTERED, // validator has not received its first deposit + REGISTERED, // validator has been registered by an extension contract STAKED, // validator has funds staked VERIFIED, // validator has been verified to exist on the beacon chain ACTIVE, // The validator balance is at least 32.25 ETH. The validator may not yet be active on the beacon chain. EXITING, // The validator has been requested to exit EXITED, // The validator has been verified to have a zero balance - REMOVED, // validator has funds withdrawn to this strategy contract and is removed from the SSV + REMOVED, // validator has been removed by an extension contract INVALID // The validator has been front-run and the withdrawal address is not this strategy } @@ -172,14 +168,67 @@ abstract contract CompoundingValidatorManager is Governable, Pausable { // For future use uint256[40] private __gap; + /// @param _wethAddress Address of the Erc20 WETH Token contract + /// @param _vaultAddress Address of the Vault + /// @param _beaconChainDepositContract Address of the beacon chain deposit contract + /// @param _beaconProofs Address of the Beacon Proofs contract that verifies beacon chain data + /// @param _beaconGenesisTimestamp The timestamp of the Beacon chain's genesis. + constructor( + address _wethAddress, + address _vaultAddress, + address _beaconChainDepositContract, + address _beaconProofs, + uint64 _beaconGenesisTimestamp + ) { + WETH = _wethAddress; + BEACON_CHAIN_DEPOSIT_CONTRACT = _beaconChainDepositContract; + VAULT_ADDRESS = _vaultAddress; + BEACON_PROOFS = _beaconProofs; + BEACON_GENESIS_TIMESTAMP = _beaconGenesisTimestamp; + + require( + block.timestamp > _beaconGenesisTimestamp, + "Invalid genesis timestamp" + ); + } +} + +contract CompoundingStakingStrategy is + CompoundingValidatorStorage, + InitializableAbstractStrategy +{ + using SafeERC20 for IERC20; + + /// @param _baseConfig Base strategy config with + /// `platformAddress` not used so empty address + /// `vaultAddress` the address of the OETH Vault contract + /// @param _wethAddress Address of the WETH Token contract + /// @param _beaconChainDepositContract Address of the beacon chain deposit contract + /// @param _beaconProofs Address of the Beacon Proofs contract that verifies beacon chain data + /// @param _beaconGenesisTimestamp The timestamp of the Beacon chain's genesis. + constructor( + BaseStrategyConfig memory _baseConfig, + address _wethAddress, + address _beaconChainDepositContract, + address _beaconProofs, + uint64 _beaconGenesisTimestamp + ) + InitializableAbstractStrategy(_baseConfig) + CompoundingValidatorStorage( + _wethAddress, + _baseConfig.vaultAddress, + _beaconChainDepositContract, + _beaconProofs, + _beaconGenesisTimestamp + ) + { + // Make sure nobody owns the implementation contract + _setGovernor(address(0)); + } + event RegistratorChanged(address indexed newAddress); event InitialDepositAmountChanged(uint256 amountWei); event FirstDepositReset(); - event SSVValidatorRegistered( - bytes32 indexed pubKeyHash, - uint64[] operatorIds - ); - event SSVValidatorRemoved(bytes32 indexed pubKeyHash, uint64[] operatorIds); event ETHStaked( bytes32 indexed pubKeyHash, bytes32 indexed pendingDepositRoot, @@ -224,33 +273,6 @@ abstract contract CompoundingValidatorManager is Governable, Pausable { _; } - /// @param _wethAddress Address of the Erc20 WETH Token contract - /// @param _vaultAddress Address of the Vault - /// @param _beaconChainDepositContract Address of the beacon chain deposit contract - /// @param _ssvNetwork Address of the SSV Network contract - /// @param _beaconProofs Address of the Beacon Proofs contract that verifies beacon chain data - /// @param _beaconGenesisTimestamp The timestamp of the Beacon chain's genesis. - constructor( - address _wethAddress, - address _vaultAddress, - address _beaconChainDepositContract, - address _ssvNetwork, - address _beaconProofs, - uint64 _beaconGenesisTimestamp - ) { - WETH = _wethAddress; - BEACON_CHAIN_DEPOSIT_CONTRACT = _beaconChainDepositContract; - SSV_NETWORK = _ssvNetwork; - VAULT_ADDRESS = _vaultAddress; - BEACON_PROOFS = _beaconProofs; - BEACON_GENESIS_TIMESTAMP = _beaconGenesisTimestamp; - - require( - block.timestamp > _beaconGenesisTimestamp, - "Invalid genesis timestamp" - ); - } - /** * * Admin Functions @@ -294,42 +316,6 @@ abstract contract CompoundingValidatorManager is Governable, Pausable { * */ - /// @notice Registers a single validator in a SSV Cluster. - /// Only the Registrator can call this function. - /// @param publicKey The public key of the validator - /// @param operatorIds The operator IDs of the SSV Cluster - /// @param sharesData The shares data for the validator - /// @param cluster The SSV cluster details including the validator count and SSV balance - // slither-disable-start reentrancy-no-eth - function registerSsvValidator( - bytes calldata publicKey, - uint64[] calldata operatorIds, - bytes calldata sharesData, - Cluster calldata cluster - ) external payable onlyRegistrator whenNotPaused { - // Hash the public key using the Beacon Chain's format - bytes32 pubKeyHash = _hashPubKey(publicKey); - // Check each public key has not already been used - require( - validator[pubKeyHash].state == ValidatorState.NON_REGISTERED, - "Validator already registered" - ); - - // Store the validator state as registered - validator[pubKeyHash].state = ValidatorState.REGISTERED; - - ISSVNetwork(SSV_NETWORK).registerValidator{ value: msg.value }( - publicKey, - operatorIds, - sharesData, - cluster - ); - - emit SSVValidatorRegistered(pubKeyHash, operatorIds); - } - - // slither-disable-end reentrancy-no-eth - struct ValidatorStakeData { bytes pubkey; bytes signature; @@ -368,39 +354,10 @@ abstract contract CompoundingValidatorManager is Governable, Pausable { // Hash the public key using the Beacon Chain's hashing for BLSPubkey bytes32 pubKeyHash = _hashPubKey(validatorStakeData.pubkey); - ValidatorState currentState = validator[pubKeyHash].state; - // Can only stake to a validator that has been registered, verified or active. + // Can only stake to a validator that is new, verified or active. // Can not stake to a validator that has been staked but not yet verified. - require( - (currentState == ValidatorState.REGISTERED || - currentState == ValidatorState.VERIFIED || - currentState == ValidatorState.ACTIVE), - "Not registered or verified" - ); require(depositAmountWei >= 1 ether, "Deposit too small"); - if (currentState == ValidatorState.REGISTERED) { - // Can only have one pending deposit to an unverified validator at a time. - // This is to limit front-running deposit attacks to a single deposit. - // The exiting deposit needs to be verified before another deposit can be made. - // If there was a front-running attack, the validator needs to be verified as invalid - // and the Governor calls `resetFirstDeposit` to set `firstDeposit` to false. - require(!firstDeposit, "Existing first deposit"); - // Limits the amount of ETH that can be at risk from a front-running deposit attack. - require( - depositAmountWei == initialDepositAmountWei, - "Invalid first deposit amount" - ); - // Limits the number of validator balance proofs to verifyBalances - require( - verifiedValidators.length + 1 <= MAX_VERIFIED_VALIDATORS, - "Max validators" - ); - - // Flag a deposit to an unverified validator so no other deposits can be made - // to an unverified validator. - firstDeposit = true; - validator[pubKeyHash].state = ValidatorState.STAKED; - } + _admitStake(pubKeyHash, depositAmountWei); /* 0x02 to indicate that withdrawal credentials are for a compounding validator * that was introduced with the Pectra upgrade. @@ -467,6 +424,49 @@ abstract contract CompoundingValidatorManager is Governable, Pausable { // slither-disable-end reentrancy-eth,reentrancy-no-eth + function _admitStake(bytes32 pubKeyHash, uint256 depositAmountWei) + internal + virtual + { + ValidatorState currentState = validator[pubKeyHash].state; + require( + currentState == ValidatorState.NON_REGISTERED || + currentState == ValidatorState.VERIFIED || + currentState == ValidatorState.ACTIVE, + "Not registered or verified" + ); + + if (currentState == ValidatorState.NON_REGISTERED) { + _recordFirstDeposit(pubKeyHash, depositAmountWei); + } + } + + function _recordFirstDeposit(bytes32 pubKeyHash, uint256 depositAmountWei) + internal + { + // Can only have one pending deposit to an unverified validator at a time. + // This is to limit front-running deposit attacks to a single deposit. + // The existing deposit needs to be verified before another deposit can be made. + // If there was a front-running attack, the validator needs to be verified as invalid + // and the Governor calls `resetFirstDeposit` to set `firstDeposit` to false. + require(!firstDeposit, "Existing first deposit"); + // Limits the amount of ETH that can be at risk from a front-running deposit attack. + require( + depositAmountWei == initialDepositAmountWei, + "Invalid first deposit amount" + ); + // Limits the number of validator balance proofs to verifyBalances + require( + verifiedValidators.length + 1 <= MAX_VERIFIED_VALIDATORS, + "Max validators" + ); + + // Flag a deposit to an unverified validator so no other deposits can be made + // to an unverified validator. + firstDeposit = true; + validator[pubKeyHash].state = ValidatorState.STAKED; + } + /// @notice Request a full or partial withdrawal from a validator. /// A zero amount will trigger a full withdrawal. /// If the remaining balance is < 32 ETH then only the amount in excess of 32 ETH will be withdrawn. @@ -530,68 +530,6 @@ abstract contract CompoundingValidatorManager is Governable, Pausable { // slither-disable-end reentrancy-no-eth - /// @notice Remove the validator from the SSV Cluster after: - /// - the validator has been exited from `validatorWithdrawal` or slashed - /// - the validator has incorrectly registered and can not be staked to - /// - the initial deposit was front-run and the withdrawal address is not this strategy's address. - /// Make sure `validatorWithdrawal` is called with a zero amount and the validator has exited the Beacon chain. - /// If removed before the validator has exited the beacon chain will result in the validator being slashed. - /// Only the registrator can call this function. - /// @param publicKey The public key of the validator - /// @param operatorIds The operator IDs of the SSV Cluster - /// @param cluster The SSV cluster details including the validator count and SSV balance - // slither-disable-start reentrancy-no-eth - function removeSsvValidator( - bytes calldata publicKey, - uint64[] calldata operatorIds, - Cluster calldata cluster - ) external onlyRegistrator { - // Hash the public key using the Beacon Chain's format - bytes32 pubKeyHash = _hashPubKey(publicKey); - ValidatorState currentState = validator[pubKeyHash].state; - // Can remove SSV validators that were incorrectly registered and can not be deposited to. - require( - currentState == ValidatorState.REGISTERED || - currentState == ValidatorState.EXITED || - currentState == ValidatorState.INVALID, - "Validator not regd or exited" - ); - - validator[pubKeyHash].state = ValidatorState.REMOVED; - - ISSVNetwork(SSV_NETWORK).removeValidator( - publicKey, - operatorIds, - cluster - ); - - emit SSVValidatorRemoved(pubKeyHash, operatorIds); - } - - /** - * - * SSV Management - * - */ - - // slither-disable-end reentrancy-no-eth - - /// @notice Migrate the SSV cluster to use ETH for payment instead of SSV tokens. - /// @param operatorIds The operator IDs of the SSV Cluster - /// @param cluster The SSV cluster details including the validator count and SSV balance - function migrateClusterToETH( - uint64[] memory operatorIds, - Cluster memory cluster - ) external payable onlyGovernor { - ISSVNetwork(SSV_NETWORK).migrateClusterToETH{ value: msg.value }( - operatorIds, - cluster - ); - - // The SSV Network emits - // ClusterMigratedToETH(msg.sender, operatorIds, msg.value, ssvClusterBalance, effectiveBalance, cluster) - } - /** * * Beacon Chain Proofs @@ -657,15 +595,15 @@ abstract contract CompoundingValidatorManager is Governable, Pausable { // Find and remove the deposit as the funds can not be recovered uint256 depositCount = depositList.length; for (uint256 i = 0; i < depositCount; i++) { - DepositData memory deposit = deposits[depositList[i]]; - if (deposit.pubKeyHash == pubKeyHash) { + DepositData memory depositData = deposits[depositList[i]]; + if (depositData.pubKeyHash == pubKeyHash) { // next verifyBalances will correctly account for the loss of a front-run // deposit. Doing it here accounts for the loss as soon as possible lastVerifiedEthBalance -= Math.min( lastVerifiedEthBalance, - uint256(deposit.amountGwei) * 1 gwei + uint256(depositData.amountGwei) * 1 gwei ); - _removeDeposit(depositList[i], deposit); + _removeDeposit(depositList[i], depositData); break; } } @@ -732,9 +670,14 @@ abstract contract CompoundingValidatorManager is Governable, Pausable { StrategyValidatorProofData calldata strategyValidatorData ) external { // Load into memory the previously saved deposit data - DepositData memory deposit = deposits[pendingDepositRoot]; - ValidatorData memory strategyValidator = validator[deposit.pubKeyHash]; - require(deposit.status == DepositStatus.PENDING, "Deposit not pending"); + DepositData memory depositData = deposits[pendingDepositRoot]; + ValidatorData memory strategyValidator = validator[ + depositData.pubKeyHash + ]; + require( + depositData.status == DepositStatus.PENDING, + "Deposit not pending" + ); require(firstPendingDeposit.slot != 0, "Zero 1st pending deposit slot"); // We should allow the verification of deposits for validators that have been marked as exiting @@ -751,7 +694,10 @@ abstract contract CompoundingValidatorManager is Governable, Pausable { ); // The verification slot must be after the deposit's slot. // This is needed for when the deposit queue is empty. - require(deposit.slot < depositProcessedSlot, "Slot not after deposit"); + require( + depositData.slot < depositProcessedSlot, + "Slot not after deposit" + ); uint64 snapTimestamp = snappedBalance.timestamp; @@ -826,29 +772,29 @@ abstract contract CompoundingValidatorManager is Governable, Pausable { // We can not guarantee that the deposit has been processed in that case. // solhint-enable max-line-length require( - deposit.slot < firstPendingDeposit.slot || isDepositQueueEmpty, + depositData.slot < firstPendingDeposit.slot || isDepositQueueEmpty, "Deposit likely not processed" ); // Remove the deposit now it has been verified as processed on the beacon chain. - _removeDeposit(pendingDepositRoot, deposit); + _removeDeposit(pendingDepositRoot, depositData); emit DepositVerified( pendingDepositRoot, - uint256(deposit.amountGwei) * 1 gwei + uint256(depositData.amountGwei) * 1 gwei ); } function _removeDeposit( bytes32 pendingDepositRoot, - DepositData memory deposit + DepositData memory depositData ) internal { // After verifying the proof, update the contract storage deposits[pendingDepositRoot].status = DepositStatus.VERIFIED; // Move the last deposit to the index of the verified deposit bytes32 lastDeposit = depositList[depositList.length - 1]; - depositList[deposit.depositIndex] = lastDeposit; - deposits[lastDeposit].depositIndex = deposit.depositIndex; + depositList[depositData.depositIndex] = lastDeposit; + deposits[lastDeposit].depositIndex = depositData.depositIndex; // Delete the last deposit from the list depositList.pop(); } @@ -1300,4 +1246,168 @@ abstract contract CompoundingValidatorManager is Governable, Pausable { function verifiedValidatorsLength() external view returns (uint256) { return verifiedValidators.length; } + + /// @notice Set up initial internal state. + /// @param _rewardTokenAddresses Not used so empty array + /// @param _assets Not used so empty array + /// @param _pTokens Not used so empty array + /// @param _initialDepositAmountWei The amount of ETH required for the first deposit to a new validator. + function initialize( + address[] memory _rewardTokenAddresses, + address[] memory _assets, + address[] memory _pTokens, + uint256 _initialDepositAmountWei + ) external onlyGovernor initializer { + InitializableAbstractStrategy._initialize( + _rewardTokenAddresses, + _assets, + _pTokens + ); + _setInitialDepositAmountWei(_initialDepositAmountWei); + } + + /// @notice Unlike other strategies, this does not deposit assets into the underlying platform. + /// It just checks the asset is WETH and emits the Deposit event. + /// To deposit WETH into validators, `stakeEth` must be used. + /// @param _asset Address of the WETH token. + /// @param _amount Amount of WETH that was transferred to the strategy by the vault. + function deposit(address _asset, uint256 _amount) + external + override + onlyVault + nonReentrant + { + require(_asset == WETH, "Unsupported asset"); + require(_amount > 0, "Must deposit something"); + + // Account for the new WETH + depositedWethAccountedFor += _amount; + + emit Deposit(_asset, address(0), _amount); + } + + /// @notice Unlike other strategies, this does not deposit assets into the underlying platform. + /// It just emits the Deposit event. + /// To deposit WETH into validators, `stakeEth` must be used. + function depositAll() external override onlyVault nonReentrant { + uint256 wethBalance = IERC20(WETH).balanceOf(address(this)); + uint256 newWeth = wethBalance - depositedWethAccountedFor; + + if (newWeth > 0) { + // Account for the new WETH + depositedWethAccountedFor = wethBalance; + + emit Deposit(WETH, address(0), newWeth); + } + } + + /// @notice Withdraw ETH and WETH from this strategy contract. + /// @param _recipient Address to receive withdrawn assets. + /// @param _asset Address of the WETH token. + /// @param _amount Amount of WETH to withdraw. + function withdraw( + address _recipient, + address _asset, + uint256 _amount + ) external override nonReentrant { + require(_asset == WETH, "Unsupported asset"); + require( + msg.sender == vaultAddress || msg.sender == validatorRegistrator, + "Caller not Vault or Registrator" + ); + + _withdraw(_recipient, _amount, address(this).balance); + } + + function _withdraw( + address _recipient, + uint256 _withdrawAmount, + uint256 _ethBalance + ) internal { + require(_withdrawAmount > 0, "Must withdraw something"); + require(_recipient == vaultAddress, "Recipient not Vault"); + + // Convert any ETH from validator partial withdrawals, exits + // or execution rewards to WETH and do the necessary accounting. + if (_ethBalance > 0) _convertEthToWeth(_ethBalance); + + // Transfer WETH to the recipient and do the necessary accounting. + _transferWeth(_withdrawAmount, _recipient); + + emit Withdrawal(WETH, address(0), _withdrawAmount); + } + + /// @notice Transfer all WETH deposits, ETH from validator withdrawals and ETH from + /// execution rewards in this strategy to the vault. + /// This does not withdraw from the validators. That has to be done separately with the + /// `validatorWithdrawal` operation. + function withdrawAll() external override onlyVaultOrGovernor nonReentrant { + uint256 ethBalance = address(this).balance; + uint256 withdrawAmount = IERC20(WETH).balanceOf(address(this)) + + ethBalance; + + if (withdrawAmount > 0) { + _withdraw(vaultAddress, withdrawAmount, ethBalance); + } + } + + /// @notice Accounts for all the assets managed by this strategy which includes: + /// 1. The current WETH in this strategy contract + /// 2. The last verified ETH balance, total deposits and total validator balances + /// @param _asset Address of WETH asset. + /// @return balance Total value in ETH + function checkBalance(address _asset) + external + view + override + returns (uint256 balance) + { + require(_asset == WETH, "Unsupported asset"); + + // Load the last verified balance from the storage + // and add to the latest WETH balance of this strategy. + balance = + lastVerifiedEthBalance + + IWETH9(WETH).balanceOf(address(this)); + } + + /// @notice Returns bool indicating whether asset is supported by the strategy. + /// @param _asset The address of the WETH token. + function supportsAsset(address _asset) public view override returns (bool) { + return _asset == WETH; + } + + /// @notice Does nothing but needed as this function is abstract on InitializableAbstractStrategy + function safeApproveAllTokens() public override {} + + /** + * @notice We can accept ETH directly to this contract from anyone as it does not impact our accounting + * like it did in the legacy NativeStakingStrategy. + * The new ETH will be accounted for in `checkBalance` after the next snapBalances and verifyBalances txs. + */ + receive() external payable {} + + /// @notice is not supported for this strategy as there is no platform token. + function setPTokenAddress(address, address) external pure override { + revert("Unsupported function"); + } + + /// @notice is not supported for this strategy as there is no platform token. + function removePToken(uint256) external pure override { + revert("Unsupported function"); + } + + /// @dev This strategy does not use a platform token like the old Aave and Compound strategies. + function _abstractSetPToken(address _asset, address) internal override {} + + /// @dev Consensus rewards are compounded to the validator's balance instead of being + /// swept to this strategy contract. + /// Execution rewards from MEV and tx priority accumulate as ETH in this strategy contract. + /// Withdrawals from validators also accumulate as ETH in this strategy contract. + /// It's too complex to separate the rewards from withdrawals so this function is not implemented. + /// Besides, ETH rewards are not sent to the Dripper any more. The Vault can now regulate + /// the increase in assets. + function _collectRewardTokens() internal pure override { + revert("Unsupported function"); + } } diff --git a/contracts/contracts/strategies/NativeStaking/CompoundingStakingView.sol b/contracts/contracts/strategies/NativeStaking/CompoundingStakingView.sol index b72dc9b903..fa9a7db65c 100644 --- a/contracts/contracts/strategies/NativeStaking/CompoundingStakingView.sol +++ b/contracts/contracts/strategies/NativeStaking/CompoundingStakingView.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.0; -import { CompoundingValidatorManager } from "./CompoundingValidatorManager.sol"; +import { CompoundingStakingStrategy } from "./CompoundingStakingStrategy.sol"; /** * @title Viewing contract for the Compounding Staking Strategy. @@ -9,16 +9,16 @@ import { CompoundingValidatorManager } from "./CompoundingValidatorManager.sol"; */ contract CompoundingStakingStrategyView { /// @notice The address of the Compounding Staking Strategy contract - CompoundingValidatorManager public immutable stakingStrategy; + CompoundingStakingStrategy public immutable stakingStrategy; constructor(address _stakingStrategy) { - stakingStrategy = CompoundingValidatorManager(_stakingStrategy); + stakingStrategy = CompoundingStakingStrategy(payable(_stakingStrategy)); } struct ValidatorView { bytes32 pubKeyHash; uint64 index; - CompoundingValidatorManager.ValidatorState state; + CompoundingStakingStrategy.ValidatorState state; } struct DepositView { @@ -41,7 +41,7 @@ contract CompoundingStakingStrategyView { for (uint256 i = 0; i < validatorCount; ++i) { bytes32 pubKeyHash = stakingStrategy.verifiedValidators(i); ( - CompoundingValidatorManager.ValidatorState state, + CompoundingStakingStrategy.ValidatorState state, uint64 index ) = stakingStrategy.validator(pubKeyHash); validators[i] = ValidatorView({ diff --git a/contracts/contracts/strategies/NativeStaking/ConsolidationController.sol b/contracts/contracts/strategies/NativeStaking/ConsolidationController.sol deleted file mode 100644 index 558f2a7296..0000000000 --- a/contracts/contracts/strategies/NativeStaking/ConsolidationController.sol +++ /dev/null @@ -1,499 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.0; - -import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; -import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; - -import { CompoundingStakingSSVStrategy, CompoundingValidatorManager } from "./CompoundingStakingSSVStrategy.sol"; -import { ValidatorAccountant } from "./ValidatorAccountant.sol"; -import { Cluster } from "../../interfaces/ISSVNetwork.sol"; - -/// @title Consolidation Controller -/// @notice Orchestrates the consolidation of validators from old Native Staking Strategies -/// to the new Compounding Staking Strategy. -/// @author Origin Protocol Inc -contract ConsolidationController is Ownable { - /// @dev Minimum time that must pass before a consolidation request can be processed. - /// 261 epochs * 32 slots/epoch * 12 seconds/slot = 100224 seconds (~27.8 hours) - /// Includes 256 epochs minimum withdrawability delay + 5 epochs from - /// compute_activation_exit_epoch (MAX_SEED_LOOKAHEAD + 1). - /// The actual time can be a lot longer than this depending on the number of - /// requests in the beacon chain's pending consolidation queue. - uint256 internal constant MIN_CONSOLIDATION_PERIOD = 261 * 32 * 12; - - /// @notice Address of the validator registrator account - address public immutable validatorRegistrator; - /// @dev The old Native Staking Strategy connected to the second SSV cluster - address internal immutable nativeStakingStrategy2; - /// @dev The old Native Staking Strategy connected to the third SSV cluster - address internal immutable nativeStakingStrategy3; - /// @dev The new Compounding Staking Strategy - CompoundingStakingSSVStrategy internal immutable targetStrategy; - - /// @notice Number of validators being consolidated - uint64 public consolidationCount; - /// @notice Timestamp when the consolidation process was requested - uint64 public consolidationStartTimestamp; - /// @notice The address of the source Native Staking Strategy being consolidated from - address public sourceStrategy; - /// @notice The public key hash of the target validator on the new Compounding Staking Strategy - bytes32 public targetPubKeyHash; - /// @dev Tracks source validators that were requested for a consolidation round. - /// Keyed by keccak256(sourcePubKeyHash, consolidationStartTimestamp). - mapping(bytes32 => bool) private pendingSourceInRound; - - /// @dev Throws if called by any account other than the Validator Registrator - modifier onlyRegistrator() { - require( - msg.sender == validatorRegistrator, - "Caller is not the Registrator" - ); - _; - } - - /// @param _owner The owner who can request, fail and confirm consolidations - /// @param _validatorRegistrator The validator registrator who does operations on the old staking strategy - constructor( - address _owner, - address _validatorRegistrator, - address _nativeStakingStrategy2, - address _nativeStakingStrategy3, - address _targetStrategy - ) { - _transferOwnership(_owner); - - validatorRegistrator = _validatorRegistrator; - nativeStakingStrategy2 = _nativeStakingStrategy2; - nativeStakingStrategy3 = _nativeStakingStrategy3; - targetStrategy = CompoundingStakingSSVStrategy( - payable(_targetStrategy) - ); - } - - /** - * @notice Request consolidation of validators from an old Native Staking Strategy - * to the new Compounding Staking Strategy - * @param _sourceStrategy The address of the old Native Staking Strategy - * @param sourcePubKeys The public keys of the validators to be consolidated from the old Native Staking Strategy - * @param targetPubKey The public key of the target validator on the new Compounding Staking Strategy - */ - function requestConsolidation( - address _sourceStrategy, - bytes[] calldata sourcePubKeys, - bytes calldata targetPubKey - ) external payable onlyOwner { - // Check no consolidations are already in progress - require(consolidationCount == 0, "Consolidation in progress"); - // Check at least one source validator is provided - require(sourcePubKeys.length > 0, "Empty source validators"); - // Check sourceStrategy is a valid old Native Staking Strategy - _checkSourceStrategy(_sourceStrategy); - - // Check target validator is Active on the new Compounding Staking Strategy - bytes32 targetPubKeyHashMem = _hashPubKey(targetPubKey); - (CompoundingStakingSSVStrategy.ValidatorState state, ) = targetStrategy - .validator(targetPubKeyHashMem); - require( - state == CompoundingValidatorManager.ValidatorState.ACTIVE, - "Target validator not active" - ); - // Check no pending deposits in the new target validator - require( - _hasPendingDeposit(targetPubKeyHashMem) == false, - "Target has pending deposit" - ); - - // Store the state at the start of the consolidation process - consolidationCount = SafeCast.toUint64(sourcePubKeys.length); - uint64 consolidationStartTimestampMem = uint64(block.timestamp); - consolidationStartTimestamp = consolidationStartTimestampMem; - sourceStrategy = _sourceStrategy; - targetPubKeyHash = targetPubKeyHashMem; - - // Store source validators for this consolidation round. - for (uint256 i = 0; i < sourcePubKeys.length; ++i) { - bytes32 roundKey = _sourceValidatorRoundKey( - sourcePubKeys[i], - consolidationStartTimestampMem - ); - require( - pendingSourceInRound[roundKey] == false, - "Duplicate source validator" - ); - pendingSourceInRound[roundKey] = true; - } - - // Call requestConsolidation on the old Native Staking Strategy - // to initiate the consolidations - ValidatorAccountant(_sourceStrategy).requestConsolidation{ - value: msg.value - }(sourcePubKeys, targetPubKey); - - // Snap the balances for the last time on the new Compounding Staking Strategy - // if it hasn't been called recently. Otherwise skip to prevent a DoS - // attack where an attacker front-runs this call with the permissionless snapBalances(). - (, uint64 lastSnapTimestamp, ) = targetStrategy.snappedBalance(); - if ( - uint64(block.timestamp) > - lastSnapTimestamp + targetStrategy.SNAP_BALANCES_DELAY() - ) { - targetStrategy.snapBalances(); - } - - // No event emitted as ConsolidationRequested is emitted from the old Native Staking Strategy - } - - /** - * @notice A consolidation request can fail to be processed on the beacon chain - * for various reasons. For example, the pending consolidation queue is full with 262,144 requests. - * Or the source validator has exited from a voluntary exit request. - * This reduces the consolidation count and changes the validator state back to STAKED. - * @param sourcePubKeys The public keys of the source validators that failed to be consolidated. - */ - function failConsolidation(bytes[] calldata sourcePubKeys) - external - onlyOwner - { - // Check consolidations are in progress - require(consolidationCount > 0, "No consolidation in progress"); - // There a min time before a failed consolidation can be unwound. - // This gives the beacon chain time to process the request. - require( - block.timestamp >= - consolidationStartTimestamp + MIN_CONSOLIDATION_PERIOD, - "Source not withdrawable" - ); - require( - sourcePubKeys.length <= consolidationCount, - "Exceeds consolidation count" - ); - uint64 consolidationStartTimestampMem = consolidationStartTimestamp; - - for (uint256 i = 0; i < sourcePubKeys.length; ++i) { - bytes32 roundKey = _sourceValidatorRoundKey( - sourcePubKeys[i], - consolidationStartTimestampMem - ); - require(pendingSourceInRound[roundKey], "Unknown source validator"); - pendingSourceInRound[roundKey] = false; - } - - // Read into memory in case it gets reset in storage before - // the external call to the source strategy - address sourceStrategyMem = sourceStrategy; - - // Store updated consolidation state - consolidationCount -= SafeCast.toUint64(sourcePubKeys.length); - if (consolidationCount == 0) { - // Reset the rest of the consolidation state - consolidationStartTimestamp = 0; - sourceStrategy = address(0); - targetPubKeyHash = bytes32(0); - } - - ValidatorAccountant(sourceStrategyMem).failConsolidation(sourcePubKeys); - - // No event emitted as ConsolidationFailed is emitted from the old Native Staking Strategy - } - - /** - * @notice Confirm the consolidation of validators from an old Native Staking Strategy - * to the new Compounding Staking Strategy has been completed. - * @param balanceProofs a `BalanceProofs` struct containing the following: - * - balancesContainerRoot: The merkle root of the balances container - * - balancesContainerProof: The merkle proof for the balances container to the beacon block root. - * This is 9 witness hashes of 32 bytes each concatenated together starting from the leaf node. - * - validatorBalanceLeaves: Array of leaf nodes containing the validator balance with three other balances. - * - validatorBalanceProofs: Array of merkle proofs for the validator balance to the Balances container root. - * This is 39 witness hashes of 32 bytes each concatenated together starting from the leaf node. - * @param pendingDepositProofs a `PendingDepositProofs` struct containing the following: - * - pendingDepositContainerRoot: The merkle root of the pending deposits list container - * - pendingDepositContainerProof: The merkle proof from the pending deposits list container - * to the beacon block root. - * This is 9 witness hashes of 32 bytes each concatenated together starting from the leaf node. - * - pendingDepositIndexes: Array of indexes in the pending deposits list container for each - * of the strategy's deposits. - * - pendingDepositProofs: Array of merkle proofs for each strategy deposit in the - * beacon chain's pending deposit list container to the pending deposits list container root. - * These are 28 witness hashes of 32 bytes each concatenated together starting from the leaf node. - */ - function confirmConsolidation( - CompoundingValidatorManager.BalanceProofs calldata balanceProofs, - CompoundingValidatorManager.PendingDepositProofs - calldata pendingDepositProofs - ) external onlyOwner { - // Check consolidations are in progress - require(consolidationCount > 0, "No consolidation in progress"); - // There a min time before a consolidation can be processed on the beacon chain - (, uint64 snappedTimestamp, ) = targetStrategy.snappedBalance(); - require( - uint64(snappedTimestamp) > - consolidationStartTimestamp + MIN_CONSOLIDATION_PERIOD, - "Source not withdrawable" - ); - - // Load into memory as the storage is about to be reset. - // These are used in the external contract calls - address sourceStrategyMem = sourceStrategy; - uint256 consolidationCountMem = consolidationCount; - - // Reset consolidation state before external calls - consolidationCount = 0; - consolidationStartTimestamp = 0; - sourceStrategy = address(0); - targetPubKeyHash = bytes32(0); - - // Verify balances on the new Compounding Staking Strategy and update the strategy's balance - targetStrategy.verifyBalances(balanceProofs, pendingDepositProofs); - - // Reduce the balance of the old Native Staking Strategy - ValidatorAccountant(sourceStrategyMem).confirmConsolidation( - consolidationCountMem - ); - - // No event emitted as ConsolidationConfirmed is emitted from the old Native Staking Strategy - } - - /** - * - * Functions that forward to the old Native Staking Strategy - * - */ - - /// @notice The validator registrator of the old Native Staking Strategy can call doAccounting - /// @param _sourceStrategy The address of the old Native Staking Strategy - /// @return accountingValid true if accounting was successful, false if fuse is blown - function doAccounting(address _sourceStrategy) - external - onlyRegistrator - returns (bool accountingValid) - { - // Check sourceStrategy is a valid old Native Staking Strategy - _checkSourceStrategy(_sourceStrategy); - - return ValidatorAccountant(_sourceStrategy).doAccounting(); - } - - /** - * @notice Exit of source validators are allowed during the consolidation process - * as consolidated validators will be in EXITING state hence can not be consolidated after exit. - * Only callable by the validator registrator. - * @param _sourceStrategy The address of the old Native Staking Strategy - * @param publicKey The public key of the validator to exit which must have STAKED state. - * @param operatorIds The operator IDs for the source SSV cluster - */ - function exitSsvValidator( - address _sourceStrategy, - bytes calldata publicKey, - uint64[] calldata operatorIds - ) external onlyRegistrator { - // Check sourceStrategy is a valid old Native Staking Strategy - _checkSourceStrategy(_sourceStrategy); - - ValidatorAccountant(_sourceStrategy).exitSsvValidator( - publicKey, - operatorIds - ); - } - - /** - * @notice Removing source validators is not allowed during the consolidation process - * as consolidated validators will be in EXITING state hence can not be consolidated after removal. - * Only callable by the validator registrator. - * @param _sourceStrategy The address of the old Native Staking Strategy - * @param publicKey The public key of the validator to remove which must have EXITING or REGISTERED state. - * @param operatorIds The operator IDs for the source SSV cluster - * @param cluster The SSV cluster information for the source validator - */ - function removeSsvValidator( - address _sourceStrategy, - bytes calldata publicKey, - uint64[] calldata operatorIds, - Cluster calldata cluster - ) external onlyRegistrator { - // Check sourceStrategy is a valid old Native Staking Strategy - _checkSourceStrategy(_sourceStrategy); - // Prevent removing a validator from the SSV cluster before the consolidation - // process has been completed for the source strategy being consolidated. - // This prevents validators that have been exited rather than consolidated but that's ok. - // The exited validator can be removed after the consolidation process is complete. - require(_sourceStrategy != sourceStrategy, "Consolidation in progress"); - - ValidatorAccountant(_sourceStrategy).removeSsvValidator( - publicKey, - operatorIds, - cluster - ); - } - - /** - * - * Functions that forward to the new Compounding Staking Strategy - * - */ - - /// @notice Forwards to the new Compounding Staking Strategy. - /// Is only callable by the validator registrator when a consolidation is in progress. - /// Anyone can call when there are no consolidations in progress. - function snapBalances() external { - if (consolidationCount > 0 && msg.sender != validatorRegistrator) { - revert("Consolidation in progress"); - } - if (consolidationCount > 0) { - require( - block.timestamp > - consolidationStartTimestamp + MIN_CONSOLIDATION_PERIOD, - "Source not withdrawable" - ); - } - - targetStrategy.snapBalances(); - } - - /** - * @notice Anyone can verify balances on the new Compounding Staking Strategy - * as long as there are no consolidations in progress. - * @param balanceProofs a `BalanceProofs` struct containing the following: - * - balancesContainerRoot: The merkle root of the balances container - * - balancesContainerProof: The merkle proof for the balances container to the beacon block root. - * This is 9 witness hashes of 32 bytes each concatenated together starting from the leaf node. - * - validatorBalanceLeaves: Array of leaf nodes containing the validator balance with three other balances. - * - validatorBalanceProofs: Array of merkle proofs for the validator balance to the Balances container root. - * This is 39 witness hashes of 32 bytes each concatenated together starting from the leaf node. - * @param pendingDepositProofs a `PendingDepositProofs` struct containing the following: - * - pendingDepositContainerRoot: The merkle root of the pending deposits list container - * - pendingDepositContainerProof: The merkle proof from the pending deposits list container - * to the beacon block root. - * This is 9 witness hashes of 32 bytes each concatenated together starting from the leaf node. - * - pendingDepositIndexes: Array of indexes in the pending deposits list container for each - * of the strategy's deposits. - * - pendingDepositProofs: Array of merkle proofs for each strategy deposit in the - * beacon chain's pending deposit list container to the pending deposits list container root. - * These are 28 witness hashes of 32 bytes each concatenated together starting from the leaf node. - */ - function verifyBalances( - CompoundingValidatorManager.BalanceProofs calldata balanceProofs, - CompoundingValidatorManager.PendingDepositProofs - calldata pendingDepositProofs - ) external { - (, uint64 snappedTimestamp, ) = targetStrategy.snappedBalance(); - // Can not verify balances while consolidations are in progress - // if the snap was taken after the consolidation process started. - // This still allows verifying a pre-existing snap. - if ( - consolidationCount > 0 && - snappedTimestamp > consolidationStartTimestamp - ) { - revert("Consolidation in progress"); - } - - targetStrategy.verifyBalances(balanceProofs, pendingDepositProofs); - } - - /// @notice Partial withdrawals are allowed during consolidation from the new Compounding Staking Strategy. - /// This includes partial withdrawals from the target validator. - // Full validator exits from any Compounding Staking Strategy validator are - /// not allowed during the migration period. - /// Only the registrator can call this function. - /// @param publicKey The public key of the validator - /// @param amountGwei The amount of ETH to be withdrawn from the validator in Gwei. - /// A zero amount is not allowed. - function validatorWithdrawal(bytes calldata publicKey, uint64 amountGwei) - external - payable - onlyRegistrator - { - // Prevent full exits from any new compounding validators. - // This includes when there is no consolidation in progress. - // This reduces the risk of an exit request being processed before a consolidation request - require(amountGwei > 0, "No exit during migration"); - targetStrategy.validatorWithdrawal{ value: msg.value }( - publicKey, - amountGwei - ); - } - - /** - * @notice Deposits to Compounding Staking Strategy validators that are - * not the target of a consolidation are allowed. - * Only the registrator can call this function. - * @param validatorStakeData validator data needed to stake. - * The `ValidatorStakeData` struct contains the pubkey, signature and depositDataRoot. - * @param depositAmountGwei The amount of WETH to stake to the validator in Gwei. - */ - function stakeEth( - CompoundingValidatorManager.ValidatorStakeData - calldata validatorStakeData, - uint64 depositAmountGwei - ) external onlyRegistrator { - require( - _hashPubKey(validatorStakeData.pubkey) != targetPubKeyHash, - "Stake to consolidation target" - ); - - targetStrategy.stakeEth(validatorStakeData, depositAmountGwei); - } - - /// removeSsvValidator from the new Compounding Staking Strategy is not allowed until after - /// all the validators have been consolidated. This is done by restoring the validator registrator - /// back to the account used before the consolidation upgrades. - - /** - * - * Internal Functions - * - */ - - /// @dev Check if there are any pending deposits for a validator with a given public key hash. - /// Need to iterate over the target strategy’s `deposits` - /// @return True if there is at least one pending deposit for the validator - function _hasPendingDeposit(bytes32 _targetPubKeyHash) - internal - view - returns (bool) - { - uint256 depositsCount = targetStrategy.depositListLength(); - for (uint256 i = 0; i < depositsCount; ++i) { - ( - bytes32 depositPubKeyHash, - , - , - , - CompoundingValidatorManager.DepositStatus status - ) = targetStrategy.deposits(targetStrategy.depositList(i)); - if ( - depositPubKeyHash == _targetPubKeyHash && - status == CompoundingValidatorManager.DepositStatus.PENDING - ) { - return true; - } - } - return false; - } - - /// @dev Hash a validator public key using the Beacon Chain's format - /// @param pubKey The full validator public key - /// @return The hashed public key using the Beacon Chain's hashing for BLSPubkey - function _hashPubKey(bytes memory pubKey) internal pure returns (bytes32) { - require(pubKey.length == 48, "Invalid public key"); - return sha256(abi.encodePacked(pubKey, bytes16(0))); - } - - /// @dev Build a key for tracking source validators in a consolidation round. - function _sourceValidatorRoundKey( - bytes memory sourcePubKey, - uint64 roundTimestamp - ) internal pure returns (bytes32) { - return keccak256(abi.encode(_hashPubKey(sourcePubKey), roundTimestamp)); - } - - /// @dev Check source strategy is a valid old Native Staking Strategy - /// @param _sourceStrategy The address of the old Native Staking Strategy - function _checkSourceStrategy(address _sourceStrategy) internal view { - require( - _sourceStrategy == nativeStakingStrategy2 || - _sourceStrategy == nativeStakingStrategy3, - "Invalid source strategy" - ); - } -} diff --git a/contracts/contracts/strategies/NativeStaking/README.md b/contracts/contracts/strategies/NativeStaking/README.md index d0a8da014d..7a66401299 100644 --- a/contracts/contracts/strategies/NativeStaking/README.md +++ b/contracts/contracts/strategies/NativeStaking/README.md @@ -37,9 +37,3 @@ ### Storage ![Compounding Staking Strategy Storage](../../../docs/CompoundingStakingSSVStrategyStorage.svg) - -## Consolidation Controller - -### Squashed - -![Consolidation Controller Squashed](../../../docs/ConsolidationControllerSquashed.svg) \ No newline at end of file diff --git a/contracts/deploy/mainnet/197_deploy_compounding_staking_strategy.js b/contracts/deploy/mainnet/197_deploy_compounding_staking_strategy.js new file mode 100644 index 0000000000..eb48f89e5e --- /dev/null +++ b/contracts/deploy/mainnet/197_deploy_compounding_staking_strategy.js @@ -0,0 +1,99 @@ +const addresses = require("../../utils/addresses"); +const { beaconChainGenesisTimeMainnet } = require("../../utils/constants"); +const { deploymentWithGovernanceProposal } = require("../../utils/deploy"); + +module.exports = deploymentWithGovernanceProposal( + { + deployName: "197_deploy_compounding_staking_strategy", + forceDeploy: false, + reduceQueueTime: true, + deployerIsProposer: false, + }, + async ({ deployWithConfirmation, ethers, withConfirmation }) => { + const { deployerAddr } = await getNamedAccounts(); + const sDeployer = await ethers.provider.getSigner(deployerAddr); + + const cOETHVaultProxy = await ethers.getContract("OETHVaultProxy"); + const cOETHVault = await ethers.getContractAt( + "IVault", + cOETHVaultProxy.address + ); + const cBeaconProofs = await ethers.getContract("BeaconProofs"); + + console.log("Deploy CompoundingStakingStrategyProxy"); + const dCompoundingStakingStrategyProxy = await deployWithConfirmation( + "CompoundingStakingStrategyProxy" + ); + const cCompoundingStakingStrategyProxy = await ethers.getContractAt( + "CompoundingStakingStrategyProxy", + dCompoundingStakingStrategyProxy.address + ); + + console.log("Deploy CompoundingStakingStrategy"); + const dCompoundingStakingStrategy = await deployWithConfirmation( + "CompoundingStakingStrategy", + [ + [addresses.zero, cOETHVaultProxy.address], //_baseConfig + addresses.mainnet.WETH, // wethAddress + addresses.mainnet.beaconChainDepositContract, // beaconChainDepositContract + cBeaconProofs.address, // beaconProofs + beaconChainGenesisTimeMainnet, + ] + ); + const cCompoundingStakingStrategy = await ethers.getContractAt( + "CompoundingStakingStrategy", + dCompoundingStakingStrategy.address + ); + + console.log("Encode CompoundingStakingStrategy initialize call"); + const initData = cCompoundingStakingStrategy.interface.encodeFunctionData( + "initialize(address[],address[],address[],uint256)", + [ + [], // reward token addresses + [], // asset token addresses + [], // platform token addresses + ethers.utils.parseEther("32.25"), // initial validator deposit amount + ] + ); + + console.log("Initialize CompoundingStakingStrategyProxy"); + await withConfirmation( + cCompoundingStakingStrategyProxy.connect(sDeployer)[ + // eslint-disable-next-line no-unexpected-multiline + "initialize(address,address,bytes)" + ]( + cCompoundingStakingStrategy.address, // implementation address + addresses.mainnet.Timelock, // governance + initData // data for call to the initialize function on the strategy + ) + ); + + const cStrategy = await ethers.getContractAt( + "CompoundingStakingStrategy", + cCompoundingStakingStrategyProxy.address + ); + + console.log("Deploy CompoundingStakingStrategyView"); + await deployWithConfirmation( + "CompoundingStakingStrategyView", + [cCompoundingStakingStrategyProxy.address], + "CompoundingStakingStrategyView" + ); + + return { + name: "Deploy new vanilla compounding staking strategy", + actions: [ + { + contract: cOETHVault, + signature: "approveStrategy(address)", + args: [cCompoundingStakingStrategyProxy.address], + }, + { + contract: cStrategy, + signature: "setRegistrator(address)", + args: [addresses.mainnet.validatorRegistrator], + }, + ], + }; + } +); diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index 0943564e81..24a2a7207f 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -1090,16 +1090,8 @@ async function nativeStakingSSVStrategyFixture() { if (isFork) { const { nativeStakingSSVStrategy, ssv } = fixture; - // // The Defender Relayer - // fixture.validatorRegistrator = await impersonateAndFund( - // addresses.mainnet.validatorRegistrator - // ); - // Set to the consolidation controller while the validator consolidation is ongoing - const consolidationController = await ethers.getContract( - "ConsolidationController" - ); fixture.validatorRegistrator = await impersonateAndFund( - consolidationController.address + addresses.mainnet.validatorRegistrator ); // Fund some SSV to the native staking strategy @@ -1237,6 +1229,80 @@ async function compoundingStakingSSVStrategyFixture() { return fixture; } +/** + * CompoundingStakingStrategy fixture + */ +async function compoundingStakingStrategyFixture() { + const fixture = await beaconChainFixture(); + await hotDeployOption(fixture, "compoundingStakingStrategyFixture", { + isOethFixture: true, + }); + + const { deployerAddr, governorAddr, registratorAddr } = + await getNamedAccounts(); + const sDeployer = await ethers.provider.getSigner(deployerAddr); + const sGovernor = await ethers.provider.getSigner(governorAddr); + const sRegistrator = await ethers.provider.getSigner(registratorAddr); + const { oethVault, weth, beaconProofs } = fixture; + + const cCompoundingStakingStrategyProxy = await ( + await ethers.getContractFactory("CompoundingStakingStrategyProxy") + ).deploy(); + + const dCompoundingStakingStrategy = await ( + await ethers.getContractFactory("CompoundingStakingStrategy") + ).deploy( + [addresses.zero, oethVault.address], + weth.address, + ( + await getAssetAddresses(deployments) + ).beaconChainDepositContract, + beaconProofs.address, + 1606824023 + ); + + const initData = dCompoundingStakingStrategy.interface.encodeFunctionData( + "initialize(address[],address[],address[],uint256)", + [ + [], // reward token addresses + [], // asset token addresses + [], // platform token addresses + ethers.utils.parseEther("1"), // initial validator deposit amount + ] + ); + + const initializeProxy = + cCompoundingStakingStrategyProxy.connect(sDeployer)[ + "initialize(address,address,bytes)" + ]; + await initializeProxy( + dCompoundingStakingStrategy.address, + governorAddr, + initData + ); + + fixture.compoundingStakingStrategy = await ethers.getContractAt( + "CompoundingStakingStrategy", + cCompoundingStakingStrategyProxy.address + ); + + await oethVault + .connect(sGovernor) + .approveStrategy(fixture.compoundingStakingStrategy.address); + + await fixture.compoundingStakingStrategy + .connect(sGovernor) + .setRegistrator(registratorAddr); + + await fixture.compoundingStakingStrategy + .connect(sGovernor) + .setHarvesterAddress(fixture.simpleOETHHarvester.address); + + fixture.validatorRegistrator = sRegistrator; + + return fixture; +} + async function compoundingStakingSSVStrategyMerkleProofsMockedFixture() { const fixture = await compoundingStakingSSVStrategyFixture(); @@ -1714,6 +1780,7 @@ module.exports = { instantRebaseVaultFixture, rebornFixture, nativeStakingSSVStrategyFixture, + compoundingStakingStrategyFixture, compoundingStakingSSVStrategyFixture, compoundingStakingSSVStrategyMerkleProofsMockedFixture, nodeSnapshot, diff --git a/contracts/test/strategies/compoundingStaking.js b/contracts/test/strategies/compoundingStaking.js new file mode 100644 index 0000000000..60a771f2ce --- /dev/null +++ b/contracts/test/strategies/compoundingStaking.js @@ -0,0 +1,115 @@ +const { expect } = require("chai"); +const { parseEther, parseUnits } = require("ethers").utils; + +const { MAX_UINT256 } = require("../../utils/constants"); +const { calcDepositRoot } = require("../../tasks/beaconTesting"); +const { hashPubKey } = require("../../utils/beacon"); +const { impersonateAndFund } = require("../../utils/signers"); +const { + createFixtureLoader, + compoundingStakingStrategyFixture, +} = require("../_fixture"); +const { + testValidators, +} = require("./compoundingSSVStaking-validatorsData.json"); + +const loadFixture = createFixtureLoader(compoundingStakingStrategyFixture); + +describe("Unit test: Compounding Staking Strategy", function () { + let fixture; + let sVault; + + beforeEach(async () => { + fixture = await loadFixture(); + const { compoundingStakingStrategy, josh, weth } = fixture; + sVault = await impersonateAndFund( + await compoundingStakingStrategy.vaultAddress() + ); + await weth + .connect(josh) + .approve(compoundingStakingStrategy.address, MAX_UINT256); + }); + + const depositToStrategy = async (amount) => { + const { compoundingStakingStrategy, weth, josh } = fixture; + + await weth + .connect(josh) + .transfer(compoundingStakingStrategy.address, parseEther(amount)); + await compoundingStakingStrategy.connect(sVault).depositAll(); + }; + + const stakeValidator = async ({ + validator = testValidators[0], + amount = "1", + } = {}) => { + const { compoundingStakingStrategy, validatorRegistrator } = fixture; + const depositDataRoot = await calcDepositRoot( + compoundingStakingStrategy.address, + "0x02", + validator.publicKey, + validator.signature, + amount + ); + + return compoundingStakingStrategy.connect(validatorRegistrator).stakeEth( + { + pubkey: validator.publicKey, + signature: validator.signature, + depositDataRoot, + }, + parseUnits(amount, 9) + ); + }; + + it("allows the first deposit to a vanilla validator without SSV registration", async () => { + const { compoundingStakingStrategy } = fixture; + const validator = testValidators[0]; + const pubKeyHash = hashPubKey(validator.publicKey); + + expect( + (await compoundingStakingStrategy.validator(pubKeyHash)).state + ).to.equal(0); + + await depositToStrategy("1"); + + const stakeTx = await stakeValidator({ validator }); + const receipt = await stakeTx.wait(); + const event = receipt.events.find((event) => event.event === "ETHStaked"); + + await expect(stakeTx) + .to.emit(compoundingStakingStrategy, "ETHStaked") + .withArgs( + pubKeyHash, + event.args.pendingDepositRoot, + validator.publicKey, + parseEther("1") + ); + + const validatorData = await compoundingStakingStrategy.validator( + pubKeyHash + ); + expect(validatorData.state).to.equal(2); + expect(await compoundingStakingStrategy.firstDeposit()).to.equal(true); + }); + + it("does not allow a follow-up deposit before the validator is verified or active", async () => { + const validator = testValidators[0]; + + await depositToStrategy("2"); + await stakeValidator({ validator }); + + await expect(stakeValidator({ validator })).to.be.revertedWith( + "Not registered or verified" + ); + }); + + it("still only allows one pending first deposit at a time", async () => { + await depositToStrategy("2"); + await stakeValidator({ validator: testValidators[0] }); + + await expect( + stakeValidator({ validator: testValidators[1] }) + ).to.be.revertedWith("Existing first deposit"); + }); +}); diff --git a/contracts/test/strategies/stakingConsolidation.mainnet.fork-test.js b/contracts/test/strategies/stakingConsolidation.mainnet.fork-test.js deleted file mode 100644 index 7c8d4c1394..0000000000 --- a/contracts/test/strategies/stakingConsolidation.mainnet.fork-test.js +++ /dev/null @@ -1,1408 +0,0 @@ -const { expect } = require("chai"); -const { keccak256, parseUnits } = require("ethers/lib/utils"); - -const addresses = require("../../utils/addresses"); -const { hashPubKey } = require("../../utils/beacon"); -const { resolveContract } = require("../../utils/resolvers"); -const { impersonateAndFund } = require("../../utils/signers"); - -const { createFixtureLoader, beaconChainFixture } = require("../_fixture"); -const { advanceTime } = require("../helpers"); -const { calcDepositRoot } = require("../../tasks/beaconTesting"); -const { getClusterInfo } = require("../../utils/ssv"); -const loadFixture = createFixtureLoader(beaconChainFixture); - -// 5 million wei should cover the fee when there is a high number of requests -const consolidationFee = 5e6; - -const secondClusterOperatorIds = [752, 753, 754, 755]; -const secondClusterPubKeys = [ - "0xb7e1156c6ca50c42f60fc3503d435ecc430614d9d0304442d0badea7c648de854fa1b37c3125c8ff4de9ca765823eefd", - "0xace3a5e7b04a2f9b8ddc79524181571654c4e9569d571c3d0a9742fa0fe2db8d54014fb72cfa74a5e23b9676e461fdc9", - "0xa018a0216aa11b0ab61e7adcd9bd163f567e4b5ed7de3a89c6488efc932d7965bf708999c9fe28ce37a165b8161d5681", - "0xa032b875e9d6bbeab98d6b060bcb3a145ec666d50c80aa6be6aaa4684d57402c01caf80ae28ee94a81fabbf50e9bd249", - "0xa0667d5b740b71b8ae50c07d695c1f1e4363fc52057a931dfec42c389069bfa3eed9acc5312044d3896f1cdb2d54d3df", - "0xa241e3d37b54f92c965fb3c390b900d85f416c47b7d4057331620b359e9c8f34f265cc532ca52403c59f2fca42e7c9f2", - "0xa2f883624840cb5bf744c23c477efdcb3e7cccfbbd5bb7eef00a7fd9860765f9a1c82616e5107fada49d2786c239c8ae", - "0xa9da10b01962ca118b3ad409f384ec1acf2c76020633b5c80bc432a52c2413df5aeef0a6c7d3cf6c69d79278c2d1ac91", - "0xabad67a3625c14e85922aa72f452df88bffc7a2d30e2916dbfe78f174c699cde529b8c30cef43ad43c2734b857999a0c", - "0xae12edc5d57aa4999038c8968184af8236d551e7051df5c8682dc984638cec4716c23cd40a65eb9a3fd8ba73e9877c0c", - "0xb9081e786155204e9d6d8739651bffd7e019c9104fcff1b9598cdbe596cea1fae99a99ad8cac9647be2001317da07b91", - "0x84bfb105e60735a20ba9a4c2ce8e8a117a1fb9c66e8cc20ce1d880de69cb093c068bbe72924e272098d7018eca2ed130", - "0x851149b79b15b1bfc02f70d04860c64ba85ff0e63765d42fe39117f38ef66f3bfbf071c3241ae0d96917f175b1379e37", - "0x870d2a79d2ca276158b62b10ccfc608b8435c8df8d0d1f5bb1290dd4fd396759843248fd7877be8bc2183684ab07bcfe", - "0x8757a40dc9a2351536654a1d82cc01f8bf2acd1a04c2cbc4e7a50310ba3a9bd932980b4e531dcadcdf65361691896590", - "0x897fc84f154b2a2be55058a7a2c122cbc48bdf1d4f030a6d1d0216caeb0f51ff777c23b6dc4601006a6e6ebed38be081", - "0x8a8c7ace7d84cb3cba43cafb5bc21b84df457d51b57c3c63dadd2dd4c093e17cfc95a07fde55cdc9225978df8f095bc4", - "0x8e44878278bbd96c980b66a74ffc9b71a835f3991064a645d7e1836d99de6acfd6c7061148d5034f6959e69f419f44a9", - "0x8ed6e3d45faed1f149ce35f8f0371e922cc4ff99e93cd938fde696acab2be58c09adb4c3909b54f22aa5325fefcf5a74", - "0xa0feaa07399cd3635f881f54d8ebb51c4bedf376149f4c649a5ab5a1f1eae8ea4759dd5e3521a9f367e09dcc9182cdea", - "0xa8188c3d4618f4e45c8fc0d8e4b686e92c7b128993a2efefd5c8edc9f1ed5c5a5b0421be6c6e01284c4be778b8d351af", - "0xaa70003c8439881aaa268e71fe5290185a4f66f51597f883837fea71690b419bfc2ebcd02fb21386feea7c8ac41a8ed8", - "0xab4d5f12e16034a72244f9f1a1a3ac9d4951b58106f89c604e5631530dc2accd7e5f24e7150ad5e1cbe899f1eaf1ce7d", - "0xacf48a22e69b0ba9df39d775b1799f6bceeeb0888c39fb909b3c9af4b95c7d85907b1fac37a3abe249981cdec3f52426", - "0xad4ccd890dff1e95993158c64790fcd51b44245858173619976532ab6044f584e23b09445f1ca0207161cd2ce831249a", - "0xb8d868f540cdaeac91ee049382049d6263a04ad0cd7e89f61992f4626640a22225975bc14ca896f8a62203629411447f", - "0xb97684ed60df7e58d33819e522bf248b87ed2ca431f2458d9e4bc95a43715266bbc3fb510923d51a683ab2380098de70", - "0x80a5f26411ffab3778166b6614551b2aa8cc73f56f9d36958cffad0ffa37c10bb629ba9101df605849f5b69248893d12", - "0x8d2e3214eb39d8db9a486975a0f71bda253a332938341fcddeb2ca7ffe8e18d5ce99d4844e5acd3f05110734ce43afa2", - "0x8d411e1ac3d246897d5e977722ab79e22b8f4692393770928c201ac88b98431676ab56833179fffd761ee47ba02e567b", - "0x8d875da93d02e3c0fb90ab11a4b079f8b2f9d83035ecdccf55df17afc9ae5b352674d08daf766fb2d30f39bce1ce29ba", - "0x90d1cd1c19af352d82363eca0f0fdd9d35a7fde98240f217d136c8e7841ab091d7a1a78d396542abf9b5cb1c8035f8ea", - "0x94d22fce3edc96965d2cf289969fb3099101761534bfe9d029edb319a683cadb295bf02c0610b98c91c91a904db03611", - "0xa1b4899e5460428df7b66007cccad6dee6ce3183b856897070d23cf4cb8de7fb8a1bf02a42e5b4a761cf68adcf2dc5ae", - "0xa4f24e7e1d3ec6b56d6085f2c15d6f19ebefe2e5d8c08de58830ef254131c34c8bc5a6a85b1daf60ff3331c05790929b", - "0xa577f2c72d35d5dff2d76cc8169e32c32a9a114c904dff1094d86f2efe6021b795e63671e1464a0741f015ed73ada55e", - "0xab14b385b3cf1ba09048d7177eaab385cb9a3b3ad4a44e6c6c714811b9ee1cfe9243aaf212957e7306567df8f92b962f", - "0xab53c575fc015ba508aca923062903cb6b7635d09835fd5825d98dcfde4da7445ca1ac5db52e094d4c32204344999647", - "0xb065146c773f831935702bb58d0fae499cf8e3c89db491efeb04e1545aee69dc3c077e5d5e470bbc888dbf3478891bc8", -]; - -const thirdClusterOperatorIds = [338, 339, 340, 341]; -const thirdClusterPubKeys = [ - "0x999702d1ad3f224ac76dca1edcb91c5e7d1e05bd13646cd4d0b0fa1252ad3ace4d8b34f48a7c4ca13c84a7c71f175c72", - "0xa5fe057240ad0ea978f7ac58130ff124c733de28ac271878178d00017b91353062bcc220cabaeccbc77fba71a48d4dfa", - "0xb734f922193e9f5fa82bc023703b6ff9d48b94d7dc37bd02514268881834bc8117d82fc33bae857f5a1ac1e5ee5c2395", -]; - -const activeTargetPubKey = - "0xb5d37226e27e0ab066541ccb795e04149300bb8c0b0fd528785f6a940e94c624b65ef1eb771f78a5f2685317b7e6f34f"; - -// Balance proofs after the deposit to validator with slot 13498458 has been verified. -// Run the following Hardhat task to get the proofs: -// pnpm hardhat verifyBalances --network mainnet --dryrun true --test true --over-ids 2178131,2213390 --over-bals 32.5,32.5 --slot 13928087 -const balanceProofs = { - beaconBlockRoot: - "0x93f545e9c23550f0934192e433a74438e65beec7c90f92a771b5e611ae494dfc", - balancesContainerRoot: - "0x8f27c7c7ce2f490662a385bd0e1f8cff0edbefae993e39ca2f21205429b3814a", - balancesContainerProof: - "0x85324c52a14d47585124af7b122fb4e5dc95a328195a69cdbd2ac467a380087bde78b6f9afdee27c83dcab795db1174fb31809c3e18ccd101c45ca92146a34ba0fe39f130e2611965f830d94a77f38cd694b1c92c82bb1426082fd11974bf67429a18eca977c27b0c22f5df812b50554ee42926f66b70fe930751eaee9e5577c88cbdc15d22d62cf98507f4ec9c2859d7a3604a8712b3a4696e8ca42eca1293c35ecb1dce6bdc3af85bd9cf2f1f03cb8a04d217a199b03abd30d462a261fe2e7445fd94dbbc7e4c1e17974c0745d82a97865458517ec4d426b861f9c4e90e9d0b53e114879b41538e556e5d6a209127efcd4b0fe59e0bf64de8357cdc2273d6ec0cdabace9443f7ec6183c3a82e1762c339d8dfbbd1dfca1ba16967b4f2e6e8a", - validatorBalanceLeaves: [ - "0xfd7c8373070000008984d5b626000000dba68373070000002e054f8207000000", - "0x7bc26d8107000000a9b15ccc0e0000004b9e8073070000000000000000000000", - "0x1fc47c7307000000e2b07c73070000000c927c730700000000a5269107000000", - "0x519f7b7307000000d2af7b73070000002a70028407000000f6977b7307000000", - "0xec30747307000000255474730700000000a52691070000001026e9463a000000", - ], - validatorBalanceProofs: [ - "0x917d8373070000009eaf837307000000ea7d837307000000b39e8373070000008c48c30a5f5ae1a56746099c051ca96073e376aac7551f0d6d6a15a9a48f4052d994e0b28373640577c0ab09ace51a55fef80ad962e2faed045b55947a7a4f34e54d6073f9d09e38595b3f6ecfd92945b721d23784789129babc2094a2915987da37705db46c6e770d7f8c5b9ec69d35257f92434347705e56b1d5d561f2fdaa50a600d0379af06018ac56268cf9a870f71986b9a9e60cdf51fb3c6d0d52e9f0670e19e0ff26a62093902fbca179f7f36f258d50a9073149f965612acb5773dcfd63d877accda0a33848d28d9689f0e3b83a09bef854569381d7889bbb536159d7e71add819625b8fb0ede9e5c35e0ed113de4618d5f442e7f82e0a0f582c4ac53f83b465d1d41767e25220cc72c91e6b5eceb05f2952e98dad8db50a87de1b6ed1740dd097f2086df81b875c8b39db903df524bdc88b56ac9a50f1ca1ab16ba6ecf6113eb8e424f51815a7f0a0864004680bf513454bba188ec30a1c66a9b8534965f292af9adf02e23328f62e4bef83791aeb3d729ed87cd9838fad6790dd7d66891394e3eeb07d48be1bf8604cac743a2841e87a4e163862255cf07f5195f0ebdd8cd635541d51b39455c8da6481af73661128d0b29da8a88c5da854f3144daeeea7a208232bf77025a6e81e886e674b3aadc3931bc8f0268e2b8350d63608fe6b1689256c0d385f42f5bbe2027a22c1996e110ba97c171d3e5948de92beb8d0d63c39ebade8509e0ae3c9c3876fb5fa112be18f905ecacfecb92057603ab95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a43e251b9aa4f9097ef23129626b849648d24e38fb1f16829559482a9d5da28473cddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9cfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167e71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d731206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc021352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a467657cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe18869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636b5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7c6f67e02e6e4e1bdefb994c6098953f34636ba2b6ca20a4721d2b26a886722ff1c9a7e5ff1cf48b4ad1582d3f4e4a1004f3b20d8c5a2b71387a4254ad933ebc52f075ae229646b6f6aed19a5e372cf295081401eb893ff599b3f9acc0c0d3e7d328921deb59612076801e8cd61592107b5c67c79b846595cc6320c395b46362cbfb909fdb236ad2411b4e4883810a074b840464689986c3f8a8091827e17c32755d8fb3687ba3ba49f342c77f5a1f89bec83d811446e1a467139213d640b6a748d18220000000000000000000000000000000000000000000000000000000000", - "0x5058807307000000d6c780730700000070bf7f76070000002560807307000000a360ff8aa16d43dbf04b07a5b55942294f5d57adf813a844d305c8c6b2cd993ffbf093e17c6938a06cc1a21c77904c05c21e5f710e745ca3e40b88f1671050c633b03e8a35f6247def38d97d633697663930096b94cf96bda09764ec852d3abc29848299af499e3c19da8a78b953624818e08eb50a7b4bd6cdb884e7c4e9f9de7a3b437e6ac8737e2fb5e8268cc3b4804f92bfb6efd420fe08b0f69cda9653f46e8a1707614ee210693a4ab9296bca5d1f7d5ff3e435f4d1a842d94e669f2404986fb00fe176687a08685b8333dc93c6d4a3a549a9efd75c64709d82d8c95eec837c502df948738b0e5a1e5cc552caf4f8c1a87ec43a01a890b069acbb0936c417af734a796fa2937aad9c827c0db2f0acf793fafdef4eddd71a12886765359e8d8fc5bf9ce38b8bdd8fbea19bd20cc3974d7fb337f1e513b7121df301e709733eb355e772fa45ebc65fb923d0eff6c2dc7632e422725146683ad8b8bad084f634965f292af9adf02e23328f62e4bef83791aeb3d729ed87cd9838fad6790dd7d66891394e3eeb07d48be1bf8604cac743a2841e87a4e163862255cf07f5195f0ebdd8cd635541d51b39455c8da6481af73661128d0b29da8a88c5da854f3144daeeea7a208232bf77025a6e81e886e674b3aadc3931bc8f0268e2b8350d63608fe6b1689256c0d385f42f5bbe2027a22c1996e110ba97c171d3e5948de92beb8d0d63c39ebade8509e0ae3c9c3876fb5fa112be18f905ecacfecb92057603ab95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a43e251b9aa4f9097ef23129626b849648d24e38fb1f16829559482a9d5da28473cddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9cfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167e71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d731206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc021352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a467657cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe18869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636b5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7c6f67e02e6e4e1bdefb994c6098953f34636ba2b6ca20a4721d2b26a886722ff1c9a7e5ff1cf48b4ad1582d3f4e4a1004f3b20d8c5a2b71387a4254ad933ebc52f075ae229646b6f6aed19a5e372cf295081401eb893ff599b3f9acc0c0d3e7d328921deb59612076801e8cd61592107b5c67c79b846595cc6320c395b46362cbfb909fdb236ad2411b4e4883810a074b840464689986c3f8a8091827e17c32755d8fb3687ba3ba49f342c77f5a1f89bec83d811446e1a467139213d640b6a748d18220000000000000000000000000000000000000000000000000000000000", - "0xa49a7c7307000000acf67c730700000037e57c730700000033e57c73070000008511637653965e07d7eeec0223160f5358eadfaa3c6105ba20ea3bdf05ba7c6f75e7b877dedc859f9584a998e122e6aeffa96b9f05459f83e3936af6e50659ce6544de0e1bcef3cf00b1214e085b562bbab1889bd25bcf8849fe8c1da7fdec72a5148e91dd117e0dab7a05c9d2394e7e66cb7b21096dc8690dd0d8208c336604aad72859e0cbcf82058befcb3ceb8cea7fb7067115232dd11ab59a47ed538aeb46933c6defd1f2f9ab6c19b5e9d6bdfd184d7913f2e0cc9b588116966b5f5cf0b966c56783dfe114de1a7df4dc96def908aa579b9b56e06e4b0465f7d1332fc5cf9a044bff818ea2b269f25762c367896911a93f2203a913d64e811611b1da45d7d5b62bc68abf2e770261e5ae2c128f915c56a61a166a4cdbb3372911b6c69f9abff178ccc312b22b154981de48b8a5b0650b3a4e175ab9dd3636e67e5c4eff6b93420876ef20a461a47eb645ed08b852f79e40ce93cd2b9203568fbbd6094c34b74724946c4673142c68b9fd1d7daa05f34dd5747a10f1c5010de31d47960e78c9350c52049bb9000dbda4f0a908c8eb29606b74fb3702653514a19cb6147f6db4c812b0451988f4ee7061bc49e1f7fe37e1a2548469e13db07d9f235e9023daeeea7a208232bf77025a6e81e886e674b3aadc3931bc8f0268e2b8350d63608fe6b1689256c0d385f42f5bbe2027a22c1996e110ba97c171d3e5948de92beb8d0d63c39ebade8509e0ae3c9c3876fb5fa112be18f905ecacfecb92057603ab95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a43e251b9aa4f9097ef23129626b849648d24e38fb1f16829559482a9d5da28473cddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9cfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167e71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d731206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc021352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a467657cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe18869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636b5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7c6f67e02e6e4e1bdefb994c6098953f34636ba2b6ca20a4721d2b26a886722ff1c9a7e5ff1cf48b4ad1582d3f4e4a1004f3b20d8c5a2b71387a4254ad933ebc52f075ae229646b6f6aed19a5e372cf295081401eb893ff599b3f9acc0c0d3e7d328921deb59612076801e8cd61592107b5c67c79b846595cc6320c395b46362cbfb909fdb236ad2411b4e4883810a074b840464689986c3f8a8091827e17c32755d8fb3687ba3ba49f342c77f5a1f89bec83d811446e1a467139213d640b6a748d18220000000000000000000000000000000000000000000000000000000000", - "0xe8b07b73070000006e5bb4eb34000000be957b730700000097867b730700000059e193bc2473fca31d2a10f8454520bee1957d7e3e9ced87f2e8425c0c08f6bb52cf8c48e946e9da705ac19a462706b411d24b8f373e325a628208d7047d0ffc0fee33b653cd0fd01e2ca1608a9b94c94c67f365657d8a86a53bc8dfaa3988a724504dfe79b6f5afb44ecf53753aad7efff2452183032a5b1219b5c99f384719c1022f632a5e029648ce78f33b5cbe1c606c327b7b29aa4c03cff2be1ae5ccebac8253bf25ade6c873f6509570ce4a30783c746c5875ca346078e184eac19dfc9f416299e9fb4250b04a8e3e2d22a2771eda8639962bc285055754f357667fdd80cd1d2fef1f6a297426c80d198e5821e538e851fc608333ac632937e0753b69b1b1cba2be0ab14696726eac8f6b818ef3d0396e6d563b73d378a48f0ed77cd518b72fef2551971a58c574c4fad812bbd71e0a41ed97c1c74d3f665e4f849945939fc1349559b3feaf32db756fbdb425cb7de72cee400a0a83ae1a3fc1541ee163812708e31e1ec62e05f94e47973926890f90e5e6bdfc4e034e6d75ace091b678c9350c52049bb9000dbda4f0a908c8eb29606b74fb3702653514a19cb6147f6db4c812b0451988f4ee7061bc49e1f7fe37e1a2548469e13db07d9f235e9023daeeea7a208232bf77025a6e81e886e674b3aadc3931bc8f0268e2b8350d63608fe6b1689256c0d385f42f5bbe2027a22c1996e110ba97c171d3e5948de92beb8d0d63c39ebade8509e0ae3c9c3876fb5fa112be18f905ecacfecb92057603ab95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a43e251b9aa4f9097ef23129626b849648d24e38fb1f16829559482a9d5da28473cddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9cfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167e71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d731206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc021352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a467657cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe18869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636b5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7c6f67e02e6e4e1bdefb994c6098953f34636ba2b6ca20a4721d2b26a886722ff1c9a7e5ff1cf48b4ad1582d3f4e4a1004f3b20d8c5a2b71387a4254ad933ebc52f075ae229646b6f6aed19a5e372cf295081401eb893ff599b3f9acc0c0d3e7d328921deb59612076801e8cd61592107b5c67c79b846595cc6320c395b46362cbfb909fdb236ad2411b4e4883810a074b840464689986c3f8a8091827e17c32755d8fb3687ba3ba49f342c77f5a1f89bec83d811446e1a467139213d640b6a748d18220000000000000000000000000000000000000000000000000000000000", - "0xc14ded8209000000d3399f7507000000c35c74730700000052547473070000003fd8a8a3abe91daabbcefb46c12679757b5ef84a1d1a86f88ff4f72108743082c16a90f3122651c46b9fe13ad5fa9039689f8f8310bd87fede796de7ae7e6b8b26e8b864ab910e64fac8d21420305ac13d40ff60b337734cbed410b74d045b4b191c8a834a78383c882c63cd73b5ec3045147d6866b03d668926675421d9dd9430059267161313dd5d8078f97681146f8ad9699662294f8d96838c1a02eb55950ba94ee41400881a940e82b104faa44ea94de3aafc6708bdfdfad612e5f8b3b17db9f9b3303b7fb8910a42ac46832ad3dbc1122ff9b81ce8343aea02b9dd4f08b85f6f22463648903c3e2c93d738ec5c5bbabc7969d350c5fb65538711c7651da9758deaadab0eda5cf7047a9b4cf76f32ba76d0f410e2eb0979f709ef7c3a875c6c2088e2ae731e9e64583785467c21b800cf320d9807a45210ef86d0b70e44efa206455c565617f73c5a3a4cd1b98c93d21d398f2bc25a63c5ea5095c8fd3905383a708f1ae8e6cdfef5a2aabf0fa4bf39ab9c9dd8aca35c9fd45ea283df683a83c2524a66c24e653e6fa354bbba4050ce12082d94df4108e75baf1b125bfe6db4c812b0451988f4ee7061bc49e1f7fe37e1a2548469e13db07d9f235e9023daeeea7a208232bf77025a6e81e886e674b3aadc3931bc8f0268e2b8350d63608fe6b1689256c0d385f42f5bbe2027a22c1996e110ba97c171d3e5948de92beb8d0d63c39ebade8509e0ae3c9c3876fb5fa112be18f905ecacfecb92057603ab95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a43e251b9aa4f9097ef23129626b849648d24e38fb1f16829559482a9d5da28473cddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9cfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167e71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d731206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc021352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a467657cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe18869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636b5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7c6f67e02e6e4e1bdefb994c6098953f34636ba2b6ca20a4721d2b26a886722ff1c9a7e5ff1cf48b4ad1582d3f4e4a1004f3b20d8c5a2b71387a4254ad933ebc52f075ae229646b6f6aed19a5e372cf295081401eb893ff599b3f9acc0c0d3e7d328921deb59612076801e8cd61592107b5c67c79b846595cc6320c395b46362cbfb909fdb236ad2411b4e4883810a074b840464689986c3f8a8091827e17c32755d8fb3687ba3ba49f342c77f5a1f89bec83d811446e1a467139213d640b6a748d18220000000000000000000000000000000000000000000000000000000000", - ], -}; -const pendingDepositProofs = { - pendingDepositContainerRoot: - "0x8e9a353f5d80d749c0f322bc97530477e6c5a3ca14b253d0a26264872b45bdcf", - pendingDepositContainerProof: - "0xc5a691c90b1e5a4b98433a0f4bd86eba8048f63b7d3a1b4fb66ba0c7ae8812773db8d01d9495a3bbeb4f3d22d6a373692bbbf60c638fa615738f83ac08f464c9f7507516e2aa2af211bec2d8c99165b7fad41b628ba3483ae4538d0297e9959bc78009fdf07fc56a11f122370658a353aaa542ed63e44c4bc15ff4cd105ab33c536d98837f2dd165a55d5eeae91485954472d56f246df256bf3cae19352a123ce0ef6aa1d629eca8a8ec78c2649b889f0c585e16e92862010bb41d58afd7df61445fd94dbbc7e4c1e17974c0745d82a97865458517ec4d426b861f9c4e90e9d0b53e114879b41538e556e5d6a209127efcd4b0fe59e0bf64de8357cdc2273d6ec0cdabace9443f7ec6183c3a82e1762c339d8dfbbd1dfca1ba16967b4f2e6e8a", - pendingDepositIndexes: [6791, 33011, 6790, 6789, 6788, 6786], - pendingDepositRoots: [ - "0xe43aa97c7b45d686240e148893fb944692f4a5513afca99a351c1f20e26c5597", - "0xf1947604474b44eec74edd4768599915239e72f4002737713af83febc36398cb", - "0xb052e26ca38ea7d23a9d8e2b4e0930caec9762902558a0c69a6165d2b34225ce", - "0x5884a4ac9f90f068ebec13e3be02253bc0f44b196572038dd09e77edf8ec0d5d", - "0xac9a98d48b7b13938bbf6c50c07b7d9f41c78c2dc9515280ec3cab9209ac44ba", - "0x5daaa459171d3c16aa9c68ece5f1192d0be4900291029fe9dee738493895ab8e", - ], - pendingDepositProofs: [ - "0xb052e26ca38ea7d23a9d8e2b4e0930caec9762902558a0c69a6165d2b34225cead345f10252274408791763ea728c60cfd0c0e4b830a4478358e903ea5fb8e9d16631debd57ef20f7e5d80e7309e1c5cbe09fba84a45739d0c2f7a5385b99de044e3aedd9b6413b72e62e221a47feac65b8b61639e4d8c3f9595cb28f5a2ed0928f91fd42bd629d13ea3d088dc7684890efb7bd6982995cb06b993262d317fe481cb939e8682f2529ac5b34889c745c9c3d451c6acae5603acd9e750b81054e588a198201c81b745cd183dc6b095f0281c04ec988c9d9e5a372e145eb5f03a4936392d008637fb405334e3793bcd032cec028bbfcf3de88b34bd4c893d350dc8fa58dccb16086ce949d26e8061c375a96ab0aa4dbc7d0ba7340a2732faab6dd749aee54ff9f9e931158eaca9fd12e915a1f7c205b499d7cf98fb2c9b70b55cfdff2cf7a711c6d0a64a4c0819b47e1e8c98534a134bf0a46773151d8ca393f25cfcb86c8f337e89e710accfcacd13518797b5f871b33f5f8a522d1070b5dd3851fb301c44c748be1402321a538f1f3d0ec0ed99161b33c8d7ab778e58ea4013f2ed12cb9bcd9bbcb04e046d6ae4c230f7a759e0c88be26bb8f34c33e0fa1d121dab1875e7cf07f93e85275fc8eb4755076cd072cb30830890c57ccc0ac57fec3d1bb0ac320fc57a100f5f395873579aed371896e8be97a52c5f03809c5f2aceb08fe6b1689256c0d385f42f5bbe2027a22c1996e110ba97c171d3e5948de92beb8d0d63c39ebade8509e0ae3c9c3876fb5fa112be18f905ecacfecb92057603ab95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a4f893e908917775b62bff23294dbbe3a1cd8e6cc1c35b4801887b646a6f81f17fcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9cfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167e71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d731206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc021352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a467659aa5000000000000000000000000000000000000000000000000000000000000", - "0xf7249eead0f1aea2b1fbd85cc602f80baf13e0a02cf9d3089e718e6658ce1693f792d6b43e88db568ea6dfcb3610f61591ee6b5c6eaeb6ccfef90f21d4b59639d55dd5372c0ccbe43f33259d9de97bfbb1e50625553b883a20f883ac588df75c54591b16d0df56bd9b2fef101017199b4beb0fdd39204fdee898baf569b591734284ea78eab42ac0bc127e4a33c0f1b6b1d7ce7f1c22c077571b4b4ab17cd6d15ec0911248ab6ac0fb6e40784dee43a812f1b2e6825e663b3c3bb9c8e919f1276b2bd5a3617467e8bc2a537855f2f6ace03a9fc68c3bde3fd4ea00e6428a4114a8749e8f840f1e524602b6b9ef58a896ac8ccba0e55bb89c72eae1130fcf463433b6e9dcdd3563b9304399dbabe9f1cb6c5681c72c703678e5bcb6a343607a9844c92daa1fe18120b85cdebb842c8761b2318bd50bad75742cd23e24674e55657075a0f80fc134385cae7ca7a8d0bf310debf5e875dd4c2ed98f5082e013576152827b331483b5af2d583798206b7673defe032939eaa66fba62754814cddc226b0d00c895f66aea0409aa599d3f4e99192e2bacb7a99bf00646e55d758f18bd72f2d4e1e15bc9b4649c63daf8060b6b26098a0f3837e145b78e5418c4fe23c9b58d900f5e182e3c50ef74969ea16c7726c549757cc23523c369587da729378453c470d6bb6d6d92cb4b1f45bdf68e17b3b2d513ccad61d8e91baeec8cbf2c568fe6b1689256c0d385f42f5bbe2027a22c1996e110ba97c171d3e5948de92beb8d0d63c39ebade8509e0ae3c9c3876fb5fa112be18f905ecacfecb92057603ab95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a4f893e908917775b62bff23294dbbe3a1cd8e6cc1c35b4801887b646a6f81f17fcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9cfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167e71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d731206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc021352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a467659aa5000000000000000000000000000000000000000000000000000000000000", - "0xe43aa97c7b45d686240e148893fb944692f4a5513afca99a351c1f20e26c5597ad345f10252274408791763ea728c60cfd0c0e4b830a4478358e903ea5fb8e9d16631debd57ef20f7e5d80e7309e1c5cbe09fba84a45739d0c2f7a5385b99de044e3aedd9b6413b72e62e221a47feac65b8b61639e4d8c3f9595cb28f5a2ed0928f91fd42bd629d13ea3d088dc7684890efb7bd6982995cb06b993262d317fe481cb939e8682f2529ac5b34889c745c9c3d451c6acae5603acd9e750b81054e588a198201c81b745cd183dc6b095f0281c04ec988c9d9e5a372e145eb5f03a4936392d008637fb405334e3793bcd032cec028bbfcf3de88b34bd4c893d350dc8fa58dccb16086ce949d26e8061c375a96ab0aa4dbc7d0ba7340a2732faab6dd749aee54ff9f9e931158eaca9fd12e915a1f7c205b499d7cf98fb2c9b70b55cfdff2cf7a711c6d0a64a4c0819b47e1e8c98534a134bf0a46773151d8ca393f25cfcb86c8f337e89e710accfcacd13518797b5f871b33f5f8a522d1070b5dd3851fb301c44c748be1402321a538f1f3d0ec0ed99161b33c8d7ab778e58ea4013f2ed12cb9bcd9bbcb04e046d6ae4c230f7a759e0c88be26bb8f34c33e0fa1d121dab1875e7cf07f93e85275fc8eb4755076cd072cb30830890c57ccc0ac57fec3d1bb0ac320fc57a100f5f395873579aed371896e8be97a52c5f03809c5f2aceb08fe6b1689256c0d385f42f5bbe2027a22c1996e110ba97c171d3e5948de92beb8d0d63c39ebade8509e0ae3c9c3876fb5fa112be18f905ecacfecb92057603ab95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a4f893e908917775b62bff23294dbbe3a1cd8e6cc1c35b4801887b646a6f81f17fcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9cfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167e71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d731206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc021352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a467659aa5000000000000000000000000000000000000000000000000000000000000", - "0xac9a98d48b7b13938bbf6c50c07b7d9f41c78c2dc9515280ec3cab9209ac44bad1382044558891e141307d62d35f69c7e8ef2566e8a758ac46807c46fab10b6d16631debd57ef20f7e5d80e7309e1c5cbe09fba84a45739d0c2f7a5385b99de044e3aedd9b6413b72e62e221a47feac65b8b61639e4d8c3f9595cb28f5a2ed0928f91fd42bd629d13ea3d088dc7684890efb7bd6982995cb06b993262d317fe481cb939e8682f2529ac5b34889c745c9c3d451c6acae5603acd9e750b81054e588a198201c81b745cd183dc6b095f0281c04ec988c9d9e5a372e145eb5f03a4936392d008637fb405334e3793bcd032cec028bbfcf3de88b34bd4c893d350dc8fa58dccb16086ce949d26e8061c375a96ab0aa4dbc7d0ba7340a2732faab6dd749aee54ff9f9e931158eaca9fd12e915a1f7c205b499d7cf98fb2c9b70b55cfdff2cf7a711c6d0a64a4c0819b47e1e8c98534a134bf0a46773151d8ca393f25cfcb86c8f337e89e710accfcacd13518797b5f871b33f5f8a522d1070b5dd3851fb301c44c748be1402321a538f1f3d0ec0ed99161b33c8d7ab778e58ea4013f2ed12cb9bcd9bbcb04e046d6ae4c230f7a759e0c88be26bb8f34c33e0fa1d121dab1875e7cf07f93e85275fc8eb4755076cd072cb30830890c57ccc0ac57fec3d1bb0ac320fc57a100f5f395873579aed371896e8be97a52c5f03809c5f2aceb08fe6b1689256c0d385f42f5bbe2027a22c1996e110ba97c171d3e5948de92beb8d0d63c39ebade8509e0ae3c9c3876fb5fa112be18f905ecacfecb92057603ab95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a4f893e908917775b62bff23294dbbe3a1cd8e6cc1c35b4801887b646a6f81f17fcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9cfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167e71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d731206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc021352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a467659aa5000000000000000000000000000000000000000000000000000000000000", - "0x5884a4ac9f90f068ebec13e3be02253bc0f44b196572038dd09e77edf8ec0d5dd1382044558891e141307d62d35f69c7e8ef2566e8a758ac46807c46fab10b6d16631debd57ef20f7e5d80e7309e1c5cbe09fba84a45739d0c2f7a5385b99de044e3aedd9b6413b72e62e221a47feac65b8b61639e4d8c3f9595cb28f5a2ed0928f91fd42bd629d13ea3d088dc7684890efb7bd6982995cb06b993262d317fe481cb939e8682f2529ac5b34889c745c9c3d451c6acae5603acd9e750b81054e588a198201c81b745cd183dc6b095f0281c04ec988c9d9e5a372e145eb5f03a4936392d008637fb405334e3793bcd032cec028bbfcf3de88b34bd4c893d350dc8fa58dccb16086ce949d26e8061c375a96ab0aa4dbc7d0ba7340a2732faab6dd749aee54ff9f9e931158eaca9fd12e915a1f7c205b499d7cf98fb2c9b70b55cfdff2cf7a711c6d0a64a4c0819b47e1e8c98534a134bf0a46773151d8ca393f25cfcb86c8f337e89e710accfcacd13518797b5f871b33f5f8a522d1070b5dd3851fb301c44c748be1402321a538f1f3d0ec0ed99161b33c8d7ab778e58ea4013f2ed12cb9bcd9bbcb04e046d6ae4c230f7a759e0c88be26bb8f34c33e0fa1d121dab1875e7cf07f93e85275fc8eb4755076cd072cb30830890c57ccc0ac57fec3d1bb0ac320fc57a100f5f395873579aed371896e8be97a52c5f03809c5f2aceb08fe6b1689256c0d385f42f5bbe2027a22c1996e110ba97c171d3e5948de92beb8d0d63c39ebade8509e0ae3c9c3876fb5fa112be18f905ecacfecb92057603ab95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a4f893e908917775b62bff23294dbbe3a1cd8e6cc1c35b4801887b646a6f81f17fcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9cfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167e71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d731206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc021352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a467659aa5000000000000000000000000000000000000000000000000000000000000", - "0x308dba54c2444e4683aa4c92c423532dae274d39dc0a95a2871b90006b41a357f48cf215d71cd05cb4214ebac67ba013270cc2c9b3e9cd3764671f1461135947ff01249f12eebda99afdc7882e5a0b55955740fe202576daabaf7fd026a0d73e44e3aedd9b6413b72e62e221a47feac65b8b61639e4d8c3f9595cb28f5a2ed0928f91fd42bd629d13ea3d088dc7684890efb7bd6982995cb06b993262d317fe481cb939e8682f2529ac5b34889c745c9c3d451c6acae5603acd9e750b81054e588a198201c81b745cd183dc6b095f0281c04ec988c9d9e5a372e145eb5f03a4936392d008637fb405334e3793bcd032cec028bbfcf3de88b34bd4c893d350dc8fa58dccb16086ce949d26e8061c375a96ab0aa4dbc7d0ba7340a2732faab6dd749aee54ff9f9e931158eaca9fd12e915a1f7c205b499d7cf98fb2c9b70b55cfdff2cf7a711c6d0a64a4c0819b47e1e8c98534a134bf0a46773151d8ca393f25cfcb86c8f337e89e710accfcacd13518797b5f871b33f5f8a522d1070b5dd3851fb301c44c748be1402321a538f1f3d0ec0ed99161b33c8d7ab778e58ea4013f2ed12cb9bcd9bbcb04e046d6ae4c230f7a759e0c88be26bb8f34c33e0fa1d121dab1875e7cf07f93e85275fc8eb4755076cd072cb30830890c57ccc0ac57fec3d1bb0ac320fc57a100f5f395873579aed371896e8be97a52c5f03809c5f2aceb08fe6b1689256c0d385f42f5bbe2027a22c1996e110ba97c171d3e5948de92beb8d0d63c39ebade8509e0ae3c9c3876fb5fa112be18f905ecacfecb92057603ab95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a4f893e908917775b62bff23294dbbe3a1cd8e6cc1c35b4801887b646a6f81f17fcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9cfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167e71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d731206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc021352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a467659aa5000000000000000000000000000000000000000000000000000000000000", - ], -}; -const emptyCluster = [ - 0, // validatorCount - 0, // networkFeeIndex - 0, // index - true, // active - 0, // balance -]; - -describe.skip("ForkTest: Consolidation of Staking Strategies", function () { - this.timeout(0); - - let fixture; - let nativeStakingStrategy2; - let nativeStakingStrategy3; - let compoundingStakingStrategy; - let consolidationController; - let adminSigner; - let registratorSigner; - - beforeEach(async () => { - fixture = await loadFixture(); - nativeStakingStrategy2 = await resolveContract( - "NativeStakingSSVStrategy2Proxy", - "NativeStakingSSVStrategy" - ); - nativeStakingStrategy3 = await resolveContract( - "NativeStakingSSVStrategy3Proxy", - "NativeStakingSSVStrategy" - ); - compoundingStakingStrategy = await resolveContract( - "CompoundingStakingSSVStrategyProxy", - "CompoundingStakingSSVStrategy" - ); - registratorSigner = await impersonateAndFund( - addresses.mainnet.validatorRegistrator - ); - adminSigner = await impersonateAndFund(addresses.mainnet.Guardian); - consolidationController = await resolveContract("ConsolidationController"); - }); - - const activateTargetValidators = async (fixture) => { - const { beaconRoots } = fixture; - - // Get the current block timestamp - await advanceTime(12); - const { timestamp: currentTimestamp } = await ethers.provider.getBlock( - "latest" - ); - const nextBlockTimestamp = currentTimestamp; - await beaconRoots["setBeaconRoot(uint256,bytes32)"]( - // I can't control the timestamp with Hardhat - nextBlockTimestamp + 2, - balanceProofs.beaconBlockRoot - ); - - await consolidationController.connect(registratorSigner).snapBalances(); - - await consolidationController - .connect(registratorSigner) - .verifyBalances(balanceProofs, pendingDepositProofs); - }; - - describe("When no consolidation in progress", () => { - beforeEach(async () => { - await activateTargetValidators(fixture); - }); - it("Should request consolidation of a single validator from the second cluster", async () => { - const sourceValidators = [secondClusterPubKeys[0]]; - - // Source validator pre-conditions - expect( - await nativeStakingStrategy2.validatorsStates( - keccak256(sourceValidators[0]) - ) - ).to.equal(2); // STAKED state - - // Target validator pre-conditions - const targetPubKeyHash = hashPubKey(activeTargetPubKey); - const validatorData = await compoundingStakingStrategy.validator( - targetPubKeyHash - ); - expect(validatorData.state).to.equal(4); // Active status - - const tx = await consolidationController - .connect(adminSigner) - .requestConsolidation( - nativeStakingStrategy2.address, - sourceValidators, - activeTargetPubKey, - { value: consolidationFee } - ); - - // Assert source strategy - await expect(tx) - .to.emit(nativeStakingStrategy2, "ConsolidationRequested") - .withArgs(sourceValidators, activeTargetPubKey, 1); - expect( - await nativeStakingStrategy2.validatorsStates( - keccak256(sourceValidators[0]) - ) - ).to.equal(3); // EXITING state - }); - it("Should request consolidation of multiple validators from the second cluster", async () => { - const sourceValidators = [ - secondClusterPubKeys[0], - secondClusterPubKeys[1], - ]; - - // Source validator pre-conditions - expect( - await nativeStakingStrategy2.validatorsStates( - keccak256(sourceValidators[1]) - ) - ).to.equal(2); // STAKED state - - // Target validator pre-conditions - const targetPubKeyHash = hashPubKey(activeTargetPubKey); - const validatorData = await compoundingStakingStrategy.validator( - targetPubKeyHash - ); - expect(validatorData.state).to.equal(4); // Active status - - const tx = await consolidationController - .connect(adminSigner) - .requestConsolidation( - nativeStakingStrategy2.address, - sourceValidators, - activeTargetPubKey, - { value: consolidationFee * sourceValidators.length } - ); - - // Assert source strategy - await expect(tx) - .to.emit(nativeStakingStrategy2, "ConsolidationRequested") - .withArgs(sourceValidators, activeTargetPubKey, 2); - expect( - await nativeStakingStrategy2.validatorsStates( - keccak256(sourceValidators[0]) - ) - ).to.equal(3); // EXITING state - expect( - await nativeStakingStrategy2.validatorsStates( - keccak256(sourceValidators[1]) - ) - ).to.equal(3); // EXITING state - }); - it("Fail to request consolidation when consolidation fees exceed msg.value", async () => { - const sourceValidators = [ - secondClusterPubKeys[0], - secondClusterPubKeys[1], - ]; - - // Get the current consolidation request fee - const result = await hre.ethers.provider.call({ - to: addresses.mainnet.toConsensus.consolidation, - data: "0x", - }); - const fee = hre.ethers.BigNumber.from(result); - - // Send 2 times the fee to the native staking strategy to cover two requests - await hre.ethers.provider.send("hardhat_setBalance", [ - nativeStakingStrategy2.address, - fee.mul(2).toHexString(), - ]); - - const tx = consolidationController - .connect(adminSigner) - .requestConsolidation( - nativeStakingStrategy2.address, - sourceValidators, - activeTargetPubKey, - // Only send enough to one request, not two - { value: fee } - ); - - await expect(tx).to.be.revertedWith("Insufficient consolidation fee"); - }); - it("Should request consolidation of a lot of validators from the second cluster", async () => { - expect( - await nativeStakingStrategy2.validatorsStates( - keccak256(secondClusterPubKeys[37]) - ) - ).to.equal(2); // STAKED state - - // Target validator pre-conditions - const targetPubKeyHash = hashPubKey(activeTargetPubKey); - const validatorData = await compoundingStakingStrategy.validator( - targetPubKeyHash - ); - expect(validatorData.state).to.equal(4); // Active status - - const tx = await consolidationController - .connect(adminSigner) - .requestConsolidation( - nativeStakingStrategy2.address, - secondClusterPubKeys, - activeTargetPubKey, - { value: consolidationFee * secondClusterPubKeys.length } - ); - - // Assert source strategy - await expect(tx) - .to.emit(nativeStakingStrategy2, "ConsolidationRequested") - .withArgs( - secondClusterPubKeys, - activeTargetPubKey, - secondClusterPubKeys.length - ); - expect( - await nativeStakingStrategy2.validatorsStates( - keccak256(secondClusterPubKeys[37]) - ) - ).to.equal(3); // EXITING state - }); - it("Should request consolidation of a lot of validators from the third cluster", async () => { - expect( - await nativeStakingStrategy3.validatorsStates( - keccak256(thirdClusterPubKeys[0]) - ) - ).to.equal(2); // STAKED state - - // Target validator pre-conditions - const targetPubKeyHash = hashPubKey(activeTargetPubKey); - const validatorData = await compoundingStakingStrategy.validator( - targetPubKeyHash - ); - expect(validatorData.state).to.equal(4); // Active status - - const tx = await consolidationController - .connect(adminSigner) - .requestConsolidation( - nativeStakingStrategy3.address, - thirdClusterPubKeys, - activeTargetPubKey, - { value: consolidationFee * thirdClusterPubKeys.length } - ); - - // Assert source strategy - await expect(tx) - .to.emit(nativeStakingStrategy3, "ConsolidationRequested") - .withArgs( - thirdClusterPubKeys, - activeTargetPubKey, - thirdClusterPubKeys.length - ); - expect( - await nativeStakingStrategy3.validatorsStates( - keccak256(thirdClusterPubKeys[0]) - ) - ).to.equal(3); // EXITING state - }); - it("Should skip snapBalances in requestConsolidation when a recent snap exists (OGVC-03)", async () => { - // After activateTargetValidators the snappedBalance.timestamp is 0 (cleared - // by verifyBalances), so we first take a legitimate snap and then call - // requestConsolidation before SNAP_BALANCES_DELAY elapses. - - // SNAP_BALANCES_DELAY = 35 slots * 12 seconds = 420 seconds - const snapDelay = 35 * 12; - // Advance enough time for an initial snap to succeed - await advanceTime(snapDelay + 12); - await consolidationController.connect(registratorSigner).snapBalances(); - - // Record the timestamp of that snap - const { timestamp: timestampBefore } = - await compoundingStakingStrategy.snappedBalance(); - - // Advance only a few slots – still within SNAP_BALANCES_DELAY - await advanceTime(5 * 12); // 5 slots = 60 seconds - - // requestConsolidation should succeed without emitting BalancesSnapped - const tx = await consolidationController - .connect(adminSigner) - .requestConsolidation( - nativeStakingStrategy2.address, - [secondClusterPubKeys[0]], - activeTargetPubKey, - { value: consolidationFee } - ); - - await expect(tx).to.not.emit( - compoundingStakingStrategy, - "BalancesSnapped" - ); - - // Record the timestamp of that snap - const { timestamp: timestampafter } = - await compoundingStakingStrategy.snappedBalance(); - - expect(timestampafter).to.equal(timestampBefore); - expect(await consolidationController.consolidationCount()).to.equal(1); - }); - it("Fail to request consolidation with duplicate source validators", async () => { - const sourceValidators = [ - secondClusterPubKeys[0], - secondClusterPubKeys[1], - // duplicated on purpose to testing - secondClusterPubKeys[0], - ]; - - const tx = consolidationController - .connect(adminSigner) - .requestConsolidation( - nativeStakingStrategy2.address, - sourceValidators, - activeTargetPubKey, - { value: consolidationFee * sourceValidators.length } - ); - - await expect(tx).to.revertedWith("Duplicate source validator"); - }); - it("Fail to request consolidation with empty source validators", async () => { - const tx = consolidationController - .connect(adminSigner) - .requestConsolidation( - nativeStakingStrategy2.address, - [], - activeTargetPubKey, - { value: 0 } - ); - - await expect(tx).to.be.revertedWith("Empty source validators"); - expect(await consolidationController.consolidationCount()).to.equal(0); - expect(await consolidationController.sourceStrategy()).to.equal( - ethers.constants.AddressZero - ); - expect(await consolidationController.targetPubKeyHash()).to.equal( - ethers.constants.HashZero - ); - }); - it("Fail to request consolidate from a validator that is EXITING state", async () => { - const exitedValidatorPubKey = secondClusterPubKeys[0]; - // Exit a validator from the second cluster - await consolidationController - .connect(registratorSigner) - .exitSsvValidator( - nativeStakingStrategy2.address, - exitedValidatorPubKey, - secondClusterOperatorIds - ); - - // Source validator pre-conditions - expect( - await nativeStakingStrategy2.validatorsStates( - keccak256(exitedValidatorPubKey) - ) - ).to.equal(3); // EXITING state - - const tx = consolidationController - .connect(adminSigner) - .requestConsolidation( - nativeStakingStrategy2.address, - [exitedValidatorPubKey], - activeTargetPubKey, - { value: consolidationFee } - ); - - await expect(tx).to.be.revertedWith("Source validator not staked"); - }); - it("Fail to request consolidate from a validator that is EXITED_COMPLETE state", async () => { - const previouslyExitedValidatorPubKey = - "0x8db6d9578e01ef6f1e6c655ff094a91d4ae02734d66accbdca8432eaa0b815cee503325f98b8406f2ab372a30d0f9edb"; - - // Source validator pre-conditions - expect( - await nativeStakingStrategy2.validatorsStates( - keccak256(previouslyExitedValidatorPubKey) - ) - ).to.equal(4); // EXITED_COMPLETE state - - const tx = consolidationController - .connect(adminSigner) - .requestConsolidation( - nativeStakingStrategy2.address, - [previouslyExitedValidatorPubKey], - activeTargetPubKey, - { value: consolidationFee } - ); - - await expect(tx).to.be.revertedWith("Source validator not staked"); - }); - it("Fail to request consolidation from an unkown source validator", async () => { - const unknownValidatorPubKey = - "0x808f0e79b73f968e064ecba2702a65bed93cf46149a69f0e4de921b44eab3fd456a1ca0f082887069e5831e139eb2690"; - - // Source validator pre-conditions - expect( - await nativeStakingStrategy2.validatorsStates( - keccak256(unknownValidatorPubKey) - ) - ).to.equal(0); // UNKNOWN state - - const tx = consolidationController - .connect(adminSigner) - .requestConsolidation( - nativeStakingStrategy2.address, - [unknownValidatorPubKey], - activeTargetPubKey, - { value: consolidationFee } - ); - - await expect(tx).to.be.revertedWith("Source validator not staked"); - }); - it("Fail to request consolidation to an UNKNOWN target validator", async () => { - const unknownValidatorPubKey = - "0x808f0e79b73f968e064ecba2702a65bed93cf46149a69f0e4de921b44eab3fd456a1ca0f082887069e5831e139eb2690"; - - // Target validator pre-conditions - const targetPubKeyHash = hashPubKey(unknownValidatorPubKey); - const validatorData = await compoundingStakingStrategy.validator( - targetPubKeyHash - ); - expect(validatorData.state).to.equal(0); // Unknown status - - const tx = consolidationController - .connect(adminSigner) - .requestConsolidation( - nativeStakingStrategy2.address, - [secondClusterPubKeys[0]], - unknownValidatorPubKey, - { value: consolidationFee } - ); - - await expect(tx).to.be.revertedWith("Target validator not active"); - }); - it("Fail to request consolidation to invalid source public key", async () => { - // Key only 32 bytes long instead of 48 bytes - const invalidValidatorPubKey = - "0x0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF"; - - const tx = consolidationController - .connect(adminSigner) - .requestConsolidation( - nativeStakingStrategy2.address, - [invalidValidatorPubKey], - activeTargetPubKey, - { value: consolidationFee } - ); - - await expect(tx).to.be.revertedWith("Invalid public key"); - }); - it("Fail to request consolidation to invalid target public key", async () => { - // Key only 32 bytes long instead of 48 bytes - const invalidValidatorPubKey = - "0x0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF"; - - const tx = consolidationController - .connect(adminSigner) - .requestConsolidation( - nativeStakingStrategy2.address, - [secondClusterPubKeys[0]], - invalidValidatorPubKey, - { value: consolidationFee } - ); - - await expect(tx).to.be.revertedWith("Invalid public key"); - }); - it("Fail to request consolidation to a STAKED target validator", async () => { - const stakedCompoundingValidatorPubKey = - "0xa4258aa50aba9d7441f734213ae76fad9809572a593765c25c25d7afd42b83baba06397bd9e264a9fa24c3327a308682"; - - // Target validator pre-conditions - const targetPubKeyHash = hashPubKey(stakedCompoundingValidatorPubKey); - const validatorData = await compoundingStakingStrategy.validator( - targetPubKeyHash - ); - expect(validatorData.state).to.equal(2); // Staked status - - const tx = consolidationController - .connect(adminSigner) - .requestConsolidation( - nativeStakingStrategy2.address, - [secondClusterPubKeys[0]], - stakedCompoundingValidatorPubKey, - { value: consolidationFee } - ); - - await expect(tx).to.be.revertedWith("Target validator not active"); - }); - it("Fail to request consolidation to a target validator with a pending deposit", async () => { - const activeWithDepositCompoundingValidatorPubKey = - "0x8427639adf9c746f7d7271ddee3bbcd7a1f3b4beb3bd67224c345d7c7e7cffd58d61d5bc84a3ab7d0f909ebf71da7b8b"; - - // Target validator pre-conditions - const targetPubKeyHash = hashPubKey( - activeWithDepositCompoundingValidatorPubKey - ); - const validatorData = await compoundingStakingStrategy.validator( - targetPubKeyHash - ); - expect(validatorData.state).to.equal(4); // Active status - - const tx = consolidationController - .connect(adminSigner) - .requestConsolidation( - nativeStakingStrategy2.address, - [secondClusterPubKeys[0]], - activeWithDepositCompoundingValidatorPubKey, - { value: consolidationFee } - ); - - await expect(tx).to.be.revertedWith("Target has pending deposit"); - }); - it("Fail to request consolidation on contract controller if not admin multisig", async () => { - const { josh, strategist, timelock } = fixture; - const sourceValidators = [secondClusterPubKeys[0]]; - - const users = [registratorSigner, josh, strategist, timelock]; - - for (const user of users) { - const tx = consolidationController - .connect(user) - .requestConsolidation( - nativeStakingStrategy2.address, - sourceValidators, - activeTargetPubKey, - { value: consolidationFee } - ); - - await expect(tx).to.be.revertedWith("Ownable: caller is not the owner"); - } - }); - it("Fail to request consolidation on native staking strategy if not Consolidation Controller", async () => { - const { josh, strategist, timelock } = fixture; - const sourceValidators = [secondClusterPubKeys[0]]; - - const users = [ - adminSigner, - registratorSigner, - josh, - strategist, - timelock, - ]; - - for (const user of users) { - const tx = nativeStakingStrategy2 - .connect(user) - .requestConsolidation(sourceValidators, activeTargetPubKey, { - value: consolidationFee, - }); - - await expect(tx).to.be.revertedWith("Caller is not the Registrator"); - } - }); - it("Fail to call fail consolidation when there's no active consolidation", async () => { - const tx = consolidationController - .connect(adminSigner) - .failConsolidation([secondClusterPubKeys[0]]); - - await expect(tx).to.be.revertedWith("No consolidation in progress"); - }); - it("Should call snapBalance on the Consolidation Controller by anyone when no consolidation in progress", async () => { - const { josh } = fixture; - await advanceTime(12 * 40); - - const tx = await consolidationController.connect(josh).snapBalances(); - - await expect(tx).to.emit(compoundingStakingStrategy, "BalancesSnapped"); - }); - it("Fail to directly call verifyBalance on the Compounding Staking Strategy", async () => { - await advanceTime(12 * 40); - await consolidationController.snapBalances(); - - const tx = compoundingStakingStrategy - .connect(registratorSigner) - .verifyBalances(balanceProofs, pendingDepositProofs); - - await expect(tx).to.be.revertedWith("Not Registrator"); - }); - it("Should call doAccounting via the Consolidation Controller", async () => { - await consolidationController - .connect(registratorSigner) - .doAccounting(nativeStakingStrategy2.address); - - await consolidationController - .connect(registratorSigner) - .doAccounting(nativeStakingStrategy3.address); - }); - it("Fail to directly call doAccounting via the old Native Staking Strategies", async () => { - let tx = nativeStakingStrategy2.connect(registratorSigner).doAccounting(); - await expect(tx).to.be.revertedWith("Caller is not the Registrator"); - - tx = nativeStakingStrategy3.connect(registratorSigner).doAccounting(); - await expect(tx).to.be.revertedWith("Caller is not the Registrator"); - }); - it("Should exit source validators via the consolidation controller", async () => { - await consolidationController - .connect(registratorSigner) - .exitSsvValidator( - nativeStakingStrategy2.address, - secondClusterPubKeys[0], - secondClusterOperatorIds - ); - - await consolidationController - .connect(registratorSigner) - .exitSsvValidator( - nativeStakingStrategy3.address, - thirdClusterPubKeys[0], - thirdClusterOperatorIds - ); - }); - it("Fail to exit source validators directly to the native staking strategies", async () => { - let tx = nativeStakingStrategy2 - .connect(registratorSigner) - .exitSsvValidator(secondClusterPubKeys[0], secondClusterOperatorIds); - await expect(tx).to.be.revertedWith("Caller is not the Registrator"); - - tx = nativeStakingStrategy3 - .connect(registratorSigner) - .exitSsvValidator(thirdClusterPubKeys[0], thirdClusterOperatorIds); - await expect(tx).to.be.revertedWith("Caller is not the Registrator"); - }); - it("Fail to remove source validators directly to the native staking strategies", async () => { - let tx = nativeStakingStrategy2 - .connect(registratorSigner) - .removeSsvValidator( - secondClusterPubKeys[0], - secondClusterOperatorIds, - emptyCluster - ); - await expect(tx).to.be.revertedWith("Caller is not the Registrator"); - - tx = nativeStakingStrategy3 - .connect(registratorSigner) - .removeSsvValidator( - thirdClusterPubKeys[0], - thirdClusterOperatorIds, - emptyCluster - ); - await expect(tx).to.be.revertedWith("Caller is not the Registrator"); - }); - it("Should partial withdraw from compounding validator via the consolidation controller", async () => { - const withdrawAmount = ethers.utils.parseEther("2", 9); - const targetPubKeyHash = hashPubKey(activeTargetPubKey); - - const tx = await consolidationController - .connect(registratorSigner) - .validatorWithdrawal(activeTargetPubKey, withdrawAmount, { - value: consolidationFee, - }); - - await expect(tx) - .to.emit(compoundingStakingStrategy, "ValidatorWithdraw") - .withArgs(targetPubKeyHash, withdrawAmount.mul(parseUnits("1", 9))); - }); - it("Fail validator exit from compounding validator via the consolidation controller", async () => { - const tx = consolidationController - .connect(registratorSigner) - .validatorWithdrawal(activeTargetPubKey, 0, { - value: consolidationFee, - }); - - await expect(tx).to.be.revertedWith("No exit during migration"); - }); - it("Should stake to compounding validator via the consolidation controller", async () => { - const { weth, josh } = fixture; - - const depositEth = "3"; - const depositGwei = parseUnits(depositEth, 9); - const depositWei = parseUnits(depositEth, 18); - const targetPubKeyHash = hashPubKey(activeTargetPubKey); - const emptySignature = - "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; - - // Fund the staking strategy with enough WETH to be able to stake - await weth.connect(josh).deposit({ value: depositWei }); - await weth - .connect(josh) - .transfer(compoundingStakingStrategy.address, depositWei); - - const depositDataRoot = await calcDepositRoot( - compoundingStakingStrategy.address, - "0x02", - activeTargetPubKey, - emptySignature, - depositEth - ); - - const tx = await consolidationController - .connect(registratorSigner) - .stakeEth( - { - pubkey: activeTargetPubKey, - signature: emptySignature, - depositDataRoot: depositDataRoot, - }, - depositGwei - ); - - await expect(tx) - .to.emit(compoundingStakingStrategy, "ETHStaked") - .withNamedArgs({ - pubKeyHash: targetPubKeyHash, - // pendingDepositRoot - pubKey: activeTargetPubKey, - amountWei: depositWei, - }); - }); - it("Should remove a source validator via the Consolidation Controller", async () => { - const sourceValidator = secondClusterPubKeys[0]; - - // Exit the source validator to be able to remove it - await consolidationController - .connect(registratorSigner) - .exitSsvValidator( - nativeStakingStrategy2.address, - sourceValidator, - secondClusterOperatorIds - ); - - const { cluster } = await getClusterInfo({ - ownerAddress: nativeStakingStrategy2.address, - operatorids: secondClusterOperatorIds, - chainId: hre.network.config.chainId, - ssvNetwork: addresses.SSVNetwork, - }); - - const tx = await consolidationController - .connect(registratorSigner) - .removeSsvValidator( - nativeStakingStrategy2.address, - sourceValidator, - secondClusterOperatorIds, - cluster - ); - - await expect(tx) - .to.emit(nativeStakingStrategy2, "SSVValidatorExitCompleted") - .withArgs( - keccak256(sourceValidator), - sourceValidator, - secondClusterOperatorIds - ); - }); - it("Fail to remove validator if not registrator", async () => { - const { josh } = fixture; - const sourceValidator = secondClusterPubKeys[0]; - - const tx = consolidationController - .connect(josh) - .removeSsvValidator( - nativeStakingStrategy2.address, - sourceValidator, - secondClusterOperatorIds, - emptyCluster - ); - await expect(tx).to.be.revertedWith("Caller is not the Registrator"); - }); - }); - describe("When consolidation in progress", () => { - const sourceValidators = [ - secondClusterPubKeys[0], - secondClusterPubKeys[1], - secondClusterPubKeys[2], - ]; - const minWithdrableTime = 261 * 32 * 12; // 261 epochs - beforeEach(async () => { - await activateTargetValidators(fixture); - - // Get the current block timestamp - await advanceTime(12); - const { timestamp: currentTimestamp } = await ethers.provider.getBlock( - "latest" - ); - const nextBlockTimestamp = currentTimestamp; - await fixture.beaconRoots["setBeaconRoot(uint256,bytes32)"]( - // I can't control the timestamp with Hardhat - nextBlockTimestamp + 2, - // reusing the last balances proof assuming the balances have not changed. - balanceProofs.beaconBlockRoot - ); - - await consolidationController - .connect(adminSigner) - .requestConsolidation( - nativeStakingStrategy2.address, - sourceValidators, - activeTargetPubKey, - { value: consolidationFee * sourceValidators.length } - ); - }); - it("Fail to request consolidation when there's an active consolidation", async () => { - const tx = consolidationController - .connect(adminSigner) - .requestConsolidation( - nativeStakingStrategy2.address, - [secondClusterPubKeys[3]], - activeTargetPubKey, - { value: consolidationFee } - ); - - await expect(tx).to.be.revertedWith("Consolidation in progress"); - }); - it("Should be able to verify balance when the consolidation was requested", async () => { - const tx = await consolidationController - .connect(registratorSigner) - .verifyBalances(balanceProofs, pendingDepositProofs); - - await expect(tx).to.emit(compoundingStakingStrategy, "BalancesVerified"); - }); - it("Should verify a pre-existing snap taken before consolidation started (OGVC-03)", async () => { - await advanceTime(minWithdrableTime); - - await consolidationController - .connect(adminSigner) - .failConsolidation(sourceValidators); - - // Take a valid snap first, then request consolidation before delay elapses - // so requestConsolidation does not take another snap. - const snapDelay = 35 * 12; - await advanceTime(snapDelay + 12); - - const { timestamp: snapSetupTimestamp } = await ethers.provider.getBlock( - "latest" - ); - await fixture.beaconRoots["setBeaconRoot(uint256,bytes32)"]( - snapSetupTimestamp + 2, - balanceProofs.beaconBlockRoot - ); - await consolidationController.connect(registratorSigner).snapBalances(); - - const { blockRoot: snappedBlockRoot, timestamp: snappedTimestamp } = - await compoundingStakingStrategy.snappedBalance(); - expect(snappedBlockRoot).to.equal(balanceProofs.beaconBlockRoot); - - await advanceTime(12); - - const { timestamp: currentTimestamp } = await ethers.provider.getBlock( - "latest" - ); - await fixture.beaconRoots["setBeaconRoot(uint256,bytes32)"]( - currentTimestamp + 2, - balanceProofs.beaconBlockRoot - ); - - await consolidationController - .connect(adminSigner) - .requestConsolidation( - nativeStakingStrategy2.address, - sourceValidators, - activeTargetPubKey, - { value: consolidationFee * sourceValidators.length } - ); - - const consolidationStartTimestamp = - await consolidationController.consolidationStartTimestamp(); - - expect(snappedTimestamp).to.be.lt(consolidationStartTimestamp); - - const tx = await consolidationController - .connect(registratorSigner) - .verifyBalances(balanceProofs, pendingDepositProofs); - - await expect(tx).to.emit(compoundingStakingStrategy, "BalancesVerified"); - }); - // Balance proofs after the deposit to validator 13498458 has been verified. - it("Should call snapBalance on the Consolidation Controller by the Registrator after the consolidation has started", async () => { - await advanceTime(minWithdrableTime); - await advanceTime(12 * 40); - - const tx = await consolidationController - .connect(registratorSigner) - .snapBalances(); - - await expect(tx).to.emit(compoundingStakingStrategy, "BalancesSnapped"); - }); - it("Fail snapBalance on the Consolidation Controller if called too soon during consolidation", async () => { - const tx = consolidationController - .connect(registratorSigner) - .snapBalances(); - - await expect(tx).to.be.revertedWith("Source not withdrawable"); - }); - it("Fail snapBalance on the Consolidation Controller when called by non-registrator after the consolidation has started", async () => { - const { josh } = fixture; - await advanceTime(12 * 40); - - const tx = consolidationController.connect(josh).snapBalances(); - - await expect(tx).to.be.revertedWith("Consolidation in progress"); - }); - it("Fail snapBalance on the new compounding staking strategy when called by non-registrator after the consolidation has started", async () => { - const { josh } = fixture; - await advanceTime(12 * 40); - - const tx = compoundingStakingStrategy.connect(josh).snapBalances(); - - await expect(tx).to.be.revertedWith("Not Registrator"); - }); - it("Fail to verifyBalance of a snapshot after the consolidation has started", async () => { - await advanceTime(minWithdrableTime); - await advanceTime(12 * 40); - await consolidationController.connect(registratorSigner).snapBalances(); - - const tx = consolidationController - .connect(registratorSigner) - .verifyBalances(balanceProofs, pendingDepositProofs); - - await expect(tx).to.be.revertedWith("Consolidation in progress"); - }); - it("Fail to call fail consolidation before minimum consolidation period", async () => { - const tx = consolidationController - .connect(adminSigner) - .failConsolidation([sourceValidators[0]]); - - await expect(tx).to.be.revertedWith("Source not withdrawable"); - }); - // When a consolidation has been requested - it("Should call fail consolidation of a single validator", async () => { - await advanceTime(minWithdrableTime); - - const consolidationCountBefore = - await consolidationController.consolidationCount(); - expect(consolidationCountBefore).to.equal(3); - - const tx = await consolidationController - .connect(adminSigner) - .failConsolidation([sourceValidators[0]]); - - await expect(tx) - .to.emit(nativeStakingStrategy2, "ConsolidationFailed") - .withArgs([sourceValidators[0]], 1); - await expect(await consolidationController.consolidationCount()).to.equal( - consolidationCountBefore.sub(1) - ); - expect(await consolidationController.sourceStrategy()).to.equal( - nativeStakingStrategy2.address - ); - - // Source validator post-conditions - expect( - await nativeStakingStrategy2.validatorsStates( - keccak256(sourceValidators[0]) - ) - ).to.equal(2); // STAKED state - }); - it("Should call fail consolidation for multiple validators", async () => { - await advanceTime(minWithdrableTime); - - const consolidationCountBefore = - await consolidationController.consolidationCount(); - expect(consolidationCountBefore).to.equal(3); - - const failedValidators = [sourceValidators[0], sourceValidators[1]]; - - const tx = await consolidationController - .connect(adminSigner) - .failConsolidation(failedValidators); - - await expect(tx) - .to.emit(nativeStakingStrategy2, "ConsolidationFailed") - .withArgs(failedValidators, failedValidators.length); - await expect(await consolidationController.consolidationCount()).to.equal( - consolidationCountBefore.sub(failedValidators.length) - ); - expect(await consolidationController.sourceStrategy()).to.equal( - nativeStakingStrategy2.address - ); - expect(await consolidationController.targetPubKeyHash()).to.not.equal( - ethers.constants.HashZero - ); - - // Source validator post-conditions - expect( - await nativeStakingStrategy2.validatorsStates( - keccak256(sourceValidators[1]) - ) - ).to.equal(2); // STAKED state - }); - it("Should call fail consolidation for all validators and reset state", async () => { - await advanceTime(minWithdrableTime); - - const consolidationCountBefore = - await consolidationController.consolidationCount(); - expect(consolidationCountBefore).to.equal(3); - - const tx = await consolidationController - .connect(adminSigner) - .failConsolidation(sourceValidators); - - await expect(tx) - .to.emit(nativeStakingStrategy2, "ConsolidationFailed") - .withArgs(sourceValidators, sourceValidators.length); - expect(await consolidationController.consolidationCount()).to.equal(0); - expect(await consolidationController.sourceStrategy()).to.equal( - ethers.constants.AddressZero - ); - expect(await consolidationController.targetPubKeyHash()).to.equal( - ethers.constants.HashZero - ); - }); - it("Fail to call fail consolidation if not admin multisig", async () => { - const { josh, strategist, timelock } = fixture; - const sourceValidators = [secondClusterPubKeys[0]]; - - const users = [registratorSigner, josh, strategist, timelock]; - - for (const user of users) { - const tx = consolidationController - .connect(user) - .failConsolidation([sourceValidators[0]]); - - await expect(tx).to.be.revertedWith("Ownable: caller is not the owner"); - } - }); - it("Fail to call fail consolidation to invalid source public key", async () => { - // Key only 32 bytes long instead of 48 bytes - const invalidValidatorPubKey = - "0x0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF"; - - await advanceTime(minWithdrableTime); - - const tx = consolidationController - .connect(adminSigner) - .failConsolidation([invalidValidatorPubKey]); - - await expect(tx).to.be.revertedWith("Invalid public key"); - }); - it("Fail to call fail consolidation for unknown source validator", async () => { - const unknownValidatorPubKey = - "0x808f0e79b73f968e064ecba2702a65bed93cf46149a69f0e4de921b44eab3fd456a1ca0f082887069e5831e139eb2690"; - - await advanceTime(minWithdrableTime); - - const tx = consolidationController - .connect(adminSigner) - .failConsolidation([unknownValidatorPubKey]); - - await expect(tx).to.be.revertedWith("Unknown source validator"); - }); - it("Fail to stake to target of active consolidation", async () => { - const depositEth = "3"; - const depositGwei = parseUnits(depositEth, 9); - - const emptySignature = - "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; - - const depositDataRoot = await calcDepositRoot( - compoundingStakingStrategy.address, - "0x02", - activeTargetPubKey, - emptySignature, - depositEth - ); - - const tx = consolidationController.connect(registratorSigner).stakeEth( - { - pubkey: activeTargetPubKey, - signature: emptySignature, - depositDataRoot: depositDataRoot, - }, - depositGwei - ); - - await expect(tx).to.be.revertedWith("Stake to consolidation target"); - }); - it("Fail confirm consolidation if processed too soon", async () => { - const tx = consolidationController - .connect(adminSigner) - .confirmConsolidation(balanceProofs, pendingDepositProofs); - - await expect(tx).to.be.revertedWith("Source not withdrawable"); - }); - it("Fail to remove validator when consolidation in progress", async () => { - const sourceValidator = secondClusterPubKeys[0]; - - const tx = consolidationController - .connect(registratorSigner) - .removeSsvValidator( - nativeStakingStrategy2.address, - sourceValidator, - secondClusterOperatorIds, - emptyCluster - ); - await expect(tx).to.be.revertedWith("Consolidation in progress"); - }); - it("Should remove validator from non-consolidating source strategy during consolidation", async () => { - const sourceValidator = thirdClusterPubKeys[0]; - - await consolidationController - .connect(registratorSigner) - .exitSsvValidator( - nativeStakingStrategy3.address, - sourceValidator, - thirdClusterOperatorIds - ); - - const { cluster } = await getClusterInfo({ - ownerAddress: nativeStakingStrategy3.address, - operatorids: thirdClusterOperatorIds, - chainId: hre.network.config.chainId, - ssvNetwork: addresses.SSVNetwork, - }); - - const tx = await consolidationController - .connect(registratorSigner) - .removeSsvValidator( - nativeStakingStrategy3.address, - sourceValidator, - thirdClusterOperatorIds, - cluster - ); - - await expect(tx) - .to.emit(nativeStakingStrategy3, "SSVValidatorExitCompleted") - .withArgs( - keccak256(sourceValidator), - sourceValidator, - thirdClusterOperatorIds - ); - }); - }); - describe("When consolidation in progress and balances snapped", () => { - const sourceValidators = [ - secondClusterPubKeys[0], - secondClusterPubKeys[1], - secondClusterPubKeys[2], - ]; - - // Balance proofs after the consolidation has been processed on the beacon chain - // Validator with index 2178131 has its balance increaed by 96 ETH - // Run the following Hardhat task to get the proofs: - // pnpm hardhat verifyBalances --network mainnet --dryrun true --test true --over-ids 2178131,2213390 --over-bals 128.5,32.5 --slot 13928087 - const balanceProofs = { - beaconBlockRoot: - "0x0585a4f9646714ef9af7b69c6ed1fa74d8ef44f748c193e8de4d2d994d78345e", - balancesContainerRoot: - "0x76eb3a513bc54c3167a144f41aec2b124417364bc31b4601dfda0604f071630e", - balancesContainerProof: - "0x85324c52a14d47585124af7b122fb4e5dc95a328195a69cdbd2ac467a380087bde78b6f9afdee27c83dcab795db1174fb31809c3e18ccd101c45ca92146a34ba0fe39f130e2611965f830d94a77f38cd694b1c92c82bb1426082fd11974bf67429a18eca977c27b0c22f5df812b50554ee42926f66b70fe930751eaee9e5577c88cbdc15d22d62cf98507f4ec9c2859d7a3604a8712b3a4696e8ca42eca1293c35ecb1dce6bdc3af85bd9cf2f1f03cb8a04d217a199b03abd30d462a261fe2e7445fd94dbbc7e4c1e17974c0745d82a97865458517ec4d426b861f9c4e90e9d0b53e114879b41538e556e5d6a209127efcd4b0fe59e0bf64de8357cdc2273d6ec0cdabace9443f7ec6183c3a82e1762c339d8dfbbd1dfca1ba16967b4f2e6e8a", - validatorBalanceLeaves: [ - "0xfd7c8373070000008984d5b626000000dba68373070000002e054f8207000000", - "0x7bc26d8107000000a9b15ccc0e0000004b9e8073070000000000000000000000", - "0x1fc47c7307000000e2b07c73070000000c927c7307000000006532eb1d000000", - "0x519f7b7307000000d2af7b73070000002a70028407000000f6977b7307000000", - "0xec30747307000000255474730700000000a52691070000001026e9463a000000", - ], - validatorBalanceProofs: [ - "0x917d8373070000009eaf837307000000ea7d837307000000b39e8373070000008c48c30a5f5ae1a56746099c051ca96073e376aac7551f0d6d6a15a9a48f4052d994e0b28373640577c0ab09ace51a55fef80ad962e2faed045b55947a7a4f34e54d6073f9d09e38595b3f6ecfd92945b721d23784789129babc2094a2915987da37705db46c6e770d7f8c5b9ec69d35257f92434347705e56b1d5d561f2fdaa50a600d0379af06018ac56268cf9a870f71986b9a9e60cdf51fb3c6d0d52e9f0670e19e0ff26a62093902fbca179f7f36f258d50a9073149f965612acb5773dcfd63d877accda0a33848d28d9689f0e3b83a09bef854569381d7889bbb536159d7e71add819625b8fb0ede9e5c35e0ed113de4618d5f442e7f82e0a0f582c4ac53f83b465d1d41767e25220cc72c91e6b5eceb05f2952e98dad8db50a87de1b6ed1740dd097f2086df81b875c8b39db903df524bdc88b56ac9a50f1ca1ab16ba6ecf6113eb8e424f51815a7f0a0864004680bf513454bba188ec30a1c66a9b8534965f292af9adf02e23328f62e4bef83791aeb3d729ed87cd9838fad6790dd7d66891394e3eeb07d48be1bf8604cac743a2841e87a4e163862255cf07f5195fc3d571aa74d41c43bb1109597b4fa8aea80f0a8f96490216df86e921f1c320b1daeeea7a208232bf77025a6e81e886e674b3aadc3931bc8f0268e2b8350d63608fe6b1689256c0d385f42f5bbe2027a22c1996e110ba97c171d3e5948de92beb8d0d63c39ebade8509e0ae3c9c3876fb5fa112be18f905ecacfecb92057603ab95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a43e251b9aa4f9097ef23129626b849648d24e38fb1f16829559482a9d5da28473cddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9cfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167e71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d731206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc021352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a467657cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe18869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636b5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7c6f67e02e6e4e1bdefb994c6098953f34636ba2b6ca20a4721d2b26a886722ff1c9a7e5ff1cf48b4ad1582d3f4e4a1004f3b20d8c5a2b71387a4254ad933ebc52f075ae229646b6f6aed19a5e372cf295081401eb893ff599b3f9acc0c0d3e7d328921deb59612076801e8cd61592107b5c67c79b846595cc6320c395b46362cbfb909fdb236ad2411b4e4883810a074b840464689986c3f8a8091827e17c32755d8fb3687ba3ba49f342c77f5a1f89bec83d811446e1a467139213d640b6a748d18220000000000000000000000000000000000000000000000000000000000", - "0x5058807307000000d6c780730700000070bf7f76070000002560807307000000a360ff8aa16d43dbf04b07a5b55942294f5d57adf813a844d305c8c6b2cd993ffbf093e17c6938a06cc1a21c77904c05c21e5f710e745ca3e40b88f1671050c633b03e8a35f6247def38d97d633697663930096b94cf96bda09764ec852d3abc29848299af499e3c19da8a78b953624818e08eb50a7b4bd6cdb884e7c4e9f9de7a3b437e6ac8737e2fb5e8268cc3b4804f92bfb6efd420fe08b0f69cda9653f46e8a1707614ee210693a4ab9296bca5d1f7d5ff3e435f4d1a842d94e669f2404986fb00fe176687a08685b8333dc93c6d4a3a549a9efd75c64709d82d8c95eec837c502df948738b0e5a1e5cc552caf4f8c1a87ec43a01a890b069acbb0936c417af734a796fa2937aad9c827c0db2f0acf793fafdef4eddd71a12886765359e8d8fc5bf9ce38b8bdd8fbea19bd20cc3974d7fb337f1e513b7121df301e709733eb355e772fa45ebc65fb923d0eff6c2dc7632e422725146683ad8b8bad084f634965f292af9adf02e23328f62e4bef83791aeb3d729ed87cd9838fad6790dd7d66891394e3eeb07d48be1bf8604cac743a2841e87a4e163862255cf07f5195fc3d571aa74d41c43bb1109597b4fa8aea80f0a8f96490216df86e921f1c320b1daeeea7a208232bf77025a6e81e886e674b3aadc3931bc8f0268e2b8350d63608fe6b1689256c0d385f42f5bbe2027a22c1996e110ba97c171d3e5948de92beb8d0d63c39ebade8509e0ae3c9c3876fb5fa112be18f905ecacfecb92057603ab95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a43e251b9aa4f9097ef23129626b849648d24e38fb1f16829559482a9d5da28473cddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9cfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167e71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d731206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc021352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a467657cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe18869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636b5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7c6f67e02e6e4e1bdefb994c6098953f34636ba2b6ca20a4721d2b26a886722ff1c9a7e5ff1cf48b4ad1582d3f4e4a1004f3b20d8c5a2b71387a4254ad933ebc52f075ae229646b6f6aed19a5e372cf295081401eb893ff599b3f9acc0c0d3e7d328921deb59612076801e8cd61592107b5c67c79b846595cc6320c395b46362cbfb909fdb236ad2411b4e4883810a074b840464689986c3f8a8091827e17c32755d8fb3687ba3ba49f342c77f5a1f89bec83d811446e1a467139213d640b6a748d18220000000000000000000000000000000000000000000000000000000000", - "0xa49a7c7307000000acf67c730700000037e57c730700000033e57c73070000008511637653965e07d7eeec0223160f5358eadfaa3c6105ba20ea3bdf05ba7c6f75e7b877dedc859f9584a998e122e6aeffa96b9f05459f83e3936af6e50659ce6544de0e1bcef3cf00b1214e085b562bbab1889bd25bcf8849fe8c1da7fdec72a5148e91dd117e0dab7a05c9d2394e7e66cb7b21096dc8690dd0d8208c336604aad72859e0cbcf82058befcb3ceb8cea7fb7067115232dd11ab59a47ed538aeb46933c6defd1f2f9ab6c19b5e9d6bdfd184d7913f2e0cc9b588116966b5f5cf0b966c56783dfe114de1a7df4dc96def908aa579b9b56e06e4b0465f7d1332fc5cf9a044bff818ea2b269f25762c367896911a93f2203a913d64e811611b1da45d7d5b62bc68abf2e770261e5ae2c128f915c56a61a166a4cdbb3372911b6c69f9abff178ccc312b22b154981de48b8a5b0650b3a4e175ab9dd3636e67e5c4eff6b93420876ef20a461a47eb645ed08b852f79e40ce93cd2b9203568fbbd6094c34b74724946c4673142c68b9fd1d7daa05f34dd5747a10f1c5010de31d47960e78c9350c52049bb9000dbda4f0a908c8eb29606b74fb3702653514a19cb6147f6db4c812b0451988f4ee7061bc49e1f7fe37e1a2548469e13db07d9f235e9023daeeea7a208232bf77025a6e81e886e674b3aadc3931bc8f0268e2b8350d63608fe6b1689256c0d385f42f5bbe2027a22c1996e110ba97c171d3e5948de92beb8d0d63c39ebade8509e0ae3c9c3876fb5fa112be18f905ecacfecb92057603ab95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a43e251b9aa4f9097ef23129626b849648d24e38fb1f16829559482a9d5da28473cddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9cfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167e71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d731206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc021352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a467657cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe18869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636b5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7c6f67e02e6e4e1bdefb994c6098953f34636ba2b6ca20a4721d2b26a886722ff1c9a7e5ff1cf48b4ad1582d3f4e4a1004f3b20d8c5a2b71387a4254ad933ebc52f075ae229646b6f6aed19a5e372cf295081401eb893ff599b3f9acc0c0d3e7d328921deb59612076801e8cd61592107b5c67c79b846595cc6320c395b46362cbfb909fdb236ad2411b4e4883810a074b840464689986c3f8a8091827e17c32755d8fb3687ba3ba49f342c77f5a1f89bec83d811446e1a467139213d640b6a748d18220000000000000000000000000000000000000000000000000000000000", - "0xe8b07b73070000006e5bb4eb34000000be957b730700000097867b730700000059e193bc2473fca31d2a10f8454520bee1957d7e3e9ced87f2e8425c0c08f6bb52cf8c48e946e9da705ac19a462706b411d24b8f373e325a628208d7047d0ffc0fee33b653cd0fd01e2ca1608a9b94c94c67f365657d8a86a53bc8dfaa3988a724504dfe79b6f5afb44ecf53753aad7efff2452183032a5b1219b5c99f384719c1022f632a5e029648ce78f33b5cbe1c606c327b7b29aa4c03cff2be1ae5ccebac8253bf25ade6c873f6509570ce4a30783c746c5875ca346078e184eac19dfc9f416299e9fb4250b04a8e3e2d22a2771eda8639962bc285055754f357667fdd80cd1d2fef1f6a297426c80d198e5821e538e851fc608333ac632937e0753b69b1b1cba2be0ab14696726eac8f6b818ef3d0396e6d563b73d378a48f0ed77cd518b72fef2551971a58c574c4fad812bbd71e0a41ed97c1c74d3f665e4f849945939fc1349559b3feaf32db756fbdb425cb7de72cee400a0a83ae1a3fc1541ee189c4fc42c193ce14c667e64f4eb2fc21440f68cf73e9699e07375a2fd0f8fc8a78c9350c52049bb9000dbda4f0a908c8eb29606b74fb3702653514a19cb6147f6db4c812b0451988f4ee7061bc49e1f7fe37e1a2548469e13db07d9f235e9023daeeea7a208232bf77025a6e81e886e674b3aadc3931bc8f0268e2b8350d63608fe6b1689256c0d385f42f5bbe2027a22c1996e110ba97c171d3e5948de92beb8d0d63c39ebade8509e0ae3c9c3876fb5fa112be18f905ecacfecb92057603ab95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a43e251b9aa4f9097ef23129626b849648d24e38fb1f16829559482a9d5da28473cddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9cfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167e71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d731206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc021352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a467657cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe18869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636b5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7c6f67e02e6e4e1bdefb994c6098953f34636ba2b6ca20a4721d2b26a886722ff1c9a7e5ff1cf48b4ad1582d3f4e4a1004f3b20d8c5a2b71387a4254ad933ebc52f075ae229646b6f6aed19a5e372cf295081401eb893ff599b3f9acc0c0d3e7d328921deb59612076801e8cd61592107b5c67c79b846595cc6320c395b46362cbfb909fdb236ad2411b4e4883810a074b840464689986c3f8a8091827e17c32755d8fb3687ba3ba49f342c77f5a1f89bec83d811446e1a467139213d640b6a748d18220000000000000000000000000000000000000000000000000000000000", - "0xc14ded8209000000d3399f7507000000c35c74730700000052547473070000003fd8a8a3abe91daabbcefb46c12679757b5ef84a1d1a86f88ff4f72108743082c16a90f3122651c46b9fe13ad5fa9039689f8f8310bd87fede796de7ae7e6b8b26e8b864ab910e64fac8d21420305ac13d40ff60b337734cbed410b74d045b4b191c8a834a78383c882c63cd73b5ec3045147d6866b03d668926675421d9dd9430059267161313dd5d8078f97681146f8ad9699662294f8d96838c1a02eb55950ba94ee41400881a940e82b104faa44ea94de3aafc6708bdfdfad612e5f8b3b17db9f9b3303b7fb8910a42ac46832ad3dbc1122ff9b81ce8343aea02b9dd4f08b85f6f22463648903c3e2c93d738ec5c5bbabc7969d350c5fb65538711c7651da9758deaadab0eda5cf7047a9b4cf76f32ba76d0f410e2eb0979f709ef7c3a875c6c2088e2ae731e9e64583785467c21b800cf320d9807a45210ef86d0b70e44efa206455c565617f73c5a3a4cd1b98c93d21d398f2bc25a63c5ea5095c8fd3905383a708f1ae8e6cdfef5a2aabf0fa4bf39ab9c9dd8aca35c9fd45ea283df6841e9556a1a468e0ec9db8f5e65d97cc5cb7b5f972ad5da74bde8088c87cefd5c6db4c812b0451988f4ee7061bc49e1f7fe37e1a2548469e13db07d9f235e9023daeeea7a208232bf77025a6e81e886e674b3aadc3931bc8f0268e2b8350d63608fe6b1689256c0d385f42f5bbe2027a22c1996e110ba97c171d3e5948de92beb8d0d63c39ebade8509e0ae3c9c3876fb5fa112be18f905ecacfecb92057603ab95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a43e251b9aa4f9097ef23129626b849648d24e38fb1f16829559482a9d5da28473cddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9cfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167e71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d731206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc021352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a467657cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe18869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636b5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7c6f67e02e6e4e1bdefb994c6098953f34636ba2b6ca20a4721d2b26a886722ff1c9a7e5ff1cf48b4ad1582d3f4e4a1004f3b20d8c5a2b71387a4254ad933ebc52f075ae229646b6f6aed19a5e372cf295081401eb893ff599b3f9acc0c0d3e7d328921deb59612076801e8cd61592107b5c67c79b846595cc6320c395b46362cbfb909fdb236ad2411b4e4883810a074b840464689986c3f8a8091827e17c32755d8fb3687ba3ba49f342c77f5a1f89bec83d811446e1a467139213d640b6a748d18220000000000000000000000000000000000000000000000000000000000", - ], - }; - const pendingDepositProofs = { - pendingDepositContainerRoot: - "0x8e9a353f5d80d749c0f322bc97530477e6c5a3ca14b253d0a26264872b45bdcf", - pendingDepositContainerProof: - "0xc5a691c90b1e5a4b98433a0f4bd86eba8048f63b7d3a1b4fb66ba0c7ae8812773db8d01d9495a3bbeb4f3d22d6a373692bbbf60c638fa615738f83ac08f464c9f7507516e2aa2af211bec2d8c99165b7fad41b628ba3483ae4538d0297e9959bc78009fdf07fc56a11f122370658a353aaa542ed63e44c4bc15ff4cd105ab33c536d98837f2dd165a55d5eeae91485954472d56f246df256bf3cae19352a123c6f4581fa3557c0472f9fbe2c878e71bfb3e8b60ea9e1290bce380cb644d55829445fd94dbbc7e4c1e17974c0745d82a97865458517ec4d426b861f9c4e90e9d0b53e114879b41538e556e5d6a209127efcd4b0fe59e0bf64de8357cdc2273d6ec0cdabace9443f7ec6183c3a82e1762c339d8dfbbd1dfca1ba16967b4f2e6e8a", - pendingDepositIndexes: [6791, 33011, 6790, 6789, 6788, 6786], - pendingDepositRoots: [ - "0xe43aa97c7b45d686240e148893fb944692f4a5513afca99a351c1f20e26c5597", - "0xf1947604474b44eec74edd4768599915239e72f4002737713af83febc36398cb", - "0xb052e26ca38ea7d23a9d8e2b4e0930caec9762902558a0c69a6165d2b34225ce", - "0x5884a4ac9f90f068ebec13e3be02253bc0f44b196572038dd09e77edf8ec0d5d", - "0xac9a98d48b7b13938bbf6c50c07b7d9f41c78c2dc9515280ec3cab9209ac44ba", - "0x5daaa459171d3c16aa9c68ece5f1192d0be4900291029fe9dee738493895ab8e", - ], - pendingDepositProofs: [ - "0xb052e26ca38ea7d23a9d8e2b4e0930caec9762902558a0c69a6165d2b34225cead345f10252274408791763ea728c60cfd0c0e4b830a4478358e903ea5fb8e9d16631debd57ef20f7e5d80e7309e1c5cbe09fba84a45739d0c2f7a5385b99de044e3aedd9b6413b72e62e221a47feac65b8b61639e4d8c3f9595cb28f5a2ed0928f91fd42bd629d13ea3d088dc7684890efb7bd6982995cb06b993262d317fe481cb939e8682f2529ac5b34889c745c9c3d451c6acae5603acd9e750b81054e588a198201c81b745cd183dc6b095f0281c04ec988c9d9e5a372e145eb5f03a4936392d008637fb405334e3793bcd032cec028bbfcf3de88b34bd4c893d350dc8fa58dccb16086ce949d26e8061c375a96ab0aa4dbc7d0ba7340a2732faab6dd749aee54ff9f9e931158eaca9fd12e915a1f7c205b499d7cf98fb2c9b70b55cfdff2cf7a711c6d0a64a4c0819b47e1e8c98534a134bf0a46773151d8ca393f25cfcb86c8f337e89e710accfcacd13518797b5f871b33f5f8a522d1070b5dd3851fb301c44c748be1402321a538f1f3d0ec0ed99161b33c8d7ab778e58ea4013f2ed12cb9bcd9bbcb04e046d6ae4c230f7a759e0c88be26bb8f34c33e0fa1d121dab1875e7cf07f93e85275fc8eb4755076cd072cb30830890c57ccc0ac57fec3d1bb0ac320fc57a100f5f395873579aed371896e8be97a52c5f03809c5f2aceb08fe6b1689256c0d385f42f5bbe2027a22c1996e110ba97c171d3e5948de92beb8d0d63c39ebade8509e0ae3c9c3876fb5fa112be18f905ecacfecb92057603ab95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a4f893e908917775b62bff23294dbbe3a1cd8e6cc1c35b4801887b646a6f81f17fcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9cfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167e71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d731206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc021352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a467659aa5000000000000000000000000000000000000000000000000000000000000", - "0xf7249eead0f1aea2b1fbd85cc602f80baf13e0a02cf9d3089e718e6658ce1693f792d6b43e88db568ea6dfcb3610f61591ee6b5c6eaeb6ccfef90f21d4b59639d55dd5372c0ccbe43f33259d9de97bfbb1e50625553b883a20f883ac588df75c54591b16d0df56bd9b2fef101017199b4beb0fdd39204fdee898baf569b591734284ea78eab42ac0bc127e4a33c0f1b6b1d7ce7f1c22c077571b4b4ab17cd6d15ec0911248ab6ac0fb6e40784dee43a812f1b2e6825e663b3c3bb9c8e919f1276b2bd5a3617467e8bc2a537855f2f6ace03a9fc68c3bde3fd4ea00e6428a4114a8749e8f840f1e524602b6b9ef58a896ac8ccba0e55bb89c72eae1130fcf463433b6e9dcdd3563b9304399dbabe9f1cb6c5681c72c703678e5bcb6a343607a9844c92daa1fe18120b85cdebb842c8761b2318bd50bad75742cd23e24674e55657075a0f80fc134385cae7ca7a8d0bf310debf5e875dd4c2ed98f5082e013576152827b331483b5af2d583798206b7673defe032939eaa66fba62754814cddc226b0d00c895f66aea0409aa599d3f4e99192e2bacb7a99bf00646e55d758f18bd72f2d4e1e15bc9b4649c63daf8060b6b26098a0f3837e145b78e5418c4fe23c9b58d900f5e182e3c50ef74969ea16c7726c549757cc23523c369587da729378453c470d6bb6d6d92cb4b1f45bdf68e17b3b2d513ccad61d8e91baeec8cbf2c568fe6b1689256c0d385f42f5bbe2027a22c1996e110ba97c171d3e5948de92beb8d0d63c39ebade8509e0ae3c9c3876fb5fa112be18f905ecacfecb92057603ab95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a4f893e908917775b62bff23294dbbe3a1cd8e6cc1c35b4801887b646a6f81f17fcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9cfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167e71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d731206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc021352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a467659aa5000000000000000000000000000000000000000000000000000000000000", - "0xe43aa97c7b45d686240e148893fb944692f4a5513afca99a351c1f20e26c5597ad345f10252274408791763ea728c60cfd0c0e4b830a4478358e903ea5fb8e9d16631debd57ef20f7e5d80e7309e1c5cbe09fba84a45739d0c2f7a5385b99de044e3aedd9b6413b72e62e221a47feac65b8b61639e4d8c3f9595cb28f5a2ed0928f91fd42bd629d13ea3d088dc7684890efb7bd6982995cb06b993262d317fe481cb939e8682f2529ac5b34889c745c9c3d451c6acae5603acd9e750b81054e588a198201c81b745cd183dc6b095f0281c04ec988c9d9e5a372e145eb5f03a4936392d008637fb405334e3793bcd032cec028bbfcf3de88b34bd4c893d350dc8fa58dccb16086ce949d26e8061c375a96ab0aa4dbc7d0ba7340a2732faab6dd749aee54ff9f9e931158eaca9fd12e915a1f7c205b499d7cf98fb2c9b70b55cfdff2cf7a711c6d0a64a4c0819b47e1e8c98534a134bf0a46773151d8ca393f25cfcb86c8f337e89e710accfcacd13518797b5f871b33f5f8a522d1070b5dd3851fb301c44c748be1402321a538f1f3d0ec0ed99161b33c8d7ab778e58ea4013f2ed12cb9bcd9bbcb04e046d6ae4c230f7a759e0c88be26bb8f34c33e0fa1d121dab1875e7cf07f93e85275fc8eb4755076cd072cb30830890c57ccc0ac57fec3d1bb0ac320fc57a100f5f395873579aed371896e8be97a52c5f03809c5f2aceb08fe6b1689256c0d385f42f5bbe2027a22c1996e110ba97c171d3e5948de92beb8d0d63c39ebade8509e0ae3c9c3876fb5fa112be18f905ecacfecb92057603ab95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a4f893e908917775b62bff23294dbbe3a1cd8e6cc1c35b4801887b646a6f81f17fcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9cfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167e71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d731206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc021352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a467659aa5000000000000000000000000000000000000000000000000000000000000", - "0xac9a98d48b7b13938bbf6c50c07b7d9f41c78c2dc9515280ec3cab9209ac44bad1382044558891e141307d62d35f69c7e8ef2566e8a758ac46807c46fab10b6d16631debd57ef20f7e5d80e7309e1c5cbe09fba84a45739d0c2f7a5385b99de044e3aedd9b6413b72e62e221a47feac65b8b61639e4d8c3f9595cb28f5a2ed0928f91fd42bd629d13ea3d088dc7684890efb7bd6982995cb06b993262d317fe481cb939e8682f2529ac5b34889c745c9c3d451c6acae5603acd9e750b81054e588a198201c81b745cd183dc6b095f0281c04ec988c9d9e5a372e145eb5f03a4936392d008637fb405334e3793bcd032cec028bbfcf3de88b34bd4c893d350dc8fa58dccb16086ce949d26e8061c375a96ab0aa4dbc7d0ba7340a2732faab6dd749aee54ff9f9e931158eaca9fd12e915a1f7c205b499d7cf98fb2c9b70b55cfdff2cf7a711c6d0a64a4c0819b47e1e8c98534a134bf0a46773151d8ca393f25cfcb86c8f337e89e710accfcacd13518797b5f871b33f5f8a522d1070b5dd3851fb301c44c748be1402321a538f1f3d0ec0ed99161b33c8d7ab778e58ea4013f2ed12cb9bcd9bbcb04e046d6ae4c230f7a759e0c88be26bb8f34c33e0fa1d121dab1875e7cf07f93e85275fc8eb4755076cd072cb30830890c57ccc0ac57fec3d1bb0ac320fc57a100f5f395873579aed371896e8be97a52c5f03809c5f2aceb08fe6b1689256c0d385f42f5bbe2027a22c1996e110ba97c171d3e5948de92beb8d0d63c39ebade8509e0ae3c9c3876fb5fa112be18f905ecacfecb92057603ab95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a4f893e908917775b62bff23294dbbe3a1cd8e6cc1c35b4801887b646a6f81f17fcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9cfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167e71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d731206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc021352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a467659aa5000000000000000000000000000000000000000000000000000000000000", - "0x5884a4ac9f90f068ebec13e3be02253bc0f44b196572038dd09e77edf8ec0d5dd1382044558891e141307d62d35f69c7e8ef2566e8a758ac46807c46fab10b6d16631debd57ef20f7e5d80e7309e1c5cbe09fba84a45739d0c2f7a5385b99de044e3aedd9b6413b72e62e221a47feac65b8b61639e4d8c3f9595cb28f5a2ed0928f91fd42bd629d13ea3d088dc7684890efb7bd6982995cb06b993262d317fe481cb939e8682f2529ac5b34889c745c9c3d451c6acae5603acd9e750b81054e588a198201c81b745cd183dc6b095f0281c04ec988c9d9e5a372e145eb5f03a4936392d008637fb405334e3793bcd032cec028bbfcf3de88b34bd4c893d350dc8fa58dccb16086ce949d26e8061c375a96ab0aa4dbc7d0ba7340a2732faab6dd749aee54ff9f9e931158eaca9fd12e915a1f7c205b499d7cf98fb2c9b70b55cfdff2cf7a711c6d0a64a4c0819b47e1e8c98534a134bf0a46773151d8ca393f25cfcb86c8f337e89e710accfcacd13518797b5f871b33f5f8a522d1070b5dd3851fb301c44c748be1402321a538f1f3d0ec0ed99161b33c8d7ab778e58ea4013f2ed12cb9bcd9bbcb04e046d6ae4c230f7a759e0c88be26bb8f34c33e0fa1d121dab1875e7cf07f93e85275fc8eb4755076cd072cb30830890c57ccc0ac57fec3d1bb0ac320fc57a100f5f395873579aed371896e8be97a52c5f03809c5f2aceb08fe6b1689256c0d385f42f5bbe2027a22c1996e110ba97c171d3e5948de92beb8d0d63c39ebade8509e0ae3c9c3876fb5fa112be18f905ecacfecb92057603ab95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a4f893e908917775b62bff23294dbbe3a1cd8e6cc1c35b4801887b646a6f81f17fcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9cfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167e71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d731206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc021352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a467659aa5000000000000000000000000000000000000000000000000000000000000", - "0x308dba54c2444e4683aa4c92c423532dae274d39dc0a95a2871b90006b41a357f48cf215d71cd05cb4214ebac67ba013270cc2c9b3e9cd3764671f1461135947ff01249f12eebda99afdc7882e5a0b55955740fe202576daabaf7fd026a0d73e44e3aedd9b6413b72e62e221a47feac65b8b61639e4d8c3f9595cb28f5a2ed0928f91fd42bd629d13ea3d088dc7684890efb7bd6982995cb06b993262d317fe481cb939e8682f2529ac5b34889c745c9c3d451c6acae5603acd9e750b81054e588a198201c81b745cd183dc6b095f0281c04ec988c9d9e5a372e145eb5f03a4936392d008637fb405334e3793bcd032cec028bbfcf3de88b34bd4c893d350dc8fa58dccb16086ce949d26e8061c375a96ab0aa4dbc7d0ba7340a2732faab6dd749aee54ff9f9e931158eaca9fd12e915a1f7c205b499d7cf98fb2c9b70b55cfdff2cf7a711c6d0a64a4c0819b47e1e8c98534a134bf0a46773151d8ca393f25cfcb86c8f337e89e710accfcacd13518797b5f871b33f5f8a522d1070b5dd3851fb301c44c748be1402321a538f1f3d0ec0ed99161b33c8d7ab778e58ea4013f2ed12cb9bcd9bbcb04e046d6ae4c230f7a759e0c88be26bb8f34c33e0fa1d121dab1875e7cf07f93e85275fc8eb4755076cd072cb30830890c57ccc0ac57fec3d1bb0ac320fc57a100f5f395873579aed371896e8be97a52c5f03809c5f2aceb08fe6b1689256c0d385f42f5bbe2027a22c1996e110ba97c171d3e5948de92beb8d0d63c39ebade8509e0ae3c9c3876fb5fa112be18f905ecacfecb92057603ab95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a4f893e908917775b62bff23294dbbe3a1cd8e6cc1c35b4801887b646a6f81f17fcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9cfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167e71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d731206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc021352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a467659aa5000000000000000000000000000000000000000000000000000000000000", - ], - }; - const minWithdrableTime = 261 * 32 * 12; // 261 epochs - - beforeEach(async () => { - await activateTargetValidators(fixture); - - await consolidationController - .connect(adminSigner) - .requestConsolidation( - nativeStakingStrategy2.address, - sourceValidators, - activeTargetPubKey, - { value: consolidationFee * sourceValidators.length } - ); - - await advanceTime(minWithdrableTime); - - // Get the current block timestamp - await advanceTime(12 * 40); - const { timestamp: currentTimestamp } = await ethers.provider.getBlock( - "latest" - ); - const nextBlockTimestamp = currentTimestamp; - await fixture.beaconRoots["setBeaconRoot(uint256,bytes32)"]( - // I can't control the timestamp with Hardhat - nextBlockTimestamp + 2, - balanceProofs.beaconBlockRoot - ); - - await consolidationController.connect(registratorSigner).snapBalances(); - }); - - it("Should confirm consolidation", async () => { - const { weth } = fixture; - - const compoundingStrategyBalanceBefore = - await compoundingStakingStrategy.checkBalance(weth.address); - const nativeStrategyBalanceBefore = - await nativeStakingStrategy2.checkBalance(weth.address); - const consolidationCountBefore = - await consolidationController.consolidationCount(); - expect(consolidationCountBefore).to.equal(3); - const activeDepositedValidatorsBefore = - await nativeStakingStrategy2.activeDepositedValidators(); - - const tx = await consolidationController - .connect(adminSigner) - .confirmConsolidation(balanceProofs, pendingDepositProofs); - - await expect(tx) - .to.emit(nativeStakingStrategy2, "ConsolidationConfirmed") - .withArgs( - consolidationCountBefore, - activeDepositedValidatorsBefore.sub(consolidationCountBefore) - ); - expect(await consolidationController.consolidationCount()).to.equal(0); - expect(await consolidationController.sourceStrategy()).to.equal( - ethers.constants.AddressZero - ); - expect(await consolidationController.targetPubKeyHash()).to.equal( - ethers.constants.HashZero - ); - expect(await nativeStakingStrategy2.activeDepositedValidators()).to.equal( - activeDepositedValidatorsBefore.sub(consolidationCountBefore) - ); - - // then changes in the strategy balances should net off - const compoundingStrategyBalanceAfter = - await compoundingStakingStrategy.checkBalance(weth.address); - const nativeStrategyBalanceAfter = - await nativeStakingStrategy2.checkBalance(weth.address); - expect( - compoundingStrategyBalanceAfter - .sub(compoundingStrategyBalanceBefore) - .add(nativeStrategyBalanceAfter.sub(nativeStrategyBalanceBefore)) - ).to.equal(0); - }); - it("Should not confirm consolidation twice", async () => { - await consolidationController - .connect(adminSigner) - .confirmConsolidation(balanceProofs, pendingDepositProofs); - - const tx = consolidationController - .connect(adminSigner) - .confirmConsolidation(balanceProofs, pendingDepositProofs); - - await expect(tx).to.be.revertedWith("No consolidation in progress"); - }); - it("Fail confirm consolidation if not admin multisig", async () => { - const { josh, strategist, timelock } = fixture; - const users = [registratorSigner, josh, strategist, timelock]; - - for (const user of users) { - const tx = consolidationController - .connect(user) - .confirmConsolidation(balanceProofs, pendingDepositProofs); - - await expect(tx).to.be.revertedWith("Ownable: caller is not the owner"); - } - }); - it("Fail confirm consolidation with invalid balance proofs", async () => { - const invalidBalanceProofs = structuredClone(balanceProofs); - invalidBalanceProofs.validatorBalanceLeaves[0] = - "0x0000000000000000000000000000000000000000000000000000000000000000"; - - const tx = consolidationController - .connect(adminSigner) - .confirmConsolidation(invalidBalanceProofs, pendingDepositProofs); - - await expect(tx).to.be.revertedWith("Invalid balance proof"); - }); - it("Fail confirm consolidation with invalid pending deposit proofs", async () => { - const invalidPendingDepositProofs = structuredClone(pendingDepositProofs); - invalidPendingDepositProofs.pendingDepositIndexes[0] = "1234"; - - const tx = consolidationController - .connect(adminSigner) - .confirmConsolidation(balanceProofs, invalidPendingDepositProofs); - - await expect(tx).to.be.revertedWith("Invalid deposit proof"); - }); - }); -}); From 9b71bfab805890da5c3221132060d3b37e214ab2 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 29 May 2026 15:37:33 +1000 Subject: [PATCH 02/29] Moved the initialize function up in the contract --- .../CompoundingStakingStrategy.sol | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/contracts/contracts/strategies/NativeStaking/CompoundingStakingStrategy.sol b/contracts/contracts/strategies/NativeStaking/CompoundingStakingStrategy.sol index 0f34825d05..999bcdf634 100644 --- a/contracts/contracts/strategies/NativeStaking/CompoundingStakingStrategy.sol +++ b/contracts/contracts/strategies/NativeStaking/CompoundingStakingStrategy.sol @@ -273,6 +273,25 @@ contract CompoundingStakingStrategy is _; } + /// @notice Set up initial internal state. + /// @param _rewardTokenAddresses Not used so empty array + /// @param _assets Not used so empty array + /// @param _pTokens Not used so empty array + /// @param _initialDepositAmountWei The amount of ETH required for the first deposit to a new validator. + function initialize( + address[] memory _rewardTokenAddresses, + address[] memory _assets, + address[] memory _pTokens, + uint256 _initialDepositAmountWei + ) external onlyGovernor initializer { + InitializableAbstractStrategy._initialize( + _rewardTokenAddresses, + _assets, + _pTokens + ); + _setInitialDepositAmountWei(_initialDepositAmountWei); + } + /** * * Admin Functions @@ -1247,25 +1266,6 @@ contract CompoundingStakingStrategy is return verifiedValidators.length; } - /// @notice Set up initial internal state. - /// @param _rewardTokenAddresses Not used so empty array - /// @param _assets Not used so empty array - /// @param _pTokens Not used so empty array - /// @param _initialDepositAmountWei The amount of ETH required for the first deposit to a new validator. - function initialize( - address[] memory _rewardTokenAddresses, - address[] memory _assets, - address[] memory _pTokens, - uint256 _initialDepositAmountWei - ) external onlyGovernor initializer { - InitializableAbstractStrategy._initialize( - _rewardTokenAddresses, - _assets, - _pTokens - ); - _setInitialDepositAmountWei(_initialDepositAmountWei); - } - /// @notice Unlike other strategies, this does not deposit assets into the underlying platform. /// It just checks the asset is WETH and emits the Deposit event. /// To deposit WETH into validators, `stakeEth` must be used. From d2e979714359755b4e24deee1f33af9531f13b13 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 29 May 2026 16:32:28 +1000 Subject: [PATCH 03/29] Restore the ConsolidationController. Add registerSsvValidators and stakeEth forwarding functions to the old NativeStakingStrategy --- .../NativeStaking/CompoundingStakingView.sol | 22 +- .../NativeStaking/ConsolidationController.sol | 529 ++++++++++++++++++ ...197_deploy_compounding_staking_strategy.js | 27 +- 3 files changed, 571 insertions(+), 7 deletions(-) create mode 100644 contracts/contracts/strategies/NativeStaking/ConsolidationController.sol diff --git a/contracts/contracts/strategies/NativeStaking/CompoundingStakingView.sol b/contracts/contracts/strategies/NativeStaking/CompoundingStakingView.sol index fa9a7db65c..2181b7472b 100644 --- a/contracts/contracts/strategies/NativeStaking/CompoundingStakingView.sol +++ b/contracts/contracts/strategies/NativeStaking/CompoundingStakingView.sol @@ -18,7 +18,7 @@ contract CompoundingStakingStrategyView { struct ValidatorView { bytes32 pubKeyHash; uint64 index; - CompoundingStakingStrategy.ValidatorState state; + uint8 state; } struct DepositView { @@ -40,10 +40,7 @@ contract CompoundingStakingStrategyView { validators = new ValidatorView[](validatorCount); for (uint256 i = 0; i < validatorCount; ++i) { bytes32 pubKeyHash = stakingStrategy.verifiedValidators(i); - ( - CompoundingStakingStrategy.ValidatorState state, - uint64 index - ) = stakingStrategy.validator(pubKeyHash); + (uint8 state, uint64 index) = _validator(pubKeyHash); validators[i] = ValidatorView({ pubKeyHash: pubKeyHash, index: index, @@ -79,4 +76,19 @@ contract CompoundingStakingStrategyView { }); } } + + function _validator(bytes32 pubKeyHash) + internal + view + returns (uint8 state, uint64 index) + { + (bool success, bytes memory data) = address(stakingStrategy).staticcall( + abi.encodeWithSelector( + stakingStrategy.validator.selector, + pubKeyHash + ) + ); + require(success, "validator call failed"); + return abi.decode(data, (uint8, uint64)); + } } diff --git a/contracts/contracts/strategies/NativeStaking/ConsolidationController.sol b/contracts/contracts/strategies/NativeStaking/ConsolidationController.sol new file mode 100644 index 0000000000..a8acad2a43 --- /dev/null +++ b/contracts/contracts/strategies/NativeStaking/ConsolidationController.sol @@ -0,0 +1,529 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; + +import { CompoundingStakingStrategy, CompoundingValidatorStorage } from "./CompoundingStakingStrategy.sol"; +import { ValidatorAccountant } from "./ValidatorAccountant.sol"; +import { ValidatorStakeData } from "./ValidatorRegistrator.sol"; +import { Cluster } from "../../interfaces/ISSVNetwork.sol"; + +/// @title Consolidation Controller +/// @notice Orchestrates the consolidation of validators from the old Native Staking Strategy +/// to the new Compounding Staking Strategy. +/// @author Origin Protocol Inc +contract ConsolidationController is Ownable { + /// @dev Minimum time that must pass before a consolidation request can be processed. + /// 261 epochs * 32 slots/epoch * 12 seconds/slot = 100224 seconds (~27.8 hours) + /// Includes 256 epochs minimum withdrawability delay + 5 epochs from + /// compute_activation_exit_epoch (MAX_SEED_LOOKAHEAD + 1). + /// The actual time can be a lot longer than this depending on the number of + /// requests in the beacon chain's pending consolidation queue. + uint256 internal constant MIN_CONSOLIDATION_PERIOD = 261 * 32 * 12; + + /// @notice Address of the validator registrator account + address public immutable validatorRegistrator; + /// @dev The old Native Staking Strategy connected to the second SSV cluster + address internal immutable nativeStakingStrategy2; + /// @dev The new Compounding Staking Strategy + CompoundingStakingStrategy internal immutable targetStrategy; + + /// @notice Number of validators being consolidated + uint64 public consolidationCount; + /// @notice Timestamp when the consolidation process was requested + uint64 public consolidationStartTimestamp; + /// @notice The address of the source Native Staking Strategy being consolidated from + address public sourceStrategy; + /// @notice The public key hash of the target validator on the new Compounding Staking Strategy + bytes32 public targetPubKeyHash; + /// @dev Tracks source validators that were requested for a consolidation round. + /// Keyed by keccak256(sourcePubKeyHash, consolidationStartTimestamp). + mapping(bytes32 => bool) private pendingSourceInRound; + + /// @dev Throws if called by any account other than the Validator Registrator + modifier onlyRegistrator() { + require( + msg.sender == validatorRegistrator, + "Caller is not the Registrator" + ); + _; + } + + /// @param _owner The owner who can request, fail and confirm consolidations + /// @param _validatorRegistrator The validator registrator who does operations on the old staking strategy + constructor( + address _owner, + address _validatorRegistrator, + address _nativeStakingStrategy2, + address _targetStrategy + ) { + _transferOwnership(_owner); + + validatorRegistrator = _validatorRegistrator; + nativeStakingStrategy2 = _nativeStakingStrategy2; + targetStrategy = CompoundingStakingStrategy(payable(_targetStrategy)); + } + + /** + * @notice Request consolidation of validators from an old Native Staking Strategy + * to the new Compounding Staking Strategy + * @param _sourceStrategy The address of the old Native Staking Strategy + * @param sourcePubKeys The public keys of the validators to be consolidated from the old Native Staking Strategy + * @param targetPubKey The public key of the target validator on the new Compounding Staking Strategy + */ + function requestConsolidation( + address _sourceStrategy, + bytes[] calldata sourcePubKeys, + bytes calldata targetPubKey + ) external payable onlyOwner { + // Check no consolidations are already in progress + require(consolidationCount == 0, "Consolidation in progress"); + // Check at least one source validator is provided + require(sourcePubKeys.length > 0, "Empty source validators"); + // Check sourceStrategy is a valid old Native Staking Strategy + _checkSourceStrategy(_sourceStrategy); + + // Check target validator is Active on the new Compounding Staking Strategy + bytes32 targetPubKeyHashMem = _hashPubKey(targetPubKey); + (CompoundingValidatorStorage.ValidatorState state, ) = targetStrategy + .validator(targetPubKeyHashMem); + require( + state == CompoundingValidatorStorage.ValidatorState.ACTIVE, + "Target validator not active" + ); + // Check no pending deposits in the new target validator + require( + _hasPendingDeposit(targetPubKeyHashMem) == false, + "Target has pending deposit" + ); + + // Store the state at the start of the consolidation process + consolidationCount = SafeCast.toUint64(sourcePubKeys.length); + uint64 consolidationStartTimestampMem = uint64(block.timestamp); + consolidationStartTimestamp = consolidationStartTimestampMem; + sourceStrategy = _sourceStrategy; + targetPubKeyHash = targetPubKeyHashMem; + + // Store source validators for this consolidation round. + for (uint256 i = 0; i < sourcePubKeys.length; ++i) { + bytes32 roundKey = _sourceValidatorRoundKey( + sourcePubKeys[i], + consolidationStartTimestampMem + ); + require( + pendingSourceInRound[roundKey] == false, + "Duplicate source validator" + ); + pendingSourceInRound[roundKey] = true; + } + + // Call requestConsolidation on the old Native Staking Strategy + // to initiate the consolidations + ValidatorAccountant(_sourceStrategy).requestConsolidation{ + value: msg.value + }(sourcePubKeys, targetPubKey); + + // Snap the balances for the last time on the new Compounding Staking Strategy + // if it hasn't been called recently. Otherwise skip to prevent a DoS + // attack where an attacker front-runs this call with the permissionless snapBalances(). + (, uint64 lastSnapTimestamp, ) = targetStrategy.snappedBalance(); + if ( + uint64(block.timestamp) > + lastSnapTimestamp + targetStrategy.SNAP_BALANCES_DELAY() + ) { + targetStrategy.snapBalances(); + } + + // No event emitted as ConsolidationRequested is emitted from the old Native Staking Strategy + } + + /** + * @notice A consolidation request can fail to be processed on the beacon chain + * for various reasons. For example, the pending consolidation queue is full with 262,144 requests. + * Or the source validator has exited from a voluntary exit request. + * This reduces the consolidation count and changes the validator state back to STAKED. + * @param sourcePubKeys The public keys of the source validators that failed to be consolidated. + */ + function failConsolidation(bytes[] calldata sourcePubKeys) + external + onlyOwner + { + // Check consolidations are in progress + require(consolidationCount > 0, "No consolidation in progress"); + // There a min time before a failed consolidation can be unwound. + // This gives the beacon chain time to process the request. + require( + block.timestamp >= + consolidationStartTimestamp + MIN_CONSOLIDATION_PERIOD, + "Source not withdrawable" + ); + require( + sourcePubKeys.length <= consolidationCount, + "Exceeds consolidation count" + ); + uint64 consolidationStartTimestampMem = consolidationStartTimestamp; + + for (uint256 i = 0; i < sourcePubKeys.length; ++i) { + bytes32 roundKey = _sourceValidatorRoundKey( + sourcePubKeys[i], + consolidationStartTimestampMem + ); + require(pendingSourceInRound[roundKey], "Unknown source validator"); + pendingSourceInRound[roundKey] = false; + } + + // Read into memory in case it gets reset in storage before + // the external call to the source strategy + address sourceStrategyMem = sourceStrategy; + + // Store updated consolidation state + consolidationCount -= SafeCast.toUint64(sourcePubKeys.length); + if (consolidationCount == 0) { + // Reset the rest of the consolidation state + consolidationStartTimestamp = 0; + sourceStrategy = address(0); + targetPubKeyHash = bytes32(0); + } + + ValidatorAccountant(sourceStrategyMem).failConsolidation(sourcePubKeys); + + // No event emitted as ConsolidationFailed is emitted from the old Native Staking Strategy + } + + /** + * @notice Confirm the consolidation of validators from an old Native Staking Strategy + * to the new Compounding Staking Strategy has been completed. + * @param balanceProofs a `BalanceProofs` struct containing the following: + * - balancesContainerRoot: The merkle root of the balances container + * - balancesContainerProof: The merkle proof for the balances container to the beacon block root. + * This is 9 witness hashes of 32 bytes each concatenated together starting from the leaf node. + * - validatorBalanceLeaves: Array of leaf nodes containing the validator balance with three other balances. + * - validatorBalanceProofs: Array of merkle proofs for the validator balance to the Balances container root. + * This is 39 witness hashes of 32 bytes each concatenated together starting from the leaf node. + * @param pendingDepositProofs a `PendingDepositProofs` struct containing the following: + * - pendingDepositContainerRoot: The merkle root of the pending deposits list container + * - pendingDepositContainerProof: The merkle proof from the pending deposits list container + * to the beacon block root. + * This is 9 witness hashes of 32 bytes each concatenated together starting from the leaf node. + * - pendingDepositIndexes: Array of indexes in the pending deposits list container for each + * of the strategy's deposits. + * - pendingDepositProofs: Array of merkle proofs for each strategy deposit in the + * beacon chain's pending deposit list container to the pending deposits list container root. + * These are 28 witness hashes of 32 bytes each concatenated together starting from the leaf node. + */ + function confirmConsolidation( + CompoundingStakingStrategy.BalanceProofs calldata balanceProofs, + CompoundingStakingStrategy.PendingDepositProofs + calldata pendingDepositProofs + ) external onlyOwner { + // Check consolidations are in progress + require(consolidationCount > 0, "No consolidation in progress"); + // There a min time before a consolidation can be processed on the beacon chain + (, uint64 snappedTimestamp, ) = targetStrategy.snappedBalance(); + require( + uint64(snappedTimestamp) > + consolidationStartTimestamp + MIN_CONSOLIDATION_PERIOD, + "Source not withdrawable" + ); + + // Load into memory as the storage is about to be reset. + // These are used in the external contract calls + address sourceStrategyMem = sourceStrategy; + uint256 consolidationCountMem = consolidationCount; + + // Reset consolidation state before external calls + consolidationCount = 0; + consolidationStartTimestamp = 0; + sourceStrategy = address(0); + targetPubKeyHash = bytes32(0); + + // Verify balances on the new Compounding Staking Strategy and update the strategy's balance + targetStrategy.verifyBalances(balanceProofs, pendingDepositProofs); + + // Reduce the balance of the old Native Staking Strategy + ValidatorAccountant(sourceStrategyMem).confirmConsolidation( + consolidationCountMem + ); + + // No event emitted as ConsolidationConfirmed is emitted from the old Native Staking Strategy + } + + /** + * + * Functions that forward to the old Native Staking Strategy + * + */ + + /// @notice The validator registrator of the old Native Staking Strategy can call doAccounting + /// @param _sourceStrategy The address of the old Native Staking Strategy + /// @return accountingValid true if accounting was successful, false if fuse is blown + function doAccounting(address _sourceStrategy) + external + onlyRegistrator + returns (bool accountingValid) + { + // Check sourceStrategy is a valid old Native Staking Strategy + _checkSourceStrategy(_sourceStrategy); + + return ValidatorAccountant(_sourceStrategy).doAccounting(); + } + + /** + * @notice Register validators in the source Native Staking Strategy SSV cluster. + * Only callable by the validator registrator. + * @param _sourceStrategy The address of the old Native Staking Strategy + * @param publicKeys The public keys of the validators + * @param operatorIds The operator IDs of the SSV Cluster + * @param sharesData The shares data for each validator + * @param cluster The SSV cluster information for the source validators + */ + function registerSsvValidators( + address _sourceStrategy, + bytes[] calldata publicKeys, + uint64[] calldata operatorIds, + bytes[] calldata sharesData, + Cluster calldata cluster + ) external payable onlyRegistrator { + // Check sourceStrategy is a valid old Native Staking Strategy + _checkSourceStrategy(_sourceStrategy); + + ValidatorAccountant(_sourceStrategy).registerSsvValidators{ + value: msg.value + }(publicKeys, operatorIds, sharesData, cluster); + } + + /** + * @notice Stake ETH to registered source Native Staking Strategy validators. + * Only callable by the validator registrator. + * @param _sourceStrategy The address of the old Native Staking Strategy + * @param validators A list of validator data needed to stake. + */ + function stakeEth( + address _sourceStrategy, + ValidatorStakeData[] calldata validators + ) external onlyRegistrator { + // Check sourceStrategy is a valid old Native Staking Strategy + _checkSourceStrategy(_sourceStrategy); + + ValidatorAccountant(_sourceStrategy).stakeEth(validators); + } + + /** + * @notice Exit of source validators are allowed during the consolidation process + * as consolidated validators will be in EXITING state hence can not be consolidated after exit. + * Only callable by the validator registrator. + * @param _sourceStrategy The address of the old Native Staking Strategy + * @param publicKey The public key of the validator to exit which must have STAKED state. + * @param operatorIds The operator IDs for the source SSV cluster + */ + function exitSsvValidator( + address _sourceStrategy, + bytes calldata publicKey, + uint64[] calldata operatorIds + ) external onlyRegistrator { + // Check sourceStrategy is a valid old Native Staking Strategy + _checkSourceStrategy(_sourceStrategy); + + ValidatorAccountant(_sourceStrategy).exitSsvValidator( + publicKey, + operatorIds + ); + } + + /** + * @notice Removing source validators is not allowed during the consolidation process + * as consolidated validators will be in EXITING state hence can not be consolidated after removal. + * Only callable by the validator registrator. + * @param _sourceStrategy The address of the old Native Staking Strategy + * @param publicKey The public key of the validator to remove which must have EXITING or REGISTERED state. + * @param operatorIds The operator IDs for the source SSV cluster + * @param cluster The SSV cluster information for the source validator + */ + function removeSsvValidator( + address _sourceStrategy, + bytes calldata publicKey, + uint64[] calldata operatorIds, + Cluster calldata cluster + ) external onlyRegistrator { + // Check sourceStrategy is a valid old Native Staking Strategy + _checkSourceStrategy(_sourceStrategy); + // Prevent removing a validator from the SSV cluster before the consolidation + // process has been completed for the source strategy being consolidated. + // This prevents validators that have been exited rather than consolidated but that's ok. + // The exited validator can be removed after the consolidation process is complete. + require(_sourceStrategy != sourceStrategy, "Consolidation in progress"); + + ValidatorAccountant(_sourceStrategy).removeSsvValidator( + publicKey, + operatorIds, + cluster + ); + } + + /** + * + * Functions that forward to the new Compounding Staking Strategy + * + */ + + /// @notice Forwards to the new Compounding Staking Strategy. + /// Is only callable by the validator registrator when a consolidation is in progress. + /// Anyone can call when there are no consolidations in progress. + function snapBalances() external { + if (consolidationCount > 0 && msg.sender != validatorRegistrator) { + revert("Consolidation in progress"); + } + if (consolidationCount > 0) { + require( + block.timestamp > + consolidationStartTimestamp + MIN_CONSOLIDATION_PERIOD, + "Source not withdrawable" + ); + } + + targetStrategy.snapBalances(); + } + + /** + * @notice Anyone can verify balances on the new Compounding Staking Strategy + * as long as there are no consolidations in progress. + * @param balanceProofs a `BalanceProofs` struct containing the following: + * - balancesContainerRoot: The merkle root of the balances container + * - balancesContainerProof: The merkle proof for the balances container to the beacon block root. + * This is 9 witness hashes of 32 bytes each concatenated together starting from the leaf node. + * - validatorBalanceLeaves: Array of leaf nodes containing the validator balance with three other balances. + * - validatorBalanceProofs: Array of merkle proofs for the validator balance to the Balances container root. + * This is 39 witness hashes of 32 bytes each concatenated together starting from the leaf node. + * @param pendingDepositProofs a `PendingDepositProofs` struct containing the following: + * - pendingDepositContainerRoot: The merkle root of the pending deposits list container + * - pendingDepositContainerProof: The merkle proof from the pending deposits list container + * to the beacon block root. + * This is 9 witness hashes of 32 bytes each concatenated together starting from the leaf node. + * - pendingDepositIndexes: Array of indexes in the pending deposits list container for each + * of the strategy's deposits. + * - pendingDepositProofs: Array of merkle proofs for each strategy deposit in the + * beacon chain's pending deposit list container to the pending deposits list container root. + * These are 28 witness hashes of 32 bytes each concatenated together starting from the leaf node. + */ + function verifyBalances( + CompoundingStakingStrategy.BalanceProofs calldata balanceProofs, + CompoundingStakingStrategy.PendingDepositProofs + calldata pendingDepositProofs + ) external { + (, uint64 snappedTimestamp, ) = targetStrategy.snappedBalance(); + // Can not verify balances while consolidations are in progress + // if the snap was taken after the consolidation process started. + // This still allows verifying a pre-existing snap. + if ( + consolidationCount > 0 && + snappedTimestamp > consolidationStartTimestamp + ) { + revert("Consolidation in progress"); + } + + targetStrategy.verifyBalances(balanceProofs, pendingDepositProofs); + } + + /// @notice Partial withdrawals are allowed during consolidation from the new Compounding Staking Strategy. + /// This includes partial withdrawals from the target validator. + // Full validator exits from any Compounding Staking Strategy validator are + /// not allowed during the migration period. + /// Only the registrator can call this function. + /// @param publicKey The public key of the validator + /// @param amountGwei The amount of ETH to be withdrawn from the validator in Gwei. + /// A zero amount is not allowed. + function validatorWithdrawal(bytes calldata publicKey, uint64 amountGwei) + external + payable + onlyRegistrator + { + // Prevent full exits from any new compounding validators. + // This includes when there is no consolidation in progress. + // This reduces the risk of an exit request being processed before a consolidation request + require(amountGwei > 0, "No exit during migration"); + targetStrategy.validatorWithdrawal{ value: msg.value }( + publicKey, + amountGwei + ); + } + + /** + * @notice Deposits to Compounding Staking Strategy validators that are + * not the target of a consolidation are allowed. + * Only the registrator can call this function. + * @param validatorStakeData validator data needed to stake. + * The `ValidatorStakeData` struct contains the pubkey, signature and depositDataRoot. + * @param depositAmountGwei The amount of WETH to stake to the validator in Gwei. + */ + function stakeEth( + CompoundingStakingStrategy.ValidatorStakeData + calldata validatorStakeData, + uint64 depositAmountGwei + ) external onlyRegistrator { + require( + _hashPubKey(validatorStakeData.pubkey) != targetPubKeyHash, + "Stake to consolidation target" + ); + + targetStrategy.stakeEth(validatorStakeData, depositAmountGwei); + } + + /** + * + * Internal Functions + * + */ + + /// @dev Check if there are any pending deposits for a validator with a given public key hash. + /// Need to iterate over the target strategy's `deposits` + /// @return True if there is at least one pending deposit for the validator + function _hasPendingDeposit(bytes32 _targetPubKeyHash) + internal + view + returns (bool) + { + uint256 depositsCount = targetStrategy.depositListLength(); + for (uint256 i = 0; i < depositsCount; ++i) { + ( + bytes32 depositPubKeyHash, + , + , + , + CompoundingValidatorStorage.DepositStatus status + ) = targetStrategy.deposits(targetStrategy.depositList(i)); + if ( + depositPubKeyHash == _targetPubKeyHash && + status == CompoundingValidatorStorage.DepositStatus.PENDING + ) { + return true; + } + } + return false; + } + + /// @dev Hash a validator public key using the Beacon Chain's format + /// @param pubKey The full validator public key + /// @return The hashed public key using the Beacon Chain's hashing for BLSPubkey + function _hashPubKey(bytes memory pubKey) internal pure returns (bytes32) { + require(pubKey.length == 48, "Invalid public key"); + return sha256(abi.encodePacked(pubKey, bytes16(0))); + } + + /// @dev Build a key for tracking source validators in a consolidation round. + function _sourceValidatorRoundKey( + bytes memory sourcePubKey, + uint64 roundTimestamp + ) internal pure returns (bytes32) { + return keccak256(abi.encode(_hashPubKey(sourcePubKey), roundTimestamp)); + } + + /// @dev Check source strategy is a valid old Native Staking Strategy + /// @param _sourceStrategy The address of the old Native Staking Strategy + function _checkSourceStrategy(address _sourceStrategy) internal view { + require( + _sourceStrategy == nativeStakingStrategy2, + "Invalid source strategy" + ); + } +} diff --git a/contracts/deploy/mainnet/197_deploy_compounding_staking_strategy.js b/contracts/deploy/mainnet/197_deploy_compounding_staking_strategy.js index eb48f89e5e..fd515f836d 100644 --- a/contracts/deploy/mainnet/197_deploy_compounding_staking_strategy.js +++ b/contracts/deploy/mainnet/197_deploy_compounding_staking_strategy.js @@ -19,6 +19,13 @@ module.exports = deploymentWithGovernanceProposal( cOETHVaultProxy.address ); const cBeaconProofs = await ethers.getContract("BeaconProofs"); + const cNativeStakingStrategy2Proxy = await ethers.getContract( + "NativeStakingSSVStrategy2Proxy" + ); + const cNativeStakingStrategy2 = await ethers.getContractAt( + "NativeStakingSSVStrategy", + cNativeStakingStrategy2Proxy.address + ); console.log("Deploy CompoundingStakingStrategyProxy"); const dCompoundingStakingStrategyProxy = await deployWithConfirmation( @@ -80,8 +87,19 @@ module.exports = deploymentWithGovernanceProposal( "CompoundingStakingStrategyView" ); + console.log("Deploy ConsolidationController"); + const dConsolidationController = await deployWithConfirmation( + "ConsolidationController", + [ + addresses.mainnet.Guardian, // Admin 5/8 multisig + addresses.mainnet.validatorRegistrator, // TODO needs to be new Talos Relayer + cNativeStakingStrategy2.address, // Old Native Staking Strategy 2 + cStrategy.address, // New Compounding Staking Strategy + ] + ); + return { - name: "Deploy new vanilla compounding staking strategy", + name: "Deploy new vanilla compounding staking strategy and consolidation controller", actions: [ { contract: cOETHVault, @@ -91,7 +109,12 @@ module.exports = deploymentWithGovernanceProposal( { contract: cStrategy, signature: "setRegistrator(address)", - args: [addresses.mainnet.validatorRegistrator], + args: [dConsolidationController.address], + }, + { + contract: cNativeStakingStrategy2, + signature: "setRegistrator(address)", + args: [dConsolidationController.address], }, ], }; From 5983775a1867239314e0b4841a51aef9a801b3c1 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 29 May 2026 17:57:46 +1000 Subject: [PATCH 04/29] Added Talos Relayer to deploy script --- .../deploy/mainnet/197_deploy_compounding_staking_strategy.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/deploy/mainnet/197_deploy_compounding_staking_strategy.js b/contracts/deploy/mainnet/197_deploy_compounding_staking_strategy.js index fd515f836d..48fada8088 100644 --- a/contracts/deploy/mainnet/197_deploy_compounding_staking_strategy.js +++ b/contracts/deploy/mainnet/197_deploy_compounding_staking_strategy.js @@ -92,7 +92,7 @@ module.exports = deploymentWithGovernanceProposal( "ConsolidationController", [ addresses.mainnet.Guardian, // Admin 5/8 multisig - addresses.mainnet.validatorRegistrator, // TODO needs to be new Talos Relayer + addresses.mainnet.talosRelayer, // New Talos Relayer cNativeStakingStrategy2.address, // Old Native Staking Strategy 2 cStrategy.address, // New Compounding Staking Strategy ] From 0e012dc175aab45494184f14ca358b337bb1c0cb Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 29 May 2026 19:17:10 +1000 Subject: [PATCH 05/29] Change max initial deposit to be 2048 --- .../strategies/NativeStaking/CompoundingStakingStrategy.sol | 4 +++- contracts/test/strategies/compoundingSSVStaking.js | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/contracts/contracts/strategies/NativeStaking/CompoundingStakingStrategy.sol b/contracts/contracts/strategies/NativeStaking/CompoundingStakingStrategy.sol index 999bcdf634..deb71dd4e8 100644 --- a/contracts/contracts/strategies/NativeStaking/CompoundingStakingStrategy.sol +++ b/contracts/contracts/strategies/NativeStaking/CompoundingStakingStrategy.sol @@ -30,6 +30,8 @@ abstract contract CompoundingValidatorStorage is Governable, Pausable { uint256 internal constant MAX_DEPOSITS = 32; /// @dev The maximum number of validators that can be verified. uint256 internal constant MAX_VERIFIED_VALIDATORS = 48; + /// @dev The maximum amount of ETH that can be used for the initial deposit to a new validator. + uint256 internal constant MAX_INITIAL_DEPOSIT_AMOUNT_WEI = 2048 ether; /// @dev The default withdrawable epoch value on the Beacon chain. /// A value in the far future means the validator is not exiting. uint64 internal constant FAR_FUTURE_EPOCH = type(uint64).max; @@ -1181,7 +1183,7 @@ contract CompoundingStakingStrategy is { require(_initialDepositAmountWei >= 1 ether, "Deposit too small"); require( - _initialDepositAmountWei <= MIN_ACTIVATION_BALANCE_GWEI * 1e9, + _initialDepositAmountWei <= MAX_INITIAL_DEPOSIT_AMOUNT_WEI, "Deposit too large" ); diff --git a/contracts/test/strategies/compoundingSSVStaking.js b/contracts/test/strategies/compoundingSSVStaking.js index cd5a94eb4e..ed86560aa7 100644 --- a/contracts/test/strategies/compoundingSSVStaking.js +++ b/contracts/test/strategies/compoundingSSVStaking.js @@ -185,7 +185,7 @@ describe("Unit test: Compounding SSV Staking Strategy", function () { it("Governor should be able to change the first deposit amount", async () => { const { compoundingStakingSSVStrategy } = fixture; - const updatedAmount = parseEther("32.25"); + const updatedAmount = parseEther("2048"); const tx = await compoundingStakingSSVStrategy .connect(sGov) .setInitialDepositAmount(updatedAmount); @@ -215,13 +215,13 @@ describe("Unit test: Compounding SSV Staking Strategy", function () { .setInitialDepositAmount(parseUnits("0.5", 18)) ).to.be.revertedWith("Deposit too small"); }); - it("Should revert when setting the first deposit amount above 32.25 ETH", async () => { + it("Should revert when setting the first deposit amount above 2048 ETH", async () => { const { compoundingStakingSSVStrategy } = fixture; await expect( compoundingStakingSSVStrategy .connect(sGov) - .setInitialDepositAmount(parseEther("32.25").add(1)) + .setInitialDepositAmount(parseEther("2048").add(1)) ).to.be.revertedWith("Deposit too large"); }); it("Should not collect rewards", async () => { From 86d24521ed072bbe89e033cb0973c59a55559900 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 29 May 2026 19:21:54 +1000 Subject: [PATCH 06/29] Initial deposit changed to 2030 --- .../deploy/mainnet/197_deploy_compounding_staking_strategy.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/deploy/mainnet/197_deploy_compounding_staking_strategy.js b/contracts/deploy/mainnet/197_deploy_compounding_staking_strategy.js index 48fada8088..d5ba09e4df 100644 --- a/contracts/deploy/mainnet/197_deploy_compounding_staking_strategy.js +++ b/contracts/deploy/mainnet/197_deploy_compounding_staking_strategy.js @@ -59,7 +59,7 @@ module.exports = deploymentWithGovernanceProposal( [], // reward token addresses [], // asset token addresses [], // platform token addresses - ethers.utils.parseEther("32.25"), // initial validator deposit amount + ethers.utils.parseEther("2030"), // initial validator deposit amount ] ); From d79440b824b6eb8df76c2cd3c1da5f4b7f9f4059 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 29 May 2026 19:41:57 +1000 Subject: [PATCH 07/29] Added talosRelayer address --- contracts/utils/addresses.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/contracts/utils/addresses.js b/contracts/utils/addresses.js index a05685af3a..a82cca4155 100644 --- a/contracts/utils/addresses.js +++ b/contracts/utils/addresses.js @@ -336,6 +336,9 @@ addresses.mainnet.NativeStakingSSVStrategy3Proxy = addresses.mainnet.validatorRegistrator = "0x4b91827516f79d6F6a1F292eD99671663b09169a"; +// Talos Relayer +addresses.mainnet.talosRelayer = "0x739212d5bAfE6AAC8Be49a60B7d003bD41DBf38b"; + // Lido Withdrawal Queue addresses.mainnet.LidoWithdrawalQueue = "0x889edC2eDab5f40e902b864aD4d7AdE8E412F9B1"; From c3f2d52e2856244db81bc1fc957384e19be6f54b Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Mon, 1 Jun 2026 12:20:26 +1000 Subject: [PATCH 08/29] Allow CompoundingStakingStrategy initial deposits to be less than the stored initialDepositAmountWei --- .../CompoundingStakingStrategy.sol | 4 +-- .../test/strategies/compoundingSSVStaking.js | 2 +- .../test/strategies/compoundingStaking.js | 26 +++++++++++++++++++ 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/contracts/contracts/strategies/NativeStaking/CompoundingStakingStrategy.sol b/contracts/contracts/strategies/NativeStaking/CompoundingStakingStrategy.sol index deb71dd4e8..01d3b06889 100644 --- a/contracts/contracts/strategies/NativeStaking/CompoundingStakingStrategy.sol +++ b/contracts/contracts/strategies/NativeStaking/CompoundingStakingStrategy.sol @@ -344,7 +344,7 @@ contract CompoundingStakingStrategy is } /// @notice Stakes WETH in this strategy to a compounding validator. - /// The first deposit to a new validator must be exactly `initialDepositAmountWei`. + /// The first deposit to a new validator must be less than or equal to `initialDepositAmountWei`. /// Once verified on the beacon chain, rewards can push the validator's balance above /// the activation threshold so it can become active without requiring a second deposit. /// Does not convert any ETH sitting in this strategy to WETH. @@ -473,7 +473,7 @@ contract CompoundingStakingStrategy is require(!firstDeposit, "Existing first deposit"); // Limits the amount of ETH that can be at risk from a front-running deposit attack. require( - depositAmountWei == initialDepositAmountWei, + depositAmountWei <= initialDepositAmountWei, "Invalid first deposit amount" ); // Limits the number of validator balance proofs to verifyBalances diff --git a/contracts/test/strategies/compoundingSSVStaking.js b/contracts/test/strategies/compoundingSSVStaking.js index ed86560aa7..330851bb63 100644 --- a/contracts/test/strategies/compoundingSSVStaking.js +++ b/contracts/test/strategies/compoundingSSVStaking.js @@ -963,7 +963,7 @@ describe("Unit test: Compounding SSV Staking Strategy", function () { ).to.equal(stratBalanceBefore); }); - it("Should revert when first stake amount is not exactly the initial deposit amount", async () => { + it("Should revert when first stake amount is above the initial deposit amount", async () => { const { compoundingStakingSSVStrategy, validatorRegistrator } = fixture; const testValidator = testValidators[0]; diff --git a/contracts/test/strategies/compoundingStaking.js b/contracts/test/strategies/compoundingStaking.js index 60a771f2ce..ca8ede964e 100644 --- a/contracts/test/strategies/compoundingStaking.js +++ b/contracts/test/strategies/compoundingStaking.js @@ -93,6 +93,32 @@ describe("Unit test: Compounding Staking Strategy", function () { expect(await compoundingStakingStrategy.firstDeposit()).to.equal(true); }); + it("allows the first deposit to be less than the initial deposit amount", async () => { + const { compoundingStakingStrategy, governor } = fixture; + const validator = testValidators[0]; + const pubKeyHash = hashPubKey(validator.publicKey); + + await compoundingStakingStrategy + .connect(governor) + .setInitialDepositAmount(parseEther("2")); + await depositToStrategy("1"); + + const stakeTx = await stakeValidator({ validator, amount: "1" }); + const receipt = await stakeTx.wait(); + const event = receipt.events.find((event) => event.event === "ETHStaked"); + + await expect(stakeTx) + .to.emit(compoundingStakingStrategy, "ETHStaked") + .withArgs( + pubKeyHash, + event.args.pendingDepositRoot, + validator.publicKey, + parseEther("1") + ); + + expect(await compoundingStakingStrategy.firstDeposit()).to.equal(true); + }); + it("does not allow a follow-up deposit before the validator is verified or active", async () => { const validator = testValidators[0]; From 8ef0ba065920427970a73142a9f0896d6b118bbc Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Mon, 1 Jun 2026 12:21:05 +1000 Subject: [PATCH 09/29] Set the new cCompoundingStakingStrategy as the default strategy of the OETH Vault --- .../mainnet/197_deploy_compounding_staking_strategy.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/contracts/deploy/mainnet/197_deploy_compounding_staking_strategy.js b/contracts/deploy/mainnet/197_deploy_compounding_staking_strategy.js index d5ba09e4df..6c7a521f66 100644 --- a/contracts/deploy/mainnet/197_deploy_compounding_staking_strategy.js +++ b/contracts/deploy/mainnet/197_deploy_compounding_staking_strategy.js @@ -106,6 +106,11 @@ module.exports = deploymentWithGovernanceProposal( signature: "approveStrategy(address)", args: [cCompoundingStakingStrategyProxy.address], }, + { + contract: cOETHVault, + signature: "setDefaultStrategy(address)", + args: [cCompoundingStakingStrategyProxy.address], + }, { contract: cStrategy, signature: "setRegistrator(address)", From 2e58be9c96c04310a7a2d9808196bc6e60732f25 Mon Sep 17 00:00:00 2001 From: Nick Addison Date: Mon, 1 Jun 2026 13:24:23 +1000 Subject: [PATCH 10/29] Fix contracts cron image pnpm version (#2904) * Fix contracts cron image pnpm version * Changed package.json to use pnpm --- contracts/dockerfile | 4 ++-- contracts/package.json | 29 +++++++++++++++-------------- package.json | 2 +- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/contracts/dockerfile b/contracts/dockerfile index 986dd65b94..aa8e2ee691 100644 --- a/contracts/dockerfile +++ b/contracts/dockerfile @@ -33,9 +33,9 @@ RUN curl -fsSLO "$SUPERCRONIC_URL" \ WORKDIR /app -# Enable pnpm via corepack and install dependencies first for better caching. +# Install pinned pnpm and install dependencies first for better caching. COPY pnpm-lock.yaml package.json pnpm-workspace.yaml ./ -RUN corepack enable \ +RUN npm install -g pnpm@10.18.3 \ && pnpm install --frozen-lockfile # Copy the rest of the contracts workspace. diff --git a/contracts/package.json b/contracts/package.json index 1d45e429cd..8c68a521ec 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -3,6 +3,7 @@ "version": "0.1.0", "description": "Origin DeFi Contracts", "main": "index.js", + "packageManager": "pnpm@10.18.3", "scripts": { "deploy": "rm -rf deployments/hardhat && npx hardhat deploy", "deploy:mainnet": "VERIFY_CONTRACTS=true npx hardhat deploy --network mainnet --verbose", @@ -15,25 +16,25 @@ "deploy:hyperevm": "VERIFY_CONTRACTS=true NETWORK_NAME=hyperevm npx hardhat deploy --network hyperevm --verbose", "abi:generate": "(rm -rf deployments/hardhat && mkdir -p dist/abi && npx hardhat deploy --export '../dist/network.json')", "abi:dist": "find ./artifacts/contracts -name \"*.json\" -type f -exec cp {} ./dist/abi \\; && rm -rf dist/abi/*.dbg.json dist/abi/Mock*.json && cp ./abi.package.json dist/package.json && cp ./.npmrc.abi dist/.npmrc", - "node": "yarn run node:fork", + "node": "pnpm run node:fork", "node:fork": "./node.sh fork", - "node:arb": "FORK_NETWORK_NAME=arbitrumOne yarn run node:fork", - "node:hol": "FORK_NETWORK_NAME=holesky yarn run node:fork", - "node:base": "FORK_NETWORK_NAME=base yarn run node:fork", - "node:sonic": "FORK_NETWORK_NAME=sonic yarn run node:fork", - "node:plume": "FORK_NETWORK_NAME=plume yarn run node:fork", - "node:hoodi": "FORK_NETWORK_NAME=hoodi yarn run node:fork", - "node:hyperevm": "FORK_NETWORK_NAME=hyperevm yarn run node:fork", + "node:arb": "FORK_NETWORK_NAME=arbitrumOne pnpm run node:fork", + "node:hol": "FORK_NETWORK_NAME=holesky pnpm run node:fork", + "node:base": "FORK_NETWORK_NAME=base pnpm run node:fork", + "node:sonic": "FORK_NETWORK_NAME=sonic pnpm run node:fork", + "node:plume": "FORK_NETWORK_NAME=plume pnpm run node:fork", + "node:hoodi": "FORK_NETWORK_NAME=hoodi pnpm run node:fork", + "node:hyperevm": "FORK_NETWORK_NAME=hyperevm pnpm run node:fork", "node:anvil": "anvil --fork-url $PROVIDER_URL --port 8545 --block-base-fee-per-gas 0 --auto-impersonate --disable-block-gas-limit", "node:anvil:base": "anvil --fork-url $BASE_PROVIDER_URL --port 8545 --block-base-fee-per-gas 0 --auto-impersonate --disable-block-gas-limit", "node:anvil:plume": "anvil --fork-url $PLUME_PROVIDER_URL --port 8545 --block-base-fee-per-gas 0 --auto-impersonate --disable-block-gas-limit", "node:anvil:sonic": "anvil --fork-url $SONIC_PROVIDER_URL --port 8545 --block-base-fee-per-gas 0 --auto-impersonate --disable-block-gas-limit", "node:anvil:hoodi": "anvil --fork-url $HOODI_PROVIDER_URL --port 8545 --block-base-fee-per-gas 0 --auto-impersonate --disable-block-gas-limit", "node:anvil:hyperevm": "anvil --fork-url $HYPEREVM_PROVIDER_URL --port 8545 --block-base-fee-per-gas 0 --auto-impersonate --disable-block-gas-limit", - "lint": "yarn run lint:js && yarn run lint:sol", + "lint": "pnpm run lint:js && pnpm run lint:sol", "lint:js": "eslint \"test/**/*.js\" \"tasks/**/*.js\" \"deploy/**/*.js\"", "lint:sol": "solhint \"contracts/**/*.sol\"", - "prettier": "yarn run prettier:js && yarn run prettier:sol", + "prettier": "pnpm run prettier:js && pnpm run prettier:sol", "prettier:check": "prettier -c \"*.js\" \"deploy/**/*.js\" \"scripts/**/*.js\" \"scripts/**/*.js\" \"tasks/**/*.js\" \"test/**/*.js\" \"utils/**/*.js\"", "prettier:js": "prettier --write \"*.js\" \"deploy/**/*.js\" \"scripts/**/*.js\" \"scripts/**/*.js\" \"tasks/**/*.js\" \"test/**/*.js\" \"utils/**/*.js\"", "prettier:sol": "prettier --write --plugin=prettier-plugin-solidity \"contracts/**/*.sol\"", @@ -50,11 +51,11 @@ "test:hyperevm-fork": "FORK_NETWORK_NAME=hyperevm ./fork-test.sh", "test:fork:w_trace": "TRACE=true ./fork-test.sh", "fund": "FORK=true npx hardhat fund --network localhost", - "echidna": "yarn run clean && rm -rf echidna-corpus && echidna . --contract Echidna --config echidna-config.yaml", + "echidna": "pnpm run clean && rm -rf echidna-corpus && echidna . --contract Echidna --config echidna-config.yaml", "compute-merkle-proofs-local": "HARDHAT_NETWORK=localhost node scripts/staking/airDrop.js reimbursements.csv scripts/staking/merkleProofedAccountsToBeCompensated.json && cp scripts/staking/merkleProofedAccountsToBeCompensated.json ../dapp/src/constants/merkleProofedAccountsToBeCompensated.json", "compute-merkle-proofs-mainnet": "HARDHAT_NETWORK=mainnet node scripts/staking/airDrop.js reimbursements.csv scripts/staking/merkleProofedAccountsToBeCompensated.json && cp scripts/staking/merkleProofedAccountsToBeCompensated.json ../dapp/src/constants/merkleProofedAccountsToBeCompensated.json", - "slither": "yarn run clean && slither . --config-file slither.config.json", - "slither:triage": "yarn run clean && slither . --triage --config-file slither.config.json", + "slither": "pnpm run clean && slither . --config-file slither.config.json", + "slither:triage": "pnpm run clean && slither . --triage --config-file slither.config.json", "clean": "rm -rf build crytic-export artifacts cache deployments/local*", "cache-clean:light": "rm -rf cache/hardhat-network-fork/network-1337/", "test:coverage": "IS_TEST=true npx hardhat coverage", @@ -123,7 +124,7 @@ }, "husky": { "hooks": { - "pre-push": "yarn run prettier:check" + "pre-push": "pnpm run prettier:check" } }, "dependencies": { diff --git a/package.json b/package.json index 1362bd4af6..8f15a1cd47 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "prepare": "husky" }, "engines": { - "node": "20" + "node": "22" }, "devDependencies": { "danger": "^11.2.8", From 2aa73831a9ee4e0b97cf3a8837fd8eb84b5da1b1 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Mon, 1 Jun 2026 14:00:49 +1000 Subject: [PATCH 11/29] Moved deploy 197 into 196 --- ...96_deploy_compounding_staking_strategy.js} | 2 +- ...ade_compounding_staking_initial_deposit.js | 52 ------------------- 2 files changed, 1 insertion(+), 53 deletions(-) rename contracts/deploy/mainnet/{197_deploy_compounding_staking_strategy.js => 196_deploy_compounding_staking_strategy.js} (98%) delete mode 100644 contracts/deploy/mainnet/196_upgrade_compounding_staking_initial_deposit.js diff --git a/contracts/deploy/mainnet/197_deploy_compounding_staking_strategy.js b/contracts/deploy/mainnet/196_deploy_compounding_staking_strategy.js similarity index 98% rename from contracts/deploy/mainnet/197_deploy_compounding_staking_strategy.js rename to contracts/deploy/mainnet/196_deploy_compounding_staking_strategy.js index 6c7a521f66..0bd5fe029f 100644 --- a/contracts/deploy/mainnet/197_deploy_compounding_staking_strategy.js +++ b/contracts/deploy/mainnet/196_deploy_compounding_staking_strategy.js @@ -4,7 +4,7 @@ const { deploymentWithGovernanceProposal } = require("../../utils/deploy"); module.exports = deploymentWithGovernanceProposal( { - deployName: "197_deploy_compounding_staking_strategy", + deployName: "196_deploy_compounding_staking_strategy", forceDeploy: false, reduceQueueTime: true, deployerIsProposer: false, diff --git a/contracts/deploy/mainnet/196_upgrade_compounding_staking_initial_deposit.js b/contracts/deploy/mainnet/196_upgrade_compounding_staking_initial_deposit.js deleted file mode 100644 index 86d9a60bd8..0000000000 --- a/contracts/deploy/mainnet/196_upgrade_compounding_staking_initial_deposit.js +++ /dev/null @@ -1,52 +0,0 @@ -const addresses = require("../../utils/addresses"); -const { beaconChainGenesisTimeMainnet } = require("../../utils/constants"); -const { deploymentWithGovernanceProposal } = require("../../utils/deploy"); - -module.exports = deploymentWithGovernanceProposal( - { - deployName: "196_upgrade_compounding_staking_initial_deposit", - forceDeploy: false, - reduceQueueTime: true, - deployerIsProposer: false, - }, - async ({ deployWithConfirmation, ethers }) => { - const cOETHVaultProxy = await ethers.getContract("OETHVaultProxy"); - const cCompoundingStakingStrategyProxy = await ethers.getContract( - "CompoundingStakingSSVStrategyProxy" - ); - const cCompoundingStakingSSVStrategy = await ethers.getContractAt( - "CompoundingStakingSSVStrategy", - cCompoundingStakingStrategyProxy.address - ); - const cBeaconProofs = await ethers.getContract("BeaconProofs"); - - console.log("Deploy CompoundingStakingSSVStrategy"); - const dCompoundingStakingStrategy = await deployWithConfirmation( - "CompoundingStakingSSVStrategy", - [ - [addresses.zero, cOETHVaultProxy.address], //_baseConfig - addresses.mainnet.WETH, // wethAddress - addresses.mainnet.SSVNetwork, // ssvNetwork - addresses.mainnet.beaconChainDepositContract, // beaconChainDepositContract - cBeaconProofs.address, // beaconProofs - beaconChainGenesisTimeMainnet, - ] - ); - - return { - name: "Upgrade the compounding staking strategy initial deposit to 32.25 ETH", - actions: [ - { - contract: cCompoundingStakingStrategyProxy, - signature: "upgradeTo(address)", - args: [dCompoundingStakingStrategy.address], - }, - { - contract: cCompoundingStakingSSVStrategy, - signature: "setInitialDepositAmount(uint256)", - args: [ethers.utils.parseEther("32.25")], - }, - ], - }; - } -); From e9acf9bf10c675d8899ea2a4a9a5c95642103c89 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Mon, 1 Jun 2026 14:23:20 +1000 Subject: [PATCH 12/29] Remove old CompoundingStakingSSVStrategy --- .../mainnet/196_deploy_compounding_staking_strategy.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/contracts/deploy/mainnet/196_deploy_compounding_staking_strategy.js b/contracts/deploy/mainnet/196_deploy_compounding_staking_strategy.js index 0bd5fe029f..4d14fbcc0e 100644 --- a/contracts/deploy/mainnet/196_deploy_compounding_staking_strategy.js +++ b/contracts/deploy/mainnet/196_deploy_compounding_staking_strategy.js @@ -19,6 +19,9 @@ module.exports = deploymentWithGovernanceProposal( cOETHVaultProxy.address ); const cBeaconProofs = await ethers.getContract("BeaconProofs"); + const cCompoundingStakingSSVStrategyProxy = await ethers.getContract( + "CompoundingStakingSSVStrategyProxy" + ); const cNativeStakingStrategy2Proxy = await ethers.getContract( "NativeStakingSSVStrategy2Proxy" ); @@ -111,6 +114,11 @@ module.exports = deploymentWithGovernanceProposal( signature: "setDefaultStrategy(address)", args: [cCompoundingStakingStrategyProxy.address], }, + { + contract: cOETHVault, + signature: "removeStrategy(address)", + args: [cCompoundingStakingSSVStrategyProxy.address], + }, { contract: cStrategy, signature: "setRegistrator(address)", From cf8461db84724a742d5600d81ca74fb060cce735 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Mon, 1 Jun 2026 19:46:45 +1000 Subject: [PATCH 13/29] Removed migrateClusterToETH from NativeStakingStrategy Added withdrawSsvClusterEth to the NativeStakingStrategy and CompoundingStakingSSVStrategy Deploy script now upgrades old NativeStakingStrategy and CompoundingStakingSSVStrategy --- contracts/contracts/mocks/MockSSVNetwork.sol | 10 ++- .../CompoundingStakingSSVStrategy.sol | 59 +++++++++------ .../NativeStakingSSVStrategy.sol | 25 ++++++- .../NativeStaking/ValidatorRegistrator.sol | 16 ---- ...196_deploy_compounding_staking_strategy.js | 74 ++++++++++++++++++- .../test/strategies/compoundingSSVStaking.js | 14 ++-- 6 files changed, 146 insertions(+), 52 deletions(-) diff --git a/contracts/contracts/mocks/MockSSVNetwork.sol b/contracts/contracts/mocks/MockSSVNetwork.sol index 393c1aeaa2..1a728f75c8 100644 --- a/contracts/contracts/mocks/MockSSVNetwork.sol +++ b/contracts/contracts/mocks/MockSSVNetwork.sol @@ -4,12 +4,20 @@ pragma solidity ^0.8.0; import { Cluster } from "./../interfaces/ISSVNetwork.sol"; contract MockSSVNetwork { + error ValidatorAlreadyExists(); + + mapping(bytes32 => bool) public validatorExists; + function registerValidator( bytes calldata publicKey, uint64[] calldata operatorIds, bytes calldata sharesData, Cluster memory cluster - ) external payable {} + ) external payable { + bytes32 pubKeyHash = keccak256(publicKey); + if (validatorExists[pubKeyHash]) revert ValidatorAlreadyExists(); + validatorExists[pubKeyHash] = true; + } function bulkRegisterValidator( bytes[] calldata publicKeys, diff --git a/contracts/contracts/strategies/NativeStaking/CompoundingStakingSSVStrategy.sol b/contracts/contracts/strategies/NativeStaking/CompoundingStakingSSVStrategy.sol index 11a0a29089..eb1d72ad9e 100644 --- a/contracts/contracts/strategies/NativeStaking/CompoundingStakingSSVStrategy.sol +++ b/contracts/contracts/strategies/NativeStaking/CompoundingStakingSSVStrategy.sol @@ -14,10 +14,9 @@ contract CompoundingStakingSSVStrategy is CompoundingStakingStrategy { // For future use uint256[50] private __gap; - event SSVValidatorRegistered( - bytes32 indexed pubKeyHash, - uint64[] operatorIds - ); + error CannotRemoveSsvValidator(); + error NotRegisteredOrVerified(); + event SSVValidatorRemoved(bytes32 indexed pubKeyHash, uint64[] operatorIds); /// @param _baseConfig Base strategy config with @@ -53,13 +52,13 @@ contract CompoundingStakingSSVStrategy is CompoundingStakingStrategy { * */ + // slither-disable-start reentrancy-no-eth /// @notice Registers a single validator in a SSV Cluster. /// Only the Registrator can call this function. /// @param publicKey The public key of the validator /// @param operatorIds The operator IDs of the SSV Cluster /// @param sharesData The shares data for the validator /// @param cluster The SSV cluster details including the validator count and SSV balance - // slither-disable-start reentrancy-no-eth function registerSsvValidator( bytes calldata publicKey, uint64[] calldata operatorIds, @@ -68,11 +67,6 @@ contract CompoundingStakingSSVStrategy is CompoundingStakingStrategy { ) external payable onlyRegistrator whenNotPaused { // Hash the public key using the Beacon Chain's format bytes32 pubKeyHash = _hashPubKey(publicKey); - // Check each public key has not already been used - require( - validator[pubKeyHash].state == ValidatorState.NON_REGISTERED, - "Validator already registered" - ); // Store the validator state as registered validator[pubKeyHash].state = ValidatorState.REGISTERED; @@ -83,8 +77,6 @@ contract CompoundingStakingSSVStrategy is CompoundingStakingStrategy { sharesData, cluster ); - - emit SSVValidatorRegistered(pubKeyHash, operatorIds); } /// @notice Remove the validator from the SSV Cluster after: @@ -106,12 +98,13 @@ contract CompoundingStakingSSVStrategy is CompoundingStakingStrategy { bytes32 pubKeyHash = _hashPubKey(publicKey); ValidatorState currentState = validator[pubKeyHash].state; // Can remove SSV validators that were incorrectly registered and can not be deposited to. - require( - currentState == ValidatorState.REGISTERED || - currentState == ValidatorState.EXITED || - currentState == ValidatorState.INVALID, - "Validator not regd or exited" - ); + if ( + currentState != ValidatorState.REGISTERED && + currentState != ValidatorState.EXITED && + currentState != ValidatorState.INVALID + ) { + revert CannotRemoveSsvValidator(); + } validator[pubKeyHash].state = ValidatorState.REMOVED; @@ -124,6 +117,23 @@ contract CompoundingStakingSSVStrategy is CompoundingStakingStrategy { emit SSVValidatorRemoved(pubKeyHash, operatorIds); } + /// @notice Withdraw ETH funding from this strategy's SSV cluster. + /// @param operatorIds The operator IDs of the SSV Cluster + /// @param amount The amount of ETH to withdraw from the SSV cluster + /// @param cluster The SSV cluster details including the validator count and ETH balance + function withdrawSsvClusterEth( + uint64[] calldata operatorIds, + uint256 amount, + Cluster calldata cluster + ) external onlyGovernor { + ISSVNetwork(SSV_NETWORK).withdraw(operatorIds, amount, cluster); + + uint256 ethBalance = address(this).balance; + if (ethBalance > 0) { + _withdraw(vaultAddress, ethBalance, ethBalance); + } + } + // slither-disable-end reentrancy-no-eth function _admitStake(bytes32 pubKeyHash, uint256 depositAmountWei) @@ -131,12 +141,13 @@ contract CompoundingStakingSSVStrategy is CompoundingStakingStrategy { override { ValidatorState currentState = validator[pubKeyHash].state; - require( - currentState == ValidatorState.REGISTERED || - currentState == ValidatorState.VERIFIED || - currentState == ValidatorState.ACTIVE, - "Not registered or verified" - ); + if ( + currentState != ValidatorState.REGISTERED && + currentState != ValidatorState.VERIFIED && + currentState != ValidatorState.ACTIVE + ) { + revert NotRegisteredOrVerified(); + } if (currentState == ValidatorState.REGISTERED) { _recordFirstDeposit(pubKeyHash, depositAmountWei); diff --git a/contracts/contracts/strategies/NativeStaking/NativeStakingSSVStrategy.sol b/contracts/contracts/strategies/NativeStaking/NativeStakingSSVStrategy.sol index 7191e47ceb..cc1f2a13e1 100644 --- a/contracts/contracts/strategies/NativeStaking/NativeStakingSSVStrategy.sol +++ b/contracts/contracts/strategies/NativeStaking/NativeStakingSSVStrategy.sol @@ -9,7 +9,7 @@ import { InitializableAbstractStrategy } from "../../utils/InitializableAbstract import { IWETH9 } from "../../interfaces/IWETH9.sol"; import { FeeAccumulator } from "./FeeAccumulator.sol"; import { ValidatorAccountant } from "./ValidatorAccountant.sol"; -import { ISSVNetwork } from "../../interfaces/ISSVNetwork.sol"; +import { ISSVNetwork, Cluster } from "../../interfaces/ISSVNetwork.sol"; struct ValidatorStakeData { bytes pubkey; @@ -124,6 +124,25 @@ contract NativeStakingSSVStrategy is ); } + /// @notice Withdraw ETH funding from this strategy's SSV cluster. + /// @param operatorIds The operator IDs of the SSV Cluster + /// @param amount The amount of ETH to withdraw from the SSV cluster + /// @param cluster The SSV cluster details including the validator count and ETH balance + function withdrawSsvClusterEth( + uint64[] calldata operatorIds, + uint256 amount, + Cluster calldata cluster + ) external onlyGovernor nonReentrant { + ISSVNetwork(SSV_NETWORK).withdraw(operatorIds, amount, cluster); + + uint256 ethBalance = address(this).balance; + if (ethBalance > 0) { + IWETH9(WETH).deposit{ value: ethBalance }(); + IERC20(WETH).safeTransfer(vaultAddress, ethBalance); + emit Withdrawal(WETH, address(0), ethBalance); + } + } + /// @notice Unlike other strategies, this does not deposit assets into the underlying platform. /// It just checks the asset is WETH and emits the Deposit event. /// To deposit WETH into validators `registerSsvValidator` and `stakeEth` must be used. @@ -282,7 +301,9 @@ contract NativeStakingSSVStrategy is */ receive() external payable { require( - msg.sender == FEE_ACCUMULATOR_ADDRESS || msg.sender == WETH, + msg.sender == FEE_ACCUMULATOR_ADDRESS || + msg.sender == WETH || + msg.sender == SSV_NETWORK, "Eth not from allowed contracts" ); } diff --git a/contracts/contracts/strategies/NativeStaking/ValidatorRegistrator.sol b/contracts/contracts/strategies/NativeStaking/ValidatorRegistrator.sol index 635eee7991..43547e8d26 100644 --- a/contracts/contracts/strategies/NativeStaking/ValidatorRegistrator.sol +++ b/contracts/contracts/strategies/NativeStaking/ValidatorRegistrator.sol @@ -338,22 +338,6 @@ abstract contract ValidatorRegistrator is Governable, Pausable { // slither-disable-end reentrancy-no-eth - /// @notice Migrate the SSV cluster to use ETH for payment instead of SSV tokens. - /// @param operatorIds The operator IDs of the SSV Cluster - /// @param cluster The SSV cluster details including the validator count and SSV balance - function migrateClusterToETH( - uint64[] memory operatorIds, - Cluster memory cluster - ) external payable onlyGovernor { - ISSVNetwork(SSV_NETWORK).migrateClusterToETH{ value: msg.value }( - operatorIds, - cluster - ); - - // The SSV Network emits - // ClusterMigratedToETH(msg.sender, operatorIds, msg.value, ssvClusterBalance, effectiveBalance, cluster) - } - /*************************************** Consolidation functions ****************************************/ diff --git a/contracts/deploy/mainnet/196_deploy_compounding_staking_strategy.js b/contracts/deploy/mainnet/196_deploy_compounding_staking_strategy.js index 4d14fbcc0e..285219aeae 100644 --- a/contracts/deploy/mainnet/196_deploy_compounding_staking_strategy.js +++ b/contracts/deploy/mainnet/196_deploy_compounding_staking_strategy.js @@ -1,6 +1,9 @@ const addresses = require("../../utils/addresses"); const { beaconChainGenesisTimeMainnet } = require("../../utils/constants"); const { deploymentWithGovernanceProposal } = require("../../utils/deploy"); +const { getClusterInfo, splitOperatorIds } = require("../../utils/ssv"); + +const compoundingSsvClusterOperatorIds = "2070,2071,2072,2073"; module.exports = deploymentWithGovernanceProposal( { @@ -11,6 +14,7 @@ module.exports = deploymentWithGovernanceProposal( }, async ({ deployWithConfirmation, ethers, withConfirmation }) => { const { deployerAddr } = await getNamedAccounts(); + const { chainId } = await ethers.provider.getNetwork(); const sDeployer = await ethers.provider.getSigner(deployerAddr); const cOETHVaultProxy = await ethers.getContract("OETHVaultProxy"); @@ -25,10 +29,58 @@ module.exports = deploymentWithGovernanceProposal( const cNativeStakingStrategy2Proxy = await ethers.getContract( "NativeStakingSSVStrategy2Proxy" ); + const cNativeStakingFeeAccumulator2Proxy = await ethers.getContract( + "NativeStakingFeeAccumulator2Proxy" + ); const cNativeStakingStrategy2 = await ethers.getContractAt( "NativeStakingSSVStrategy", cNativeStakingStrategy2Proxy.address ); + const compoundingOperatorIds = splitOperatorIds( + compoundingSsvClusterOperatorIds + ); + const { cluster: compoundingSsvCluster } = await getClusterInfo({ + chainId, + operatorids: compoundingOperatorIds.join(","), + ownerAddress: cCompoundingStakingSSVStrategyProxy.address, + }); + const compoundingSsvClusterEthBalance = ethers.BigNumber.from( + compoundingSsvCluster.ethBalance || 0 + ); + const compoundingSsvClusterForWithdraw = { + validatorCount: compoundingSsvCluster.validatorCount, + networkFeeIndex: compoundingSsvCluster.networkFeeIndex, + index: compoundingSsvCluster.index, + active: compoundingSsvCluster.active, + balance: compoundingSsvClusterEthBalance, + }; + + console.log("Deploy CompoundingStakingSSVStrategy implementation"); + const dCompoundingStakingSSVStrategy = await deployWithConfirmation( + "CompoundingStakingSSVStrategy", + [ + [addresses.zero, cOETHVaultProxy.address], //_baseConfig + addresses.mainnet.WETH, // wethAddress + addresses.mainnet.SSVNetwork, // ssvNetwork + addresses.mainnet.beaconChainDepositContract, // beaconChainDepositContract + cBeaconProofs.address, // beaconProofs + beaconChainGenesisTimeMainnet, + ] + ); + + console.log("Deploy NativeStakingSSVStrategy2 implementation"); + const dNativeStakingStrategy2 = await deployWithConfirmation( + "NativeStakingSSVStrategy", + [ + [addresses.zero, cOETHVaultProxy.address], //_baseConfig + addresses.mainnet.WETH, // wethAddress + addresses.mainnet.SSV, // ssvToken + addresses.mainnet.SSVNetwork, // ssvNetwork + 500, // maxValidators + cNativeStakingFeeAccumulator2Proxy.address, // feeAccumulator + addresses.mainnet.beaconChainDepositContract, // beaconChainDepositContract + ] + ); console.log("Deploy CompoundingStakingStrategyProxy"); const dCompoundingStakingStrategyProxy = await deployWithConfirmation( @@ -102,8 +154,28 @@ module.exports = deploymentWithGovernanceProposal( ); return { - name: "Deploy new vanilla compounding staking strategy and consolidation controller", + name: "Deploy new vanilla compounding staking strategy, upgrade SSV strategies and deploy consolidation controller", actions: [ + { + contract: cCompoundingStakingSSVStrategyProxy, + signature: "upgradeTo(address)", + args: [dCompoundingStakingSSVStrategy.address], + }, + { + contract: cCompoundingStakingSSVStrategyProxy, + signature: + "withdrawSsvClusterEth(uint64[],uint256,(uint32,uint64,uint64,bool,uint256))", + args: [ + compoundingOperatorIds, + compoundingSsvClusterEthBalance, + compoundingSsvClusterForWithdraw, + ], + }, + { + contract: cNativeStakingStrategy2Proxy, + signature: "upgradeTo(address)", + args: [dNativeStakingStrategy2.address], + }, { contract: cOETHVault, signature: "approveStrategy(address)", diff --git a/contracts/test/strategies/compoundingSSVStaking.js b/contracts/test/strategies/compoundingSSVStaking.js index 895a24b5da..70c7516872 100644 --- a/contracts/test/strategies/compoundingSSVStaking.js +++ b/contracts/test/strategies/compoundingSSVStaking.js @@ -770,9 +770,7 @@ describe("Unit test: Compounding SSV Staking Strategy", function () { { value: ethAmount } ); - await expect(regTx) - .to.emit(compoundingStakingSSVStrategy, "SSVValidatorRegistered") - .withArgs(testValidator.publicKeyHash, testValidator.operatorIds); + await regTx.wait(); expect( ( @@ -1083,7 +1081,7 @@ describe("Unit test: Compounding SSV Staking Strategy", function () { emptyCluster, { value: ethUnits("2") } ) - ).to.be.revertedWith("Validator already registered"); + ).to.be.reverted; }); it("Should revert when staking because of insufficient ETH balance", async () => { @@ -1124,7 +1122,7 @@ describe("Unit test: Compounding SSV Staking Strategy", function () { ETHInGwei // 1e9 Gwei = 1 ETH ); - await expect(tx).to.be.revertedWith("Not registered or verified"); + await expect(tx).to.be.reverted; }); // Full validator exit @@ -1464,7 +1462,7 @@ describe("Unit test: Compounding SSV Staking Strategy", function () { emptyCluster ); - await expect(removeTx).to.be.revertedWith("Validator not regd or exited"); + await expect(removeTx).to.be.reverted; }); it("Should remove a validator when validator is exited", async () => { @@ -1512,7 +1510,7 @@ describe("Unit test: Compounding SSV Staking Strategy", function () { ).length ).to.equal(0); - const removeTx = await compoundingStakingSSVStrategy + const removeTx = compoundingStakingSSVStrategy .connect(validatorRegistrator) .removeSsvValidator( testValidators[3].publicKey, @@ -3091,7 +3089,7 @@ describe("Unit test: Compounding SSV Staking Strategy", function () { "0x" // empty proof as it is not verified in the mock ); - const tx = await compoundingStakingSSVStrategy + const tx = compoundingStakingSSVStrategy .connect(validatorRegistrator) .removeSsvValidator( testValidator.publicKey, From 1a832361eea3887b5cd40e8eb26ede3f36c0f77b Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Mon, 1 Jun 2026 20:09:15 +1000 Subject: [PATCH 14/29] Fixed fork tests --- ...196_deploy_compounding_staking_strategy.js | 110 ++++++++++-------- .../beacon/beaconProofs.mainnet.fork-test.js | 33 +++--- 2 files changed, 77 insertions(+), 66 deletions(-) diff --git a/contracts/deploy/mainnet/196_deploy_compounding_staking_strategy.js b/contracts/deploy/mainnet/196_deploy_compounding_staking_strategy.js index 285219aeae..aab688fed9 100644 --- a/contracts/deploy/mainnet/196_deploy_compounding_staking_strategy.js +++ b/contracts/deploy/mainnet/196_deploy_compounding_staking_strategy.js @@ -1,4 +1,5 @@ const addresses = require("../../utils/addresses"); +const { isFork } = require("../../test/helpers"); const { beaconChainGenesisTimeMainnet } = require("../../utils/constants"); const { deploymentWithGovernanceProposal } = require("../../utils/deploy"); const { getClusterInfo, splitOperatorIds } = require("../../utils/ssv"); @@ -26,6 +27,10 @@ module.exports = deploymentWithGovernanceProposal( const cCompoundingStakingSSVStrategyProxy = await ethers.getContract( "CompoundingStakingSSVStrategyProxy" ); + const cCompoundingStakingSSVStrategy = await ethers.getContractAt( + "CompoundingStakingSSVStrategy", + cCompoundingStakingSSVStrategyProxy.address + ); const cNativeStakingStrategy2Proxy = await ethers.getContract( "NativeStakingSSVStrategy2Proxy" ); @@ -153,55 +158,66 @@ module.exports = deploymentWithGovernanceProposal( ] ); + const shouldWithdrawCompoundingSsvCluster = + !isFork || Number(compoundingSsvCluster.validatorCount) === 0; + + const actions = [ + { + contract: cCompoundingStakingSSVStrategyProxy, + signature: "upgradeTo(address)", + args: [dCompoundingStakingSSVStrategy.address], + }, + { + contract: cNativeStakingStrategy2Proxy, + signature: "upgradeTo(address)", + args: [dNativeStakingStrategy2.address], + }, + { + contract: cOETHVault, + signature: "approveStrategy(address)", + args: [cCompoundingStakingStrategyProxy.address], + }, + { + contract: cOETHVault, + signature: "setDefaultStrategy(address)", + args: [cCompoundingStakingStrategyProxy.address], + }, + { + contract: cStrategy, + signature: "setRegistrator(address)", + args: [dConsolidationController.address], + }, + { + contract: cNativeStakingStrategy2, + signature: "setRegistrator(address)", + args: [dConsolidationController.address], + }, + ]; + + // This can be simplified once the compounding SSV cluster has been fully exited and withdrawn, + // but for now we need to withdraw the cluster from the old strategy and remove the old strategy + // from the vault within the same proposal to ensure the safety of users' funds. + if (shouldWithdrawCompoundingSsvCluster) { + actions.splice(1, 0, { + contract: cCompoundingStakingSSVStrategy, + signature: + "withdrawSsvClusterEth(uint64[],uint256,(uint32,uint64,uint64,bool,uint256))", + args: [ + compoundingOperatorIds, + compoundingSsvClusterEthBalance, + compoundingSsvClusterForWithdraw, + ], + }); + actions.splice(5, 0, { + contract: cOETHVault, + signature: "removeStrategy(address)", + args: [cCompoundingStakingSSVStrategyProxy.address], + }); + } + return { name: "Deploy new vanilla compounding staking strategy, upgrade SSV strategies and deploy consolidation controller", - actions: [ - { - contract: cCompoundingStakingSSVStrategyProxy, - signature: "upgradeTo(address)", - args: [dCompoundingStakingSSVStrategy.address], - }, - { - contract: cCompoundingStakingSSVStrategyProxy, - signature: - "withdrawSsvClusterEth(uint64[],uint256,(uint32,uint64,uint64,bool,uint256))", - args: [ - compoundingOperatorIds, - compoundingSsvClusterEthBalance, - compoundingSsvClusterForWithdraw, - ], - }, - { - contract: cNativeStakingStrategy2Proxy, - signature: "upgradeTo(address)", - args: [dNativeStakingStrategy2.address], - }, - { - contract: cOETHVault, - signature: "approveStrategy(address)", - args: [cCompoundingStakingStrategyProxy.address], - }, - { - contract: cOETHVault, - signature: "setDefaultStrategy(address)", - args: [cCompoundingStakingStrategyProxy.address], - }, - { - contract: cOETHVault, - signature: "removeStrategy(address)", - args: [cCompoundingStakingSSVStrategyProxy.address], - }, - { - contract: cStrategy, - signature: "setRegistrator(address)", - args: [dConsolidationController.address], - }, - { - contract: cNativeStakingStrategy2, - signature: "setRegistrator(address)", - args: [dConsolidationController.address], - }, - ], + actions, }; } ); diff --git a/contracts/test/beacon/beaconProofs.mainnet.fork-test.js b/contracts/test/beacon/beaconProofs.mainnet.fork-test.js index 75bf8eeee9..dcff81fca2 100644 --- a/contracts/test/beacon/beaconProofs.mainnet.fork-test.js +++ b/contracts/test/beacon/beaconProofs.mainnet.fork-test.js @@ -18,6 +18,12 @@ const log = require("../../utils/logger")("test:fork:beacon:oracle"); const loadFixture = createFixtureLoader(beaconChainFixture); +const activeValidatorIndex = 1938267; +const activeValidatorWithdrawalCredential = + "0x02000000000000000000000084750fc0837b32afdde943051b2634d05ced8e15"; +const exitedValidatorIndex = 1998612; +const exitedValidatorWithdrawableEpoch = 384221; + describe("ForkTest: Beacon Proofs", function () { this.timeout(0); @@ -44,28 +50,23 @@ describe("ForkTest: Beacon Proofs", function () { it("Should verify validator public key", async () => { const { beaconProofs } = fixture; - const validatorIndex = 1804300; - const { proof, leaf, pubKey } = await generateValidatorPubKeyProof({ blockView, blockTree, stateView, - validatorIndex, + validatorIndex: activeValidatorIndex, }); const pubKeyHash = hashPubKey(pubKey); expect(pubKeyHash).to.eq(leaf); - const withdrawalCredential = - "0x020000000000000000000000f80432285c9d2055449330bbd7686a5ecf2a7247"; - log(`About to verify validator public key`); await beaconProofs.verifyValidator( beaconBlockRoot, pubKeyHash, proof, - validatorIndex, - withdrawalCredential + activeValidatorIndex, + activeValidatorWithdrawalCredential ); }); @@ -92,20 +93,16 @@ describe("ForkTest: Beacon Proofs", function () { } it("Should verify validator withdrawable epoch that is not exiting", async () => { - const validatorIndex = 1804301; - const withdrawableEpoch = await assertValidatorWithdrawableEpoch( - validatorIndex + activeValidatorIndex ); expect(withdrawableEpoch).to.equal(MAX_UINT64); }); it("Should verify validator withdrawable epoch that has exited", async () => { - const validatorIndex = 1804300; - const withdrawableEpoch = await assertValidatorWithdrawableEpoch( - validatorIndex + exitedValidatorIndex ); - expect(withdrawableEpoch).to.equal(380333); + expect(withdrawableEpoch).to.equal(exitedValidatorWithdrawableEpoch); }); it("Should verify balances container", async () => { @@ -124,13 +121,11 @@ describe("ForkTest: Beacon Proofs", function () { it("Should verify validator balance in balances container", async () => { const { beaconProofs } = fixture; - const validatorIndex = 1804300; - const { proof, leaf, root } = await generateBalanceProof({ blockView, blockTree, stateView, - validatorIndex, + validatorIndex: activeValidatorIndex, }); log(`About to verify validator balance in balances container`); @@ -138,7 +133,7 @@ describe("ForkTest: Beacon Proofs", function () { root, leaf, proof, - validatorIndex + activeValidatorIndex ); }); From 2efa2a31222dc3937c0fbf290211d07b95422d9b Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Tue, 2 Jun 2026 06:09:04 +0200 Subject: [PATCH 15/29] change pre-commit hook to pnpm (#2910) --- .husky/pre-commit | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.husky/pre-commit b/.husky/pre-commit index 8f7c2fad96..c3766bff17 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,4 @@ #!/bin/sh cd contracts -yarn run lint:js +pnpm run lint:js From ab2e67910dc0d162a20795ecebf6c1b178e94e7e Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 2 Jun 2026 14:51:08 +1000 Subject: [PATCH 16/29] Added more staking unit tests --- .../test/strategies/compoundingStaking.js | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/contracts/test/strategies/compoundingStaking.js b/contracts/test/strategies/compoundingStaking.js index ca8ede964e..75018bc25b 100644 --- a/contracts/test/strategies/compoundingStaking.js +++ b/contracts/test/strategies/compoundingStaking.js @@ -119,6 +119,74 @@ describe("Unit test: Compounding Staking Strategy", function () { expect(await compoundingStakingStrategy.firstDeposit()).to.equal(true); }); + it("allows a large 2030 ETH initial deposit to a compounding validator", async () => { + const { compoundingStakingStrategy, governor } = fixture; + const validator = testValidators[0]; + const pubKeyHash = hashPubKey(validator.publicKey); + + expect( + (await compoundingStakingStrategy.validator(pubKeyHash)).state + ).to.equal(0); + + await compoundingStakingStrategy + .connect(governor) + .setInitialDepositAmount(parseEther("2030")); + await depositToStrategy("2030"); + + const stakeTx = await stakeValidator({ validator, amount: "2030" }); + const receipt = await stakeTx.wait(); + const event = receipt.events.find((event) => event.event === "ETHStaked"); + + await expect(stakeTx) + .to.emit(compoundingStakingStrategy, "ETHStaked") + .withArgs( + pubKeyHash, + event.args.pendingDepositRoot, + validator.publicKey, + parseEther("2030") + ); + + const validatorData = await compoundingStakingStrategy.validator( + pubKeyHash + ); + expect(validatorData.state).to.equal(2); + expect(await compoundingStakingStrategy.firstDeposit()).to.equal(true); + }); + + it("allows a 32.25 ETH initial deposit when initialDepositAmount is set to 2040 ETH", async () => { + const { compoundingStakingStrategy, governor } = fixture; + const validator = testValidators[0]; + const pubKeyHash = hashPubKey(validator.publicKey); + + expect( + (await compoundingStakingStrategy.validator(pubKeyHash)).state + ).to.equal(0); + + await compoundingStakingStrategy + .connect(governor) + .setInitialDepositAmount(parseEther("2040")); + await depositToStrategy("32.25"); + + const stakeTx = await stakeValidator({ validator, amount: "32.25" }); + const receipt = await stakeTx.wait(); + const event = receipt.events.find((event) => event.event === "ETHStaked"); + + await expect(stakeTx) + .to.emit(compoundingStakingStrategy, "ETHStaked") + .withArgs( + pubKeyHash, + event.args.pendingDepositRoot, + validator.publicKey, + parseEther("32.25") + ); + + const validatorData = await compoundingStakingStrategy.validator( + pubKeyHash + ); + expect(validatorData.state).to.equal(2); + expect(await compoundingStakingStrategy.firstDeposit()).to.equal(true); + }); + it("does not allow a follow-up deposit before the validator is verified or active", async () => { const validator = testValidators[0]; From 398eea837626b70c02f28fd7ac5026f1261c2642 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 2 Jun 2026 15:05:36 +1000 Subject: [PATCH 17/29] Allow resetFirstDeposit to be called by the Strategist --- .../CompoundingStakingStrategy.sol | 2 +- .../test/strategies/compoundingStaking.js | 40 +++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/contracts/contracts/strategies/NativeStaking/CompoundingStakingStrategy.sol b/contracts/contracts/strategies/NativeStaking/CompoundingStakingStrategy.sol index 01d3b06889..2a004cee78 100644 --- a/contracts/contracts/strategies/NativeStaking/CompoundingStakingStrategy.sol +++ b/contracts/contracts/strategies/NativeStaking/CompoundingStakingStrategy.sol @@ -315,7 +315,7 @@ contract CompoundingStakingStrategy is } /// @notice Reset the `firstDeposit` flag to false so deposits to unverified validators can be made again. - function resetFirstDeposit() external onlyGovernor { + function resetFirstDeposit() external onlyGovernorOrStrategist { require(firstDeposit, "No first deposit"); firstDeposit = false; diff --git a/contracts/test/strategies/compoundingStaking.js b/contracts/test/strategies/compoundingStaking.js index 75018bc25b..892c688f40 100644 --- a/contracts/test/strategies/compoundingStaking.js +++ b/contracts/test/strategies/compoundingStaking.js @@ -206,4 +206,44 @@ describe("Unit test: Compounding Staking Strategy", function () { stakeValidator({ validator: testValidators[1] }) ).to.be.revertedWith("Existing first deposit"); }); + + it("allows the governor to reset the first deposit flag", async () => { + const { compoundingStakingStrategy, governor } = fixture; + + await depositToStrategy("1"); + await stakeValidator({ validator: testValidators[0] }); + + await expect( + compoundingStakingStrategy.connect(governor).resetFirstDeposit() + ).to.emit(compoundingStakingStrategy, "FirstDepositReset"); + + expect(await compoundingStakingStrategy.firstDeposit()).to.equal(false); + }); + + it("allows the strategist to reset the first deposit flag", async () => { + const { compoundingStakingStrategy, oethVault } = fixture; + const strategist = await impersonateAndFund( + await oethVault.strategistAddr() + ); + + await depositToStrategy("1"); + await stakeValidator({ validator: testValidators[0] }); + + await expect( + compoundingStakingStrategy.connect(strategist).resetFirstDeposit() + ).to.emit(compoundingStakingStrategy, "FirstDepositReset"); + + expect(await compoundingStakingStrategy.firstDeposit()).to.equal(false); + }); + + it("does not allow a regular user to reset the first deposit flag", async () => { + const { compoundingStakingStrategy, josh } = fixture; + + await depositToStrategy("1"); + await stakeValidator({ validator: testValidators[0] }); + + await expect( + compoundingStakingStrategy.connect(josh).resetFirstDeposit() + ).to.be.revertedWith("Caller is not the Strategist or Governor"); + }); }); From 41b2ab0b84ed0b3e3ca79a7133685873c6db6b72 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 2 Jun 2026 22:24:20 +1000 Subject: [PATCH 18/29] Got CompoundingStakingSSVStrategy undersize --- .../CompoundingStakingSSVStrategy.sol | 4 +- .../CompoundingStakingStrategy.sol | 47 ++++++++++++------- .../test/strategies/compoundingSSVStaking.js | 31 ++++++------ 3 files changed, 49 insertions(+), 33 deletions(-) diff --git a/contracts/contracts/strategies/NativeStaking/CompoundingStakingSSVStrategy.sol b/contracts/contracts/strategies/NativeStaking/CompoundingStakingSSVStrategy.sol index eb1d72ad9e..ba132fa879 100644 --- a/contracts/contracts/strategies/NativeStaking/CompoundingStakingSSVStrategy.sol +++ b/contracts/contracts/strategies/NativeStaking/CompoundingStakingSSVStrategy.sol @@ -14,8 +14,8 @@ contract CompoundingStakingSSVStrategy is CompoundingStakingStrategy { // For future use uint256[50] private __gap; - error CannotRemoveSsvValidator(); - error NotRegisteredOrVerified(); + error CannotRemoveSsvValidator(); // 0x2c45bd75 + error NotRegisteredOrVerified(); // 0x99088a6b event SSVValidatorRemoved(bytes32 indexed pubKeyHash, uint64[] operatorIds); diff --git a/contracts/contracts/strategies/NativeStaking/CompoundingStakingStrategy.sol b/contracts/contracts/strategies/NativeStaking/CompoundingStakingStrategy.sol index 2a004cee78..b6d513097e 100644 --- a/contracts/contracts/strategies/NativeStaking/CompoundingStakingStrategy.sol +++ b/contracts/contracts/strategies/NativeStaking/CompoundingStakingStrategy.sol @@ -255,6 +255,13 @@ contract CompoundingStakingStrategy is uint256 ethBalance ); + error NotRegistratorOrGovernor(); // 0xbf454a2d + error NotRegistrator(); // 0x929df920 + error NoFirstDeposit(); // 0x30e60c37 + error InvalidFirstDepositAmount(); // 0x29829bdd + error UnsupportedAsset(); // 0x24a01144 + error UnsupportedFunction(); // 0xea1c702e + /// @dev Throws if called by any account other than the Registrator modifier onlyRegistrator() { _onlyRegistrator(); @@ -263,15 +270,16 @@ contract CompoundingStakingStrategy is /// @dev internal function used to reduce contract size function _onlyRegistrator() internal view { - require(msg.sender == validatorRegistrator, "Not Registrator"); + if (msg.sender != validatorRegistrator) { + revert NotRegistrator(); + } } /// @dev Throws if called by any account other than the Registrator or Governor modifier onlyRegistratorOrGovernor() { - require( - msg.sender == validatorRegistrator || isGovernor(), - "Not Registrator or Governor" - ); + if (msg.sender != validatorRegistrator && !isGovernor()) { + revert NotRegistratorOrGovernor(); + } _; } @@ -316,7 +324,9 @@ contract CompoundingStakingStrategy is /// @notice Reset the `firstDeposit` flag to false so deposits to unverified validators can be made again. function resetFirstDeposit() external onlyGovernorOrStrategist { - require(firstDeposit, "No first deposit"); + if (!firstDeposit) { + revert NoFirstDeposit(); + } firstDeposit = false; @@ -472,10 +482,9 @@ contract CompoundingStakingStrategy is // and the Governor calls `resetFirstDeposit` to set `firstDeposit` to false. require(!firstDeposit, "Existing first deposit"); // Limits the amount of ETH that can be at risk from a front-running deposit attack. - require( - depositAmountWei <= initialDepositAmountWei, - "Invalid first deposit amount" - ); + if (depositAmountWei > initialDepositAmountWei) { + revert InvalidFirstDepositAmount(); + } // Limits the number of validator balance proofs to verifyBalances require( verifiedValidators.length + 1 <= MAX_VERIFIED_VALIDATORS, @@ -1279,7 +1288,9 @@ contract CompoundingStakingStrategy is onlyVault nonReentrant { - require(_asset == WETH, "Unsupported asset"); + if (_asset != WETH) { + revert UnsupportedAsset(); + } require(_amount > 0, "Must deposit something"); // Account for the new WETH @@ -1312,7 +1323,9 @@ contract CompoundingStakingStrategy is address _asset, uint256 _amount ) external override nonReentrant { - require(_asset == WETH, "Unsupported asset"); + if (_asset != WETH) { + revert UnsupportedAsset(); + } require( msg.sender == vaultAddress || msg.sender == validatorRegistrator, "Caller not Vault or Registrator" @@ -1364,7 +1377,9 @@ contract CompoundingStakingStrategy is override returns (uint256 balance) { - require(_asset == WETH, "Unsupported asset"); + if (_asset != WETH) { + revert UnsupportedAsset(); + } // Load the last verified balance from the storage // and add to the latest WETH balance of this strategy. @@ -1391,12 +1406,12 @@ contract CompoundingStakingStrategy is /// @notice is not supported for this strategy as there is no platform token. function setPTokenAddress(address, address) external pure override { - revert("Unsupported function"); + revert UnsupportedFunction(); } /// @notice is not supported for this strategy as there is no platform token. function removePToken(uint256) external pure override { - revert("Unsupported function"); + revert UnsupportedFunction(); } /// @dev This strategy does not use a platform token like the old Aave and Compound strategies. @@ -1410,6 +1425,6 @@ contract CompoundingStakingStrategy is /// Besides, ETH rewards are not sent to the Dripper any more. The Vault can now regulate /// the increase in assets. function _collectRewardTokens() internal pure override { - revert("Unsupported function"); + revert UnsupportedFunction(); } } diff --git a/contracts/test/strategies/compoundingSSVStaking.js b/contracts/test/strategies/compoundingSSVStaking.js index 70c7516872..90c8922ca4 100644 --- a/contracts/test/strategies/compoundingSSVStaking.js +++ b/contracts/test/strategies/compoundingSSVStaking.js @@ -234,7 +234,9 @@ describe("Unit test: Compounding SSV Staking Strategy", function () { const collectRewards = compoundingStakingSSVStrategy .connect(governor) .collectRewardTokens(); - await expect(collectRewards).to.revertedWith("Unsupported function"); + await expect(collectRewards).to.be.revertedWithCustomError( + "UnsupportedFunction()" + ); }); it("Should not set platform token", async () => { const { compoundingStakingSSVStrategy, governor, weth } = fixture; @@ -243,7 +245,7 @@ describe("Unit test: Compounding SSV Staking Strategy", function () { .connect(governor) .setPTokenAddress(weth.address, weth.address); - await expect(tx).to.revertedWith("Unsupported function"); + await expect(tx).to.be.revertedWithCustomError("UnsupportedFunction()"); }); it("Should not remove platform token", async () => { const { compoundingStakingSSVStrategy, governor } = fixture; @@ -252,24 +254,21 @@ describe("Unit test: Compounding SSV Staking Strategy", function () { .connect(governor) .removePToken(0); - await expect(tx).to.revertedWith("Unsupported function"); + await expect(tx).to.be.revertedWithCustomError("UnsupportedFunction()"); }); - it("Non governor should not be able to reset the first deposit flag", async () => { - const { compoundingStakingSSVStrategy, strategist, josh } = fixture; + it("Regular user should not be able to reset the first deposit flag", async () => { + const { compoundingStakingSSVStrategy, josh } = fixture; - const signers = [strategist, josh]; - for (const signer of signers) { - await expect( - compoundingStakingSSVStrategy.connect(signer).resetFirstDeposit() - ).to.be.revertedWith("Caller is not the Governor"); - } + await expect( + compoundingStakingSSVStrategy.connect(josh).resetFirstDeposit() + ).to.be.revertedWith("Caller is not the Strategist or Governor"); }); it("Should revert reset of first deposit if there is no first deposit", async () => { const { compoundingStakingSSVStrategy, governor } = fixture; await expect( compoundingStakingSSVStrategy.connect(governor).resetFirstDeposit() - ).to.be.revertedWith("No first deposit"); + ).to.be.revertedWithCustomError("NoFirstDeposit()"); }); it("Registrator or governor should be the only ones to pause the strategy", async () => { const { @@ -286,7 +285,7 @@ describe("Unit test: Compounding SSV Staking Strategy", function () { await expect( compoundingStakingSSVStrategy.connect(josh).pause() - ).to.be.revertedWith("Not Registrator or Governor"); + ).to.be.revertedWithCustomError("NotRegistratorOrGovernor()"); }); }); @@ -999,7 +998,9 @@ describe("Unit test: Compounding SSV Staking Strategy", function () { parseUnits("32", 9) ); - await expect(stakeTx).to.be.revertedWith("Invalid first deposit amount"); + await expect(stakeTx).to.be.revertedWithCustomError( + "InvalidFirstDepositAmount()" + ); }); it("Should revert registerSsvValidator when contract paused", async () => { @@ -2028,7 +2029,7 @@ describe("Unit test: Compounding SSV Staking Strategy", function () { compoundingStakingSSVStrategy .connect(sVault) .withdraw(josh.address, josh.address, parseEther("10")) - ).to.be.revertedWith("Unsupported asset"); + ).to.be.revertedWithCustomError("UnsupportedAsset()"); }); it("Should revert when withdrawing 0 ETH from the strategy", async () => { From dd4a4f98938c0d4bf4a4b6829c6ccc084f84d352 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Wed, 3 Jun 2026 11:53:52 +1000 Subject: [PATCH 19/29] Fix stakeValidator Hardhat task for initial deposits --- contracts/tasks/validatorCompound.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/contracts/tasks/validatorCompound.js b/contracts/tasks/validatorCompound.js index ad8995612d..f86b5ad6f9 100644 --- a/contracts/tasks/validatorCompound.js +++ b/contracts/tasks/validatorCompound.js @@ -11,6 +11,7 @@ const { calcSlot, getValidatorBalance, getBeaconBlock, + hashPubKey, } = require("../utils/beacon"); const { getNetworkName } = require("../utils/hardhat-helpers"); const { getSigner } = require("../utils/signers"); @@ -33,6 +34,8 @@ const { const log = require("../utils/logger")("task:validator:compounding"); +const VALIDATOR_STATE_REGISTERED = 1; + async function snapBalances({ consol = false }) { const signer = await getSigner(); @@ -182,11 +185,18 @@ async function stakeValidator({ const amountWei = parseUnits(amount.toString(), 18); const initialDepositAmountWei = await strategy.initialDepositAmountWei(); + const validator = await strategy.validator(hashPubKey(pubkey)); + const isCreatingDeposit = BigNumber.from(validator.state).eq( + VALIDATOR_STATE_REGISTERED + ); - if (amountWei.eq(initialDepositAmountWei)) { + if (isCreatingDeposit) { if (!sig) { throw new Error( - `The signature is required for the first deposit of ${formatUnits( + `The signature is required for the first deposit to a registered validator. Deposit amount: ${formatUnits( + amountWei, + 18 + )} ETH, initial deposit cap: ${formatUnits( initialDepositAmountWei, 18 )} ETH` From 957631c837a7d0c6f1325b411bca8be68f998014 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Wed, 3 Jun 2026 12:06:01 +1000 Subject: [PATCH 20/29] Set default first-deposit cap after SSV staking strategy upgrades --- contracts/deploy/deployActions.js | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/contracts/deploy/deployActions.js b/contracts/deploy/deployActions.js index 642ef74f06..8e8bbf4cff 100644 --- a/contracts/deploy/deployActions.js +++ b/contracts/deploy/deployActions.js @@ -237,6 +237,30 @@ const upgradeCompoundingStakingSSVStrategy = async () => { strategyProxy.connect(sDeployer).upgradeTo(dStrategyImpl.address) ); + const cStrategy = await ethers.getContractAt( + "CompoundingStakingSSVStrategy", + strategyProxy.address + ); + const initialDepositAmountWei = await cStrategy.initialDepositAmountWei(); + if (initialDepositAmountWei.eq(0)) { + const defaultInitialDepositAmount = ethers.utils.parseEther("1"); + + console.log( + `Setting default initial deposit amount to ${defaultInitialDepositAmount.toString()} wei` + ); + await withConfirmation( + cStrategy + .connect(sDeployer) + .setInitialDepositAmount(defaultInitialDepositAmount) + ); + } + + const updatedInitialDepositAmountWei = + await cStrategy.initialDepositAmountWei(); + if (updatedInitialDepositAmountWei.eq(0)) { + throw new Error("initialDepositAmountWei must be set after upgrade"); + } + console.log( `Upgraded CompoundingStakingSSVStrategyProxy to implementation at ${dStrategyImpl.address}` ); From 08c2f4df6af0b94f44c9b123a8c5ae8027daeb3a Mon Sep 17 00:00:00 2001 From: Nick Addison Date: Tue, 9 Jun 2026 19:06:19 +1000 Subject: [PATCH 21/29] Fix removeSsvValidator to work with Clusters with ETH payments (#2908) --- contracts/utils/ssv.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/contracts/utils/ssv.js b/contracts/utils/ssv.js index 9594038ea3..c189496b64 100644 --- a/contracts/utils/ssv.js +++ b/contracts/utils/ssv.js @@ -15,6 +15,17 @@ const emptyCluster = { balance: 0, }; +const normalizeCluster = (cluster) => ({ + validatorCount: cluster.validatorCount, + networkFeeIndex: cluster.networkFeeIndex, + index: cluster.index, + active: cluster.active, + balance: + cluster.migrated && cluster.ethBalance !== undefined + ? cluster.ethBalance + : cluster.balance, +}); + const splitValidatorKey = async ({ keystorelocation, keystorepass, @@ -130,7 +141,7 @@ const getClusterInfo = async ({ ownerAddress, operatorids, chainId }) => { return { block: response.data.cluster.blockNumber, - cluster: response.data.cluster, + cluster: normalizeCluster(response.data.cluster), }; } catch (err) { if (err.response) { @@ -196,6 +207,7 @@ module.exports = { printClusterInfo, getClusterInfo, getClusterNonce, + normalizeCluster, sortOperatorIds, splitOperatorIds, splitValidatorKey, From 649afd57c38ed7c5e90246dd44e11c57c9a3c64c Mon Sep 17 00:00:00 2001 From: Nick Addison Date: Tue, 9 Jun 2026 19:19:07 +1000 Subject: [PATCH 22/29] Allow old Compounding Staking Strategy SSV validators to be removed (#2914) * All removeSsvValidator to be called for the Compounding Staking SSV Strategy on the ConsolidationController * Split withdrawSsvClusterEth and removeStrategy into separate gov prop so the 10 validators can be removed post upgrade --- .../NativeStaking/ConsolidationController.sol | 22 ++- ...196_deploy_compounding_staking_strategy.js | 53 +----- ...197_remove_old_compounding_ssv_strategy.js | 178 ++++++++++++++++++ contracts/tasks/tasks.js | 6 + contracts/tasks/validatorCompound.js | 21 ++- 5 files changed, 222 insertions(+), 58 deletions(-) create mode 100644 contracts/deploy/mainnet/197_remove_old_compounding_ssv_strategy.js diff --git a/contracts/contracts/strategies/NativeStaking/ConsolidationController.sol b/contracts/contracts/strategies/NativeStaking/ConsolidationController.sol index a8acad2a43..f97785300b 100644 --- a/contracts/contracts/strategies/NativeStaking/ConsolidationController.sol +++ b/contracts/contracts/strategies/NativeStaking/ConsolidationController.sol @@ -26,6 +26,8 @@ contract ConsolidationController is Ownable { address public immutable validatorRegistrator; /// @dev The old Native Staking Strategy connected to the second SSV cluster address internal immutable nativeStakingStrategy2; + /// @dev The old Compounding Staking SSV Strategy connected to the SSV cluster being removed + address internal immutable compoundingStakingSsvStrategy; /// @dev The new Compounding Staking Strategy CompoundingStakingStrategy internal immutable targetStrategy; @@ -56,12 +58,14 @@ contract ConsolidationController is Ownable { address _owner, address _validatorRegistrator, address _nativeStakingStrategy2, + address _compoundingStakingSsvStrategy, address _targetStrategy ) { _transferOwnership(_owner); validatorRegistrator = _validatorRegistrator; nativeStakingStrategy2 = _nativeStakingStrategy2; + compoundingStakingSsvStrategy = _compoundingStakingSsvStrategy; targetStrategy = CompoundingStakingStrategy(payable(_targetStrategy)); } @@ -332,11 +336,13 @@ contract ConsolidationController is Ownable { } /** - * @notice Removing source validators is not allowed during the consolidation process - * as consolidated validators will be in EXITING state hence can not be consolidated after removal. + * @notice Removes a validator from an old staking strategy's SSV cluster. + * @dev Removing validators from the native source strategy is not allowed while + * that strategy is being consolidated, as consolidated validators are in EXITING + * state and can not be consolidated after removal. * Only callable by the validator registrator. - * @param _sourceStrategy The address of the old Native Staking Strategy - * @param publicKey The public key of the validator to remove which must have EXITING or REGISTERED state. + * @param _sourceStrategy The address of the old Native Staking Strategy or old Compounding Staking SSV Strategy + * @param publicKey The public key of the validator to remove * @param operatorIds The operator IDs for the source SSV cluster * @param cluster The SSV cluster information for the source validator */ @@ -346,8 +352,12 @@ contract ConsolidationController is Ownable { uint64[] calldata operatorIds, Cluster calldata cluster ) external onlyRegistrator { - // Check sourceStrategy is a valid old Native Staking Strategy - _checkSourceStrategy(_sourceStrategy); + // Check sourceStrategy is a valid old staking strategy + require( + _sourceStrategy == nativeStakingStrategy2 || + _sourceStrategy == compoundingStakingSsvStrategy, + "Invalid source strategy" + ); // Prevent removing a validator from the SSV cluster before the consolidation // process has been completed for the source strategy being consolidated. // This prevents validators that have been exited rather than consolidated but that's ok. diff --git a/contracts/deploy/mainnet/196_deploy_compounding_staking_strategy.js b/contracts/deploy/mainnet/196_deploy_compounding_staking_strategy.js index aab688fed9..76e10052e9 100644 --- a/contracts/deploy/mainnet/196_deploy_compounding_staking_strategy.js +++ b/contracts/deploy/mainnet/196_deploy_compounding_staking_strategy.js @@ -1,10 +1,6 @@ const addresses = require("../../utils/addresses"); -const { isFork } = require("../../test/helpers"); const { beaconChainGenesisTimeMainnet } = require("../../utils/constants"); const { deploymentWithGovernanceProposal } = require("../../utils/deploy"); -const { getClusterInfo, splitOperatorIds } = require("../../utils/ssv"); - -const compoundingSsvClusterOperatorIds = "2070,2071,2072,2073"; module.exports = deploymentWithGovernanceProposal( { @@ -15,7 +11,6 @@ module.exports = deploymentWithGovernanceProposal( }, async ({ deployWithConfirmation, ethers, withConfirmation }) => { const { deployerAddr } = await getNamedAccounts(); - const { chainId } = await ethers.provider.getNetwork(); const sDeployer = await ethers.provider.getSigner(deployerAddr); const cOETHVaultProxy = await ethers.getContract("OETHVaultProxy"); @@ -41,24 +36,6 @@ module.exports = deploymentWithGovernanceProposal( "NativeStakingSSVStrategy", cNativeStakingStrategy2Proxy.address ); - const compoundingOperatorIds = splitOperatorIds( - compoundingSsvClusterOperatorIds - ); - const { cluster: compoundingSsvCluster } = await getClusterInfo({ - chainId, - operatorids: compoundingOperatorIds.join(","), - ownerAddress: cCompoundingStakingSSVStrategyProxy.address, - }); - const compoundingSsvClusterEthBalance = ethers.BigNumber.from( - compoundingSsvCluster.ethBalance || 0 - ); - const compoundingSsvClusterForWithdraw = { - validatorCount: compoundingSsvCluster.validatorCount, - networkFeeIndex: compoundingSsvCluster.networkFeeIndex, - index: compoundingSsvCluster.index, - active: compoundingSsvCluster.active, - balance: compoundingSsvClusterEthBalance, - }; console.log("Deploy CompoundingStakingSSVStrategy implementation"); const dCompoundingStakingSSVStrategy = await deployWithConfirmation( @@ -154,19 +131,22 @@ module.exports = deploymentWithGovernanceProposal( addresses.mainnet.Guardian, // Admin 5/8 multisig addresses.mainnet.talosRelayer, // New Talos Relayer cNativeStakingStrategy2.address, // Old Native Staking Strategy 2 + cCompoundingStakingSSVStrategy.address, // Old Compounding Staking SSV Strategy cStrategy.address, // New Compounding Staking Strategy ] ); - const shouldWithdrawCompoundingSsvCluster = - !isFork || Number(compoundingSsvCluster.validatorCount) === 0; - const actions = [ { contract: cCompoundingStakingSSVStrategyProxy, signature: "upgradeTo(address)", args: [dCompoundingStakingSSVStrategy.address], }, + { + contract: cCompoundingStakingSSVStrategy, + signature: "setRegistrator(address)", + args: [dConsolidationController.address], + }, { contract: cNativeStakingStrategy2Proxy, signature: "upgradeTo(address)", @@ -194,27 +174,6 @@ module.exports = deploymentWithGovernanceProposal( }, ]; - // This can be simplified once the compounding SSV cluster has been fully exited and withdrawn, - // but for now we need to withdraw the cluster from the old strategy and remove the old strategy - // from the vault within the same proposal to ensure the safety of users' funds. - if (shouldWithdrawCompoundingSsvCluster) { - actions.splice(1, 0, { - contract: cCompoundingStakingSSVStrategy, - signature: - "withdrawSsvClusterEth(uint64[],uint256,(uint32,uint64,uint64,bool,uint256))", - args: [ - compoundingOperatorIds, - compoundingSsvClusterEthBalance, - compoundingSsvClusterForWithdraw, - ], - }); - actions.splice(5, 0, { - contract: cOETHVault, - signature: "removeStrategy(address)", - args: [cCompoundingStakingSSVStrategyProxy.address], - }); - } - return { name: "Deploy new vanilla compounding staking strategy, upgrade SSV strategies and deploy consolidation controller", actions, diff --git a/contracts/deploy/mainnet/197_remove_old_compounding_ssv_strategy.js b/contracts/deploy/mainnet/197_remove_old_compounding_ssv_strategy.js new file mode 100644 index 0000000000..75816ac10b --- /dev/null +++ b/contracts/deploy/mainnet/197_remove_old_compounding_ssv_strategy.js @@ -0,0 +1,178 @@ +const addresses = require("../../utils/addresses"); +const { deploymentWithGovernanceProposal } = require("../../utils/deploy"); +const { impersonateAndFund } = require("../../utils/signers"); +const { + getClusterInfo, + normalizeCluster, + splitOperatorIds, +} = require("../../utils/ssv"); +const { isFork } = require("../../test/helpers"); +const { hashPubKey } = require("../../utils/beacon"); +const { + getStorageAt, + setStorageAt, +} = require("@nomicfoundation/hardhat-network-helpers"); +const { BigNumber } = require("ethers"); +const { keccak256, solidityPack, hexZeroPad } = require("ethers/lib/utils"); + +const compoundingSsvClusterOperatorIds = "2070,2071,2072,2073"; +const validatorMappingSlot = 55; +const exitedValidatorState = 6; +const compoundingSsvValidatorPubkeys = [ + "0x8b5f08ce7af02245ae664a96fde5af1585edaa257f852490e5bb1957f1b3433b9b213577a3e475d6b034618f641f3bee", + "0x8a29191751a94a7eb921cd832972a875376b629bd3978c92ab3daaf596cab5e3cd1877cf63e65fe59ccc769c3b77a607", + "0xb5d37226e27e0ab066541ccb795e04149300bb8c0b0fd528785f6a940e94c624b65ef1eb771f78a5f2685317b7e6f34f", + "0x87b76ce8ea170a8a6db6842848eca2f3117367ada43120401a7f3095498a910a1455352bd12d15f5a07693f61e5b8c37", + "0x8427639adf9c746f7d7271ddee3bbcd7a1f3b4beb3bd67224c345d7c7e7cffd58d61d5bc84a3ab7d0f909ebf71da7b8b", + "0x84ef4399aa33bbea588965cad4f1df99a2586ef2791cc2527677f1f10a922996ad9b6cd7c8287ca215dc7dffb2e7946d", + "0x9695233248996e2d288baef676ee03ef30467eba161258894abeb382fa89ed7381dac05745a5c95df456533ef8a5fdad", + "0x9226889b28bee5478d0039a86bb913b645769ec3af18f08cc93ab46421fe8b3493e7e13b381682cf48fd3d5fa67c2f08", + "0x8f52f57132e409e749f0fc8305c4e2784c33abf43f80f1cc329b06ca94f7c50638b47a350b03c2ef4cc72860fee29730", + "0xa4258aa50aba9d7441f734213ae76fad9809572a593765c25c25d7afd42b83baba06397bd9e264a9fa24c3327a308682", +]; + +const setForkValidatorExited = async (strategy, pubkey) => { + const validatorSlot = keccak256( + solidityPack( + ["bytes32", "uint256"], + [hashPubKey(pubkey), validatorMappingSlot] + ) + ); + const existingValue = BigNumber.from( + await getStorageAt(strategy.address, validatorSlot) + ); + const exitedValue = existingValue + .and(BigNumber.from(2).pow(256).sub(256)) + .or(exitedValidatorState); + + await setStorageAt( + strategy.address, + validatorSlot, + hexZeroPad(exitedValue.toHexString(), 32) + ); +}; + +const parseRemovedCluster = (ssvNetwork, receipt) => { + for (const log of receipt.logs) { + if (log.address.toLowerCase() !== ssvNetwork.address.toLowerCase()) { + continue; + } + + try { + const parsed = ssvNetwork.interface.parseLog(log); + if (parsed.name === "ValidatorRemoved") { + return parsed.args.cluster; + } + } catch (err) { + // Ignore logs from other SSV events. + } + } + + throw new Error("ValidatorRemoved event not found"); +}; + +module.exports = deploymentWithGovernanceProposal( + { + deployName: "197_remove_old_compounding_ssv_strategy", + forceDeploy: false, + reduceQueueTime: true, + deployerIsProposer: false, + proposalId: "", + }, + async ({ ethers }) => { + const { chainId } = await ethers.provider.getNetwork(); + + const cOETHVaultProxy = await ethers.getContract("OETHVaultProxy"); + const cOETHVault = await ethers.getContractAt( + "IVault", + cOETHVaultProxy.address + ); + const cCompoundingStakingSSVStrategyProxy = await ethers.getContract( + "CompoundingStakingSSVStrategyProxy" + ); + const cCompoundingStakingSSVStrategy = await ethers.getContractAt( + "CompoundingStakingSSVStrategy", + cCompoundingStakingSSVStrategyProxy.address + ); + + const compoundingOperatorIds = splitOperatorIds( + compoundingSsvClusterOperatorIds + ); + let { cluster: compoundingSsvCluster } = await getClusterInfo({ + chainId, + operatorids: compoundingOperatorIds.join(","), + ownerAddress: cCompoundingStakingSSVStrategyProxy.address, + }); + + if (isFork && Number(compoundingSsvCluster.validatorCount) !== 0) { + const cConsolidationController = await ethers.getContract( + "ConsolidationController" + ); + const cSsvNetwork = await ethers.getContractAt( + "ISSVNetwork", + addresses.mainnet.SSVNetwork + ); + const sValidatorRegistrator = await impersonateAndFund( + addresses.mainnet.talosRelayer + ); + + console.log( + `Removing ${compoundingSsvValidatorPubkeys.length} validators from old compounding SSV cluster on fork` + ); + + for (const pubkey of compoundingSsvValidatorPubkeys) { + await setForkValidatorExited(cCompoundingStakingSSVStrategy, pubkey); + + const tx = await cConsolidationController + .connect(sValidatorRegistrator) + .removeSsvValidator( + cCompoundingStakingSSVStrategy.address, + pubkey, + compoundingOperatorIds, + compoundingSsvCluster + ); + const receipt = await tx.wait(); + compoundingSsvCluster = normalizeCluster( + parseRemovedCluster(cSsvNetwork, receipt) + ); + } + } + + if (Number(compoundingSsvCluster.validatorCount) !== 0) { + throw new Error( + `Compounding SSV cluster still has ${compoundingSsvCluster.validatorCount} validators` + ); + } + + const compoundingSsvClusterEthBalance = ethers.BigNumber.from( + compoundingSsvCluster.balance + ); + + console.log( + `Withdrawing ${ethers.utils.formatEther( + compoundingSsvClusterEthBalance + )} ETH from the old compounding SSV cluster` + ); + + return { + name: "Withdraw old compounding SSV cluster ETH and remove strategy from OETH Vault", + actions: [ + { + contract: cCompoundingStakingSSVStrategy, + signature: + "withdrawSsvClusterEth(uint64[],uint256,(uint32,uint64,uint64,bool,uint256))", + args: [ + compoundingOperatorIds, + compoundingSsvClusterEthBalance, + compoundingSsvCluster, + ], + }, + { + contract: cOETHVault, + signature: "removeStrategy(address)", + args: [cCompoundingStakingSSVStrategyProxy.address], + }, + ], + }; + } +); diff --git a/contracts/tasks/tasks.js b/contracts/tasks/tasks.js index af66480577..1961bd26de 100644 --- a/contracts/tasks/tasks.js +++ b/contracts/tasks/tasks.js @@ -2452,6 +2452,12 @@ subtask( undefined, types.string ) + .addOptionalParam( + "consol", + "Call the consolidation controller instead of the strategy", + false, + types.boolean + ) .setAction(async (taskArgs) => { const signer = await getSigner(); await removeValidator({ ...taskArgs, signer }); diff --git a/contracts/tasks/validatorCompound.js b/contracts/tasks/validatorCompound.js index f86b5ad6f9..dbde601040 100644 --- a/contracts/tasks/validatorCompound.js +++ b/contracts/tasks/validatorCompound.js @@ -890,7 +890,7 @@ async function setRegistrator({ account, type }) { await logTxDetails(tx, "setRegistrator"); } -async function removeValidator({ pubkey, operatorids }) { +async function removeValidator({ pubkey, operatorids, consol = false }) { const signer = await getSigner(); log(`Splitting operator IDs ${operatorids}`); @@ -900,6 +900,9 @@ async function removeValidator({ pubkey, operatorids }) { "CompoundingStakingSSVStrategyProxy", "CompoundingStakingSSVStrategy" ); + const contract = consol + ? await resolveContract("ConsolidationController") + : strategy; // Cluster details const { chainId } = await ethers.provider.getNetwork(); @@ -909,10 +912,18 @@ async function removeValidator({ pubkey, operatorids }) { ownerAddress: strategy.address, }); - log(`About to remove compounding validator with pubkey ${pubkey}`); - const tx = await strategy - .connect(signer) - .removeSsvValidator(pubkey, operatorIds, cluster); + log( + `About to remove compounding validator with pubkey ${pubkey} via ${ + consol ? "ConsolidationController" : "CompoundingStakingSSVStrategy" + }` + ); + const tx = consol + ? await contract + .connect(signer) + .removeSsvValidator(strategy.address, pubkey, operatorIds, cluster) + : await contract + .connect(signer) + .removeSsvValidator(pubkey, operatorIds, cluster); await logTxDetails(tx, "removeSsvValidator"); } From c523ee2a56f09abaeb0a2235acb8029b6e05e077 Mon Sep 17 00:00:00 2001 From: Nick Addison Date: Tue, 9 Jun 2026 19:31:36 +1000 Subject: [PATCH 23/29] Updated ValidatorState.NON_REGISTERED comment (#2916) --- .../strategies/NativeStaking/CompoundingStakingStrategy.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/contracts/strategies/NativeStaking/CompoundingStakingStrategy.sol b/contracts/contracts/strategies/NativeStaking/CompoundingStakingStrategy.sol index b6d513097e..19c54188fc 100644 --- a/contracts/contracts/strategies/NativeStaking/CompoundingStakingStrategy.sol +++ b/contracts/contracts/strategies/NativeStaking/CompoundingStakingStrategy.sol @@ -116,7 +116,7 @@ abstract contract CompoundingValidatorStorage is Governable, Pausable { bytes32[] public depositList; enum ValidatorState { - NON_REGISTERED, // validator has not received its first deposit + NON_REGISTERED, // validator has not staked its first deposit REGISTERED, // validator has been registered by an extension contract STAKED, // validator has funds staked VERIFIED, // validator has been verified to exist on the beacon chain From 49609a2e96ccf2bce2b512c767ec24c8b6e3af89 Mon Sep 17 00:00:00 2001 From: Nick Addison Date: Tue, 9 Jun 2026 19:44:36 +1000 Subject: [PATCH 24/29] Added back NON_REGISTERED check on registerSsvValidator (#2915) * All removeSsvValidator to be called for the Compounding Staking SSV Strategy on the ConsolidationController * Split withdrawSsvClusterEth and removeStrategy into separate gov prop so the 10 validators can be removed post upgrade * Added back NON_REGISTERED check on registerSsvValidator --- .../CompoundingStakingSSVStrategy.sol | 4 ++ .../test/strategies/compoundingSSVStaking.js | 47 ++++++++++++++++++- 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/contracts/contracts/strategies/NativeStaking/CompoundingStakingSSVStrategy.sol b/contracts/contracts/strategies/NativeStaking/CompoundingStakingSSVStrategy.sol index ba132fa879..dd4b1b3675 100644 --- a/contracts/contracts/strategies/NativeStaking/CompoundingStakingSSVStrategy.sol +++ b/contracts/contracts/strategies/NativeStaking/CompoundingStakingSSVStrategy.sol @@ -68,6 +68,10 @@ contract CompoundingStakingSSVStrategy is CompoundingStakingStrategy { // Hash the public key using the Beacon Chain's format bytes32 pubKeyHash = _hashPubKey(publicKey); + if (validator[pubKeyHash].state != ValidatorState.NON_REGISTERED) { + revert NotRegisteredOrVerified(); + } + // Store the validator state as registered validator[pubKeyHash].state = ValidatorState.REGISTERED; diff --git a/contracts/test/strategies/compoundingSSVStaking.js b/contracts/test/strategies/compoundingSSVStaking.js index 90c8922ca4..4497eadfa1 100644 --- a/contracts/test/strategies/compoundingSSVStaking.js +++ b/contracts/test/strategies/compoundingSSVStaking.js @@ -1082,7 +1082,52 @@ describe("Unit test: Compounding SSV Staking Strategy", function () { emptyCluster, { value: ethUnits("2") } ) - ).to.be.reverted; + ).to.be.revertedWithCustomError("NotRegisteredOrVerified()"); + }); + + it("Should revert when re-registering a removed validator", async () => { + const { compoundingStakingSSVStrategy, validatorRegistrator } = fixture; + + const testValidator = testValidators[0]; + + // Register a new validator with the SSV Network + await compoundingStakingSSVStrategy + .connect(validatorRegistrator) + .registerSsvValidator( + testValidator.publicKey, + testValidator.operatorIds, + testValidator.sharesData, + emptyCluster, + { value: ethUnits("2") } + ); + + await compoundingStakingSSVStrategy + .connect(validatorRegistrator) + .removeSsvValidator( + testValidator.publicKey, + testValidator.operatorIds, + emptyCluster + ); + + expect( + ( + await compoundingStakingSSVStrategy.validator( + testValidator.publicKeyHash + ) + ).state + ).to.equal(7, "Validator state not 7 (REMOVED)"); + + await expect( + compoundingStakingSSVStrategy + .connect(validatorRegistrator) + .registerSsvValidator( + testValidator.publicKey, + testValidator.operatorIds, + testValidator.sharesData, + emptyCluster, + { value: ethUnits("2") } + ) + ).to.be.revertedWithCustomError("NotRegisteredOrVerified()"); }); it("Should revert when staking because of insufficient ETH balance", async () => { From a1c7d1408edab4a0acf6c7111bd6063795bd7664 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Tue, 9 Jun 2026 22:01:13 +0200 Subject: [PATCH 25/29] Migrate vault & cross-chain operators to the new Talos signer (+ unpause rebases) (#2911) * migrate contracts to Talso signer * fix fork tests * add the migration for the OGN rewards module * refactor deployment * add some comments * add base migration files * add remaining migrations for base and one for sonic * add hyperevm migration * remove the old relayer roles --- ...051_migrate_base_operators_to_talos_kms.js | 45 +++++ ..._migrate_base_merkl_module_to_talos_kms.js | 41 ++++ ...ant_base_claim_bribes_module1_talos_kms.js | 39 ++++ ...ant_base_claim_bribes_module3_talos_kms.js | 39 ++++ ...igrate_crosschain_strategy_to_talos_kms.js | 30 +++ .../196_migrate_operators_to_talos_kms.js | 73 +++++++ .../197_migrate_xogn_module6_to_talos_kms.js | 37 ++++ ...migrate_strategist_modules_to_talos_kms.js | 45 +++++ ...30_migrate_sonic_operators_to_talos_kms.js | 49 +++++ contracts/test/_fixture-hyperevm.js | 6 +- contracts/test/_fixture.js | 4 +- contracts/utils/addresses.js | 2 +- contracts/utils/deploy.js | 183 ++++++++++++++++++ 13 files changed, 590 insertions(+), 3 deletions(-) create mode 100644 contracts/deploy/base/051_migrate_base_operators_to_talos_kms.js create mode 100644 contracts/deploy/base/052_migrate_base_merkl_module_to_talos_kms.js create mode 100644 contracts/deploy/base/053_grant_base_claim_bribes_module1_talos_kms.js create mode 100644 contracts/deploy/base/054_grant_base_claim_bribes_module3_talos_kms.js create mode 100644 contracts/deploy/hyperevm/003_migrate_crosschain_strategy_to_talos_kms.js create mode 100644 contracts/deploy/mainnet/196_migrate_operators_to_talos_kms.js create mode 100644 contracts/deploy/mainnet/197_migrate_xogn_module6_to_talos_kms.js create mode 100644 contracts/deploy/mainnet/198_migrate_strategist_modules_to_talos_kms.js create mode 100644 contracts/deploy/sonic/030_migrate_sonic_operators_to_talos_kms.js diff --git a/contracts/deploy/base/051_migrate_base_operators_to_talos_kms.js b/contracts/deploy/base/051_migrate_base_operators_to_talos_kms.js new file mode 100644 index 0000000000..6a83594b35 --- /dev/null +++ b/contracts/deploy/base/051_migrate_base_operators_to_talos_kms.js @@ -0,0 +1,45 @@ +const { deployOnBase } = require("../../utils/deploy-l2"); +const addresses = require("../../utils/addresses"); + +// Executed by base.governor 5/8 -> Base Timelock (these setters are onlyGovernor). +// Re-points the Base CrossChainRemoteStrategy operator and the OETHBaseVault +// operatorAddr to the new Talos signer, and unpauses OETHb rebases so Talos can +// rebase the vault directly (operator-gated) — replacing the PermissionedRebaseModule. +module.exports = deployOnBase( + { + deployName: "051_migrate_base_operators_to_talos", + }, + async () => { + const cCrossChainRemoteStrategy = await ethers.getContractAt( + "CrossChainRemoteStrategy", + addresses.base.CrossChainRemoteStrategy + ); + + const cOETHbVaultProxy = await ethers.getContract("OETHBaseVaultProxy"); + const cOETHbVault = await ethers.getContractAt( + "IVault", + cOETHbVaultProxy.address + ); + + return { + name: "Migrate the Base CrossChainRemoteStrategy operator and OETHBaseVault operatorAddr to the new Talos signer, and unpause OETHb rebases.", + actions: [ + { + contract: cCrossChainRemoteStrategy, + signature: "setOperator(address)", + args: [addresses.talosRelayer], + }, + { + contract: cOETHbVault, + signature: "setOperatorAddr(address)", + args: [addresses.talosRelayer], + }, + { + contract: cOETHbVault, + signature: "unpauseRebase()", + args: [], + }, + ], + }; + } +); diff --git a/contracts/deploy/base/052_migrate_base_merkl_module_to_talos_kms.js b/contracts/deploy/base/052_migrate_base_merkl_module_to_talos_kms.js new file mode 100644 index 0000000000..6d4e327413 --- /dev/null +++ b/contracts/deploy/base/052_migrate_base_merkl_module_to_talos_kms.js @@ -0,0 +1,41 @@ +const addresses = require("../../utils/addresses"); +const { deploymentWithGnosisSafe } = require("../../utils/deploy"); + +// The Base MerklPoolBoosterBribesModule is admined by the multichainStrategist +// 2/8 Safe (its DEFAULT_ADMIN_ROLE holder), so granting OPERATOR_ROLE to the new +// Talos signer is a plain Safe transaction +module.exports = deploymentWithGnosisSafe( + { + deployName: "052_migrate_base_merkl_module_to_talos", + safe: addresses.multichainStrategist, + network: "base", + forceDeploy: false, + }, + async () => { + const cMerklModule = await ethers.getContractAt( + "MerklPoolBoosterBribesModule", + addresses.base.MerklPoolBoosterBribesModule + ); + + const OPERATOR_ROLE = ethers.utils.keccak256( + ethers.utils.toUtf8Bytes("OPERATOR_ROLE") + ); + + return { + name: "Grant the OPERATOR_ROLE of the Base MerklPoolBoosterBribesModule to the new Talos signer, and revoke it from the old relayer.", + actions: [ + { + contract: cMerklModule, + signature: "grantRole(bytes32,address)", + args: [OPERATOR_ROLE, addresses.talosRelayer], + }, + // grantRole is additive — the old relayer keeps OPERATOR_ROLE, so revoke it. + { + contract: cMerklModule, + signature: "revokeRole(bytes32,address)", + args: [OPERATOR_ROLE, addresses.base.OZRelayerAddress], + }, + ], + }; + } +); diff --git a/contracts/deploy/base/053_grant_base_claim_bribes_module1_talos_kms.js b/contracts/deploy/base/053_grant_base_claim_bribes_module1_talos_kms.js new file mode 100644 index 0000000000..66f42989ba --- /dev/null +++ b/contracts/deploy/base/053_grant_base_claim_bribes_module1_talos_kms.js @@ -0,0 +1,39 @@ +const addresses = require("../../utils/addresses"); +const { deploymentWithGnosisSafe } = require("../../utils/deploy"); + +// ClaimBribesSafeModule1 is admined by the ClaimBribes 2/8 Safe (its +// DEFAULT_ADMIN_ROLE holder and immutable safeContract()), so granting +// OPERATOR_ROLE to the new Talos signer is a plain Safe transaction. +module.exports = deploymentWithGnosisSafe( + { + deployName: "053_grant_base_claim_bribes_module1_talos", + network: "base", + forceDeploy: false, + }, + async () => { + const cModule = await ethers.getContract("ClaimBribesSafeModule1"); + const safe = await cModule.safeContract(); + + const OPERATOR_ROLE = ethers.utils.keccak256( + ethers.utils.toUtf8Bytes("OPERATOR_ROLE") + ); + + return { + safe, + name: "Grant the OPERATOR_ROLE of the Base ClaimBribesSafeModule1 to the new Talos signer, and revoke it from the old relayer.", + actions: [ + { + contract: cModule, + signature: "grantRole(bytes32,address)", + args: [OPERATOR_ROLE, addresses.talosRelayer], + }, + // grantRole is additive — the old relayer keeps OPERATOR_ROLE, so revoke it. + { + contract: cModule, + signature: "revokeRole(bytes32,address)", + args: [OPERATOR_ROLE, addresses.base.OZRelayerAddress], + }, + ], + }; + } +); diff --git a/contracts/deploy/base/054_grant_base_claim_bribes_module3_talos_kms.js b/contracts/deploy/base/054_grant_base_claim_bribes_module3_talos_kms.js new file mode 100644 index 0000000000..64dbb3f406 --- /dev/null +++ b/contracts/deploy/base/054_grant_base_claim_bribes_module3_talos_kms.js @@ -0,0 +1,39 @@ +const addresses = require("../../utils/addresses"); +const { deploymentWithGnosisSafe } = require("../../utils/deploy"); + +// ClaimBribesSafeModule3 is admined by the base.strategist 1/2 Safe (its +// DEFAULT_ADMIN_ROLE holder and immutable safeContract()), so granting +// OPERATOR_ROLE to the new Talos signer is a plain Safe transaction. +module.exports = deploymentWithGnosisSafe( + { + deployName: "054_grant_base_claim_bribes_module3_talos", + network: "base", + forceDeploy: false, + }, + async () => { + const cModule = await ethers.getContract("ClaimBribesSafeModule3"); + const safe = await cModule.safeContract(); + + const OPERATOR_ROLE = ethers.utils.keccak256( + ethers.utils.toUtf8Bytes("OPERATOR_ROLE") + ); + + return { + safe, + name: "Grant the OPERATOR_ROLE of the Base ClaimBribesSafeModule3 to the new Talos signer, and revoke it from the old relayer.", + actions: [ + { + contract: cModule, + signature: "grantRole(bytes32,address)", + args: [OPERATOR_ROLE, addresses.talosRelayer], + }, + // grantRole is additive — the old relayer keeps OPERATOR_ROLE, so revoke it. + { + contract: cModule, + signature: "revokeRole(bytes32,address)", + args: [OPERATOR_ROLE, addresses.base.OZRelayerAddress], + }, + ], + }; + } +); diff --git a/contracts/deploy/hyperevm/003_migrate_crosschain_strategy_to_talos_kms.js b/contracts/deploy/hyperevm/003_migrate_crosschain_strategy_to_talos_kms.js new file mode 100644 index 0000000000..34170e007e --- /dev/null +++ b/contracts/deploy/hyperevm/003_migrate_crosschain_strategy_to_talos_kms.js @@ -0,0 +1,30 @@ +const { deployOnHyperEVM } = require("../../utils/deploy-l2"); +const addresses = require("../../utils/addresses"); + +// Re-point the HyperEVM CrossChainRemoteStrategy operator to the new Talos KMS +// signer (from the old relayer 0xC79A…0517). setOperator is onlyGovernor and the +// strategy's governor is the HyperEVM Timelock, so this is executed via that +// timelock (scheduled/executed by the hyperevm 5/8 admin). deployOnHyperEVM +// writes the schedule + execute Safe Transaction Builder JSON for the 5/8 admin. +module.exports = deployOnHyperEVM( + { + deployName: "003_migrate_crosschain_strategy_to_talos", + }, + async () => { + const cCrossChainRemoteStrategy = await ethers.getContractAt( + "CrossChainRemoteStrategy", + addresses.hyperevm.CrossChainRemoteStrategy + ); + + return { + name: "Migrate the HyperEVM CrossChainRemoteStrategy operator to the new Talos signer.", + actions: [ + { + contract: cCrossChainRemoteStrategy, + signature: "setOperator(address)", + args: [addresses.talosRelayer], + }, + ], + }; + } +); diff --git a/contracts/deploy/mainnet/196_migrate_operators_to_talos_kms.js b/contracts/deploy/mainnet/196_migrate_operators_to_talos_kms.js new file mode 100644 index 0000000000..b96300957c --- /dev/null +++ b/contracts/deploy/mainnet/196_migrate_operators_to_talos_kms.js @@ -0,0 +1,73 @@ +const addresses = require("../../utils/addresses"); +const { deploymentWithGovernanceProposal } = require("../../utils/deploy"); + +// the governor to execute this proposal is OGN governance +module.exports = deploymentWithGovernanceProposal( + { + deployName: "196_migrate_operators_to_talos", + forceDeploy: false, + reduceQueueTime: true, + deployerIsProposer: false, + proposalId: "", // fill in after the proposal is submitted on-chain + }, + async () => { + // OUSD Vault (proxy "VaultProxy") + OETH Vault — IVault exposes both setters + const cVaultProxy = await ethers.getContract("VaultProxy"); + const cOUSDVault = await ethers.getContractAt( + "IVault", + cVaultProxy.address + ); + + const cOETHVaultProxy = await ethers.getContract("OETHVaultProxy"); + const cOETHVault = await ethers.getContractAt( + "IVault", + cOETHVaultProxy.address + ); + + // Cross-chain strategies (same contract code, two Create2 proxies) + const cCrossChainMasterStrategy = await ethers.getContractAt( + "CrossChainMasterStrategy", + addresses.mainnet.CrossChainMasterStrategy + ); + const cCrossChainHyperEVMMasterStrategy = await ethers.getContractAt( + "CrossChainMasterStrategy", + addresses.mainnet.CrossChainHyperEVMMasterStrategy + ); + + return { + name: "Migrate scheduled-action operator of the OUSD/OETH vaults and the Crosschain (Base + HyperEVM) strategies to the new signer, and unpause OUSD/OETH rebases.", + actions: [ + { + contract: cOUSDVault, + signature: "setOperatorAddr(address)", + args: [addresses.talosRelayer], + }, + { + contract: cOETHVault, + signature: "setOperatorAddr(address)", + args: [addresses.talosRelayer], + }, + { + contract: cCrossChainMasterStrategy, + signature: "setOperator(address)", + args: [addresses.talosRelayer], + }, + { + contract: cCrossChainHyperEVMMasterStrategy, + signature: "setOperator(address)", + args: [addresses.talosRelayer], + }, + { + contract: cOUSDVault, + signature: "unpauseRebase()", + args: [], + }, + { + contract: cOETHVault, + signature: "unpauseRebase()", + args: [], + }, + ], + }; + } +); diff --git a/contracts/deploy/mainnet/197_migrate_xogn_module6_to_talos_kms.js b/contracts/deploy/mainnet/197_migrate_xogn_module6_to_talos_kms.js new file mode 100644 index 0000000000..54121e3ed2 --- /dev/null +++ b/contracts/deploy/mainnet/197_migrate_xogn_module6_to_talos_kms.js @@ -0,0 +1,37 @@ +const addresses = require("../../utils/addresses"); +const { deploymentWithGnosisSafe } = require("../../utils/deploy"); + +// the governor to execute this proposal is Gnosis 5/8 Multisig +module.exports = deploymentWithGnosisSafe( + { + deployName: "197_migrate_xogn_module6_to_talos", + safe: addresses.mainnet.Guardian, + forceDeploy: false, + }, + async () => { + const cCollectXOGNRewardsModule6 = await ethers.getContract( + "CollectXOGNRewardsModule6" + ); + + const OPERATOR_ROLE = ethers.utils.keccak256( + ethers.utils.toUtf8Bytes("OPERATOR_ROLE") + ); + + return { + name: "Grant the OPERATOR_ROLE of CollectXOGNRewardsModule6 to the new Talos signer, and revoke it from the old relayer.", + actions: [ + { + contract: cCollectXOGNRewardsModule6, + signature: "grantRole(bytes32,address)", + args: [OPERATOR_ROLE, addresses.talosRelayer], + }, + // grantRole is additive — the old relayer keeps OPERATOR_ROLE, so revoke it. + { + contract: cCollectXOGNRewardsModule6, + signature: "revokeRole(bytes32,address)", + args: [OPERATOR_ROLE, addresses.mainnet.validatorRegistrator], + }, + ], + }; + } +); diff --git a/contracts/deploy/mainnet/198_migrate_strategist_modules_to_talos_kms.js b/contracts/deploy/mainnet/198_migrate_strategist_modules_to_talos_kms.js new file mode 100644 index 0000000000..fa5b966f57 --- /dev/null +++ b/contracts/deploy/mainnet/198_migrate_strategist_modules_to_talos_kms.js @@ -0,0 +1,45 @@ +const addresses = require("../../utils/addresses"); +const { deploymentWithGnosisSafe } = require("../../utils/deploy"); + +// the governor to execute this proposal is 2/8 Cross chain strategist +module.exports = deploymentWithGnosisSafe( + { + deployName: "198_migrate_strategist_modules_to_talos", + safe: addresses.multichainStrategist, + forceDeploy: false, + }, + async () => { + const moduleNames = [ + "ClaimStrategyRewardsSafeModule", + "AutoWithdrawalModule", + "MerklPoolBoosterBribesModule", + "CurvePoolBoosterBribesModule", + ]; + const modules = []; + for (const name of moduleNames) { + modules.push(await ethers.getContract(name)); + } + + const OPERATOR_ROLE = ethers.utils.keccak256( + ethers.utils.toUtf8Bytes("OPERATOR_ROLE") + ); + + return { + name: "Grant the OPERATOR_ROLE of all strategist safe modules to the new Talos signer, and revoke it from the old relayer.", + // grantRole is additive — the old relayer keeps OPERATOR_ROLE, so for each + // module grant the new signer AND revoke the old relayer. + actions: modules.flatMap((cModule) => [ + { + contract: cModule, + signature: "grantRole(bytes32,address)", + args: [OPERATOR_ROLE, addresses.talosRelayer], + }, + { + contract: cModule, + signature: "revokeRole(bytes32,address)", + args: [OPERATOR_ROLE, addresses.mainnet.validatorRegistrator], + }, + ]), + }; + } +); diff --git a/contracts/deploy/sonic/030_migrate_sonic_operators_to_talos_kms.js b/contracts/deploy/sonic/030_migrate_sonic_operators_to_talos_kms.js new file mode 100644 index 0000000000..0cbc4561f7 --- /dev/null +++ b/contracts/deploy/sonic/030_migrate_sonic_operators_to_talos_kms.js @@ -0,0 +1,49 @@ +const addresses = require("../../utils/addresses"); +const { deployOnSonic } = require("../../utils/deploy-l2"); + +// Migrate the Sonic operator/registrator roles from the old relayer EOA to the +// new Talos KMS signer. All three actions are governor-gated and executed via +// the Sonic Timelock (scheduled by the Sonic 5/8 admin). The vault rebase is +// operator-gated and currently paused, so we also unpause it once the operator +// is set. +module.exports = deployOnSonic( + { + deployName: "030_migrate_sonic_operators_to_talos", + }, + async ({ ethers }) => { + const cOSonicVaultProxy = await ethers.getContract("OSonicVaultProxy"); + const cOSonicVault = await ethers.getContractAt( + "IVault", + cOSonicVaultProxy.address + ); + + const cSonicStakingStrategyProxy = await ethers.getContract( + "SonicStakingStrategyProxy" + ); + const cSonicStakingStrategy = await ethers.getContractAt( + "SonicStakingStrategy", + cSonicStakingStrategyProxy.address + ); + + return { + name: "Migrate Sonic operators to the Talos KMS signer", + actions: [ + { + contract: cOSonicVault, + signature: "setOperatorAddr(address)", + args: [addresses.talosRelayer], + }, + { + contract: cOSonicVault, + signature: "unpauseRebase()", + args: [], + }, + { + contract: cSonicStakingStrategy, + signature: "setRegistrator(address)", + args: [addresses.talosRelayer], + }, + ], + }; + } +); diff --git a/contracts/test/_fixture-hyperevm.js b/contracts/test/_fixture-hyperevm.js index 4717489092..88ae881501 100644 --- a/contracts/test/_fixture-hyperevm.js +++ b/contracts/test/_fixture-hyperevm.js @@ -105,7 +105,11 @@ const crossChainHyperEVMFixture = deployments.createFixture(async () => { addresses.CCTPTokenMessengerV2 ); - const relayer = await impersonateAndFund(addresses.hyperevm.OZRelayerAddress); + // The cross-chain operator is re-pointed during the Talos signer migration + // (deploy 003), so read it from the strategy instead of hardcoding a relayer. + const relayer = await impersonateAndFund( + await fixture.crossChainRemoteStrategy.operator() + ); return { ...fixture, diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index 0943564e81..f41462c63c 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -1632,8 +1632,10 @@ async function crossChainFixture() { addresses.CCTPTokenMessengerV2 ); + // The cross-chain operator is repointed during the Talos signer migration + // (deploy 196), so read it from the strategy instead of hardcoding a relayer. fixture.relayer = await impersonateAndFund( - addresses.mainnet.validatorRegistrator + await cCrossChainMasterStrategy.operator() ); await setERC20TokenBalance( diff --git a/contracts/utils/addresses.js b/contracts/utils/addresses.js index a05685af3a..d24ba89022 100644 --- a/contracts/utils/addresses.js +++ b/contracts/utils/addresses.js @@ -8,7 +8,7 @@ addresses.createX = "0xba5Ed099633D3B313e4D5F7bdc1305d3c28ba5Ed"; addresses.multichainStrategist = "0x4FF1b9D9ba8558F5EAfCec096318eA0d8b541971"; addresses.multichainBuybackOperator = "0xBB077E716A5f1F1B63ed5244eBFf5214E50fec8c"; -addresses.talosRelayer = "0x0aBCDa6Fa7d500cf69B0eA5de9a607Cd9941221C"; +addresses.talosRelayer = "0x739212d5bAfE6AAC8Be49a60B7d003bD41DBf38b"; // new Talos signer addresses.votemarket = "0x8c2c5A295450DDFf4CB360cA73FCCC12243D14D9"; // CCTP contracts (uses same addresses on all chains) diff --git a/contracts/utils/deploy.js b/contracts/utils/deploy.js index d03b7bf1ff..4056466bef 100644 --- a/contracts/utils/deploy.js +++ b/contracts/utils/deploy.js @@ -48,6 +48,8 @@ const { getStorageAt, } = require("@nomicfoundation/hardhat-network-helpers"); const { keccak256, defaultAbiCoder } = require("ethers/lib/utils.js"); +const fs = require("fs"); +const path = require("path"); // Wait for 3 blocks confirmation on Mainnet. let NUM_CONFIRMATIONS = isMainnet ? 3 : 0; @@ -993,6 +995,47 @@ async function buildGnosisSafeJson( }); } +// Inlined to avoid a circular import: deploy-l2.js already requires deploy.js, +// so its getNetworkName() can't be imported here. +function safeOpsNetworkName() { + if (isForkTest) return "hardhat"; + if (isFork) return "localhost"; + return process.env.NETWORK_NAME || "mainnet"; +} + +function safeValueToString(value) { + if (BigNumber.isBigNumber(value)) return value.toString(); + if (Array.isArray(value)) return JSON.stringify(value.map(safeValueToString)); + if (typeof value === "number" || typeof value === "boolean") { + return String(value); + } + // address / bytes32 / bytes / string pass through unchanged + return String(value); +} + +// (contract, signature, args) -> { paramName: stringValue } for the Gnosis Safe +// Transaction Builder. Param names come from the ABI fragment. Tuple/struct +// params are unsupported (not used by the Safe migrations) and throw loudly. +function buildContractInputsValues(contract, signature, args) { + const inputs = contract.interface.getFunction(signature).inputs; + if (inputs.length !== args.length) { + throw new Error( + `${signature}: expected ${inputs.length} args, got ${args.length}` + ); + } + const out = {}; + inputs.forEach((input, i) => { + if (input.baseType === "tuple" || input.components) { + throw new Error( + `${signature}: tuple/struct param "${input.name}" is not supported` + ); + } + const name = input.name && input.name.length ? input.name : `arg${i}`; + out[name] = safeValueToString(args[i]); + }); + return out; +} + async function getProposalExecutionValue(governor, proposalId) { const actions = await governor.getActions(proposalId); const rawValues = @@ -1311,6 +1354,145 @@ function deploymentWithGuardianGovernor(opts, fn) { return main; } +/** + * Shortcut to create a deployment executed by a plain Gnosis Safe (NOT GovernorSix + * governance, NOT the Guardian-via-timelock path). The deploy fn returns a single + * list of actions ({ contract, signature, args, value }). The helper always builds + * the Gnosis Safe Transaction Builder JSON and writes it to + * deployments//operations/.json (mainnet -> committed + * artifact, fork -> gitignored). On fork it additionally executes the actions by + * impersonating the Safe, so fork tests pass and downstream deploys see the state. + * + * @param {Object} opts deployment options. `safe` is the executing Safe address. + * @param {Function} fn async (tools) => { name?, safe?, actions: [...] } + * @returns {Function} main object used by hardhat + */ +function deploymentWithGnosisSafe(opts, fn) { + const { deployName, dependencies, forceDeploy, onlyOnFork, forceSkip } = opts; + const optsSafe = opts.safe; + // Target network the Safe lives on; gates the real-deploy run + skip. Defaults + // to mainnet so existing mainnet deploys are unaffected. + const targetNetwork = opts.network || "mainnet"; + + const runDeployment = async (hre) => { + // getAssetAddresses is mainnet-centric (resolves mock assets); on L2s it can + // throw. Safe-batch deploys don't need it, so tolerate failure. + let assetAddresses = {}; + try { + assetAddresses = await getAssetAddresses(hre.deployments); + } catch (e) { + log( + `getAssetAddresses unavailable (${e.message}); continuing without it.` + ); + } + const proposal = await fn({ + assetAddresses, + deployWithConfirmation, + ethers, + getTxOpts, + withConfirmation, + }); + + if (!proposal?.actions?.length) { + log("No Safe proposal actions."); + return; + } + + const safeAddress = proposal.safe || optsSafe; + if (!safeAddress) { + throw new Error( + `deploymentWithGnosisSafe (${deployName}): no Safe address. ` + + `Set opts.safe or return { safe } from the deploy fn.` + ); + } + + const { actions } = proposal; + + // Build + write the Safe Transaction Builder JSON (every environment). + const safeJson = await buildGnosisSafeJson( + safeAddress, + actions.map((a) => a.contract.address), + actions.map((a) => constructContractMethod(a.contract, a.signature)), + actions.map((a) => + buildContractInputsValues(a.contract, a.signature, a.args) + ), + actions.map((a) => BigNumber.from(a.value ?? 0).toString()) + ); + const filePath = path.resolve( + __dirname, + `./../deployments/${safeOpsNetworkName()}/operations/${deployName}.json` + ); + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.writeFileSync(filePath, JSON.stringify(safeJson, null, 2)); + console.log(`Safe batch JSON written to ${filePath}`); + + if (!isFork) { + // Real deploy: JSON only. The operator imports it into the Safe + // Transaction Builder for the Safe to execute. + console.log( + `Import ${deployName}.json into the Gnosis Safe (${safeAddress}) Transaction Builder to execute.` + ); + return; + } + + // Fork: execute the batch by impersonating the Safe. + const safeSigner = await impersonateAndFund(safeAddress); + console.log(`Impersonating Safe ${safeAddress} to execute actions on fork`); + for (const action of actions) { + const { contract, signature, args, value } = action; + const txOpts = { + ...(await getTxOpts()), + ...(value ? { value } : {}), + }; + log(`Sending Safe action ${signature} to ${contract.address}`); + await withConfirmation( + contract.connect(safeSigner)[signature](...args, txOpts) + ); + console.log(`... ${signature} completed`); + } + }; + + const main = async (hre) => { + console.log(`Running ${deployName} deployment...`); + if (!hre) { + hre = require("hardhat"); + } + await runDeployment(hre); + console.log(`${deployName} deploy done.`); + return true; + }; + + main.id = deployName; + main.dependencies = dependencies; + // L2 fixtures filter deploys by network tag (deployments.fixture(["base"])); + // mainnet's fixture runs all deploys untagged, so only tag for non-mainnet. + if (targetNetwork !== "mainnet") { + main.tags = [targetNetwork]; + } + if (forceSkip) { + main.skip = () => true; + } else if (forceDeploy) { + main.skip = () => false; + } else { + main.skip = async () => { + if (isFork) { + const networkName = isForkTest ? "hardhat" : "localhost"; + const migrations = require(`./../deployments/${networkName}/.migrations.json`); + return Boolean(migrations[deployName]); + } else { + const onTarget = + targetNetwork === "base" + ? isBase + : targetNetwork === "sonic" + ? isSonic + : isMainnet; + return onlyOnFork ? true : isSmokeTest || !onTarget; + } + }; + } + return main; +} + function encodeSaltForCreateX(deployer, crosschainProtectionFlag, salt) { // Generate encoded salt (deployer address || crosschainProtectionFlag || bytes11(keccak256(rewardToken, gauge))) @@ -1471,6 +1653,7 @@ module.exports = { executeProposalOnFork, deploymentWithGovernanceProposal, deploymentWithGuardianGovernor, + deploymentWithGnosisSafe, constructContractMethod, buildGnosisSafeJson, From e7d573bcad0a96ea676335a39fb8c89416d6a617 Mon Sep 17 00:00:00 2001 From: Nick Addison Date: Wed, 10 Jun 2026 11:26:09 +1000 Subject: [PATCH 26/29] Nicka/vanilla staking ir 1 (#2918) * Migrate vault & cross-chain operators to the new Talos signer (+ unpause rebases) (#2911) * migrate contracts to Talso signer * fix fork tests * add the migration for the OGN rewards module * refactor deployment * add some comments * add base migration files * add remaining migrations for base and one for sonic * add hyperevm migration * remove the old relayer roles * Bump deploy numbers * Add prop id to deploy 196 script * Fix Hardhat task that deposits to compounding validators * Hardhat task support for different compounding staking strategies --------- Co-authored-by: Domen Grabec --- ...051_migrate_base_operators_to_talos_kms.js | 45 +++++ ..._migrate_base_merkl_module_to_talos_kms.js | 41 ++++ ...ant_base_claim_bribes_module1_talos_kms.js | 39 ++++ ...ant_base_claim_bribes_module3_talos_kms.js | 39 ++++ ...igrate_crosschain_strategy_to_talos_kms.js | 30 +++ .../196_migrate_operators_to_talos_kms.js | 74 +++++++ .../197_migrate_xogn_module6_to_talos_kms.js | 37 ++++ ...migrate_strategist_modules_to_talos_kms.js | 45 +++++ ...99_deploy_compounding_staking_strategy.js} | 2 +- ...00_remove_old_compounding_ssv_strategy.js} | 2 +- ...30_migrate_sonic_operators_to_talos_kms.js | 49 +++++ contracts/tasks/tasks.js | 48 +++++ contracts/tasks/validatorCompound.js | 179 +++++++++++------ contracts/test/_fixture-hyperevm.js | 6 +- contracts/test/_fixture.js | 4 +- contracts/utils/addresses.js | 2 +- contracts/utils/deploy.js | 183 ++++++++++++++++++ 17 files changed, 756 insertions(+), 69 deletions(-) create mode 100644 contracts/deploy/base/051_migrate_base_operators_to_talos_kms.js create mode 100644 contracts/deploy/base/052_migrate_base_merkl_module_to_talos_kms.js create mode 100644 contracts/deploy/base/053_grant_base_claim_bribes_module1_talos_kms.js create mode 100644 contracts/deploy/base/054_grant_base_claim_bribes_module3_talos_kms.js create mode 100644 contracts/deploy/hyperevm/003_migrate_crosschain_strategy_to_talos_kms.js create mode 100644 contracts/deploy/mainnet/196_migrate_operators_to_talos_kms.js create mode 100644 contracts/deploy/mainnet/197_migrate_xogn_module6_to_talos_kms.js create mode 100644 contracts/deploy/mainnet/198_migrate_strategist_modules_to_talos_kms.js rename contracts/deploy/mainnet/{196_deploy_compounding_staking_strategy.js => 199_deploy_compounding_staking_strategy.js} (99%) rename contracts/deploy/mainnet/{197_remove_old_compounding_ssv_strategy.js => 200_remove_old_compounding_ssv_strategy.js} (99%) create mode 100644 contracts/deploy/sonic/030_migrate_sonic_operators_to_talos_kms.js diff --git a/contracts/deploy/base/051_migrate_base_operators_to_talos_kms.js b/contracts/deploy/base/051_migrate_base_operators_to_talos_kms.js new file mode 100644 index 0000000000..6a83594b35 --- /dev/null +++ b/contracts/deploy/base/051_migrate_base_operators_to_talos_kms.js @@ -0,0 +1,45 @@ +const { deployOnBase } = require("../../utils/deploy-l2"); +const addresses = require("../../utils/addresses"); + +// Executed by base.governor 5/8 -> Base Timelock (these setters are onlyGovernor). +// Re-points the Base CrossChainRemoteStrategy operator and the OETHBaseVault +// operatorAddr to the new Talos signer, and unpauses OETHb rebases so Talos can +// rebase the vault directly (operator-gated) — replacing the PermissionedRebaseModule. +module.exports = deployOnBase( + { + deployName: "051_migrate_base_operators_to_talos", + }, + async () => { + const cCrossChainRemoteStrategy = await ethers.getContractAt( + "CrossChainRemoteStrategy", + addresses.base.CrossChainRemoteStrategy + ); + + const cOETHbVaultProxy = await ethers.getContract("OETHBaseVaultProxy"); + const cOETHbVault = await ethers.getContractAt( + "IVault", + cOETHbVaultProxy.address + ); + + return { + name: "Migrate the Base CrossChainRemoteStrategy operator and OETHBaseVault operatorAddr to the new Talos signer, and unpause OETHb rebases.", + actions: [ + { + contract: cCrossChainRemoteStrategy, + signature: "setOperator(address)", + args: [addresses.talosRelayer], + }, + { + contract: cOETHbVault, + signature: "setOperatorAddr(address)", + args: [addresses.talosRelayer], + }, + { + contract: cOETHbVault, + signature: "unpauseRebase()", + args: [], + }, + ], + }; + } +); diff --git a/contracts/deploy/base/052_migrate_base_merkl_module_to_talos_kms.js b/contracts/deploy/base/052_migrate_base_merkl_module_to_talos_kms.js new file mode 100644 index 0000000000..6d4e327413 --- /dev/null +++ b/contracts/deploy/base/052_migrate_base_merkl_module_to_talos_kms.js @@ -0,0 +1,41 @@ +const addresses = require("../../utils/addresses"); +const { deploymentWithGnosisSafe } = require("../../utils/deploy"); + +// The Base MerklPoolBoosterBribesModule is admined by the multichainStrategist +// 2/8 Safe (its DEFAULT_ADMIN_ROLE holder), so granting OPERATOR_ROLE to the new +// Talos signer is a plain Safe transaction +module.exports = deploymentWithGnosisSafe( + { + deployName: "052_migrate_base_merkl_module_to_talos", + safe: addresses.multichainStrategist, + network: "base", + forceDeploy: false, + }, + async () => { + const cMerklModule = await ethers.getContractAt( + "MerklPoolBoosterBribesModule", + addresses.base.MerklPoolBoosterBribesModule + ); + + const OPERATOR_ROLE = ethers.utils.keccak256( + ethers.utils.toUtf8Bytes("OPERATOR_ROLE") + ); + + return { + name: "Grant the OPERATOR_ROLE of the Base MerklPoolBoosterBribesModule to the new Talos signer, and revoke it from the old relayer.", + actions: [ + { + contract: cMerklModule, + signature: "grantRole(bytes32,address)", + args: [OPERATOR_ROLE, addresses.talosRelayer], + }, + // grantRole is additive — the old relayer keeps OPERATOR_ROLE, so revoke it. + { + contract: cMerklModule, + signature: "revokeRole(bytes32,address)", + args: [OPERATOR_ROLE, addresses.base.OZRelayerAddress], + }, + ], + }; + } +); diff --git a/contracts/deploy/base/053_grant_base_claim_bribes_module1_talos_kms.js b/contracts/deploy/base/053_grant_base_claim_bribes_module1_talos_kms.js new file mode 100644 index 0000000000..66f42989ba --- /dev/null +++ b/contracts/deploy/base/053_grant_base_claim_bribes_module1_talos_kms.js @@ -0,0 +1,39 @@ +const addresses = require("../../utils/addresses"); +const { deploymentWithGnosisSafe } = require("../../utils/deploy"); + +// ClaimBribesSafeModule1 is admined by the ClaimBribes 2/8 Safe (its +// DEFAULT_ADMIN_ROLE holder and immutable safeContract()), so granting +// OPERATOR_ROLE to the new Talos signer is a plain Safe transaction. +module.exports = deploymentWithGnosisSafe( + { + deployName: "053_grant_base_claim_bribes_module1_talos", + network: "base", + forceDeploy: false, + }, + async () => { + const cModule = await ethers.getContract("ClaimBribesSafeModule1"); + const safe = await cModule.safeContract(); + + const OPERATOR_ROLE = ethers.utils.keccak256( + ethers.utils.toUtf8Bytes("OPERATOR_ROLE") + ); + + return { + safe, + name: "Grant the OPERATOR_ROLE of the Base ClaimBribesSafeModule1 to the new Talos signer, and revoke it from the old relayer.", + actions: [ + { + contract: cModule, + signature: "grantRole(bytes32,address)", + args: [OPERATOR_ROLE, addresses.talosRelayer], + }, + // grantRole is additive — the old relayer keeps OPERATOR_ROLE, so revoke it. + { + contract: cModule, + signature: "revokeRole(bytes32,address)", + args: [OPERATOR_ROLE, addresses.base.OZRelayerAddress], + }, + ], + }; + } +); diff --git a/contracts/deploy/base/054_grant_base_claim_bribes_module3_talos_kms.js b/contracts/deploy/base/054_grant_base_claim_bribes_module3_talos_kms.js new file mode 100644 index 0000000000..64dbb3f406 --- /dev/null +++ b/contracts/deploy/base/054_grant_base_claim_bribes_module3_talos_kms.js @@ -0,0 +1,39 @@ +const addresses = require("../../utils/addresses"); +const { deploymentWithGnosisSafe } = require("../../utils/deploy"); + +// ClaimBribesSafeModule3 is admined by the base.strategist 1/2 Safe (its +// DEFAULT_ADMIN_ROLE holder and immutable safeContract()), so granting +// OPERATOR_ROLE to the new Talos signer is a plain Safe transaction. +module.exports = deploymentWithGnosisSafe( + { + deployName: "054_grant_base_claim_bribes_module3_talos", + network: "base", + forceDeploy: false, + }, + async () => { + const cModule = await ethers.getContract("ClaimBribesSafeModule3"); + const safe = await cModule.safeContract(); + + const OPERATOR_ROLE = ethers.utils.keccak256( + ethers.utils.toUtf8Bytes("OPERATOR_ROLE") + ); + + return { + safe, + name: "Grant the OPERATOR_ROLE of the Base ClaimBribesSafeModule3 to the new Talos signer, and revoke it from the old relayer.", + actions: [ + { + contract: cModule, + signature: "grantRole(bytes32,address)", + args: [OPERATOR_ROLE, addresses.talosRelayer], + }, + // grantRole is additive — the old relayer keeps OPERATOR_ROLE, so revoke it. + { + contract: cModule, + signature: "revokeRole(bytes32,address)", + args: [OPERATOR_ROLE, addresses.base.OZRelayerAddress], + }, + ], + }; + } +); diff --git a/contracts/deploy/hyperevm/003_migrate_crosschain_strategy_to_talos_kms.js b/contracts/deploy/hyperevm/003_migrate_crosschain_strategy_to_talos_kms.js new file mode 100644 index 0000000000..34170e007e --- /dev/null +++ b/contracts/deploy/hyperevm/003_migrate_crosschain_strategy_to_talos_kms.js @@ -0,0 +1,30 @@ +const { deployOnHyperEVM } = require("../../utils/deploy-l2"); +const addresses = require("../../utils/addresses"); + +// Re-point the HyperEVM CrossChainRemoteStrategy operator to the new Talos KMS +// signer (from the old relayer 0xC79A…0517). setOperator is onlyGovernor and the +// strategy's governor is the HyperEVM Timelock, so this is executed via that +// timelock (scheduled/executed by the hyperevm 5/8 admin). deployOnHyperEVM +// writes the schedule + execute Safe Transaction Builder JSON for the 5/8 admin. +module.exports = deployOnHyperEVM( + { + deployName: "003_migrate_crosschain_strategy_to_talos", + }, + async () => { + const cCrossChainRemoteStrategy = await ethers.getContractAt( + "CrossChainRemoteStrategy", + addresses.hyperevm.CrossChainRemoteStrategy + ); + + return { + name: "Migrate the HyperEVM CrossChainRemoteStrategy operator to the new Talos signer.", + actions: [ + { + contract: cCrossChainRemoteStrategy, + signature: "setOperator(address)", + args: [addresses.talosRelayer], + }, + ], + }; + } +); diff --git a/contracts/deploy/mainnet/196_migrate_operators_to_talos_kms.js b/contracts/deploy/mainnet/196_migrate_operators_to_talos_kms.js new file mode 100644 index 0000000000..7057eac970 --- /dev/null +++ b/contracts/deploy/mainnet/196_migrate_operators_to_talos_kms.js @@ -0,0 +1,74 @@ +const addresses = require("../../utils/addresses"); +const { deploymentWithGovernanceProposal } = require("../../utils/deploy"); + +// the governor to execute this proposal is OGN governance +module.exports = deploymentWithGovernanceProposal( + { + deployName: "196_migrate_operators_to_talos", + forceDeploy: false, + reduceQueueTime: true, + deployerIsProposer: false, + proposalId: + "22961702059927464053626280658057526947925126482574006865526656537485409437624", + }, + async () => { + // OUSD Vault (proxy "VaultProxy") + OETH Vault — IVault exposes both setters + const cVaultProxy = await ethers.getContract("VaultProxy"); + const cOUSDVault = await ethers.getContractAt( + "IVault", + cVaultProxy.address + ); + + const cOETHVaultProxy = await ethers.getContract("OETHVaultProxy"); + const cOETHVault = await ethers.getContractAt( + "IVault", + cOETHVaultProxy.address + ); + + // Cross-chain strategies (same contract code, two Create2 proxies) + const cCrossChainMasterStrategy = await ethers.getContractAt( + "CrossChainMasterStrategy", + addresses.mainnet.CrossChainMasterStrategy + ); + const cCrossChainHyperEVMMasterStrategy = await ethers.getContractAt( + "CrossChainMasterStrategy", + addresses.mainnet.CrossChainHyperEVMMasterStrategy + ); + + return { + name: "Migrate scheduled-action operator of the OUSD/OETH vaults and the Crosschain (Base + HyperEVM) strategies to the new signer, and unpause OUSD/OETH rebases.", + actions: [ + { + contract: cOUSDVault, + signature: "setOperatorAddr(address)", + args: [addresses.talosRelayer], + }, + { + contract: cOETHVault, + signature: "setOperatorAddr(address)", + args: [addresses.talosRelayer], + }, + { + contract: cCrossChainMasterStrategy, + signature: "setOperator(address)", + args: [addresses.talosRelayer], + }, + { + contract: cCrossChainHyperEVMMasterStrategy, + signature: "setOperator(address)", + args: [addresses.talosRelayer], + }, + { + contract: cOUSDVault, + signature: "unpauseRebase()", + args: [], + }, + { + contract: cOETHVault, + signature: "unpauseRebase()", + args: [], + }, + ], + }; + } +); diff --git a/contracts/deploy/mainnet/197_migrate_xogn_module6_to_talos_kms.js b/contracts/deploy/mainnet/197_migrate_xogn_module6_to_talos_kms.js new file mode 100644 index 0000000000..54121e3ed2 --- /dev/null +++ b/contracts/deploy/mainnet/197_migrate_xogn_module6_to_talos_kms.js @@ -0,0 +1,37 @@ +const addresses = require("../../utils/addresses"); +const { deploymentWithGnosisSafe } = require("../../utils/deploy"); + +// the governor to execute this proposal is Gnosis 5/8 Multisig +module.exports = deploymentWithGnosisSafe( + { + deployName: "197_migrate_xogn_module6_to_talos", + safe: addresses.mainnet.Guardian, + forceDeploy: false, + }, + async () => { + const cCollectXOGNRewardsModule6 = await ethers.getContract( + "CollectXOGNRewardsModule6" + ); + + const OPERATOR_ROLE = ethers.utils.keccak256( + ethers.utils.toUtf8Bytes("OPERATOR_ROLE") + ); + + return { + name: "Grant the OPERATOR_ROLE of CollectXOGNRewardsModule6 to the new Talos signer, and revoke it from the old relayer.", + actions: [ + { + contract: cCollectXOGNRewardsModule6, + signature: "grantRole(bytes32,address)", + args: [OPERATOR_ROLE, addresses.talosRelayer], + }, + // grantRole is additive — the old relayer keeps OPERATOR_ROLE, so revoke it. + { + contract: cCollectXOGNRewardsModule6, + signature: "revokeRole(bytes32,address)", + args: [OPERATOR_ROLE, addresses.mainnet.validatorRegistrator], + }, + ], + }; + } +); diff --git a/contracts/deploy/mainnet/198_migrate_strategist_modules_to_talos_kms.js b/contracts/deploy/mainnet/198_migrate_strategist_modules_to_talos_kms.js new file mode 100644 index 0000000000..fa5b966f57 --- /dev/null +++ b/contracts/deploy/mainnet/198_migrate_strategist_modules_to_talos_kms.js @@ -0,0 +1,45 @@ +const addresses = require("../../utils/addresses"); +const { deploymentWithGnosisSafe } = require("../../utils/deploy"); + +// the governor to execute this proposal is 2/8 Cross chain strategist +module.exports = deploymentWithGnosisSafe( + { + deployName: "198_migrate_strategist_modules_to_talos", + safe: addresses.multichainStrategist, + forceDeploy: false, + }, + async () => { + const moduleNames = [ + "ClaimStrategyRewardsSafeModule", + "AutoWithdrawalModule", + "MerklPoolBoosterBribesModule", + "CurvePoolBoosterBribesModule", + ]; + const modules = []; + for (const name of moduleNames) { + modules.push(await ethers.getContract(name)); + } + + const OPERATOR_ROLE = ethers.utils.keccak256( + ethers.utils.toUtf8Bytes("OPERATOR_ROLE") + ); + + return { + name: "Grant the OPERATOR_ROLE of all strategist safe modules to the new Talos signer, and revoke it from the old relayer.", + // grantRole is additive — the old relayer keeps OPERATOR_ROLE, so for each + // module grant the new signer AND revoke the old relayer. + actions: modules.flatMap((cModule) => [ + { + contract: cModule, + signature: "grantRole(bytes32,address)", + args: [OPERATOR_ROLE, addresses.talosRelayer], + }, + { + contract: cModule, + signature: "revokeRole(bytes32,address)", + args: [OPERATOR_ROLE, addresses.mainnet.validatorRegistrator], + }, + ]), + }; + } +); diff --git a/contracts/deploy/mainnet/196_deploy_compounding_staking_strategy.js b/contracts/deploy/mainnet/199_deploy_compounding_staking_strategy.js similarity index 99% rename from contracts/deploy/mainnet/196_deploy_compounding_staking_strategy.js rename to contracts/deploy/mainnet/199_deploy_compounding_staking_strategy.js index 76e10052e9..15d3afeea0 100644 --- a/contracts/deploy/mainnet/196_deploy_compounding_staking_strategy.js +++ b/contracts/deploy/mainnet/199_deploy_compounding_staking_strategy.js @@ -4,7 +4,7 @@ const { deploymentWithGovernanceProposal } = require("../../utils/deploy"); module.exports = deploymentWithGovernanceProposal( { - deployName: "196_deploy_compounding_staking_strategy", + deployName: "199_deploy_compounding_staking_strategy", forceDeploy: false, reduceQueueTime: true, deployerIsProposer: false, diff --git a/contracts/deploy/mainnet/197_remove_old_compounding_ssv_strategy.js b/contracts/deploy/mainnet/200_remove_old_compounding_ssv_strategy.js similarity index 99% rename from contracts/deploy/mainnet/197_remove_old_compounding_ssv_strategy.js rename to contracts/deploy/mainnet/200_remove_old_compounding_ssv_strategy.js index 75816ac10b..38e73540e2 100644 --- a/contracts/deploy/mainnet/197_remove_old_compounding_ssv_strategy.js +++ b/contracts/deploy/mainnet/200_remove_old_compounding_ssv_strategy.js @@ -73,7 +73,7 @@ const parseRemovedCluster = (ssvNetwork, receipt) => { module.exports = deploymentWithGovernanceProposal( { - deployName: "197_remove_old_compounding_ssv_strategy", + deployName: "200_remove_old_compounding_ssv_strategy", forceDeploy: false, reduceQueueTime: true, deployerIsProposer: false, diff --git a/contracts/deploy/sonic/030_migrate_sonic_operators_to_talos_kms.js b/contracts/deploy/sonic/030_migrate_sonic_operators_to_talos_kms.js new file mode 100644 index 0000000000..0cbc4561f7 --- /dev/null +++ b/contracts/deploy/sonic/030_migrate_sonic_operators_to_talos_kms.js @@ -0,0 +1,49 @@ +const addresses = require("../../utils/addresses"); +const { deployOnSonic } = require("../../utils/deploy-l2"); + +// Migrate the Sonic operator/registrator roles from the old relayer EOA to the +// new Talos KMS signer. All three actions are governor-gated and executed via +// the Sonic Timelock (scheduled by the Sonic 5/8 admin). The vault rebase is +// operator-gated and currently paused, so we also unpause it once the operator +// is set. +module.exports = deployOnSonic( + { + deployName: "030_migrate_sonic_operators_to_talos", + }, + async ({ ethers }) => { + const cOSonicVaultProxy = await ethers.getContract("OSonicVaultProxy"); + const cOSonicVault = await ethers.getContractAt( + "IVault", + cOSonicVaultProxy.address + ); + + const cSonicStakingStrategyProxy = await ethers.getContract( + "SonicStakingStrategyProxy" + ); + const cSonicStakingStrategy = await ethers.getContractAt( + "SonicStakingStrategy", + cSonicStakingStrategyProxy.address + ); + + return { + name: "Migrate Sonic operators to the Talos KMS signer", + actions: [ + { + contract: cOSonicVault, + signature: "setOperatorAddr(address)", + args: [addresses.talosRelayer], + }, + { + contract: cOSonicVault, + signature: "unpauseRebase()", + args: [], + }, + { + contract: cSonicStakingStrategy, + signature: "setRegistrator(address)", + args: [addresses.talosRelayer], + }, + ], + }; + } +); diff --git a/contracts/tasks/tasks.js b/contracts/tasks/tasks.js index 1961bd26de..01cfc61c6a 100644 --- a/contracts/tasks/tasks.js +++ b/contracts/tasks/tasks.js @@ -2057,6 +2057,12 @@ subtask( "new", types.string ) + .addOptionalParam( + "ssv", + "Use the SSV compounding staking strategy instead of the non-SSV compounding staking strategy.", + false, + types.boolean + ) .setAction(setRegistrator); task("setRegistrator").setAction(async (_, __, runSuper) => { return runSuper(); @@ -2383,6 +2389,12 @@ subtask( false, types.boolean ) + .addOptionalParam( + "ssv", + "Use the SSV compounding staking strategy instead of the non-SSV compounding staking strategy.", + false, + types.boolean + ) .setAction(async (taskArgs) => { const signer = await getSigner(); await autoValidatorDeposits({ ...taskArgs, signer }); @@ -2419,6 +2431,12 @@ subtask( false, types.boolean ) + .addOptionalParam( + "ssv", + "Use the SSV compounding staking strategy instead of the non-SSV compounding staking strategy.", + false, + types.boolean + ) .setAction(async (taskArgs) => { const signer = await getSigner(); if (taskArgs.direct && taskArgs.consol) { @@ -2482,6 +2500,12 @@ subtask( false, types.boolean ) + .addOptionalParam( + "ssv", + "Use the SSV compounding staking strategy instead of the non-SSV compounding staking strategy.", + false, + types.boolean + ) .setAction(async (taskArgs) => { const signer = await getSigner(); await autoValidatorWithdrawals({ ...taskArgs, signer }); @@ -2506,6 +2530,12 @@ subtask( false, types.boolean ) + .addOptionalParam( + "ssv", + "Use the SSV compounding staking strategy instead of the non-SSV compounding staking strategy.", + false, + types.boolean + ) .setAction(stakeValidator); task("stakeValidatorUuid").setAction(async (_, __, runSuper) => { return runSuper(); @@ -2557,6 +2587,12 @@ subtask( false, types.boolean ) + .addOptionalParam( + "ssv", + "Use the SSV compounding staking strategy instead of the non-SSV compounding staking strategy.", + false, + types.boolean + ) .addOptionalParam( "dryrun", "Do not call stakeEth on the strategy contract. Just log the params and verify the deposit signature", @@ -2575,6 +2611,12 @@ subtask("snapBalances", "Takes a snapshot of the staking strategy's balance") false, types.boolean ) + .addOptionalParam( + "ssv", + "Use the SSV compounding staking strategy instead of the non-SSV compounding staking strategy.", + false, + types.boolean + ) .setAction(snapBalances); task("snapBalances").setAction(async (_, __, runSuper) => { return runSuper(); @@ -2593,6 +2635,12 @@ subtask("snapStakingStrat", "Dumps the staking strategy's data") 100, types.int ) + .addOptionalParam( + "ssv", + "Use the SSV compounding staking strategy instead of the non-SSV compounding staking strategy.", + false, + types.boolean + ) .setAction(snapStakingStrategy); task("snapStakingStrat").setAction(async (_, __, runSuper) => { return runSuper(); diff --git a/contracts/tasks/validatorCompound.js b/contracts/tasks/validatorCompound.js index dbde601040..22563d618f 100644 --- a/contracts/tasks/validatorCompound.js +++ b/contracts/tasks/validatorCompound.js @@ -34,19 +34,76 @@ const { const log = require("../utils/logger")("task:validator:compounding"); +const VALIDATOR_STATE_NON_REGISTERED = 0; const VALIDATOR_STATE_REGISTERED = 1; -async function snapBalances({ consol = false }) { +const resolveCompoundingStakingContract = async (ssv = false) => { + if (ssv) { + return { + creatingDepositState: VALIDATOR_STATE_REGISTERED, + strategy: await resolveContract( + "CompoundingStakingSSVStrategyProxy", + "CompoundingStakingSSVStrategy" + ), + }; + } + + return { + creatingDepositState: VALIDATOR_STATE_NON_REGISTERED, + strategy: await resolveContract( + "CompoundingStakingStrategyProxy", + "CompoundingStakingStrategy" + ), + }; +}; + +const toNumber = (value) => + BigNumber.isBigNumber(value) ? value.toNumber() : value; + +const getVerifiedValidators = async (strategy, blockTag = "latest") => { + const validatorCount = await strategy.verifiedValidatorsLength({ blockTag }); + const validators = []; + + for (let i = 0; i < toNumber(validatorCount); i++) { + const pubKeyHash = await strategy.verifiedValidators(i, { blockTag }); + const validator = await strategy.validator(pubKeyHash, { blockTag }); + validators.push({ + pubKeyHash, + index: validator.index, + state: validator.state, + }); + } + + return validators; +}; + +const getPendingDeposits = async (strategy, blockTag = "latest") => { + const depositCount = await strategy.depositListLength({ blockTag }); + const deposits = []; + + for (let i = 0; i < toNumber(depositCount); i++) { + const pendingDepositRoot = await strategy.depositList(i, { blockTag }); + const deposit = await strategy.deposits(pendingDepositRoot, { blockTag }); + deposits.push({ + pendingDepositRoot, + pubKeyHash: deposit.pubKeyHash, + amountGwei: deposit.amountGwei, + slot: deposit.slot, + }); + } + + return deposits; +}; + +async function snapBalances({ consol = false, ssv = false }) { const signer = await getSigner(); // TODO check the slot of the first pending deposit is not zero + const { strategy } = await resolveCompoundingStakingContract(ssv); const contract = consol ? await resolveContract("ConsolidationController") - : await resolveContract( - "CompoundingStakingSSVStrategyProxy", - "CompoundingStakingSSVStrategy" - ); + : strategy; log(`About to snap balances on ${contract.address}`); const tx = await contract.connect(signer).snapBalances(); @@ -56,10 +113,6 @@ async function snapBalances({ consol = false }) { // When called via ConsolidationController the BalancesSnapped event is emitted // by the target strategy, so decode logs against the strategy's interface. - const strategy = await resolveContract( - "CompoundingStakingSSVStrategyProxy", - "CompoundingStakingSSVStrategy" - ); const eventTopic = strategy.interface.getEventTopic("BalancesSnapped"); const rawLog = receipt.logs.find( (l) => @@ -151,6 +204,7 @@ async function stakeValidator({ forkVersion, uuid, consol = false, + ssv = false, }) { const signer = await getSigner(); @@ -171,23 +225,25 @@ async function stakeValidator({ forkVersion = _forkVersion; } - const strategy = await resolveContract( - "CompoundingStakingSSVStrategyProxy", - "CompoundingStakingSSVStrategy" - ); + const { creatingDepositState, strategy: depositStrategy } = + await resolveCompoundingStakingContract(ssv); const contract = consol ? await resolveContract("ConsolidationController") - : strategy; + : depositStrategy; if (!withdrawalCredentials) { - withdrawalCredentials = calcWithdrawalCredential("0x02", strategy.address); + withdrawalCredentials = calcWithdrawalCredential( + "0x02", + depositStrategy.address + ); } const amountWei = parseUnits(amount.toString(), 18); - const initialDepositAmountWei = await strategy.initialDepositAmountWei(); - const validator = await strategy.validator(hashPubKey(pubkey)); + const initialDepositAmountWei = + await depositStrategy.initialDepositAmountWei(); + const validator = await depositStrategy.validator(hashPubKey(pubkey)); const isCreatingDeposit = BigNumber.from(validator.state).eq( - VALIDATOR_STATE_REGISTERED + creatingDepositState ); if (isCreatingDeposit) { @@ -217,7 +273,7 @@ async function stakeValidator({ } const depositDataRoot = await calcDepositRoot( - strategy.address, + depositStrategy.address, "0x02", pubkey, sig, @@ -259,15 +315,12 @@ async function autoValidatorDeposits({ buffer: bufferBps = 100, // 1% buffer minStrategyWithdrawAmount = parseUnits("0.1", 18), dryrun = false, + ssv = false, }) { const networkName = await getNetworkName(); const wethAddress = addresses[networkName].WETH; const weth = await ethers.getContractAt("IERC20", wethAddress); - const strategy = await resolveContract( - "CompoundingStakingSSVStrategyProxy", - "CompoundingStakingSSVStrategy" - ); - const strategyView = await resolveContract("CompoundingStakingStrategyView"); + const { strategy } = await resolveCompoundingStakingContract(ssv); const vault = await resolveContract("OETHVaultProxy", "IVault"); // 1. Calculate the WETH available in the vault = WETH balance - withdrawals queued + withdrawals claimed @@ -318,11 +371,11 @@ async function autoValidatorDeposits({ // 5. Get the staking strategy's active validators and pending deposits - const verifiedValidators = await strategyView.getVerifiedValidators(); + const verifiedValidators = await getVerifiedValidators(strategy); const activeValidators = verifiedValidators.filter( - (validator) => validator.state === 4 // ACTIVE + (validator) => BigNumber.from(validator.state).eq(4) // ACTIVE ); - const pendingDeposits = await strategyView.getPendingDeposits(); + const pendingDeposits = await getPendingDeposits(strategy); // 6. Calculate validators balances after all the pending deposits have been processed @@ -451,13 +504,17 @@ async function autoValidatorDeposits({ } } -async function withdrawValidator({ pubkey, amount, signer, consol = false }) { - const strategy = consol +async function withdrawValidator({ + pubkey, + amount, + signer, + consol = false, + ssv = false, +}) { + const { strategy } = await resolveCompoundingStakingContract(ssv); + const contract = consol ? await resolveContract("ConsolidationController") - : await resolveContract( - "CompoundingStakingSSVStrategyProxy", - "CompoundingStakingSSVStrategy" - ); + : strategy; /// Get the validator's balance const balance = await getValidatorBalance(pubkey); @@ -484,7 +541,7 @@ async function withdrawValidator({ pubkey, amount, signer, consol = false }) { ); } // Send 1 wei of value to cover the request withdrawal fee - const tx = await strategy + const tx = await contract .connect(signer) .validatorWithdrawal(pubkey, amountGwei, { value: 1 }); await logTxDetails(tx, "validatorWithdrawal"); @@ -497,17 +554,14 @@ async function autoValidatorWithdrawals({ minValidatorWithdrawAmount = BigInt(10e18), minStrategyWithdrawAmount = parseUnits("0.1", 18), dryrun = false, + ssv = false, }) { const networkName = await getNetworkName(); const wethAddress = addresses[networkName].WETH; const weth = await ethers.getContractAt("IERC20", wethAddress); const vaultAddress = addresses[networkName].OETHVaultProxy; const vault = await ethers.getContractAt("IVault", vaultAddress); - const strategy = await resolveContract( - "CompoundingStakingSSVStrategyProxy", - "CompoundingStakingSSVStrategy" - ); - const strategyView = await resolveContract("CompoundingStakingStrategyView"); + const { strategy } = await resolveCompoundingStakingContract(ssv); // 1. Calculate the WETH available in the vault = WETH balance - withdrawals queued + withdrawals claimed @@ -519,8 +573,8 @@ async function autoValidatorWithdrawals({ // 2. Get the staking strategy's active validator indexes - const activeValidators = await strategyView.getVerifiedValidators(); - const validatorIndexes = activeValidators.map((v) => v.index.toNumber()); + const activeValidators = await getVerifiedValidators(strategy); + const validatorIndexes = activeValidators.map((v) => toNumber(v.index)); // 3. Calculate pending validator partial withdrawal = sum amount in the partial withdrawal from the beacon chain data @@ -651,6 +705,7 @@ async function autoValidatorWithdrawals({ async function snapStakingStrategy({ buffer: bufferBps = 100, // 1% buffer block, + ssv = false, }) { let blockTag = await getBlock(block); // Don't use the latest block as the slot probably won't be available yet @@ -665,18 +720,14 @@ async function snapStakingStrategy({ const wethAddress = addresses[networkName].WETH; const weth = await ethers.getContractAt("IERC20", wethAddress); - const ssvAddress = addresses[networkName].SSV; - const ssv = await ethers.getContractAt("IERC20", ssvAddress); - - const strategy = await resolveContract( - "CompoundingStakingSSVStrategyProxy", - "CompoundingStakingSSVStrategy" - ); - const strategyView = await resolveContract("CompoundingStakingStrategyView"); + const ssvToken = addresses[networkName].SSV + ? await ethers.getContractAt("IERC20", addresses[networkName].SSV) + : undefined; + const { strategy } = await resolveCompoundingStakingContract(ssv); const vault = await resolveContract("OETHVaultProxy", "IVault"); // Pending deposits - const totalDeposits = await logDeposits(strategyView, blockTag, stateView); + const totalDeposits = await logDeposits(strategy, blockTag, stateView); if (stateView.pendingDeposits.length === 0) { console.log("No pending beacon chain deposits"); @@ -692,8 +743,8 @@ async function snapStakingStrategy({ } // Pending withdrawals - const activeValidators = await strategyView.getVerifiedValidators(); - const validatorIndexes = activeValidators.map((v) => v.index.toNumber()); + const activeValidators = await getVerifiedValidators(strategy); + const validatorIndexes = activeValidators.map((v) => toNumber(v.index)); const totalWithdrawals = await totalPartialWithdrawals( stateView, @@ -702,9 +753,7 @@ async function snapStakingStrategy({ ); // Verified validators - const verifiedValidators = await strategyView.getVerifiedValidators({ - blockTag, - }); + const verifiedValidators = await getVerifiedValidators(strategy, blockTag); console.log(`\n${verifiedValidators.length || "No"} verified validators:`); if (verifiedValidators.length > 0) { console.log( @@ -740,7 +789,10 @@ async function snapStakingStrategy({ strategy.address, blockTag ); - const stratSsvBalance = await ssv.balanceOf(strategy.address, { blockTag }); + const stratSsvBalance = + ssvToken && ssv + ? await ssvToken.balanceOf(strategy.address, { blockTag }) + : undefined; const stratBalance = await strategy.checkBalance(wethAddress, { blockTag, }); @@ -801,14 +853,16 @@ async function snapStakingStrategy({ console.log( `Last snap slot : ${snappedSlot} (${slot - snappedSlot} slots ago)` ); - console.log(`SSV balance : ${formatUnits(stratSsvBalance, 18)}`); + if (stratSsvBalance !== undefined) { + console.log(`SSV balance : ${formatUnits(stratSsvBalance, 18)}`); + } console.log( `WETH Deposits : ${formatUnits(depositedWethAccountedFor, 18)}` ); } -async function logDeposits(strategyView, blockTag = "latest", stateView) { - const deposits = await strategyView.getPendingDeposits({ blockTag }); +async function logDeposits(strategy, blockTag = "latest", stateView) { + const deposits = await getPendingDeposits(strategy, blockTag); let totalDeposits = BigNumber.from(0); console.log(`\n${deposits.length || "No"} pending strategy deposits:`); if (deposits.length > 0) { @@ -872,15 +926,12 @@ function validatorStatus(status) { } } -async function setRegistrator({ account, type }) { +async function setRegistrator({ account, type, ssv = false }) { const signer = await getSigner(); const strategy = type === "new" - ? await resolveContract( - "CompoundingStakingSSVStrategyProxy", - "CompoundingStakingSSVStrategy" - ) + ? (await resolveCompoundingStakingContract(ssv)).strategy : await resolveContract( "NativeStakingSSVStrategyProxy", "NativeStakingSSVStrategy" diff --git a/contracts/test/_fixture-hyperevm.js b/contracts/test/_fixture-hyperevm.js index 4717489092..88ae881501 100644 --- a/contracts/test/_fixture-hyperevm.js +++ b/contracts/test/_fixture-hyperevm.js @@ -105,7 +105,11 @@ const crossChainHyperEVMFixture = deployments.createFixture(async () => { addresses.CCTPTokenMessengerV2 ); - const relayer = await impersonateAndFund(addresses.hyperevm.OZRelayerAddress); + // The cross-chain operator is re-pointed during the Talos signer migration + // (deploy 003), so read it from the strategy instead of hardcoding a relayer. + const relayer = await impersonateAndFund( + await fixture.crossChainRemoteStrategy.operator() + ); return { ...fixture, diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index 24a2a7207f..d41e47da02 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -1698,8 +1698,10 @@ async function crossChainFixture() { addresses.CCTPTokenMessengerV2 ); + // The cross-chain operator is repointed during the Talos signer migration + // (deploy 196), so read it from the strategy instead of hardcoding a relayer. fixture.relayer = await impersonateAndFund( - addresses.mainnet.validatorRegistrator + await cCrossChainMasterStrategy.operator() ); await setERC20TokenBalance( diff --git a/contracts/utils/addresses.js b/contracts/utils/addresses.js index a82cca4155..e71805ad57 100644 --- a/contracts/utils/addresses.js +++ b/contracts/utils/addresses.js @@ -8,7 +8,7 @@ addresses.createX = "0xba5Ed099633D3B313e4D5F7bdc1305d3c28ba5Ed"; addresses.multichainStrategist = "0x4FF1b9D9ba8558F5EAfCec096318eA0d8b541971"; addresses.multichainBuybackOperator = "0xBB077E716A5f1F1B63ed5244eBFf5214E50fec8c"; -addresses.talosRelayer = "0x0aBCDa6Fa7d500cf69B0eA5de9a607Cd9941221C"; +addresses.talosRelayer = "0x739212d5bAfE6AAC8Be49a60B7d003bD41DBf38b"; // new Talos signer addresses.votemarket = "0x8c2c5A295450DDFf4CB360cA73FCCC12243D14D9"; // CCTP contracts (uses same addresses on all chains) diff --git a/contracts/utils/deploy.js b/contracts/utils/deploy.js index d03b7bf1ff..4056466bef 100644 --- a/contracts/utils/deploy.js +++ b/contracts/utils/deploy.js @@ -48,6 +48,8 @@ const { getStorageAt, } = require("@nomicfoundation/hardhat-network-helpers"); const { keccak256, defaultAbiCoder } = require("ethers/lib/utils.js"); +const fs = require("fs"); +const path = require("path"); // Wait for 3 blocks confirmation on Mainnet. let NUM_CONFIRMATIONS = isMainnet ? 3 : 0; @@ -993,6 +995,47 @@ async function buildGnosisSafeJson( }); } +// Inlined to avoid a circular import: deploy-l2.js already requires deploy.js, +// so its getNetworkName() can't be imported here. +function safeOpsNetworkName() { + if (isForkTest) return "hardhat"; + if (isFork) return "localhost"; + return process.env.NETWORK_NAME || "mainnet"; +} + +function safeValueToString(value) { + if (BigNumber.isBigNumber(value)) return value.toString(); + if (Array.isArray(value)) return JSON.stringify(value.map(safeValueToString)); + if (typeof value === "number" || typeof value === "boolean") { + return String(value); + } + // address / bytes32 / bytes / string pass through unchanged + return String(value); +} + +// (contract, signature, args) -> { paramName: stringValue } for the Gnosis Safe +// Transaction Builder. Param names come from the ABI fragment. Tuple/struct +// params are unsupported (not used by the Safe migrations) and throw loudly. +function buildContractInputsValues(contract, signature, args) { + const inputs = contract.interface.getFunction(signature).inputs; + if (inputs.length !== args.length) { + throw new Error( + `${signature}: expected ${inputs.length} args, got ${args.length}` + ); + } + const out = {}; + inputs.forEach((input, i) => { + if (input.baseType === "tuple" || input.components) { + throw new Error( + `${signature}: tuple/struct param "${input.name}" is not supported` + ); + } + const name = input.name && input.name.length ? input.name : `arg${i}`; + out[name] = safeValueToString(args[i]); + }); + return out; +} + async function getProposalExecutionValue(governor, proposalId) { const actions = await governor.getActions(proposalId); const rawValues = @@ -1311,6 +1354,145 @@ function deploymentWithGuardianGovernor(opts, fn) { return main; } +/** + * Shortcut to create a deployment executed by a plain Gnosis Safe (NOT GovernorSix + * governance, NOT the Guardian-via-timelock path). The deploy fn returns a single + * list of actions ({ contract, signature, args, value }). The helper always builds + * the Gnosis Safe Transaction Builder JSON and writes it to + * deployments//operations/.json (mainnet -> committed + * artifact, fork -> gitignored). On fork it additionally executes the actions by + * impersonating the Safe, so fork tests pass and downstream deploys see the state. + * + * @param {Object} opts deployment options. `safe` is the executing Safe address. + * @param {Function} fn async (tools) => { name?, safe?, actions: [...] } + * @returns {Function} main object used by hardhat + */ +function deploymentWithGnosisSafe(opts, fn) { + const { deployName, dependencies, forceDeploy, onlyOnFork, forceSkip } = opts; + const optsSafe = opts.safe; + // Target network the Safe lives on; gates the real-deploy run + skip. Defaults + // to mainnet so existing mainnet deploys are unaffected. + const targetNetwork = opts.network || "mainnet"; + + const runDeployment = async (hre) => { + // getAssetAddresses is mainnet-centric (resolves mock assets); on L2s it can + // throw. Safe-batch deploys don't need it, so tolerate failure. + let assetAddresses = {}; + try { + assetAddresses = await getAssetAddresses(hre.deployments); + } catch (e) { + log( + `getAssetAddresses unavailable (${e.message}); continuing without it.` + ); + } + const proposal = await fn({ + assetAddresses, + deployWithConfirmation, + ethers, + getTxOpts, + withConfirmation, + }); + + if (!proposal?.actions?.length) { + log("No Safe proposal actions."); + return; + } + + const safeAddress = proposal.safe || optsSafe; + if (!safeAddress) { + throw new Error( + `deploymentWithGnosisSafe (${deployName}): no Safe address. ` + + `Set opts.safe or return { safe } from the deploy fn.` + ); + } + + const { actions } = proposal; + + // Build + write the Safe Transaction Builder JSON (every environment). + const safeJson = await buildGnosisSafeJson( + safeAddress, + actions.map((a) => a.contract.address), + actions.map((a) => constructContractMethod(a.contract, a.signature)), + actions.map((a) => + buildContractInputsValues(a.contract, a.signature, a.args) + ), + actions.map((a) => BigNumber.from(a.value ?? 0).toString()) + ); + const filePath = path.resolve( + __dirname, + `./../deployments/${safeOpsNetworkName()}/operations/${deployName}.json` + ); + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.writeFileSync(filePath, JSON.stringify(safeJson, null, 2)); + console.log(`Safe batch JSON written to ${filePath}`); + + if (!isFork) { + // Real deploy: JSON only. The operator imports it into the Safe + // Transaction Builder for the Safe to execute. + console.log( + `Import ${deployName}.json into the Gnosis Safe (${safeAddress}) Transaction Builder to execute.` + ); + return; + } + + // Fork: execute the batch by impersonating the Safe. + const safeSigner = await impersonateAndFund(safeAddress); + console.log(`Impersonating Safe ${safeAddress} to execute actions on fork`); + for (const action of actions) { + const { contract, signature, args, value } = action; + const txOpts = { + ...(await getTxOpts()), + ...(value ? { value } : {}), + }; + log(`Sending Safe action ${signature} to ${contract.address}`); + await withConfirmation( + contract.connect(safeSigner)[signature](...args, txOpts) + ); + console.log(`... ${signature} completed`); + } + }; + + const main = async (hre) => { + console.log(`Running ${deployName} deployment...`); + if (!hre) { + hre = require("hardhat"); + } + await runDeployment(hre); + console.log(`${deployName} deploy done.`); + return true; + }; + + main.id = deployName; + main.dependencies = dependencies; + // L2 fixtures filter deploys by network tag (deployments.fixture(["base"])); + // mainnet's fixture runs all deploys untagged, so only tag for non-mainnet. + if (targetNetwork !== "mainnet") { + main.tags = [targetNetwork]; + } + if (forceSkip) { + main.skip = () => true; + } else if (forceDeploy) { + main.skip = () => false; + } else { + main.skip = async () => { + if (isFork) { + const networkName = isForkTest ? "hardhat" : "localhost"; + const migrations = require(`./../deployments/${networkName}/.migrations.json`); + return Boolean(migrations[deployName]); + } else { + const onTarget = + targetNetwork === "base" + ? isBase + : targetNetwork === "sonic" + ? isSonic + : isMainnet; + return onlyOnFork ? true : isSmokeTest || !onTarget; + } + }; + } + return main; +} + function encodeSaltForCreateX(deployer, crosschainProtectionFlag, salt) { // Generate encoded salt (deployer address || crosschainProtectionFlag || bytes11(keccak256(rewardToken, gauge))) @@ -1471,6 +1653,7 @@ module.exports = { executeProposalOnFork, deploymentWithGovernanceProposal, deploymentWithGuardianGovernor, + deploymentWithGnosisSafe, constructContractMethod, buildGnosisSafeJson, From f73714f3be486014d8e0a7a800ac0bd3eb8ee9f5 Mon Sep 17 00:00:00 2001 From: Nick Addison Date: Wed, 10 Jun 2026 11:34:52 +1000 Subject: [PATCH 27/29] Clearer error when SSV validator has already been registered (#2919) * Migrate vault & cross-chain operators to the new Talos signer (+ unpause rebases) (#2911) * migrate contracts to Talso signer * fix fork tests * add the migration for the OGN rewards module * refactor deployment * add some comments * add base migration files * add remaining migrations for base and one for sonic * add hyperevm migration * remove the old relayer roles * Bump deploy numbers * Add prop id to deploy 196 script * Clear error when SSV validator has already been registered --------- Co-authored-by: Domen Grabec --- .../NativeStaking/CompoundingStakingSSVStrategy.sol | 3 ++- contracts/test/strategies/compoundingSSVStaking.js | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/contracts/contracts/strategies/NativeStaking/CompoundingStakingSSVStrategy.sol b/contracts/contracts/strategies/NativeStaking/CompoundingStakingSSVStrategy.sol index dd4b1b3675..1001e6a15f 100644 --- a/contracts/contracts/strategies/NativeStaking/CompoundingStakingSSVStrategy.sol +++ b/contracts/contracts/strategies/NativeStaking/CompoundingStakingSSVStrategy.sol @@ -15,6 +15,7 @@ contract CompoundingStakingSSVStrategy is CompoundingStakingStrategy { uint256[50] private __gap; error CannotRemoveSsvValidator(); // 0x2c45bd75 + error AlreadyRegistered(); // 0x3a81d6fc error NotRegisteredOrVerified(); // 0x99088a6b event SSVValidatorRemoved(bytes32 indexed pubKeyHash, uint64[] operatorIds); @@ -69,7 +70,7 @@ contract CompoundingStakingSSVStrategy is CompoundingStakingStrategy { bytes32 pubKeyHash = _hashPubKey(publicKey); if (validator[pubKeyHash].state != ValidatorState.NON_REGISTERED) { - revert NotRegisteredOrVerified(); + revert AlreadyRegistered(); } // Store the validator state as registered diff --git a/contracts/test/strategies/compoundingSSVStaking.js b/contracts/test/strategies/compoundingSSVStaking.js index 4497eadfa1..e59826d12b 100644 --- a/contracts/test/strategies/compoundingSSVStaking.js +++ b/contracts/test/strategies/compoundingSSVStaking.js @@ -1082,7 +1082,7 @@ describe("Unit test: Compounding SSV Staking Strategy", function () { emptyCluster, { value: ethUnits("2") } ) - ).to.be.revertedWithCustomError("NotRegisteredOrVerified()"); + ).to.be.revertedWithCustomError("AlreadyRegistered()"); }); it("Should revert when re-registering a removed validator", async () => { @@ -1127,7 +1127,7 @@ describe("Unit test: Compounding SSV Staking Strategy", function () { emptyCluster, { value: ethUnits("2") } ) - ).to.be.revertedWithCustomError("NotRegisteredOrVerified()"); + ).to.be.revertedWithCustomError("AlreadyRegistered()"); }); it("Should revert when staking because of insufficient ETH balance", async () => { From d9954c0f4bebe48f9149cc1345b3fb5bdb9b2d16 Mon Sep 17 00:00:00 2001 From: Nick Addison Date: Wed, 10 Jun 2026 12:02:56 +1000 Subject: [PATCH 28/29] Fixed withdrawSsvClusterEth to ignore consensus rewards in NativeStakingSSVStrategy (#2920) --- .../NativeStaking/NativeStakingSSVStrategy.sol | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/contracts/contracts/strategies/NativeStaking/NativeStakingSSVStrategy.sol b/contracts/contracts/strategies/NativeStaking/NativeStakingSSVStrategy.sol index cc1f2a13e1..abfb18ae3d 100644 --- a/contracts/contracts/strategies/NativeStaking/NativeStakingSSVStrategy.sol +++ b/contracts/contracts/strategies/NativeStaking/NativeStakingSSVStrategy.sol @@ -133,13 +133,15 @@ contract NativeStakingSSVStrategy is uint256 amount, Cluster calldata cluster ) external onlyGovernor nonReentrant { + uint256 ethBalanceBefore = address(this).balance; + ISSVNetwork(SSV_NETWORK).withdraw(operatorIds, amount, cluster); - uint256 ethBalance = address(this).balance; - if (ethBalance > 0) { - IWETH9(WETH).deposit{ value: ethBalance }(); - IERC20(WETH).safeTransfer(vaultAddress, ethBalance); - emit Withdrawal(WETH, address(0), ethBalance); + uint256 withdrawn = address(this).balance - ethBalanceBefore; + if (withdrawn > 0) { + IWETH9(WETH).deposit{ value: withdrawn }(); + IERC20(WETH).safeTransfer(vaultAddress, withdrawn); + emit Withdrawal(WETH, address(0), withdrawn); } } From a3a783cc05eccfaa2f15fed3ceb45bcf297d81a7 Mon Sep 17 00:00:00 2001 From: Nick Addison Date: Wed, 10 Jun 2026 15:12:26 +1000 Subject: [PATCH 29/29] Add prop id to deploy 196 script (#2917) --- contracts/deploy/mainnet/196_migrate_operators_to_talos_kms.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contracts/deploy/mainnet/196_migrate_operators_to_talos_kms.js b/contracts/deploy/mainnet/196_migrate_operators_to_talos_kms.js index b96300957c..7057eac970 100644 --- a/contracts/deploy/mainnet/196_migrate_operators_to_talos_kms.js +++ b/contracts/deploy/mainnet/196_migrate_operators_to_talos_kms.js @@ -8,7 +8,8 @@ module.exports = deploymentWithGovernanceProposal( forceDeploy: false, reduceQueueTime: true, deployerIsProposer: false, - proposalId: "", // fill in after the proposal is submitted on-chain + proposalId: + "22961702059927464053626280658057526947925126482574006865526656537485409437624", }, async () => { // OUSD Vault (proxy "VaultProxy") + OETH Vault — IVault exposes both setters