diff --git a/contracts/0.8.25/vaults/ValidatorConsolidationRequests.sol b/contracts/0.8.25/vaults/ValidatorConsolidationRequests.sol index 9248561c5..e5ca677a2 100644 --- a/contracts/0.8.25/vaults/ValidatorConsolidationRequests.sol +++ b/contracts/0.8.25/vaults/ValidatorConsolidationRequests.sol @@ -6,157 +6,143 @@ pragma solidity 0.8.25; import {VaultHub} from "contracts/0.8.25/vaults/VaultHub.sol"; import {Dashboard} from "contracts/0.8.25/vaults/dashboard/Dashboard.sol"; +import {NodeOperatorFee} from "contracts/0.8.25/vaults/dashboard/NodeOperatorFee.sol"; import {ILidoLocator} from "contracts/common/interfaces/ILidoLocator.sol"; -/** - * @title A contract for EIP-7251: Increase the MAX_EFFECTIVE_BALANCE. - * Allow validators to have larger effective balances, while maintaining the 32 ETH lower bound. - */ + /** + * @title ValidatorConsolidationRequests + * @author kovalgek + * @notice Contract for consolidating validators into staking vaults (EIP-7251) + * and adjusting rewards. Built to work with Vault CLI tooling and to + * support batched execution (EIP-5792). + * + * This contract is strictly for an account that: + * - has its address as withdrawal credentials for pubkeys to consolidate from + * - has the `NODE_OPERATOR_REWARDS_ADJUST_ROLE` role assigned in Dashboard. + */ contract ValidatorConsolidationRequests { /// @notice EIP-7251 consolidation requests contract address. address public constant CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS = 0x0000BBdDc7CE488642fb579F8B00f3a590007251; uint256 internal constant PUBLIC_KEY_LENGTH = 48; uint256 internal constant CONSOLIDATION_REQUEST_CALLDATA_LENGTH = PUBLIC_KEY_LENGTH * 2; + uint256 internal constant MINIMUM_VALIDATOR_BALANCE = 16 ether; /// @notice Lido Locator contract. ILidoLocator public immutable LIDO_LOCATOR; - /// @notice This contract address. - address public immutable THIS; - /// @param _lidoLocator Lido Locator contract. constructor(address _lidoLocator) { if (_lidoLocator == address(0)) revert ZeroArgument("_lidoLocator"); LIDO_LOCATOR = ILidoLocator(_lidoLocator); - THIS = address(this); } /** - * @notice Send EIP-7251 consolidation requests for the specified public keys. - * Each request instructs a validator to consolidate its stake to the target validator. - * - * Requirements: - * - The caller must have the `NODE_OPERATOR_REWARDS_ADJUST_ROLE` to perform reward adjustment. - * - The vault into which consolidation occurs must be connected to the vault hub. - * - The function must be called with a non-zero msg.value that is sufficient to cover all consolidation fees. - * - The required amount can be obtained by calling `getConsolidationRequestFee()`, but note that this value is only - * valid for the current block and may change. It is therefore advised to provide a slightly higher amount; - * any excess will be refunded to the `_refundRecipient` address. - * - The `sourcePublicKeys` and `targetPublicKey` must be valid and belong to registered validators. - * - `_adjustmentIncrease` must match the total balance of source validators on the Consensus Layer. - * - A valid Staking Vault contract address must be provided. - * - * Execution Flows: - * This function can be called from a Withdrawal Credentials (WC) account, which may be an EOA, a Gnosis Safe, or another smart contract. + * @notice Return the encoded calls for EIP-7251 consolidation requests and the rewards adjustment increase. + * + * Use case: + * - If your withdrawal credentials are an EOA or multisig and you want to + * consolidate validator balances into staking vaults, call this method to + * generate the encoded consolidation and rewards-adjustment calls. + * These calls can later be submitted via EIP-5792. + * - Rewards adjustment calls can only be executed by an account with the + * `NODE_OPERATOR_REWARDS_ADJUST_ROLE`. The node operator may grant this + * role to the withdrawal credentials account. + * + * Recommendations: + * - It is recommended to call this function via the Vault CLI using WalletConnect signing. + * It performs pre-checks of source and target validator states, verifies their withdrawal + * credential prefixes, calculates current validator balances, generates the request + * calldata using this method, and then submits these call data in batched transactions + * via EIP-5792. * - * 1. **Externally Owned Account (EOA)**: - * - The EOA owner should invoke this function via a delegate call using account abstraction (EIP-7702 delegation). + * @param _sourcePubkeys An array of tightly packed arrays of 48-byte public keys corresponding to validators + * requesting consolidation. + * | ----- public key (48 bytes) ----- || ----- public key (48 bytes) ----- | ... * - * 2. **Smart Contract**: - * - The smart contract should invoke this function via delegatecall if such functionality is supported. - * - Alternatively, if the contract is behind a proxy, this functionality can be added via an upgrade. - * - * 3. **Gnosis Safe**: - * a. Build a transaction with delegatecall flag enabled. - * b. Sign the transaction with the minimum number of owners required to meet the Safe's threshold, then execute it. + * @param _targetPubkeys An array of 48-byte public keys corresponding to validators to consolidate to. + * | ----- public key (48 bytes) ----- || ----- public key (48 bytes) ----- | ... * - * Notes: - * Consolidation requests are asynchronous and handled on the Consensus Layer. The function optimistically - * assumes that the consolidation will succeed and immediately increases the node operator's reward adjustment - * via the Dashboard contract. However, if the consolidation fails, the function does not take - * responsibility for rolling back the adjustment. It is the responsibility of the Node Operator and Vault Owner to call - * `setRewardsAdjustment` on the Dashboard contract to correct the adjustment value in such cases. + * @param _dashboard The address of the dashboard contract. + * @param _allSourceValidatorBalancesWei The total balance (in wei) of all source validators. + * This value is used to adjust the rewards amount before charging the node operator fee. * - * Additionally, this function assumes that the provided source and target pubkeys are valid, and that the reward - * adjustment value is appropriate. Because of this, it is highly recommended to use the `Vault CLI` tool to interact - * with this function. `Vault CLI` performs pre-checks to ensure the correctness of public keys and the adjustment value, - * and also monitors post-execution state on the CL to verify that the consolidation was successful. + * Node operator fee is applied only on rewards, which are defined as + * "all external ether that appeared in the vault on top of the initially deposited one". + * Without this adjustment, consolidated validator balances would incorrectly + * be included in the rewards base, which would lead to overcharging. * - * @param _sourcePubkeys An array of tightly packed arrays of 48-byte public keys corresponding to validators requesting consolidation. - * | ----- public key (48 bytes) ----- || ----- public key (48 bytes) ----- | ... + * By passing the sum of all source validator balances, you ensure that these + * balances are excluded from the reward calculation, and the node operator fee + * is charged only on the actual rewards. * - * @param _targetPubkeys An array of 48-byte public keys corresponding to validators to consolidate to. - * | ----- public key (48 bytes) ----- || ----- public key (48 bytes) ----- | ... - * - * @param _refundRecipient The address to refund the excess consolidation fee to. - * @param _stakingVault The address of the staking vault contract. - * @param _adjustmentIncrease The sum of the balances of the source validators to increase the rewards adjustment by. + * ⚠️ Note: this is not a precise method. It does not account for the future + * rewards that the consolidated validators may earn after this call, so in some + * setups additional correction may be required. + * @return adjustmentIncreaseEncodedCall The encoded call to increase the rewards adjustment + * (or empty if zero sum of source validator balances passed). + * @return consolidationRequestEncodedCalls The encoded calls for the consolidation requests. */ - function addConsolidationRequests( + function getConsolidationRequestsAndAdjustmentIncreaseEncodedCalls( bytes[] calldata _sourcePubkeys, bytes[] calldata _targetPubkeys, - address _refundRecipient, - address _stakingVault, - uint256 _adjustmentIncrease - ) external payable onlyDelegateCall { - if (msg.value == 0) revert ZeroArgument("msg.value"); + address _dashboard, + uint256 _allSourceValidatorBalancesWei + ) external view returns ( + bytes memory adjustmentIncreaseEncodedCall, + bytes[] memory consolidationRequestEncodedCalls + ) { if (_sourcePubkeys.length == 0) revert ZeroArgument("sourcePubkeys"); if (_targetPubkeys.length == 0) revert ZeroArgument("targetPubkeys"); - if (_stakingVault == address(0)) revert ZeroArgument("stakingVault"); - - // If the refund recipient is not set, use the sender as the refund recipient - if (_refundRecipient == address(0)) { - _refundRecipient = msg.sender; - } - + if (_dashboard == address(0)) revert ZeroArgument("dashboard"); if (_sourcePubkeys.length != _targetPubkeys.length) { revert MismatchingSourceAndTargetPubkeysCount(_sourcePubkeys.length, _targetPubkeys.length); } VaultHub vaultHub = VaultHub(payable(LIDO_LOCATOR.vaultHub())); - VaultHub.VaultConnection memory vaultConnection = vaultHub.vaultConnection(_stakingVault); - - if(vaultConnection.vaultIndex == 0 || vaultHub.isPendingDisconnect(_stakingVault)) { + address stakingVault = address(Dashboard(payable(_dashboard)).stakingVault()); + if (!vaultHub.isVaultConnected(stakingVault) || vaultHub.isPendingDisconnect(stakingVault)) { revert VaultNotConnected(); } - uint256 totalSourcePubkeysCount = 0; - for (uint256 i = 0; i < _sourcePubkeys.length; i++) { - totalSourcePubkeysCount += _validateAndCountPubkeys(_sourcePubkeys[i]); - if (_targetPubkeys[i].length != PUBLIC_KEY_LENGTH) { - revert MalformedTargetPubkey(); - } + VaultHub.VaultConnection memory vaultConnection = vaultHub.vaultConnection(stakingVault); + if (_dashboard != vaultConnection.owner) { + revert DashboardNotOwnerOfStakingVault(); } - uint256 feePerRequest = _getConsolidationRequestFee(); - uint256 totalFee = totalSourcePubkeysCount * feePerRequest; - if (msg.value < totalFee) revert InsufficientValidatorConsolidationFee(msg.value, totalFee); + uint256 consolidationRequestsCount = _validatePubkeysAndCountConsolidationRequests( + _sourcePubkeys, + _targetPubkeys + ); - for (uint256 i = 0; i < _sourcePubkeys.length; i++) { - _processConsolidationRequest( - _sourcePubkeys[i], - _targetPubkeys[i], - feePerRequest - ); + if (_allSourceValidatorBalancesWei != 0 && + _allSourceValidatorBalancesWei < consolidationRequestsCount * MINIMUM_VALIDATOR_BALANCE) { + revert InvalidAllSourceValidatorBalancesWei(); } - uint256 excess = msg.value - totalFee; - if (excess > 0) { - (bool success, ) = _refundRecipient.call{value: excess}(""); - if (!success) revert ConsolidationFeeRefundFailed(_refundRecipient, excess); - } + consolidationRequestEncodedCalls = _consolidationCalldatas( + _sourcePubkeys, + _targetPubkeys, + consolidationRequestsCount + ); - if(_adjustmentIncrease > 0) { - Dashboard(payable(vaultConnection.owner)).increaseRewardsAdjustment(_adjustmentIncrease); + if (_allSourceValidatorBalancesWei > 0) { + adjustmentIncreaseEncodedCall = abi.encodeWithSelector( + NodeOperatorFee.increaseRewardsAdjustment.selector, + _allSourceValidatorBalancesWei + ); } - - emit ConsolidationRequestsAdded(msg.sender, _sourcePubkeys, _targetPubkeys, _refundRecipient, excess, _adjustmentIncrease); } /** - * @dev Retrieves the current EIP-7251 consolidation fee. This fee is valid only for the current block and may change in subsequent blocks. + * @dev Retrieves the current EIP-7251 consolidation fee. This fee is valid only for the current block and may + * change in subsequent blocks. * @return The minimum fee required per consolidation request. */ function getConsolidationRequestFee() external view returns (uint256) { return _getConsolidationRequestFee(); } - modifier onlyDelegateCall() { - if(address(this) == THIS) revert NotDelegateCall(); - _; - } - function _getConsolidationRequestFee() private view returns (uint256) { (bool success, bytes memory feeData) = CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS.staticcall(""); @@ -171,72 +157,60 @@ contract ValidatorConsolidationRequests { return abi.decode(feeData, (uint256)); } - function _copyPubkeysToMemory(bytes memory _target, uint256 _targetIndex, bytes calldata _source, uint256 _sourceIndex) private pure { - assembly { - calldatacopy(add(_target, add(32, mul(_targetIndex, PUBLIC_KEY_LENGTH))), add(_source.offset, mul(_sourceIndex, PUBLIC_KEY_LENGTH)), PUBLIC_KEY_LENGTH) + function _consolidationCalldatas( + bytes[] calldata _sourcePubkeys, + bytes[] calldata _targetPubkeys, + uint256 _consolidationRequestsCount + ) private pure returns (bytes[] memory consolidationRequestEncodedCalls) { + consolidationRequestEncodedCalls = new bytes[](_consolidationRequestsCount); + + uint256 k = 0; + for (uint256 i = 0; i < _sourcePubkeys.length; i++) { + uint256 sourcePubkeysCount = _sourcePubkeys[i].length / PUBLIC_KEY_LENGTH; + + for (uint256 j = 0; j < sourcePubkeysCount; j++) { + uint256 offset = j * PUBLIC_KEY_LENGTH; + uint256 end = offset + PUBLIC_KEY_LENGTH; + + consolidationRequestEncodedCalls[k] = bytes.concat(_sourcePubkeys[i][offset : end], _targetPubkeys[i]); + unchecked { k++; } + } } } - function _validateAndCountPubkeys(bytes calldata _pubkeys) private pure returns (uint256) { + function _validateAndCountPubkeysInBatch(bytes calldata _pubkeys) private pure returns (uint256) { if (_pubkeys.length % PUBLIC_KEY_LENGTH != 0) { - revert MalformedPubkeysArray(); + revert MalformedSourcePubkeysArray(); } - uint256 keysCount = _pubkeys.length / PUBLIC_KEY_LENGTH; if (keysCount == 0) { revert NoConsolidationRequests(); } - return keysCount; } - function _processConsolidationRequest( - bytes calldata _sourcePubkeys, - bytes calldata _targetPubkey, - uint256 _feePerRequest - ) private { - uint256 sourcePubkeysCount = _validateAndCountPubkeys(_sourcePubkeys); - bytes memory callData = new bytes(CONSOLIDATION_REQUEST_CALLDATA_LENGTH); - - for (uint256 j = 0; j < sourcePubkeysCount; j++) { - _copyPubkeysToMemory(callData, 0, _sourcePubkeys, j); - _copyPubkeysToMemory(callData, 1, _targetPubkey, 0); - - (bool success, ) = CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS.call{value: _feePerRequest}(callData); - if (!success) { - revert ConsolidationRequestAdditionFailed(callData); + function _validatePubkeysAndCountConsolidationRequests( + bytes[] calldata _sourcePubkeys, + bytes[] calldata _targetPubkeys + ) private pure returns (uint256) { + uint256 consolidationRequestsCount = 0; + for (uint256 i = 0; i < _sourcePubkeys.length; i++) { + if (_targetPubkeys[i].length != PUBLIC_KEY_LENGTH) { + revert MalformedTargetPubkey(); } + consolidationRequestsCount += _validateAndCountPubkeysInBatch(_sourcePubkeys[i]); } + return consolidationRequestsCount; } - /** - * @notice Emitted when the consolidation requests are added - * @param sender The address of the sender - * @param sourcePubkeys The source public keys - * @param targetPubkeys The target public keys - * @param refundRecipient The address of the refund recipient - * @param excess The excess consolidation fee - * @param adjustmentIncrease The adjustment increase - */ - event ConsolidationRequestsAdded( - address indexed sender, - bytes[] sourcePubkeys, - bytes[] targetPubkeys, - address indexed refundRecipient, - uint256 excess, - uint256 adjustmentIncrease - ); - - error ConsolidationFeeReadFailed(); - error ConsolidationFeeInvalidData(); - error ConsolidationFeeRefundFailed(address recipient, uint256 amount); - error ConsolidationRequestAdditionFailed(bytes callData); - error NoConsolidationRequests(); - error MalformedPubkeysArray(); + error ZeroArgument(string argName); + error MalformedSourcePubkeysArray(); error MalformedTargetPubkey(); error MismatchingSourceAndTargetPubkeysCount(uint256 sourcePubkeysCount, uint256 targetPubkeysCount); - error InsufficientValidatorConsolidationFee(uint256 provided, uint256 required); - error ZeroArgument(string argName); error VaultNotConnected(); - error NotDelegateCall(); + error DashboardNotOwnerOfStakingVault(); + error NoConsolidationRequests(); + error InvalidAllSourceValidatorBalancesWei(); + error ConsolidationFeeReadFailed(); + error ConsolidationFeeInvalidData(); } diff --git a/test/0.8.25/vaults/consolidation/consolidationHelper.ts b/test/0.8.25/vaults/consolidation/consolidationHelper.ts index 0defa8ad6..f5354603d 100644 --- a/test/0.8.25/vaults/consolidation/consolidationHelper.ts +++ b/test/0.8.25/vaults/consolidation/consolidationHelper.ts @@ -2,6 +2,8 @@ import { BytesLike } from "ethers"; import { SecretKey } from "@chainsafe/blst"; +import { ether } from "lib"; + export function generateConsolidationRequestPayload(numberOfRequests: number): { sourcePubkeys: BytesLike[]; targetPubkeys: BytesLike[]; @@ -12,18 +14,19 @@ export function generateConsolidationRequestPayload(numberOfRequests: number): { const targetPubkeys: BytesLike[] = []; let adjustmentIncrease: bigint = 0n; let totalSourcePubkeysCount = 0; - const numberOfSourcePubkeys = 50; + const numberOfSourcePubkeysMax = 50; for (let i = 1; i <= numberOfRequests; i++) { let tempSourcePubkeys: Uint8Array = new Uint8Array(); + const numberOfSourcePubkeys = Math.floor(Math.random() * numberOfSourcePubkeysMax) + 1; totalSourcePubkeysCount += numberOfSourcePubkeys; for (let j = 1; j <= numberOfSourcePubkeys; j++) { const publicKey = generateRandomPublicKey(i * j); tempSourcePubkeys = concatUint8Arrays([tempSourcePubkeys, publicKey]); + adjustmentIncrease += ether("17"); } sourcePubkeys.push(tempSourcePubkeys); const publicKey = generateRandomPublicKey(i * numberOfSourcePubkeys + 1); targetPubkeys.push(publicKey); - adjustmentIncrease += 32n; } return { diff --git a/test/0.8.25/vaults/consolidation/eip7251Mock.ts b/test/0.8.25/vaults/consolidation/eip7251Mock.ts deleted file mode 100644 index c23af0028..000000000 --- a/test/0.8.25/vaults/consolidation/eip7251Mock.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { expect } from "chai"; -import { BytesLike, ContractTransactionReceipt, ContractTransactionResponse } from "ethers"; -import { ethers } from "hardhat"; - -import { findEventsWithInterfaces } from "lib"; - -const eventName = "ConsolidationRequestAdded__Mock"; -const eip7251MockEventABI = [`event ${eventName}(bytes request, address sender, uint256 fee)`]; -const eip7251MockInterface = new ethers.Interface(eip7251MockEventABI); -const KEY_LENGTH = 48; - -export function findEIP7251MockEvents(receipt: ContractTransactionReceipt) { - return findEventsWithInterfaces(receipt!, eventName, [eip7251MockInterface]); -} - -const dashboardMockEventName = "RewardsAdjustmentIncreased"; -const dashboardMockEventABI = [`event ${dashboardMockEventName}(uint256 _amount)`]; -const dashboardMockInterface = new ethers.Interface(dashboardMockEventABI); - -export function findDashboardMockEvents(receipt: ContractTransactionReceipt) { - return findEventsWithInterfaces(receipt!, dashboardMockEventName, [dashboardMockInterface]); -} - -export const testEIP7251Mock = async ( - addConsolidationRequests: () => Promise, - sender: string, - expectedSourcePubkeys: BytesLike[], - expectedTargetPubkeys: BytesLike[], - expectedFee: bigint, -): Promise<{ tx: ContractTransactionResponse; receipt: ContractTransactionReceipt }> => { - const tx = await addConsolidationRequests(); - const receipt = (await tx.wait()) as ContractTransactionReceipt; - - const totalPubkeysCount = expectedSourcePubkeys.reduce( - (acc, pubkeys) => acc + BigInt(Math.floor(pubkeys.length / KEY_LENGTH)), - 0n, - ); - const events = findEIP7251MockEvents(receipt); - expect(events.length).to.equal(totalPubkeysCount); - - for (let i = 0; i < expectedSourcePubkeys.length; i++) { - const pubkeysCount = Math.floor(expectedSourcePubkeys[i].length / KEY_LENGTH); - for (let j = 0; j < pubkeysCount; j++) { - const expectedSourcePubkey = expectedSourcePubkeys[i].slice(j * KEY_LENGTH, (j + 1) * KEY_LENGTH); - const result = ethers.concat([expectedSourcePubkey, expectedTargetPubkeys[i]]); - expect(events[i * pubkeysCount + j].args[0]).to.equal(result); - expect(events[i * pubkeysCount + j].args[1]).to.equal(sender); - expect(events[i * pubkeysCount + j].args[2]).to.equal(expectedFee); - } - } - - return { tx, receipt }; -}; diff --git a/test/0.8.25/vaults/consolidation/validatorConsolidationRequests.test.ts b/test/0.8.25/vaults/consolidation/validatorConsolidationRequests.test.ts index 95ad694f1..3970fbda5 100644 --- a/test/0.8.25/vaults/consolidation/validatorConsolidationRequests.test.ts +++ b/test/0.8.25/vaults/consolidation/validatorConsolidationRequests.test.ts @@ -6,7 +6,6 @@ import { setBalance } from "@nomicfoundation/hardhat-network-helpers"; import { Dashboard__Mock, - DelegateCaller, EIP7251MaxEffectiveBalanceRequest__Mock, LidoLocator, ValidatorConsolidationRequests, @@ -19,33 +18,23 @@ import { deployLidoLocator } from "test/deploy"; import { Snapshot } from "test/suite"; import { generateConsolidationRequestPayload } from "./consolidationHelper"; -import { findDashboardMockEvents, findEIP7251MockEvents, testEIP7251Mock } from "./eip7251Mock"; -const EMPTY_PUBKEYS = "0x"; +const PUBKEY = "0x800276cfb86f1c08a1e7238c76a9ca45d5528d2072e51500b343266203d5d7794e6fc848ce7948e9c81960f71f821b42"; const KEY_LENGTH = 48; describe("ValidatorConsolidationRequests.sol", () => { let actor: HardhatEthersSigner; - let receiver: HardhatEthersSigner; - let stakingVault: HardhatEthersSigner; - let consolidationRequestPredeployed: EIP7251MaxEffectiveBalanceRequest__Mock; - let validatorConsolidationRequestsAddress: string; let validatorConsolidationRequests: ValidatorConsolidationRequests; let dashboard: Dashboard__Mock; let dashboardAddress: string; let originalState: string; let locator: LidoLocator; let vaultHub: VaultHub__MockForDashboard; - let delegateCaller: DelegateCaller; - - async function getConsolidationRequestPredeployedContractBalance(): Promise { - const contractAddress = await consolidationRequestPredeployed.getAddress(); - return await ethers.provider.getBalance(contractAddress); - } + let stakingVault: HardhatEthersSigner; before(async () => { - [actor, receiver, stakingVault] = await ethers.getSigners(); + [actor, stakingVault] = await ethers.getSigners(); // Set a high balance for the actor account await setBalance(actor.address, ether("1000000")); @@ -53,10 +42,11 @@ describe("ValidatorConsolidationRequests.sol", () => { dashboard = await ethers.deployContract("Dashboard__Mock"); dashboardAddress = await dashboard.getAddress(); - delegateCaller = await ethers.deployContract("DelegateCaller", [], { from: actor }); consolidationRequestPredeployed = await deployEIP7251MaxEffectiveBalanceRequestContract(1n); vaultHub = await ethers.deployContract("VaultHub__MockForDashboard", [ethers.ZeroAddress, ethers.ZeroAddress]); - await vaultHub.mock__setVaultConnection(stakingVault.address, { + + await dashboard.mock__setStakingVault(stakingVault); + await vaultHub.mock__setVaultConnection(stakingVault, { owner: dashboardAddress, shareLimit: 0, vaultIndex: 1, @@ -68,14 +58,13 @@ describe("ValidatorConsolidationRequests.sol", () => { reservationFeeBP: 0, isBeaconDepositsManuallyPaused: false, }); + await vaultHub.mock__setPendingDisconnect(false); locator = await deployLidoLocator({ vaultHub: vaultHub, }); validatorConsolidationRequests = await ethers.deployContract("ValidatorConsolidationRequests", [locator]); - validatorConsolidationRequestsAddress = await validatorConsolidationRequests.getAddress(); - await dashboard.mock__setStakingVault(stakingVault.address); expect(await consolidationRequestPredeployed.getAddress()).to.equal(EIP7251_ADDRESS); }); @@ -83,18 +72,10 @@ describe("ValidatorConsolidationRequests.sol", () => { afterEach(async () => await Snapshot.restore(originalState)); - async function getFee(): Promise { - return await validatorConsolidationRequests.getConsolidationRequestFee(); - } - context("eip 7251 max effective balance request contract", () => { it("Should return the address of the EIP 7251 max effective balance request contract", async function () { expect(await validatorConsolidationRequests.CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS()).to.equal(EIP7251_ADDRESS); }); - - it("Should THIS point to contract address", async function () { - expect(await validatorConsolidationRequests.THIS()).to.equal(validatorConsolidationRequestsAddress); - }); }); context("get consolidation request fee", () => { @@ -123,91 +104,73 @@ describe("ValidatorConsolidationRequests.sol", () => { }); }); - context("add consolidation requests", () => { + context("get consolidation requests and adjustment increase encoded calls", () => { it("Should revert if empty parameters are provided", async function () { await expect( - delegateCaller.callDelegate( - validatorConsolidationRequestsAddress, - validatorConsolidationRequests.interface.encodeFunctionData("addConsolidationRequests", [ - [], - [], - receiver.address, - stakingVault.address, - 0, - ]), - ), - ) - .to.be.revertedWithCustomError(validatorConsolidationRequests, "ZeroArgument") - .withArgs("msg.value"); - - await expect( - delegateCaller.callDelegate( - validatorConsolidationRequestsAddress, - validatorConsolidationRequests.interface.encodeFunctionData("addConsolidationRequests", [ - [], - [], - receiver.address, - stakingVault.address, - 0, - ]), - { value: 1n }, + validatorConsolidationRequests.getConsolidationRequestsAndAdjustmentIncreaseEncodedCalls( + [], + [], + dashboardAddress, + 0, ), ) .to.be.revertedWithCustomError(validatorConsolidationRequests, "ZeroArgument") .withArgs("sourcePubkeys"); await expect( - delegateCaller.callDelegate( - validatorConsolidationRequestsAddress, - validatorConsolidationRequests.interface.encodeFunctionData("addConsolidationRequests", [ - [EMPTY_PUBKEYS], - [], - receiver.address, - stakingVault.address, - 0, - ]), - { value: 1n }, + validatorConsolidationRequests.getConsolidationRequestsAndAdjustmentIncreaseEncodedCalls( + [PUBKEY], + [], + dashboardAddress, + 0, ), ) .to.be.revertedWithCustomError(validatorConsolidationRequests, "ZeroArgument") .withArgs("targetPubkeys"); await expect( - delegateCaller.callDelegate( - validatorConsolidationRequestsAddress, - validatorConsolidationRequests.interface.encodeFunctionData("addConsolidationRequests", [ - [EMPTY_PUBKEYS], - [EMPTY_PUBKEYS], - receiver.address, - ethers.ZeroAddress, - 0, - ]), - { value: 1n }, + validatorConsolidationRequests.getConsolidationRequestsAndAdjustmentIncreaseEncodedCalls( + [PUBKEY], + [PUBKEY], + ethers.ZeroAddress, + 0, ), ) .to.be.revertedWithCustomError(validatorConsolidationRequests, "ZeroArgument") - .withArgs("stakingVault"); + .withArgs("dashboard"); }); }); - it("Should revert if called from non-delegatecall", async function () { + it("getConsolidationRequestsAndAdjustmentIncreaseEncodedCalls should revert if vault is not connected", async function () { + // index is 0 + await vaultHub.mock__setVaultConnection(stakingVault, { + owner: dashboardAddress, + shareLimit: 0, + vaultIndex: 0, + disconnectInitiatedTs: DISCONNECT_NOT_INITIATED, + reserveRatioBP: 0, + forcedRebalanceThresholdBP: 0, + infraFeeBP: 0, + liquidityFeeBP: 0, + reservationFeeBP: 0, + isBeaconDepositsManuallyPaused: false, + }); + await vaultHub.mock__setPendingDisconnect(false); + await expect( - validatorConsolidationRequests.addConsolidationRequests( - [EMPTY_PUBKEYS], - [EMPTY_PUBKEYS], - receiver.address, - stakingVault.address, + validatorConsolidationRequests.getConsolidationRequestsAndAdjustmentIncreaseEncodedCalls( + [PUBKEY], + [PUBKEY], + dashboardAddress, 1n, - { value: 1n }, ), - ).to.be.revertedWithCustomError(validatorConsolidationRequests, "NotDelegateCall"); - }); + ).to.be.revertedWithCustomError(validatorConsolidationRequests, "VaultNotConnected"); - it("Should revert if vault is not connected", async function () { - await vaultHub.mock__setVaultConnection(stakingVault.address, { - owner: actor.address, + // pending disconnect is true + await vaultHub.mock__setVaultConnection(stakingVault, { + owner: dashboardAddress, shareLimit: 0, - vaultIndex: 0, + vaultIndex: 1, disconnectInitiatedTs: DISCONNECT_NOT_INITIATED, reserveRatioBP: 0, forcedRebalanceThresholdBP: 0, @@ -216,22 +179,19 @@ describe("ValidatorConsolidationRequests.sol", () => { reservationFeeBP: 0, isBeaconDepositsManuallyPaused: false, }); + await vaultHub.mock__setPendingDisconnect(true); await expect( - delegateCaller.callDelegate( - validatorConsolidationRequestsAddress, - validatorConsolidationRequests.interface.encodeFunctionData("addConsolidationRequests", [ - [EMPTY_PUBKEYS], - [EMPTY_PUBKEYS], - receiver.address, - stakingVault.address, - 1n, - ]), - { value: 1n }, + validatorConsolidationRequests.getConsolidationRequestsAndAdjustmentIncreaseEncodedCalls( + [PUBKEY], + [PUBKEY], + dashboardAddress, + 1n, ), ).to.be.revertedWithCustomError(validatorConsolidationRequests, "VaultNotConnected"); - await vaultHub.mock__setVaultConnection(stakingVault.address, { + // owner is not the dashboard + await vaultHub.mock__setVaultConnection(stakingVault, { owner: actor.address, shareLimit: 0, vaultIndex: 1, @@ -243,426 +203,68 @@ describe("ValidatorConsolidationRequests.sol", () => { reservationFeeBP: 0, isBeaconDepositsManuallyPaused: false, }); - - await vaultHub.mock__setPendingDisconnect(true); + await vaultHub.mock__setPendingDisconnect(false); await expect( - delegateCaller.callDelegate( - validatorConsolidationRequestsAddress, - validatorConsolidationRequests.interface.encodeFunctionData("addConsolidationRequests", [ - [EMPTY_PUBKEYS], - [EMPTY_PUBKEYS], - receiver.address, - stakingVault.address, - 1n, - ]), - { value: 1n }, + validatorConsolidationRequests.getConsolidationRequestsAndAdjustmentIncreaseEncodedCalls( + [PUBKEY], + [PUBKEY], + dashboardAddress, + 1n, ), - ).to.be.revertedWithCustomError(validatorConsolidationRequests, "VaultNotConnected"); + ).to.be.revertedWithCustomError(validatorConsolidationRequests, "DashboardNotOwnerOfStakingVault"); }); - it("Should revert if array lengths do not match", async function () { + it("getConsolidationRequestsAndAdjustmentIncreaseEncodedCalls should revert if array lengths do not match", async function () { await expect( - delegateCaller.callDelegate( - validatorConsolidationRequestsAddress, - validatorConsolidationRequests.interface.encodeFunctionData("addConsolidationRequests", [ - [EMPTY_PUBKEYS], - [EMPTY_PUBKEYS, EMPTY_PUBKEYS], - receiver.address, - stakingVault.address, - 1n, - ]), - { value: 1n }, + validatorConsolidationRequests.getConsolidationRequestsAndAdjustmentIncreaseEncodedCalls( + [PUBKEY], + [PUBKEY, PUBKEY], + dashboardAddress, + 1n, ), ) .to.be.revertedWithCustomError(validatorConsolidationRequests, "MismatchingSourceAndTargetPubkeysCount") .withArgs(1, 2); }); - it("Should revert if not enough fee is sent", async function () { - const { sourcePubkeys, targetPubkeys, adjustmentIncrease } = generateConsolidationRequestPayload(1); - - await consolidationRequestPredeployed.mock__setFee(3n); // Set fee to 3 gwei - - const insufficientFee = 2n; - await expect( - delegateCaller.callDelegate( - validatorConsolidationRequestsAddress, - validatorConsolidationRequests.interface.encodeFunctionData("addConsolidationRequests", [ - sourcePubkeys, - targetPubkeys, - receiver.address, - stakingVault.address, - adjustmentIncrease, - ]), - { value: insufficientFee }, - ), - ).to.be.revertedWithCustomError(validatorConsolidationRequests, "InsufficientValidatorConsolidationFee"); - }); - - it("Should revert if pubkey is not 48 bytes", async function () { - // Invalid pubkey (only 2 bytes) - const invalidPubkeyHexString = "0x1234"; - const { sourcePubkeys, targetPubkeys, adjustmentIncrease } = generateConsolidationRequestPayload(1); - - const fee = await getFee(); - - await expect( - delegateCaller.callDelegate( - validatorConsolidationRequestsAddress, - validatorConsolidationRequests.interface.encodeFunctionData("addConsolidationRequests", [ - [invalidPubkeyHexString], - targetPubkeys, - receiver.address, - stakingVault.address, - adjustmentIncrease, - ]), - { value: fee }, - ), - ).to.be.revertedWithCustomError(validatorConsolidationRequests, "MalformedPubkeysArray"); - - await expect( - delegateCaller.callDelegate( - validatorConsolidationRequestsAddress, - validatorConsolidationRequests.interface.encodeFunctionData("addConsolidationRequests", [ - sourcePubkeys, - [invalidPubkeyHexString], - receiver.address, - stakingVault.address, - adjustmentIncrease, - ]), - { value: fee }, - ), - ).to.be.revertedWithCustomError(validatorConsolidationRequests, "MalformedTargetPubkey"); - }); - - it("Should revert if last pubkey not 48 bytes", async function () { - const validPubkey = - "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f"; - const invalidPubkey = "1234"; - const sourcePubkeys = [`0x${validPubkey}${invalidPubkey}`]; - const { targetPubkeys, adjustmentIncrease } = generateConsolidationRequestPayload(1); - - const fee = await getFee(); - - await expect( - delegateCaller.callDelegate( - validatorConsolidationRequestsAddress, - validatorConsolidationRequests.interface.encodeFunctionData("addConsolidationRequests", [ - sourcePubkeys, - targetPubkeys, - receiver.address, - stakingVault.address, - adjustmentIncrease, - ]), - { value: fee }, - ), - ).to.be.revertedWithCustomError(validatorConsolidationRequests, "MalformedPubkeysArray"); - }); - - it("Should revert if addition fails at the consolidation request contract", async function () { - const { sourcePubkeys, targetPubkeys, totalSourcePubkeysCount, adjustmentIncrease } = - generateConsolidationRequestPayload(1); - - const fee = (await getFee()) * BigInt(totalSourcePubkeysCount); - - // Set mock to fail on add - await consolidationRequestPredeployed.mock__setFailOnAddRequest(true); - - await expect( - delegateCaller.callDelegate( - validatorConsolidationRequestsAddress, - validatorConsolidationRequests.interface.encodeFunctionData("addConsolidationRequests", [ - sourcePubkeys, - targetPubkeys, - receiver.address, - stakingVault.address, - adjustmentIncrease, - ]), - { value: fee }, - ), - ).to.be.revertedWithCustomError(validatorConsolidationRequests, "ConsolidationRequestAdditionFailed"); - }); - - it("Should revert when balance is less than total consolidation fee", async function () { - const keysCount = 2; - const fee = 10n; - const balance = 19n; - - const { sourcePubkeys, targetPubkeys, adjustmentIncrease } = generateConsolidationRequestPayload(keysCount); - - await consolidationRequestPredeployed.mock__setFee(fee); - await setBalance(await validatorConsolidationRequests.getAddress(), balance); + it("getConsolidationRequestsAndAdjustmentIncreaseEncodedCalls should revert if the adjustment increase is less than the minimum validator balance", async function () { + const requestCount = 2; + const { sourcePubkeys, targetPubkeys, totalSourcePubkeysCount } = generateConsolidationRequestPayload(requestCount); await expect( - delegateCaller.callDelegate( - validatorConsolidationRequestsAddress, - validatorConsolidationRequests.interface.encodeFunctionData("addConsolidationRequests", [ - sourcePubkeys, - targetPubkeys, - receiver.address, - stakingVault.address, - adjustmentIncrease, - ]), - { value: fee }, - ), - ).to.be.revertedWithCustomError(validatorConsolidationRequests, "InsufficientValidatorConsolidationFee"); - }); - - it("Should accept consolidation requests when the provided fee matches the exact required amount", async function () { - const requestCount = 1; - const { sourcePubkeys, targetPubkeys, totalSourcePubkeysCount, adjustmentIncrease } = - generateConsolidationRequestPayload(requestCount); - - const fee = 3n; - await consolidationRequestPredeployed.mock__setFee(fee); - - await testEIP7251Mock( - () => - delegateCaller.callDelegate( - validatorConsolidationRequestsAddress, - validatorConsolidationRequests.interface.encodeFunctionData("addConsolidationRequests", [ - sourcePubkeys, - targetPubkeys, - receiver.address, - stakingVault.address, - adjustmentIncrease, - ]), - { value: fee * BigInt(totalSourcePubkeysCount) }, - ), - await delegateCaller.getAddress(), - sourcePubkeys, - targetPubkeys, - fee, - ); - - // Check extremely high fee - const highFee = ether("10"); - await consolidationRequestPredeployed.mock__setFee(highFee); - - await testEIP7251Mock( - () => - delegateCaller.callDelegate( - validatorConsolidationRequestsAddress, - validatorConsolidationRequests.interface.encodeFunctionData("addConsolidationRequests", [ - sourcePubkeys, - targetPubkeys, - receiver.address, - stakingVault.address, - adjustmentIncrease, - ]), - { value: highFee * BigInt(totalSourcePubkeysCount) }, - ), - await delegateCaller.getAddress(), - sourcePubkeys, - targetPubkeys, - highFee, - ); - }); - - it("Should accept consolidation requests when the provided fee exceeds the required amount", async function () { - const requestCount = 1; - const { sourcePubkeys, targetPubkeys, totalSourcePubkeysCount, adjustmentIncrease } = - generateConsolidationRequestPayload(requestCount); - - await consolidationRequestPredeployed.mock__setFee(3n); - const excessFee = 4n; - - await testEIP7251Mock( - () => - delegateCaller.callDelegate( - validatorConsolidationRequestsAddress, - validatorConsolidationRequests.interface.encodeFunctionData("addConsolidationRequests", [ - sourcePubkeys, - targetPubkeys, - receiver.address, - stakingVault.address, - adjustmentIncrease, - ]), - { value: excessFee * BigInt(totalSourcePubkeysCount) }, - ), - await delegateCaller.getAddress(), - sourcePubkeys, - targetPubkeys, - 3n, - ); - - // Check when the provided fee extremely exceeds the required amount - const extremelyHighFee = ether("10"); - await setBalance( - await validatorConsolidationRequests.getAddress(), - extremelyHighFee * BigInt(totalSourcePubkeysCount), - ); - - await testEIP7251Mock( - () => - delegateCaller.callDelegate( - validatorConsolidationRequestsAddress, - validatorConsolidationRequests.interface.encodeFunctionData("addConsolidationRequests", [ - sourcePubkeys, - targetPubkeys, - receiver.address, - stakingVault.address, - adjustmentIncrease, - ]), - { value: extremelyHighFee * BigInt(totalSourcePubkeysCount) }, - ), - await delegateCaller.getAddress(), - sourcePubkeys, - targetPubkeys, - 3n, - ); - }); - - it("Should correctly deduct the exact fee amount from the contract balance", async function () { - const requestCount = 3; - const { sourcePubkeys, targetPubkeys, totalSourcePubkeysCount, adjustmentIncrease } = - generateConsolidationRequestPayload(requestCount); - - const fee = 4n; - await consolidationRequestPredeployed.mock__setFee(fee); - - const expectedTotalConsolidationFee = fee * BigInt(totalSourcePubkeysCount); - const initialBalance = await ethers.provider.getBalance(actor.address); - const tx = await delegateCaller.callDelegate( - validatorConsolidationRequestsAddress, - validatorConsolidationRequests.interface.encodeFunctionData("addConsolidationRequests", [ + validatorConsolidationRequests.getConsolidationRequestsAndAdjustmentIncreaseEncodedCalls( sourcePubkeys, targetPubkeys, - receiver.address, - stakingVault.address, - adjustmentIncrease, - ]), - { value: expectedTotalConsolidationFee }, - ); - const receipt = await tx.wait(); - - if (!receipt) { - expect(false).to.equal(true); - } - - const gasUsed = receipt!.gasUsed; - const gasPrice = receipt!.gasPrice; - const totalCost = BigInt(gasUsed) * gasPrice + expectedTotalConsolidationFee; - - expect(await ethers.provider.getBalance(actor.address)).to.equal(initialBalance - totalCost); - }); - - it("Should transfer the total calculated fee to the EIP-7251 consolidation request contract", async function () { - const requestCount = 3; - const { sourcePubkeys, targetPubkeys, totalSourcePubkeysCount, adjustmentIncrease } = - generateConsolidationRequestPayload(requestCount); - - const fee = 3n; - await consolidationRequestPredeployed.mock__setFee(fee); - const expectedTotalConsolidationFee = fee * BigInt(totalSourcePubkeysCount); - const initialBalance = await getConsolidationRequestPredeployedContractBalance(); - await delegateCaller.callDelegate( - validatorConsolidationRequestsAddress, - validatorConsolidationRequests.interface.encodeFunctionData("addConsolidationRequests", [ - sourcePubkeys, - targetPubkeys, - receiver.address, - stakingVault.address, - adjustmentIncrease, - ]), - { value: expectedTotalConsolidationFee }, - ); - expect(await getConsolidationRequestPredeployedContractBalance()).to.equal( - initialBalance + expectedTotalConsolidationFee, - ); + dashboardAddress, + BigInt(totalSourcePubkeysCount) * ether("16") - 1n, + ), + ).to.be.revertedWithCustomError(validatorConsolidationRequests, "InvalidAllSourceValidatorBalancesWei"); }); - it("Should ensure consolidation requests are encoded as expected with two 48-byte pubkeys", async function () { - const requestCount = 16; - const { sourcePubkeys, targetPubkeys, totalSourcePubkeysCount, adjustmentIncrease } = - generateConsolidationRequestPayload(requestCount); - - const fee = 3n; - await consolidationRequestPredeployed.mock__setFee(fee); - const expectedTotalConsolidationFee = fee * BigInt(totalSourcePubkeysCount); - - const tx = await delegateCaller.callDelegate( - validatorConsolidationRequestsAddress, - validatorConsolidationRequests.interface.encodeFunctionData("addConsolidationRequests", [ + it("Should get correct encoded calls for consolidation requests and adjustment increase", async function () { + const { sourcePubkeys, targetPubkeys, adjustmentIncrease } = generateConsolidationRequestPayload(1); + const { adjustmentIncreaseEncodedCall, consolidationRequestEncodedCalls } = + await validatorConsolidationRequests.getConsolidationRequestsAndAdjustmentIncreaseEncodedCalls( sourcePubkeys, targetPubkeys, - receiver.address, - stakingVault.address, + dashboardAddress, adjustmentIncrease, - ]), - { value: expectedTotalConsolidationFee }, - ); - const receipt = await tx.wait(); - - const events = findEIP7251MockEvents(receipt!); - expect(events.length).to.equal(totalSourcePubkeysCount); - - for (let i = 0; i < requestCount; i++) { - const pubkeysCount = Math.floor(sourcePubkeys[i].length / KEY_LENGTH); - for (let j = 0; j < pubkeysCount; j++) { - const expectedSourcePubkey = sourcePubkeys[i].slice(j * KEY_LENGTH, (j + 1) * KEY_LENGTH); - const encodedRequest = events[i * pubkeysCount + j].args[0]; - - expect(encodedRequest.length).to.equal(2 + KEY_LENGTH * 2 + KEY_LENGTH * 2); - - expect(encodedRequest.slice(0, 2)).to.equal("0x"); - const sourcePubkeyFromEvent = "0x" + encodedRequest.slice(2, KEY_LENGTH * 2 + 2); - expect(sourcePubkeyFromEvent).to.equal(ethers.hexlify(expectedSourcePubkey)); - - const targetPubkeyFromEvent = "0x" + encodedRequest.slice(KEY_LENGTH * 2 + 2, KEY_LENGTH * 4 + 2); - expect(targetPubkeyFromEvent).to.equal(ethers.hexlify(targetPubkeys[i])); + ); + let k = 0; + for (let i = 0; i < targetPubkeys.length; i++) { + const sourcePubkeysCount = sourcePubkeys[i].length / KEY_LENGTH; + for (let j = 0; j < sourcePubkeysCount; j++) { + const targetPubkey = targetPubkeys[i]; + const sourcePubkey = sourcePubkeys[i].slice(j * KEY_LENGTH, (j + 1) * KEY_LENGTH); + const concatenatedKeys = ethers.hexlify(sourcePubkey) + ethers.hexlify(targetPubkey).slice(2); + expect(consolidationRequestEncodedCalls[k]).to.equal(concatenatedKeys); + expect(consolidationRequestEncodedCalls[k].length).to.equal(2 + KEY_LENGTH * 2 + KEY_LENGTH * 2); + k++; } } - }); - - it("Should not call the dashboard if the adjustment increase is 0", async function () { - const requestCount = 1; - const { sourcePubkeys, targetPubkeys, totalSourcePubkeysCount } = generateConsolidationRequestPayload(requestCount); - - const fee = 3n; - await consolidationRequestPredeployed.mock__setFee(fee); - - const tx = await delegateCaller.callDelegate( - validatorConsolidationRequestsAddress, - validatorConsolidationRequests.interface.encodeFunctionData("addConsolidationRequests", [ - sourcePubkeys, - targetPubkeys, - receiver.address, - stakingVault.address, - 0, - ]), - { value: fee * BigInt(totalSourcePubkeysCount) }, - ); - const receipt = await tx.wait(); - - const events = findDashboardMockEvents(receipt!); - expect(events.length).to.equal(0); - }); - - it("Should ensure the dashboard is called with the correct adjustment increases", async function () { - const requestCount = 3; - const { sourcePubkeys, targetPubkeys, totalSourcePubkeysCount, adjustmentIncrease } = - generateConsolidationRequestPayload(requestCount); - - const fee = 3n; - await consolidationRequestPredeployed.mock__setFee(fee); - - const tx = await delegateCaller.callDelegate( - validatorConsolidationRequestsAddress, - validatorConsolidationRequests.interface.encodeFunctionData("addConsolidationRequests", [ - sourcePubkeys, - targetPubkeys, - receiver.address, - stakingVault.address, - adjustmentIncrease, - ]), - { value: fee * BigInt(totalSourcePubkeysCount) }, - ); - const receipt = await tx.wait(); - - const events = findDashboardMockEvents(receipt!); - expect(events.length).to.equal(1); - expect(events[0].args._amount).to.equal(adjustmentIncrease); + const iface = new ethers.Interface(["function increaseRewardsAdjustment(uint256)"]); + const calldata = iface.encodeFunctionData("increaseRewardsAdjustment", [adjustmentIncrease]); + expect(adjustmentIncreaseEncodedCall).to.equal(calldata); }); }); diff --git a/test/integration/vaults/validator-consolidation-requests.integration.ts b/test/integration/vaults/validator-consolidation-requests.integration.ts index 494bf60f1..e7e1a116e 100644 --- a/test/integration/vaults/validator-consolidation-requests.integration.ts +++ b/test/integration/vaults/validator-consolidation-requests.integration.ts @@ -1,12 +1,10 @@ import { expect } from "chai"; -import { ContractTransactionReceipt } from "ethers"; import { ethers } from "hardhat"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; -import { Dashboard, StakingVault } from "typechain-types"; +import { Dashboard } from "typechain-types"; -import { EIP7251_ADDRESS } from "lib"; import { createVaultWithDashboard, getProtocolContext, ProtocolContext } from "lib/protocol"; import { generateConsolidationRequestPayload } from "test/0.8.25/vaults/consolidation/consolidationHelper"; @@ -20,18 +18,16 @@ describe("Integration: ValidatorConsolidationRequests", () => { let originalSnapshot: string; let owner: HardhatEthersSigner; - let stranger: HardhatEthersSigner; let nodeOperator: HardhatEthersSigner; let dashboard: Dashboard; - let stakingVault: StakingVault; before(async () => { ctx = await getProtocolContext(); originalSnapshot = await Snapshot.take(); - [owner, stranger, nodeOperator] = await ethers.getSigners(); + [owner, nodeOperator] = await ethers.getSigners(); - ({ dashboard, stakingVault } = await createVaultWithDashboard( + ({ dashboard } = await createVaultWithDashboard( ctx, ctx.contracts.stakingVaultFactory, owner, @@ -45,55 +41,41 @@ describe("Integration: ValidatorConsolidationRequests", () => { afterEach(async () => await Snapshot.restore(snapshot)); after(async () => await Snapshot.restore(originalSnapshot)); - it("Consolidates validators by calling max effective balance increaser through contract using delegatecall", async () => { + it("Consolidates validators by calling addConsolidationRequestsAndIncreaseRewardsAdjustment", async () => { const { validatorConsolidationRequests } = ctx.contracts; - const payload = generateConsolidationRequestPayload(1); - const { sourcePubkeys, targetPubkeys } = payload; - - const delegateCaller = await ethers.deployContract("DelegateCaller", [], { from: owner }); - const delegateCallerAddress = await delegateCaller.getAddress(); - const stakingVaultAddress = await stakingVault.getAddress(); - - // send empty tx to EIP7251 to get fee per request - const feeForRequest = BigInt(await ethers.provider.call({ to: EIP7251_ADDRESS, data: "0x" })); - const totalFee = BigInt(payload.totalSourcePubkeysCount) * feeForRequest; + const { sourcePubkeys, targetPubkeys, adjustmentIncrease } = generateConsolidationRequestPayload(1); + const dashboardAddress = await dashboard.getAddress(); await dashboard .connect(nodeOperator) - .grantRole(await dashboard.NODE_OPERATOR_REWARDS_ADJUST_ROLE(), delegateCallerAddress); + .grantRole(await dashboard.NODE_OPERATOR_REWARDS_ADJUST_ROLE(), validatorConsolidationRequests); - const tx = await delegateCaller.callDelegate( - validatorConsolidationRequests.address, - validatorConsolidationRequests.interface.encodeFunctionData("addConsolidationRequests", [ + const { adjustmentIncreaseEncodedCall, consolidationRequestEncodedCalls } = + await validatorConsolidationRequests.getConsolidationRequestsAndAdjustmentIncreaseEncodedCalls( sourcePubkeys, targetPubkeys, - stranger.address, - stakingVaultAddress, - payload.adjustmentIncrease, - ]), - { value: totalFee }, - ); - const receipt = (await tx.wait()) as ContractTransactionReceipt; - - const totalPubkeysCount = sourcePubkeys.reduce( - (acc, pubkeys) => acc + BigInt(Math.floor(pubkeys.length / KEY_LENGTH)), - 0n, - ); - - const eip7251Events = receipt.logs.filter((log) => log.address === EIP7251_ADDRESS); - expect(eip7251Events.length).to.equal(totalPubkeysCount); + dashboardAddress, + adjustmentIncrease, + ); // verify mainnet format of the events, on scratch we use a mock, so no need to verify anything except the number if (!ctx.isScratch) { - for (let i = 0; i < sourcePubkeys.length; i++) { - const pubkeysCount = Math.floor(sourcePubkeys[i].length / KEY_LENGTH); - for (let j = 0; j < pubkeysCount; j++) { - const expectedSourcePubkey = sourcePubkeys[i].slice(j * KEY_LENGTH, (j + 1) * KEY_LENGTH); - const result = ethers.concat([delegateCallerAddress, expectedSourcePubkey, targetPubkeys[i]]); - expect(eip7251Events[i * pubkeysCount + j].data).to.equal(result); + let k = 0; + for (let i = 0; i < targetPubkeys.length; i++) { + const sourcePubkeysCount = sourcePubkeys[i].length / KEY_LENGTH; + for (let j = 0; j < sourcePubkeysCount; j++) { + const targetPubkey = targetPubkeys[i]; + const sourcePubkey = sourcePubkeys[i].slice(j * KEY_LENGTH, (j + 1) * KEY_LENGTH); + const concatenatedKeys = ethers.hexlify(sourcePubkey) + ethers.hexlify(targetPubkey).slice(2); + expect(consolidationRequestEncodedCalls[k]).to.equal(concatenatedKeys); + expect(consolidationRequestEncodedCalls[k].length).to.equal(2 + KEY_LENGTH * 2 + KEY_LENGTH * 2); + k++; } } + const iface = new ethers.Interface(["function increaseRewardsAdjustment(uint256)"]); + const calldata = iface.encodeFunctionData("increaseRewardsAdjustment", [adjustmentIncrease]); + expect(adjustmentIncreaseEncodedCall).to.equal(calldata); } }); });