diff --git a/script/el-exits/pectra-executions/Consolidate-validators.s.sol b/script/el-exits/pectra-executions/Consolidate-validators.s.sol new file mode 100644 index 00000000..baaa0ede --- /dev/null +++ b/script/el-exits/pectra-executions/Consolidate-validators.s.sol @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import "../../../src/EtherFiTimelock.sol"; +import "../../../src/interfaces/IEtherFiNode.sol"; +import "../../../src/EtherFiNodesManager.sol"; +import {IEigenPod, IEigenPodTypes} from "../../../src/eigenlayer-interfaces/IEigenPod.sol"; +import "forge-std/Script.sol"; +import "forge-std/console2.sol"; + +/** +forge script script/el-exits/pectra-executions/Consolidate-validators.s.sol:ConsolidateValidators --fork-url -vvvv +*/ + +contract ConsolidateValidators is Script { + EtherFiTimelock etherFiTimelock = + EtherFiTimelock(payable(0x9f26d4C958fD811A1F59B01B86Be7dFFc9d20761)); + EtherFiTimelock etherFiOperatingTimelock = EtherFiTimelock(payable(0xcD425f44758a08BaAB3C4908f3e3dE5776e45d7a)); + + //-------------------------------------------------------------------------------------- + //------------------------- Existing Users/Proxies ------------------------------------- + //-------------------------------------------------------------------------------------- + address constant ETHERFI_NODES_MANAGER_ADDRESS = + 0x8B71140AD2e5d1E7018d2a7f8a288BD3CD38916F; + + EtherFiNodesManager etherFiNodesManager = EtherFiNodesManager(payable(ETHERFI_NODES_MANAGER_ADDRESS)); + + address constant ETHERFI_NODES_MANAGER_ADMIN_ROLE = 0x2aCA71020De61bb532008049e1Bd41E451aE8AdC; + + address constant EIGEN_POD_ADDRESS = 0x9563794BEf554667f4650eaAe192FfeC1C656C23; // 20 validators + + address constant ETHERFI_OPERATING_ADMIN = + 0x2aCA71020De61bb532008049e1Bd41E451aE8AdC; + address constant TIMELOCK_CONTROLLER = 0xcdd57D11476c22d265722F68390b036f3DA48c21; + + address constant EL_TRIGGER_EXITER = 0x12582A27E5e19492b4FcD194a60F8f5e1aa31B0F; + + uint256 MIN_DELAY_OPERATING_TIMELOCK = 28800; // 8 hours + + //consolidate the following validators: + bytes constant PK_54043 = hex"8014c4704f081bd4b8470cb93722601095a314c3db7ccf79c129189d01c432db968a64131f23a94c8ff1e280500ae3d3"; // linked in EtherfiNodesManager + bytes constant PK_54045 = hex"820cf0499d0d908d10c19d85027ed4077322096cd4fb322a763c3bf5e4eb70db30b44ef1284e6fb713421a195735d942"; + // bytes constant PK_54041 = hex"87d657860a8b0450d7e700d60aa88a42ee5e6fdedeeb25dd3aee7e1112697f837b4b2e94d37167a900921e6b90c7f3ac"; + + function run() public { + console2.log("================================================"); + console2.log("======================== Running Consolidate Validators ========================"); + console2.log("================================================"); + console2.log(""); + + // uint256[] memory legacyIdsForOneValidator = new uint256[](1); + // legacyIdsForOneValidator[0] = 54043; + // bytes[] memory pubkeysForOneValidator = new bytes[](1); + // pubkeysForOneValidator[0] = PK_54043; + + // vm.prank(address(etherFiOperatingTimelock)); + // etherFiNodesManager.linkLegacyValidatorIds(legacyIdsForOneValidator, pubkeysForOneValidator); + // vm.stopPrank(); + // console2.log("Linking legacy validator ids for one validator complete"); + + vm.deal(address(etherFiOperatingTimelock), 1 ether); + scheduleConsolidation(); + } + + function scheduleConsolidation() public { + ( , IEigenPod pod) = _resolvePod(PK_54043); + console2.log("Pod address:", address(pod)); + bytes[] memory validatorPubkeys = new bytes[](2); + validatorPubkeys[0] = PK_54043; + validatorPubkeys[1] = PK_54045; + IEigenPodTypes.ConsolidationRequest[] memory reqs = _consolidationRequestsFromPubkeys(validatorPubkeys); + + uint256 feePer = pod.getConsolidationRequestFee(); + uint256 n = reqs.length; + uint256 valueToSend = feePer * n; + console2.log("Value to send:", valueToSend); + console2.log("Fee per:", feePer); + console2.log("Number of requests:", n); + + address[] memory targets = new address[](1); + targets[0] = address(etherFiNodesManager); + uint256[] memory values = new uint256[](1); + values[0] = valueToSend; + bytes[] memory data = new bytes[](1); + data[0] = abi.encodeWithSelector( + etherFiNodesManager.requestConsolidation.selector, + reqs + ); + + bytes32 timelockSalt = keccak256(abi.encode(targets, data, block.number)); + bytes memory scheduleCalldata = abi.encodeWithSelector( + etherFiOperatingTimelock.scheduleBatch.selector, + targets, + values, + data, + bytes32(0), // predecessor + timelockSalt, + MIN_DELAY_OPERATING_TIMELOCK // minDelay + ); + console2.log("Scheduled consolidation request Tx"); + console2.log("================================================"); + console2.logBytes(scheduleCalldata); + console2.log("================================================"); + console2.log(""); + + bytes memory executeCalldata = abi.encodeWithSelector( + etherFiOperatingTimelock.executeBatch.selector, + targets, + values, + data, + bytes32(0), // predecessor + timelockSalt, + MIN_DELAY_OPERATING_TIMELOCK // minDelay + ); + console2.log("Executed consolidation request Tx"); + console2.log("================================================"); + console2.logBytes(executeCalldata); + console2.log("================================================"); + console2.log(""); + + vm.prank(address(ETHERFI_NODES_MANAGER_ADMIN_ROLE)); + etherFiOperatingTimelock.scheduleBatch(targets, values, data, bytes32(0), timelockSalt, MIN_DELAY_OPERATING_TIMELOCK); + vm.stopPrank(); + vm.warp(block.timestamp + MIN_DELAY_OPERATING_TIMELOCK + 1); // +1 to ensure it's past the delay + vm.prank(address(ETHERFI_NODES_MANAGER_ADMIN_ROLE)); + etherFiOperatingTimelock.executeBatch(targets, values, data, bytes32(0), timelockSalt); + vm.stopPrank(); + } + + function _consolidationRequestsFromPubkeys(bytes[] memory pubkeys) + internal + pure + returns (IEigenPodTypes.ConsolidationRequest[] memory reqs) { + reqs = new IEigenPodTypes.ConsolidationRequest[](pubkeys.length); + for (uint256 i = 0; i < pubkeys.length; ++i) { + reqs[i] = IEigenPodTypes.ConsolidationRequest({ + srcPubkey: pubkeys[i], + targetPubkey: PK_54043 // same pod consolidation + }); + } + } + + function _resolvePod(bytes memory pubkey) internal view returns (IEtherFiNode etherFiNode, IEigenPod pod) { + bytes32 pkHash = etherFiNodesManager.calculateValidatorPubkeyHash(pubkey); + etherFiNode = etherFiNodesManager.etherFiNodeFromPubkeyHash(pkHash); + pod = etherFiNode.getEigenPod(); + require(address(pod) != address(0), "test: node has no pod"); + } +} \ No newline at end of file diff --git a/script/el-exits/pectra-executions/EL-withdrawal-for-node.s.sol b/script/el-exits/pectra-executions/EL-withdrawal-for-node.s.sol new file mode 100644 index 00000000..4200e03e --- /dev/null +++ b/script/el-exits/pectra-executions/EL-withdrawal-for-node.s.sol @@ -0,0 +1,220 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import "../../../src/EtherFiTimelock.sol"; +import "../../../src/interfaces/IEtherFiNode.sol"; +// import {IEtherFiNodesManager} from "../../../src/interfaces/IEtherFiNodesManager.sol"; +import "../../../src/EtherFiNodesManager.sol"; +import {IEigenPod, IEigenPodTypes} from "../../../src/eigenlayer-interfaces/IEigenPod.sol"; +import "forge-std/Script.sol"; +import "forge-std/console2.sol"; + +/** +forge script script/el-exits/pectra-executions/EL-withdrawal-for-node.s.sol:ELWithdrawalForNode --fork-url -vvvv +*/ + +contract ELWithdrawalForNode is Script { + EtherFiTimelock etherFiTimelock = + EtherFiTimelock(payable(0x9f26d4C958fD811A1F59B01B86Be7dFFc9d20761)); + EtherFiTimelock etherFiOperatingTimelock = EtherFiTimelock(payable(0xcD425f44758a08BaAB3C4908f3e3dE5776e45d7a)); + + //-------------------------------------------------------------------------------------- + //------------------------- Existing Users/Proxies ------------------------------------- + //-------------------------------------------------------------------------------------- + address constant ETHERFI_NODES_MANAGER_ADDRESS = + 0x8B71140AD2e5d1E7018d2a7f8a288BD3CD38916F; + + EtherFiNodesManager etherFiNodesManager = EtherFiNodesManager(payable(ETHERFI_NODES_MANAGER_ADDRESS)); + + address constant ETHERFI_NODES_MANAGER_ADMIN_ROLE = 0x2aCA71020De61bb532008049e1Bd41E451aE8AdC; + + address constant EIGEN_POD_ADDRESS = 0x9563794BEf554667f4650eaAe192FfeC1C656C23; // 20 validators + + address constant ETHERFI_OPERATING_ADMIN = + 0x2aCA71020De61bb532008049e1Bd41E451aE8AdC; + address constant TIMELOCK_CONTROLLER = 0xcdd57D11476c22d265722F68390b036f3DA48c21; + + address constant EL_TRIGGER_EXITER = 0x12582A27E5e19492b4FcD194a60F8f5e1aa31B0F; + + uint256 MIN_DELAY_OPERATING_TIMELOCK = 28800; // 8 hours + uint256 MIN_DELAY_TIMELOCK = 259200; // 72 hours + + bytes constant PK_54043 = hex"8014c4704f081bd4b8470cb93722601095a314c3db7ccf79c129189d01c432db968a64131f23a94c8ff1e280500ae3d3"; + bytes constant PK_54045 = hex"820cf0499d0d908d10c19d85027ed4077322096cd4fb322a763c3bf5e4eb70db30b44ef1284e6fb713421a195735d942"; + bytes constant PK_54041 = hex"87d657860a8b0450d7e700d60aa88a42ee5e6fdedeeb25dd3aee7e1112697f837b4b2e94d37167a900921e6b90c7f3ac"; + bytes constant PK_54050 = hex"8838691e23c67fc8a3021d1dfe49abdbd803469dd3471960f973fa6ed08eb180104d1ddd66938b4dc6264f04e489c6e7"; + bytes constant PK_54040 = hex"895186127b42e8202949b9fff00c112df8070c25d96fbe4238fab3a133c49f4a6401dc55828ad5b89093af7602487a2c"; + bytes constant PK_54049 = hex"8c64ee1865c48b01ef1af561c41b0e3b363f2a83d99b835d6312638b380d2f698c78f2d405133d8b2aec8c439820f50e"; + bytes constant PK_54044 = hex"917f7c7916ee8ba15289810ea118d458c8b4fefb88caa5bef970aa972f7605ed47b54451d9b7c5128c484cf3ce989c1c"; + bytes constant PK_54048 = hex"9310d317f4df0b52f8fa2191b6826e93ccd613b2fb4f4e1463c5b112be9102b689ad628c717f201ededf18555d775b43"; + bytes constant PK_54035 = hex"936dfac756c52488607b7f07d8905cbff4ae647aaeea75645f28f73a46ae3f6cc9dd55be57ea8ebbf1052197be97bb28"; + bytes constant PK_54053 = hex"a160a281269a464e89ca8cdd0521200232c2d37140f218daaa9393aa008c6b3b5ed48040a3e4f46b059532c6b07af360"; + bytes constant PK_54039 = hex"a3b265189b2cb571a2d48043a7da96c918df480983c89f86480a1ae5af8a6aa14c381ef091387c201fe5c886b8c27755"; + bytes constant PK_54051 = hex"a3e60bce7da064113757a74572909242403654e11ad48c9ee646ea926c026ebf9e60bde5054117b0c74600762693f90c"; + bytes constant PK_54047 = hex"a3fb69cd6685e6df16d9dc6e5a0fd174d4460e393bf151b62d4ed62fc4524125f665c3ef91e8673154ce30a934562127"; + bytes constant PK_54042 = hex"a481a9553e9cd70a28685c3c86da72cb7cc47fc4132aa79609bc121f3d1c1fe693fb82b70ab1792267837f03fd8b480a"; + bytes constant PK_54036 = hex"a702539fb750fa7524a382f0a80572161786be488e77cd680d41d3ba47acd2bd9218036e218e61f7a397b25f6fd473b9"; + bytes constant PK_54052 = hex"a9e0d47715165646896c9dd1eaad3822824340f9432978fe2220d1d849509711fa822080d69a5604a7df372dcd7290ff"; + bytes constant PK_54037 = hex"ab82fc4c1eee2b1c26d3adcf6a1240beb18aafe86ac0ca897cd94611eefae7a88e3b3c80dd768b6f2da9326ecebfcc54"; + bytes constant PK_54054 = hex"b72d2761774961ee47a8b9a917110a70425b4500909ccd972f72c7219e3f0c76f086de76cb80a4598ecb44245effeaa9"; + bytes constant PK_54046 = hex"b7ca7c4dccc74cc17a1b2325a95e690aca40fd4d2980bd65180d98bf0da447cf84254da073c28c1b1efc959611e5cfe1"; + bytes constant PK_54038 = hex"b974967fa38e82a9983019ccdb754df1c176edf5e542ec7377c23bcce730c094007fb84d9816f5fb22f09b725e46d99a"; + + function run() public { + console2.log("================================================"); + console2.log("======================== Running EL Withdrawal For Node Transactions ========================"); + console2.log("================================================"); + console2.log(""); + + // vm.startBroadcast(ETHERFI_OPERATING_ADMIN); + // vm.prank(ETHERFI_OPERATING_ADMIN); + linkLegacyValidatorIds(); + // vm.stopPrank(); + // vm.stopBroadcast(); + + // vm.startBroadcast(EL_TRIGGER_EXITER); + executeELWithdrawalForNodeTransactions(); + // vm.stopBroadcast(); + } + + function linkLegacyValidatorIds() public { + uint256[] memory legacyIdsForOneValidator = new uint256[](1); + legacyIdsForOneValidator[0] = 54043; + bytes[] memory pubkeysForOneValidator = new bytes[](1); + pubkeysForOneValidator[0] = PK_54043; + + address[] memory targets = new address[](1); + targets[0] = address(etherFiNodesManager); + uint256[] memory values = new uint256[](1); + values[0] = 0; + bytes[] memory data = new bytes[](1); + data[0] = abi.encodeWithSelector( + etherFiNodesManager.linkLegacyValidatorIds.selector, + legacyIdsForOneValidator, + pubkeysForOneValidator + ); + + bytes32 timelockSalt = keccak256( + abi.encode(targets, data, block.number) + ); + + bytes memory scheduleCalldata = abi.encodeWithSelector( + etherFiOperatingTimelock.scheduleBatch.selector, + targets, + values, + data, + bytes32(0), //predecessor + timelockSalt, + MIN_DELAY_OPERATING_TIMELOCK + ); + console2.log("Scheduled linkLegacyValidatorIds Tx"); + console2.log("================================================"); + console2.logBytes(scheduleCalldata); + console2.log("================================================"); + console2.log(""); + + bytes memory executeCalldata = abi.encodeWithSelector( + etherFiOperatingTimelock.executeBatch.selector, + targets, + values, + data, + bytes32(0), //predecessor + timelockSalt, + MIN_DELAY_OPERATING_TIMELOCK + ); + console2.log("Executed linkLegacyValidatorIds Tx"); + console2.log("================================================"); + console2.logBytes(executeCalldata); + console2.log("================================================"); + console2.log(""); + + vm.prank(address(ETHERFI_OPERATING_ADMIN)); + etherFiNodesManager.linkLegacyValidatorIds(legacyIdsForOneValidator, pubkeysForOneValidator); + vm.stopPrank(); + + // uncomment to run against fork + // console2.log("=== SCHEDULING BATCH ==="); + // // console2.log("Current timestamp:", block.timestamp); + // etherFiOperatingTimelock.scheduleBatch(targets, values, data, bytes32(0), timelockSalt, MIN_DELAY_OPERATING_TIMELOCK); + + // console2.log("=== FAST FORWARDING TIME ==="); + // vm.warp(block.timestamp + MIN_DELAY_OPERATING_TIMELOCK + 1); // +1 to ensure it's past the delay + // // console2.log("New timestamp:", block.timestamp); + // etherFiOperatingTimelock.executeBatch(targets, values, data, bytes32(0), timelockSalt); + } + + function executeELWithdrawalForNodeTransactions() public { + console2.log("Executing EL Withdrawal For Node Transactions"); + + // STEP 1: GET EIGEN POD + IEigenPod pod = IEigenPod(EIGEN_POD_ADDRESS); + + // STEP 2: GET WITHDRAWAL REQUESTS + IEigenPodTypes.WithdrawalRequest[] memory withdrawalRequests = _getWithdrawalsRequests(); + + // STEP 3: GET WITHDRAWAL REQUEST FEE + uint256 feePer = pod.getWithdrawalRequestFee(); + uint256 n = withdrawalRequests.length; + uint256 valueToSend = feePer * n; + + // STEP 4: EXECUTE EL WITHDRAWAL FOR NODE TRANSACTIONS + vm.prank(address(EL_TRIGGER_EXITER)); + etherFiNodesManager.requestExecutionLayerTriggeredWithdrawal{value: valueToSend}(withdrawalRequests); + vm.stopPrank(); + } + + function _getWithdrawalsRequests() public pure returns (IEigenPodTypes.WithdrawalRequest[] memory withdrawalRequests) { + bytes[] memory pubkeys = new bytes[](20); + uint64[] memory amountsGwei = new uint64[](20); + + pubkeys[0] = PK_54043; + amountsGwei[0] = 0; + pubkeys[1] = PK_54045; + amountsGwei[1] = 0; + pubkeys[2] = PK_54041; + amountsGwei[2] = 0; + pubkeys[3] = PK_54050; + amountsGwei[3] = 0; + pubkeys[4] = PK_54040; + amountsGwei[4] = 0; + pubkeys[5] = PK_54049; + amountsGwei[5] = 0; + pubkeys[6] = PK_54044; + amountsGwei[6] = 0; + pubkeys[7] = PK_54048; + amountsGwei[7] = 0; + pubkeys[8] = PK_54035; + amountsGwei[8] = 0; + pubkeys[9] = PK_54053; + amountsGwei[9] = 0; + pubkeys[10] = PK_54039; + amountsGwei[10] = 0; + pubkeys[11] = PK_54051; + amountsGwei[11] = 0; + pubkeys[12] = PK_54047; + amountsGwei[12] = 0; + pubkeys[13] = PK_54042; + amountsGwei[13] = 0; + pubkeys[14] = PK_54036; + amountsGwei[14] = 0; + pubkeys[15] = PK_54052; + amountsGwei[15] = 0; + pubkeys[16] = PK_54037; + amountsGwei[16] = 0; + pubkeys[17] = PK_54054; + amountsGwei[17] = 0; + pubkeys[18] = PK_54046; + amountsGwei[18] = 0; + pubkeys[19] = PK_54038; + amountsGwei[19] = 0; + + withdrawalRequests = new IEigenPodTypes.WithdrawalRequest[](pubkeys.length); + for (uint256 i = 0; i < pubkeys.length; i++) { + withdrawalRequests[i] = IEigenPodTypes.WithdrawalRequest({ + pubkey: pubkeys[i], + amountGwei: amountsGwei[i] + }); + } + return withdrawalRequests; + } +} \ No newline at end of file diff --git a/test/pectra-fork-tests/EL-withdrawals.t.sol b/test/pectra-fork-tests/EL-withdrawals.t.sol new file mode 100644 index 00000000..f560b870 --- /dev/null +++ b/test/pectra-fork-tests/EL-withdrawals.t.sol @@ -0,0 +1,127 @@ + +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import "forge-std/Test.sol"; +import "forge-std/console2.sol"; +import "../../src/EtherFiNodesManager.sol"; +import "../../src/EtherFiNode.sol"; +import "../../src/EtherFiTimelock.sol"; +import "../../src/interfaces/IRoleRegistry.sol"; +import "../../src/RoleRegistry.sol"; + +import {IEigenPod, IEigenPodTypes } from "../../src/eigenlayer-interfaces/IEigenPod.sol"; + +/** + * @title ELExitsTest + * @notice test for EL exits + * @dev Run with: forge test --fork-url --match-path test/pectra-fork-tests/EL-exits.t.sol -vvvv + */ + +contract ELExitsTest is Test { + // === MAINNET CONTRACT ADDRESSES === + EtherFiNodesManager constant etherFiNodesManager = EtherFiNodesManager(payable(0x8B71140AD2e5d1E7018d2a7f8a288BD3CD38916F)); + RoleRegistry constant roleRegistry = RoleRegistry(0x62247D29B4B9BECf4BB73E0c722cf6445cfC7cE9); + + EtherFiTimelock constant etherFiOperatingTimelock = EtherFiTimelock(payable(0xcD425f44758a08BaAB3C4908f3e3dE5776e45d7a)); + address constant realElExiter = 0x12582A27E5e19492b4FcD194a60F8f5e1aa31B0F; + + bytes constant PK_28689 = hex"88d73705e9c3f29b042d3fe70bdc8781debc5506db43dd00122fd1fa7f4755535d31c7ecb2686ff53669b080ef9e18a3"; + + // MULTIPLE EL EXITS TEST + bytes constant PK_80143 = hex"811cd0bb7dd301afbbddd1d5db15ff0ca9d5f8ada78c0b1223f75b524aca1ca9ff1ba205d9efd7c37c2174576cc123e2"; + bytes constant PK_80194 = hex"b86cb11d564b29a38cdc8a3f1f9c35e6dcd2d0f85f40da60f745e479ba42b4548c83a2b049cf02277fceaa9b421d0039"; + bytes constant PK_89936 = hex"b8786ec7945d737698e374193f05a5498e932e2941263a7842837e9e3fac033af285e53a90afecf994585d178b5eedaa"; + + function setUp() public {} + + function _resolvePod(bytes memory pubkey) internal view returns (IEtherFiNode etherFiNode, IEigenPod pod) { + bytes32 pkHash = etherFiNodesManager.calculateValidatorPubkeyHash(pubkey); + etherFiNode = etherFiNodesManager.etherFiNodeFromPubkeyHash(pkHash); + pod = etherFiNode.getEigenPod(); + require(address(pod) != address(0), "test: node has no pod"); + } + + function _requestsFromPubkeys(bytes[] memory pubkeys, uint64[] memory amountsGwei) + internal + pure + returns (IEigenPod.WithdrawalRequest[] memory reqs) + { + require(pubkeys.length == amountsGwei.length, "test: length mismatch"); + reqs = new IEigenPod.WithdrawalRequest[](pubkeys.length); + for (uint256 i = 0; i < pubkeys.length; ++i) { + // NOTE: IEigenPod.WithdrawalRequest must match your interface type location + reqs[i] = IEigenPodTypes.WithdrawalRequest({pubkey: pubkeys[i], amountGwei: amountsGwei[i]}); + } + } + + function test_ELExits() public { + console2.log("=== EL EXITS TEST ==="); + + bytes[] memory pubkeys = new bytes[](1); + uint256[] memory legacyIds = new uint256[](1); + uint64[] memory amounts = new uint64[](1); + + pubkeys[0] = PK_28689; + legacyIds[0] = 28689; + amounts[0] = 0; + + vm.prank(address(etherFiOperatingTimelock)); + etherFiNodesManager.linkLegacyValidatorIds(legacyIds, pubkeys); + vm.stopPrank(); + + ( , IEigenPod pod0) = _resolvePod(pubkeys[0]); + + IEigenPod.WithdrawalRequest[] memory reqs = _requestsFromPubkeys(pubkeys, amounts); + + uint256 feePer = pod0.getWithdrawalRequestFee(); + uint256 n = reqs.length; + uint256 valueToSend = feePer * n; + + vm.expectEmit(true, true, true, true, address(etherFiNodesManager)); + emit IEtherFiNodesManager.ValidatorWithdrawalRequestSent( + address(pod0), + etherFiNodesManager.calculateValidatorPubkeyHash(pubkeys[0]), + pubkeys[0] + ); + vm.prank(realElExiter); + etherFiNodesManager.requestExecutionLayerTriggeredWithdrawal{value: valueToSend}(reqs); + vm.stopPrank(); + } + + function test_multiple_ELExits() public { + console2.log("=== MULTIPLE EL EXITS TEST ==="); + + bytes[] memory pubkeys = new bytes[](3); + uint64[] memory amounts = new uint64[](3); + + pubkeys[0] = PK_80194; + amounts[0] = 0; + + pubkeys[1] = PK_89936; + amounts[1] = 0; + + pubkeys[2] = PK_80143; + amounts[2] = 0; + + uint256[] memory linkOnlyOneValidatorlegacyId = new uint256[](1); + linkOnlyOneValidatorlegacyId[0] = 80194; + bytes[] memory linkOnlyOneValidatorPubkeys = new bytes[](1); + linkOnlyOneValidatorPubkeys[0] = PK_80194; + + vm.prank(address(etherFiOperatingTimelock)); + etherFiNodesManager.linkLegacyValidatorIds(linkOnlyOneValidatorlegacyId, linkOnlyOneValidatorPubkeys); + vm.stopPrank(); + + ( , IEigenPod pod0) = _resolvePod(pubkeys[0]); + IEigenPod.WithdrawalRequest[] memory reqs = _requestsFromPubkeys(pubkeys, amounts); + + uint256 feePer = pod0.getWithdrawalRequestFee(); + uint256 n = reqs.length; + uint256 valueToSend = feePer * n; + + vm.prank(realElExiter); + etherFiNodesManager.requestExecutionLayerTriggeredWithdrawal{value: valueToSend}(reqs); + vm.stopPrank(); + } +} \ No newline at end of file diff --git a/test/pectra-fork-tests/Request-consolidation.t.sol b/test/pectra-fork-tests/Request-consolidation.t.sol new file mode 100644 index 00000000..b614ef3c --- /dev/null +++ b/test/pectra-fork-tests/Request-consolidation.t.sol @@ -0,0 +1,190 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import "forge-std/Test.sol"; +import "forge-std/console2.sol"; +import "../../src/EtherFiNodesManager.sol"; +import "../../src/EtherFiNode.sol"; +import "../../src/EtherFiTimelock.sol"; +import "../../src/RoleRegistry.sol"; +import "../../src/interfaces/IRoleRegistry.sol"; +import {IEigenPod, IEigenPodTypes } from "../../src/eigenlayer-interfaces/IEigenPod.sol"; + +/** + * @title RequestConsolidationTest + * @notice test for request consolidation + * @dev Run with: forge test --fork-url --match-path test/pectra-fork-tests/Request-consolidation.t.sol -vvvv + */ + +contract RequestConsolidationTest is Test { + // === MAINNET CONTRACT ADDRESSES === + EtherFiNodesManager constant etherFiNodesManager = EtherFiNodesManager(payable(0x8B71140AD2e5d1E7018d2a7f8a288BD3CD38916F)); + RoleRegistry constant roleRegistry = RoleRegistry(0x62247D29B4B9BECf4BB73E0c722cf6445cfC7cE9); + + EtherFiTimelock constant etherFiOperatingTimelock = EtherFiTimelock(payable(0xcD425f44758a08BaAB3C4908f3e3dE5776e45d7a)); + address constant realElExiter = 0x12582A27E5e19492b4FcD194a60F8f5e1aa31B0F; + + bytes constant PK_28689 = hex"88d73705e9c3f29b042d3fe70bdc8781debc5506db43dd00122fd1fa7f4755535d31c7ecb2686ff53669b080ef9e18a3"; + + // Consolidate the following validators: + bytes constant PK_80143 = hex"811cd0bb7dd301afbbddd1d5db15ff0ca9d5f8ada78c0b1223f75b524aca1ca9ff1ba205d9efd7c37c2174576cc123e2"; + bytes constant PK_80194 = hex"b86cb11d564b29a38cdc8a3f1f9c35e6dcd2d0f85f40da60f745e479ba42b4548c83a2b049cf02277fceaa9b421d0039"; + bytes constant PK_89936 = hex"b8786ec7945d737698e374193f05a5498e932e2941263a7842837e9e3fac033af285e53a90afecf994585d178b5eedaa"; + // bytes constant PK_75208 = hex"b87882da67b89b06a59ec23b955ade930b534752153de08a50aa53172ea3439768dce39f547acfee53e55d985d6c4283"; + + function setUp() public {} + + function _resolvePod(bytes memory pubkey) internal view returns (IEtherFiNode etherFiNode, IEigenPod pod) { + bytes32 pkHash = etherFiNodesManager.calculateValidatorPubkeyHash(pubkey); + etherFiNode = etherFiNodesManager.etherFiNodeFromPubkeyHash(pkHash); + pod = etherFiNode.getEigenPod(); + require(address(pod) != address(0), "test: node has no pod"); + } + + function _requestsFromPubkeys(bytes[] memory pubkeys) + internal + pure + returns (IEigenPodTypes.ConsolidationRequest[] memory reqs) { + reqs = new IEigenPodTypes.ConsolidationRequest[](pubkeys.length); + for (uint256 i = 0; i < pubkeys.length; ++i) { + reqs[i] = IEigenPodTypes.ConsolidationRequest({ + srcPubkey: pubkeys[i], + targetPubkey: pubkeys[i] + }); + } + } + + function _consolidationRequestsFromPubkeys(bytes[] memory pubkeys) + internal + pure + returns (IEigenPodTypes.ConsolidationRequest[] memory reqs) { + reqs = new IEigenPodTypes.ConsolidationRequest[](pubkeys.length); + for (uint256 i = 0; i < pubkeys.length; ++i) { + reqs[i] = IEigenPodTypes.ConsolidationRequest({ + srcPubkey: pubkeys[i], + targetPubkey: pubkeys[0] // same pod consolidation + }); + } + } + + function _switchToCompoundingRequestsFromPubkeys(bytes[] memory pubkeys) + internal + pure + returns (IEigenPodTypes.ConsolidationRequest[] memory reqs) { + reqs = new IEigenPodTypes.ConsolidationRequest[](pubkeys.length); + for (uint256 i = 0; i < pubkeys.length; ++i) { + reqs[i] = IEigenPodTypes.ConsolidationRequest({ + srcPubkey: pubkeys[i], + targetPubkey: pubkeys[i] // switch to compounding + }); // same pubkey = switch to compounding + } + } + + function test_RequestConsolidation() public { + console2.log("=== REQUEST CONSOLIDATION TEST ==="); + bool hasRole = roleRegistry.hasRole(etherFiNodesManager.ETHERFI_NODES_MANAGER_EL_TRIGGER_EXIT_ROLE(), realElExiter); + require(hasRole, "test: realElExiter does not have the EL Trigger Exit Role"); + + bytes[] memory pubkeys = new bytes[](3); + pubkeys[0] = PK_80143; + pubkeys[1] = PK_80194; + pubkeys[2] = PK_89936; + + uint256[] memory legacyIdsForOneValidator = new uint256[](1); + legacyIdsForOneValidator[0] = 80143; + bytes[] memory pubkeysForOneValidator = new bytes[](1); + pubkeysForOneValidator[0] = PK_80143; + + vm.prank(address(etherFiOperatingTimelock)); + etherFiNodesManager.linkLegacyValidatorIds(legacyIdsForOneValidator, pubkeysForOneValidator); + vm.stopPrank(); + console.log("Linking legacy validator ids for one validator complete"); + + ( , IEigenPod pod0) = _resolvePod(pubkeys[0]); + + IEigenPodTypes.ConsolidationRequest[] memory reqs = _consolidationRequestsFromPubkeys(pubkeys); + + uint256 feePer = pod0.getConsolidationRequestFee(); + uint256 n = reqs.length; + uint256 valueToSend = feePer * n; + + // console.log("Fee per request:", feePer); + // console.log("Number of requests:", n); + // console.log("Value to send:", valueToSend); + + // Fund the timelock with enough ETH to pay consolidation fees + vm.deal(address(etherFiOperatingTimelock), valueToSend + 1 ether); + + vm.prank(address(etherFiOperatingTimelock)); + etherFiNodesManager.requestConsolidation{value: valueToSend}(reqs); + } + + function test_switchToCompounding() public { + console2.log("=== SWITCH TO COMPOUNDING TEST ==="); + bool hasRole = roleRegistry.hasRole(etherFiNodesManager.ETHERFI_NODES_MANAGER_EL_TRIGGER_EXIT_ROLE(), realElExiter); + require(hasRole, "test: realElExiter does not have the EL Trigger Exit Role"); + + bytes[] memory pubkeys = new bytes[](1); + uint256[] memory legacyIds = new uint256[](1); + pubkeys[0] = PK_28689; + legacyIds[0] = 28689; + + vm.prank(address(etherFiOperatingTimelock)); + etherFiNodesManager.linkLegacyValidatorIds(legacyIds, pubkeys); + vm.stopPrank(); + + ( , IEigenPod pod0) = _resolvePod(pubkeys[0]); + + IEigenPodTypes.ConsolidationRequest[] memory reqs = _switchToCompoundingRequestsFromPubkeys(pubkeys); + + uint256 feePer = pod0.getConsolidationRequestFee(); + uint256 n = reqs.length; + uint256 valueToSend = feePer * n; + + // Fund the timelock with enough ETH to pay consolidation fees + vm.deal(address(etherFiOperatingTimelock), valueToSend + 1 ether); + + vm.expectEmit(true, true, true, true, address(etherFiNodesManager)); + emit IEtherFiNodesManager.ValidatorSwitchToCompoundingRequested( + address(pod0), + etherFiNodesManager.calculateValidatorPubkeyHash(pubkeys[0]), + pubkeys[0] + ); + vm.prank(address(etherFiOperatingTimelock)); + etherFiNodesManager.requestConsolidation{value: valueToSend}(reqs); + + } + + function test_multiple_switchToCompounding() public { + console2.log("=== MULTIPLE SWITCH TO COMPOUNDING TEST ==="); + + bytes[] memory pubkeys = new bytes[](3); + uint256[] memory legacyIds = new uint256[](3); + pubkeys[0] = PK_80143; + legacyIds[0] = 80143; + pubkeys[1] = PK_80194; + legacyIds[1] = 80194; + pubkeys[2] = PK_89936; + legacyIds[2] = 89936; + + uint256[] memory linkOnlyOneValidatorlegacyId = new uint256[](1); + linkOnlyOneValidatorlegacyId[0] = 80143; + bytes[] memory linkOnlyOneValidatorPubkeys = new bytes[](1); + linkOnlyOneValidatorPubkeys[0] = PK_80143; + + ( , IEigenPod pod0) = _resolvePod(pubkeys[0]); + + IEigenPodTypes.ConsolidationRequest[] memory reqs = _switchToCompoundingRequestsFromPubkeys(pubkeys); + + uint256 feePer = pod0.getConsolidationRequestFee(); + uint256 n = reqs.length; + uint256 valueToSend = feePer * n; + + vm.deal(address(etherFiOperatingTimelock), valueToSend + 1 ether); + + vm.expectRevert(); // because all validators are not active + vm.prank(address(etherFiOperatingTimelock)); + etherFiNodesManager.requestConsolidation{value: valueToSend}(reqs); + + } +} \ No newline at end of file