diff --git a/src/LiquidityPool.sol b/src/LiquidityPool.sol index 85c01df3..b6b05270 100644 --- a/src/LiquidityPool.sol +++ b/src/LiquidityPool.sol @@ -74,6 +74,7 @@ contract LiquidityPool is Initializable, OwnableUpgradeable, UUPSUpgradeable, IL bytes32 public constant LIQUIDITY_POOL_ADMIN_ROLE = keccak256("LIQUIDITY_POOL_ADMIN_ROLE"); bytes32 public constant LIQUIDITY_POOL_VALIDATOR_APPROVER_ROLE = keccak256("LIQUIDITY_POOL_VALIDATOR_APPROVER_ROLE"); + bytes32 public constant LIQUIDITY_POOL_VALIDATOR_CREATOR_ROLE = keccak256("LIQUIDITY_POOL_VALIDATOR_CREATOR_ROLE"); //-------------------------------------------------------------------------------------- //------------------------------------- EVENTS --------------------------------------- @@ -279,8 +280,9 @@ contract LiquidityPool is Initializable, OwnableUpgradeable, UUPSUpgradeable, IL // [Liquidty Pool Staking flow] // Step 1: (Off-chain) create the keys using the desktop app - // Step 2: create validators with 1 eth deposits to official deposit contract - // Step 3: oracle approves and funds the remaining balance for the validator + // Step 2: register validator deposit data for later confirmation from the oracle before the 1eth deposit + // Step 3: create validators with 1 eth deposits to official deposit contract + // Step 4: oracle approves and funds the remaining balance for the validator /// @notice claim bids and send 1 eth deposits to deposit contract to create the provided validators. /// @dev step 2 of staking flow @@ -290,6 +292,15 @@ contract LiquidityPool is Initializable, OwnableUpgradeable, UUPSUpgradeable, IL address _etherFiNode ) external whenNotPaused { require(validatorSpawner[msg.sender].registered, "Incorrect Caller"); + stakingManager.registerBeaconValidators(_depositData, _bidIds, _etherFiNode); + } + + function batchCreateBeaconValidators( + IStakingManager.DepositData[] calldata _depositData, + uint256[] calldata _bidIds, + address _etherFiNode + ) external whenNotPaused { + if (!roleRegistry.hasRole(LIQUIDITY_POOL_VALIDATOR_CREATOR_ROLE, msg.sender)) revert IncorrectRole(); // liquidity pool supplies 1 eth per validator uint256 outboundEthAmountFromLp = 1 ether * _bidIds.length; diff --git a/src/StakingManager.sol b/src/StakingManager.sol index 58f96202..d3689833 100644 --- a/src/StakingManager.sol +++ b/src/StakingManager.sol @@ -42,6 +42,7 @@ contract StakingManager is LegacyStakingManagerState legacyState; // all legacy state in this contract has been deprecated mapping(address => bool) public deployedEtherFiNodes; + mapping(bytes32 => ValidatorCreationStatus) public validatorCreationStatus; //--------------------------------------------------------------------------- //--------------------------- ROLES --------------------------------------- @@ -49,6 +50,7 @@ contract StakingManager is bytes32 public constant STAKING_MANAGER_NODE_CREATOR_ROLE = keccak256("STAKING_MANAGER_NODE_CREATOR_ROLE"); bytes32 public constant STAKING_MANAGER_ADMIN_ROLE = keccak256("STAKING_MANAGER_ADMIN_ROLE"); + bytes32 public constant STAKING_MANAGER_VALIDATOR_INVALIDATOR_ROLE = keccak256("STAKING_MANAGER_VALIDATOR_INVALIDATOR_ROLE"); //------------------------------------------------------------------------- //----------------------------- Admin ----------------------------------- @@ -88,39 +90,69 @@ contract StakingManager is //--------------------------------------------------------------------------- //------------------------- Deposit Flow ------------------------------------ //--------------------------------------------------------------------------- + + function invalidateRegisteredBeaconValidator(DepositData calldata depositData, uint256 bidId, address etherFiNode) external { + if (!roleRegistry.hasRole(STAKING_MANAGER_VALIDATOR_INVALIDATOR_ROLE, msg.sender)) revert IncorrectRole(); + bytes32 validatorCreationDataHash = keccak256(abi.encodePacked(depositData.publicKey, depositData.signature, depositData.depositDataRoot, depositData.ipfsHashForEncryptedValidatorKey, bidId, etherFiNode)); + if (validatorCreationStatus[validatorCreationDataHash] != ValidatorCreationStatus.REGISTERED) revert InvalidValidatorCreationStatus(); + validatorCreationStatus[validatorCreationDataHash] = ValidatorCreationStatus.INVALIDATED; + emit ValidatorCreationStatusUpdated(depositData, bidId, etherFiNode, validatorCreationDataHash, ValidatorCreationStatus.INVALIDATED); + } - /// @notice send 1 eth to deposit contract to create the validator. + /// @notice send 1 eth to deposit contract to create the validator. /// The rest of the eth will not be sent until the oracle confirms the withdrawal credentials - /// @dev provided deposit data must be for a 0x02 compounding validator function createBeaconValidators(DepositData[] calldata depositData, uint256[] calldata bidIds, address etherFiNode) external payable { if (msg.sender != liquidityPool) revert InvalidCaller(); if (depositData.length != bidIds.length) revert InvalidDepositData(); - if (address(IEtherFiNode(etherFiNode).getEigenPod()) == address(0)) revert InvalidEtherFiNode(); - // process each 1 eth deposit to create validators for later verification from oracle for (uint256 i = 0; i < depositData.length; i++) { + DepositData memory d = depositData[i]; + bytes32 validatorCreationDataHash = keccak256(abi.encodePacked(d.publicKey, d.signature, d.depositDataRoot, d.ipfsHashForEncryptedValidatorKey, bidIds[i], etherFiNode)); + if (validatorCreationStatus[validatorCreationDataHash] != ValidatorCreationStatus.REGISTERED) revert InvalidValidatorCreationStatus(); - // claim the bid - if (!auctionManager.isBidActive(bidIds[i])) revert InactiveBid(); - auctionManager.updateSelectedBidInformation(bidIds[i]); - - // verify deposit root bytes memory withdrawalCredentials = etherFiNodesManager.addressToCompoundingWithdrawalCredentials(address(IEtherFiNode(etherFiNode).getEigenPod())); - bytes32 computedDataRoot = generateDepositDataRoot(depositData[i].publicKey, depositData[i].signature, withdrawalCredentials, initialDepositAmount); - if (computedDataRoot != depositData[i].depositDataRoot) revert IncorrectBeaconRoot(); + bytes32 computedDataRoot = generateDepositDataRoot(d.publicKey, d.signature, withdrawalCredentials, initialDepositAmount); + validatorCreationStatus[validatorCreationDataHash] = ValidatorCreationStatus.CONFIRMED; + auctionManager.updateSelectedBidInformation(bidIds[i]); // Link the pubkey to a node. Will revert if this pubkey is already registered to a different target - etherFiNodesManager.linkPubkeyToNode(depositData[i].publicKey, etherFiNode, bidIds[i]); + etherFiNodesManager.linkPubkeyToNode(d.publicKey, etherFiNode, bidIds[i]); // Deposit to the Beacon Chain - depositContractEth2.deposit{value: initialDepositAmount}(depositData[i].publicKey, withdrawalCredentials, depositData[i].signature, computedDataRoot); + depositContractEth2.deposit{value: initialDepositAmount}(d.publicKey, withdrawalCredentials, d.signature, computedDataRoot); - bytes32 pubkeyHash = calculateValidatorPubkeyHash(depositData[i].publicKey); - emit validatorCreated(pubkeyHash, etherFiNode, depositData[i].publicKey); + bytes32 pubkeyHash = calculateValidatorPubkeyHash(d.publicKey); + emit validatorCreated(pubkeyHash, etherFiNode, d.publicKey); emit linkLegacyValidatorId(pubkeyHash, bidIds[i]); // can remove this once we fully transition to pubkeys // legacy event for compatibility with existing tooling - emit ValidatorRegistered(auctionManager.getBidOwner(bidIds[i]), address(liquidityPool), address(liquidityPool), bidIds[i], depositData[i].publicKey, depositData[i].ipfsHashForEncryptedValidatorKey); + emit ValidatorRegistered(auctionManager.getBidOwner(bidIds[i]), address(liquidityPool), address(liquidityPool), bidIds[i], d.publicKey, d.ipfsHashForEncryptedValidatorKey); + emit ValidatorCreationStatusUpdated(d, bidIds[i], etherFiNode, validatorCreationDataHash, ValidatorCreationStatus.CONFIRMED); + } + } + + /// @notice register the beacon validators data for later confirmation from the oracle before the 1eth deposit + /// @dev provided deposit data must be for a 0x02 compounding validator + function registerBeaconValidators(DepositData[] calldata depositData, uint256[] calldata bidIds, address etherFiNode) external { + if (msg.sender != liquidityPool) revert InvalidCaller(); + if (depositData.length != bidIds.length) revert InvalidDepositData(); + if (address(IEtherFiNode(etherFiNode).getEigenPod()) == address(0) || !deployedEtherFiNodes[etherFiNode]) revert InvalidEtherFiNode(); + + // process each 1 eth deposit to create validators for later verification from oracle + for (uint256 i = 0; i < depositData.length; i++) { + + // claim the bid + if (!auctionManager.isBidActive(bidIds[i])) revert InactiveBid(); + + // verify deposit root + bytes memory withdrawalCredentials = etherFiNodesManager.addressToCompoundingWithdrawalCredentials(address(IEtherFiNode(etherFiNode).getEigenPod())); + bytes32 computedDataRoot = generateDepositDataRoot(depositData[i].publicKey, depositData[i].signature, withdrawalCredentials, initialDepositAmount); + if (computedDataRoot != depositData[i].depositDataRoot) revert IncorrectBeaconRoot(); + + bytes32 validatorCreationDataHash = keccak256(abi.encodePacked(depositData[i].publicKey, depositData[i].signature, depositData[i].depositDataRoot, depositData[i].ipfsHashForEncryptedValidatorKey, bidIds[i], etherFiNode)); + if (validatorCreationStatus[validatorCreationDataHash] != ValidatorCreationStatus.NOT_REGISTERED) revert InvalidValidatorCreationStatus(); + validatorCreationStatus[validatorCreationDataHash] = ValidatorCreationStatus.REGISTERED; + emit ValidatorCreationStatusUpdated(depositData[i], bidIds[i], etherFiNode, validatorCreationDataHash, ValidatorCreationStatus.REGISTERED); } } diff --git a/src/interfaces/ILiquidityPool.sol b/src/interfaces/ILiquidityPool.sol index 2400d7fb..1fe6cbac 100644 --- a/src/interfaces/ILiquidityPool.sol +++ b/src/interfaces/ILiquidityPool.sol @@ -62,6 +62,7 @@ interface ILiquidityPool { function requestMembershipNFTWithdraw(address recipient, uint256 amount, uint256 fee) external returns (uint256); function batchRegister(IStakingManager.DepositData[] calldata _depositData, uint256[] calldata _bidIds, address _etherFiNode) external; + function batchCreateBeaconValidators(IStakingManager.DepositData[] calldata _depositData, uint256[] calldata _bidIds, address _etherFiNode) external; function batchApproveRegistration(uint256[] memory _validatorIds, bytes[] calldata _pubkeys, bytes[] calldata _signatures) external; function confirmAndFundBeaconValidators(IStakingManager.DepositData[] calldata depositData, uint256 validatorSizeWei) external; function DEPRECATED_sendExitRequests(uint256[] calldata _validatorIds) external; diff --git a/src/interfaces/IStakingManager.sol b/src/interfaces/IStakingManager.sol index 8989618b..4c9c407c 100644 --- a/src/interfaces/IStakingManager.sol +++ b/src/interfaces/IStakingManager.sol @@ -12,8 +12,18 @@ interface IStakingManager { string ipfsHashForEncryptedValidatorKey; } + // Possible values for validator creation status + enum ValidatorCreationStatus { + NOT_REGISTERED, + REGISTERED, + CONFIRMED, + INVALIDATED + } + // deposit flow + function registerBeaconValidators(DepositData[] calldata depositData, uint256[] calldata bidIds, address etherFiNode) external; function createBeaconValidators(DepositData[] calldata depositData, uint256[] calldata bidIds, address etherFiNode) external payable; + function invalidateRegisteredBeaconValidator(DepositData calldata depositData, uint256 bidId, address etherFiNode) external; function confirmAndFundBeaconValidators(DepositData[] calldata depositData, uint256 validatorSizeWei) external payable; function calculateValidatorPubkeyHash(bytes memory pubkey) external pure returns (bytes32); function initialDepositAmount() external returns (uint256); @@ -78,6 +88,8 @@ interface IStakingManager { // legacy event still being emitted in its original form to play nice with existing external tooling event ValidatorRegistered(address indexed operator, address indexed bNftOwner, address indexed tNftOwner, uint256 validatorId, bytes validatorPubKey, string ipfsHashForEncryptedValidatorKey); + event ValidatorCreationStatusUpdated(DepositData depositData, uint256 bidId, address etherFiNode, bytes32 hashedAllData, ValidatorCreationStatus indexed status); + //-------------------------------------------------------------------------- //----------------------------- Errors ----------------------------------- //-------------------------------------------------------------------------- @@ -92,5 +104,6 @@ interface IStakingManager { error InvalidValidatorSize(); error IncorrectRole(); error InvalidUpgrade(); + error InvalidValidatorCreationStatus(); } diff --git a/test/fork-tests/validator-key-gen.t.sol b/test/fork-tests/validator-key-gen.t.sol new file mode 100644 index 00000000..d16e8909 --- /dev/null +++ b/test/fork-tests/validator-key-gen.t.sol @@ -0,0 +1,792 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import "forge-std/console2.sol"; +import "forge-std/Test.sol"; +import "../../test/common/ArrayTestHelper.sol"; + +import "../../src/libraries/DepositDataRootGenerator.sol"; + +import "../../src/interfaces/IStakingManager.sol"; +import "../../src/interfaces/IEtherFiNode.sol"; + +import "../../src/StakingManager.sol"; +import "../../src/LiquidityPool.sol"; +import "../../src/EtherFiNodesManager.sol"; +import "../../src/EtherFiNode.sol"; +import "../../src/NodeOperatorManager.sol"; +import "../../src/AuctionManager.sol"; +import "../../src/RoleRegistry.sol"; + +// Command to run this test: forge test --match-contract ValidatorKeyGenTest + +contract ValidatorKeyGenTest is Test, ArrayTestHelper { + StakingManager public constant stakingManager = StakingManager(0x25e821b7197B146F7713C3b89B6A4D83516B912d); + EtherFiNodesManager public constant etherFiNodesManager = EtherFiNodesManager(0x8B71140AD2e5d1E7018d2a7f8a288BD3CD38916F); + EtherFiNode public constant etherFiNodeBeacon = EtherFiNode(payable(0x3c55986Cfee455E2533F4D29006634EcF9B7c03F)); + LiquidityPool public constant liquidityPool = LiquidityPool(payable(0x308861A430be4cce5502d0A12724771Fc6DaF216)); + NodeOperatorManager public constant nodeOperatorManager = NodeOperatorManager(0xd5edf7730ABAd812247F6F54D7bd31a52554e35E); + AuctionManager public constant auctionManager = AuctionManager(0x00C452aFFee3a17d9Cecc1Bcd2B8d5C7635C4CB9); + RoleRegistry public constant roleRegistry = RoleRegistry(0x62247D29B4B9BECf4BB73E0c722cf6445cfC7cE9); + + address public constant stakingDepositContract = address(0x00000000219ab540356cBB839Cbe05303d7705Fa); + + address public constant admin = 0x2aCA71020De61bb532008049e1Bd41E451aE8AdC; + address public constant stakingManagerNodeCreatorRoleHolder = 0x12582A27E5e19492b4FcD194a60F8f5e1aa31B0F; + address public constant operatingTimelock = 0xcD425f44758a08BaAB3C4908f3e3dE5776e45d7a; + address public constant timelock = 0x9f26d4C958fD811A1F59B01B86Be7dFFc9d20761; + + address public tom = vm.addr(0x9999999); + + function setUp() public { + vm.selectFork(vm.createFork(vm.envString("MAINNET_RPC_URL"))); + vm.deal(tom, 100 ether); + + StakingManager stakingManagerImpl = new StakingManager( + address(liquidityPool), + address(etherFiNodesManager), + address(stakingDepositContract), + address(auctionManager), + address(etherFiNodeBeacon), + address(roleRegistry) + ); + vm.prank(stakingManager.owner()); + stakingManager.upgradeTo(address(stakingManagerImpl)); + + LiquidityPool liquidityPoolImpl = new LiquidityPool(); + vm.prank(liquidityPool.owner()); + liquidityPool.upgradeTo(address(liquidityPoolImpl)); + + vm.startPrank(roleRegistry.owner()); + roleRegistry.grantRole(liquidityPool.LIQUIDITY_POOL_VALIDATOR_CREATOR_ROLE(), admin); + roleRegistry.grantRole(etherFiNodesManager.ETHERFI_NODES_MANAGER_EIGENLAYER_ADMIN_ROLE(), address(stakingManager)); + roleRegistry.grantRole(stakingManager.STAKING_MANAGER_VALIDATOR_INVALIDATOR_ROLE(), admin); + auctionManager.updateAdmin(admin, true); + vm.stopPrank(); + } + + function helper_getDataForValidatorKeyGen() internal returns (bytes memory pubkey, bytes memory signature, bytes memory withdrawalCredentials, bytes32 depositDataRoot, IStakingManager.DepositData memory depositData, address etherFiNode) { + pubkey = vm.randomBytes(48); + signature = vm.randomBytes(96); + + vm.prank(operatingTimelock); + etherFiNode = stakingManager.instantiateEtherFiNode(true /*createEigenPod*/); + address eigenPod = address(IEtherFiNode(etherFiNode).getEigenPod()); + + withdrawalCredentials = etherFiNodesManager.addressToCompoundingWithdrawalCredentials(eigenPod); + depositDataRoot = depositDataRootGenerator.generateDepositDataRoot(pubkey, signature, withdrawalCredentials, 1 ether); + depositData = IStakingManager.DepositData({ + publicKey: pubkey, signature: signature, depositDataRoot: depositDataRoot, ipfsHashForEncryptedValidatorKey: "test_ipfs_hash" + }); + + return (pubkey, signature, withdrawalCredentials, depositDataRoot, depositData, etherFiNode); + } + + function test_liquidityPool_newFlow() public { + // STEP 1: Whitelist the user + vm.prank(admin); + nodeOperatorManager.addToWhitelist(tom); + + // STEP 2: Get the data for the validator key gen + (bytes memory pubkey, bytes memory signature, bytes memory withdrawalCredentials, bytes32 depositDataRoot, StakingManager.DepositData memory depositData, address etherFiNode) = helper_getDataForValidatorKeyGen(); + + // STEP 3: Register the node operator and create the bid + vm.deal(tom, 100 ether); + + vm.startPrank(tom); + nodeOperatorManager.registerNodeOperator("test_ipfs_hash", 1000); + uint256[] memory bidId1 = auctionManager.createBid{value: 0.1 ether}(1, 0.1 ether); + vm.stopPrank(); + + // STEP 4: Register the validator spawner and batch register the validator + vm.prank(operatingTimelock); + liquidityPool.registerValidatorSpawner(tom); + + vm.prank(tom); + liquidityPool.batchRegister(toArray(depositData), toArray_u256(bidId1[0]), etherFiNode); + assertEq(uint8(stakingManager.validatorCreationStatus(keccak256(abi.encodePacked(depositData.publicKey, depositData.signature, depositData.depositDataRoot, depositData.ipfsHashForEncryptedValidatorKey, bidId1[0], etherFiNode)))), uint8(IStakingManager.ValidatorCreationStatus.REGISTERED)); + + // STEP 5: Confirm the validator + vm.prank(admin); + liquidityPool.batchCreateBeaconValidators(toArray(depositData), toArray_u256(bidId1[0]), etherFiNode); + assertEq(uint8(stakingManager.validatorCreationStatus(keccak256(abi.encodePacked(depositData.publicKey, depositData.signature, depositData.depositDataRoot, depositData.ipfsHashForEncryptedValidatorKey, bidId1[0], etherFiNode)))), uint8(IStakingManager.ValidatorCreationStatus.CONFIRMED)); + } + + function test_batchRegister_revertsWhenNotRegisteredSpawner() public { + address unregisteredUser = vm.addr(0xDEAD); + + // Get the data for the validator key gen + (bytes memory pubkey, bytes memory signature, bytes memory withdrawalCredentials, bytes32 depositDataRoot, IStakingManager.DepositData memory depositData, address etherFiNode) = helper_getDataForValidatorKeyGen(); + + IStakingManager.DepositData[] memory depositDataArray = new IStakingManager.DepositData[](1); + depositDataArray[0] = depositData; + + uint256[] memory bidIds = new uint256[](1); + bidIds[0] = 1; + + vm.prank(unregisteredUser); + vm.expectRevert("Incorrect Caller"); + liquidityPool.batchRegister(depositDataArray, bidIds, etherFiNode); + } + + function test_batchRegister_revertsWhenPaused() public { + address spawner = vm.addr(0x1234); + + // Setup spawner + vm.prank(operatingTimelock); + liquidityPool.registerValidatorSpawner(spawner); + + // Get the data for the validator key gen + (bytes memory pubkey, bytes memory signature, bytes memory withdrawalCredentials, bytes32 depositDataRoot, IStakingManager.DepositData memory depositData, address etherFiNode) = helper_getDataForValidatorKeyGen(); + + IStakingManager.DepositData[] memory depositDataArray = new IStakingManager.DepositData[](1); + depositDataArray[0] = depositData; + + uint256[] memory bidIds = new uint256[](1); + bidIds[0] = 1; + + // Pause contract + vm.prank(admin); + liquidityPool.pauseContract(); + + vm.prank(spawner); + vm.expectRevert("Pausable: paused"); + liquidityPool.batchRegister(depositDataArray, bidIds, etherFiNode); + } + + function test_batchRegister_revertsWhenInvalidEtherFiNode() public { + address spawner = vm.addr(0x1234); + bytes memory pubkey = vm.randomBytes(48); + bytes memory signature = vm.randomBytes(96); + + vm.prank(admin); + nodeOperatorManager.addToWhitelist(spawner); + + vm.prank(operatingTimelock); + liquidityPool.registerValidatorSpawner(spawner); + + address invalidNode = address(0xDEAD); // wrong contract address + + bytes32 depositRoot = depositDataRootGenerator.generateDepositDataRoot( + pubkey, + signature, + bytes(""), + 1 ether + ); + + IStakingManager.DepositData[] memory depositData = new IStakingManager.DepositData[](1); + depositData[0] = IStakingManager.DepositData({ + publicKey: pubkey, + signature: signature, + depositDataRoot: depositRoot, + ipfsHashForEncryptedValidatorKey: "test_ipfs" + }); + + uint256[] memory bidIds = new uint256[](1); + bidIds[0] = 1; + + vm.prank(spawner); + vm.expectRevert("call to non-contract address 0x000000000000000000000000000000000000dEaD"); + liquidityPool.batchRegister(depositData, bidIds, invalidNode); + } + + function test_batchRegister_revertsWhenArrayLengthMismatch() public { + address spawner = vm.addr(0x1234); + + vm.prank(operatingTimelock); + liquidityPool.registerValidatorSpawner(spawner); + + // Get the data for the validator key gen + (bytes memory pubkey, bytes memory signature, bytes memory withdrawalCredentials, bytes32 depositDataRoot, IStakingManager.DepositData memory depositData, address etherFiNode) = helper_getDataForValidatorKeyGen(); + + // Array length mismatch: 1 depositData but 2 bidIds + IStakingManager.DepositData[] memory depositDataArray = new IStakingManager.DepositData[](1); + depositDataArray[0] = depositData; + + uint256[] memory bidIds = new uint256[](2); + bidIds[0] = 1; + bidIds[1] = 2; + + vm.prank(spawner); + vm.expectRevert(IStakingManager.InvalidDepositData.selector); + liquidityPool.batchRegister(depositDataArray, bidIds, etherFiNode); + } + + function test_batchRegister_revertsWhenBidNotActive() public { + address spawner = vm.addr(0x1234); + + vm.prank(admin); + nodeOperatorManager.addToWhitelist(spawner); + + vm.prank(operatingTimelock); + liquidityPool.registerValidatorSpawner(spawner); + + // Register node operator and create bid + vm.deal(spawner, 100 ether); + vm.prank(spawner); + nodeOperatorManager.registerNodeOperator("ipfs_hash", 1000); + + vm.prank(spawner); + uint256[] memory createdBids = auctionManager.createBid{value: 0.1 ether}(1, 0.1 ether); + + // Cancel the bid to make it inactive + vm.prank(spawner); + auctionManager.cancelBid(createdBids[0]); + + // Get the data for the validator key gen + (bytes memory pubkey, bytes memory signature, bytes memory withdrawalCredentials, bytes32 depositDataRoot, IStakingManager.DepositData memory depositData, address etherFiNode) = helper_getDataForValidatorKeyGen(); + + IStakingManager.DepositData[] memory depositDataArray = new IStakingManager.DepositData[](1); + depositDataArray[0] = depositData; + + vm.prank(spawner); + vm.expectRevert(IStakingManager.InactiveBid.selector); + liquidityPool.batchRegister(depositDataArray, createdBids, etherFiNode); + } + + function test_batchRegister_revertsWhenIncorrectDepositDataRoot() public { + address spawner = vm.addr(0x1234); + + vm.prank(admin); + nodeOperatorManager.addToWhitelist(spawner); + + vm.prank(operatingTimelock); + liquidityPool.registerValidatorSpawner(spawner); + + vm.deal(spawner, 100 ether); + vm.prank(spawner); + nodeOperatorManager.registerNodeOperator("ipfs_hash", 1000); + + vm.prank(spawner); + uint256[] memory createdBids = auctionManager.createBid{value: 0.1 ether}(1, 0.1 ether); + + // Get the data for the validator key gen + (bytes memory pubkey, bytes memory signature, bytes memory withdrawalCredentials, bytes32 depositDataRoot, IStakingManager.DepositData memory depositData, address etherFiNode) = helper_getDataForValidatorKeyGen(); + + // Use incorrect deposit data root + bytes32 incorrectRoot = keccak256("incorrect"); + + IStakingManager.DepositData[] memory depositDataArray = new IStakingManager.DepositData[](1); + depositDataArray[0] = IStakingManager.DepositData({ + publicKey: pubkey, + signature: signature, + depositDataRoot: incorrectRoot, + ipfsHashForEncryptedValidatorKey: depositData.ipfsHashForEncryptedValidatorKey + }); + + vm.prank(spawner); + // Will revert due to incorrect deposit data root + vm.expectRevert(IStakingManager.IncorrectBeaconRoot.selector); + liquidityPool.batchRegister(depositDataArray, createdBids, etherFiNode); + } + + function test_batchRegister_succeeds() public { + address spawner = vm.addr(0x1234); + + vm.prank(admin); + nodeOperatorManager.addToWhitelist(spawner); + + vm.prank(operatingTimelock); + liquidityPool.registerValidatorSpawner(spawner); + + vm.deal(spawner, 100 ether); + vm.prank(spawner); + nodeOperatorManager.registerNodeOperator("ipfs_hash", 1000); + + vm.prank(spawner); + uint256[] memory createdBids = auctionManager.createBid{value: 0.1 ether}(1, 0.1 ether); + + (bytes memory pubkey, bytes memory signature, bytes memory withdrawalCredentials, bytes32 depositDataRoot, IStakingManager.DepositData memory depositData, address etherFiNode) = helper_getDataForValidatorKeyGen(); + + IStakingManager.DepositData[] memory depositDataArray = new IStakingManager.DepositData[](1); + depositDataArray[0] = depositData; + + vm.prank(spawner); + liquidityPool.batchRegister(depositDataArray, createdBids, etherFiNode); + + // Verify validator status is REGISTERED + bytes32 validatorHash = keccak256(abi.encodePacked( + depositData.publicKey, + depositData.signature, + depositData.depositDataRoot, + depositData.ipfsHashForEncryptedValidatorKey, + createdBids[0], + etherFiNode + )); + + assertEq( + uint8(stakingManager.validatorCreationStatus(validatorHash)), + uint8(IStakingManager.ValidatorCreationStatus.REGISTERED) + ); + } + + function test_batchRegister_revertsWhenAlreadyRegistered() public { + address spawner = vm.addr(0x1234); + + vm.prank(admin); + nodeOperatorManager.addToWhitelist(spawner); + + vm.prank(operatingTimelock); + liquidityPool.registerValidatorSpawner(spawner); + + vm.deal(spawner, 100 ether); + vm.prank(spawner); + nodeOperatorManager.registerNodeOperator("ipfs_hash", 1000); + + vm.prank(spawner); + uint256[] memory createdBids = auctionManager.createBid{value: 0.1 ether}(1, 0.1 ether); + + (,,, , IStakingManager.DepositData memory depositData, address etherFiNode) = helper_getDataForValidatorKeyGen(); + + IStakingManager.DepositData[] memory depositDataArray = new IStakingManager.DepositData[](1); + depositDataArray[0] = depositData; + + vm.prank(spawner); + liquidityPool.batchRegister(depositDataArray, createdBids, etherFiNode); + + vm.prank(spawner); + vm.expectRevert(IStakingManager.InvalidValidatorCreationStatus.selector); + liquidityPool.batchRegister(depositDataArray, createdBids, etherFiNode); + } + + function test_batchRegister_revertsAfterInvalidation() public { + address spawner = vm.addr(0x1234); + + vm.prank(admin); + nodeOperatorManager.addToWhitelist(spawner); + + vm.prank(operatingTimelock); + liquidityPool.registerValidatorSpawner(spawner); + + vm.deal(spawner, 100 ether); + vm.prank(spawner); + nodeOperatorManager.registerNodeOperator("ipfs_hash", 1000); + + vm.prank(spawner); + uint256[] memory createdBids = auctionManager.createBid{value: 0.1 ether}(1, 0.1 ether); + + (bytes memory pubkey, bytes memory signature, bytes memory withdrawalCredentials, bytes32 depositDataRoot, IStakingManager.DepositData memory depositData, address etherFiNode) = helper_getDataForValidatorKeyGen(); + + IStakingManager.DepositData[] memory depositDataArray = new IStakingManager.DepositData[](1); + depositDataArray[0] = depositData; + + vm.prank(spawner); + liquidityPool.batchRegister(toArray(depositData), createdBids, etherFiNode); + + vm.prank(admin); + stakingManager.invalidateRegisteredBeaconValidator(depositData, createdBids[0], etherFiNode); + + vm.prank(spawner); + vm.expectRevert(IStakingManager.InvalidValidatorCreationStatus.selector); + liquidityPool.batchRegister(toArray(depositData), createdBids, etherFiNode); + } + + function test_roleGrant_succeeds() public { + assertEq(roleRegistry.hasRole(keccak256("STAKING_MANAGER_VALIDATOR_INVALIDATOR_ROLE"), address(admin)), true); + } + + // ==================== batchCreateBeaconValidators Tests ==================== + + function test_batchCreateBeaconValidators_revertsWhenNoRole() public { + address unauthorizedUser = vm.addr(0xDEAD); + + (bytes memory pubkey, bytes memory signature, bytes memory withdrawalCredentials, bytes32 depositDataRoot, IStakingManager.DepositData memory depositData, address etherFiNode) = helper_getDataForValidatorKeyGen(); + + IStakingManager.DepositData[] memory depositDataArray = new IStakingManager.DepositData[](1); + depositDataArray[0] = depositData; + + uint256[] memory bidIds = new uint256[](1); + bidIds[0] = 1; + + vm.prank(unauthorizedUser); + vm.expectRevert(LiquidityPool.IncorrectRole.selector); + liquidityPool.batchCreateBeaconValidators(depositDataArray, bidIds, etherFiNode); + } + + function test_batchCreateBeaconValidators_revertsWhenPaused() public { + (bytes memory pubkey, bytes memory signature, bytes memory withdrawalCredentials, bytes32 depositDataRoot, IStakingManager.DepositData memory depositData, address etherFiNode) = helper_getDataForValidatorKeyGen(); + + IStakingManager.DepositData[] memory depositDataArray = new IStakingManager.DepositData[](1); + depositDataArray[0] = depositData; + + uint256[] memory bidIds = new uint256[](1); + bidIds[0] = 1; + + // Pause contract + vm.prank(admin); + liquidityPool.pauseContract(); + + vm.prank(admin); + vm.expectRevert("Pausable: paused"); + liquidityPool.batchCreateBeaconValidators(depositDataArray, bidIds, etherFiNode); + } + + function test_batchCreateBeaconValidators_revertsWhenNotRegisteredValidator() public { + // Get the data for the validator key gen + (bytes memory pubkey, bytes memory signature, bytes memory withdrawalCredentials, bytes32 depositDataRoot, IStakingManager.DepositData memory depositData, address etherFiNode) = helper_getDataForValidatorKeyGen(); + + IStakingManager.DepositData[] memory depositDataArray = new IStakingManager.DepositData[](1); + depositDataArray[0] = depositData; + + // Use non-existent bid ID (validator not registered) + uint256[] memory bidIds = new uint256[](1); + bidIds[0] = 999999; + + vm.deal(address(liquidityPool), 100 ether); + + vm.prank(admin); + // Will revert because validator is not in REGISTERED status + vm.expectRevert(IStakingManager.InvalidValidatorCreationStatus.selector); + liquidityPool.batchCreateBeaconValidators(depositDataArray, bidIds, etherFiNode); + } + + function test_batchCreateBeaconValidators_revertsWhenEmptyArrays() public { + // Get the data for the validator key gen (just need etherFiNode) + (,,, , , address etherFiNode) = helper_getDataForValidatorKeyGen(); + + IStakingManager.DepositData[] memory depositData = new IStakingManager.DepositData[](0); + uint256[] memory bidIds = new uint256[](0); + + vm.prank(admin); + // Empty arrays are valid - function completes successfully (no revert) + // The loop in createBeaconValidators doesn't execute when length is 0 + liquidityPool.batchCreateBeaconValidators(depositData, bidIds, etherFiNode); + } + + function test_batchCreateBeaconValidators_succeeds() public { + address spawner = vm.addr(0x1234); + + vm.prank(admin); + nodeOperatorManager.addToWhitelist(spawner); + + // Setup spawner and register validator first + vm.prank(operatingTimelock); + liquidityPool.registerValidatorSpawner(spawner); + + vm.deal(spawner, 100 ether); + vm.prank(spawner); + nodeOperatorManager.registerNodeOperator("ipfs_hash", 1000); + + vm.prank(spawner); + uint256[] memory createdBids = auctionManager.createBid{value: 0.1 ether}(1, 0.1 ether); + + // Get the data for the validator key gen + (bytes memory pubkey, bytes memory signature, bytes memory withdrawalCredentials, bytes32 depositDataRoot, IStakingManager.DepositData memory depositData, address etherFiNode) = helper_getDataForValidatorKeyGen(); + + IStakingManager.DepositData[] memory depositDataArray = new IStakingManager.DepositData[](1); + depositDataArray[0] = depositData; + + // Register first + vm.prank(spawner); + liquidityPool.batchRegister(depositDataArray, createdBids, etherFiNode); + + // Fund LP with sufficient ETH (1 ether per validator) + vm.deal(address(liquidityPool), 100 ether); + + // Now create validators + vm.prank(admin); + liquidityPool.batchCreateBeaconValidators(depositDataArray, createdBids, etherFiNode); + + // Verify validator status is CONFIRMED + bytes32 validatorHash = keccak256(abi.encodePacked( + depositData.publicKey, + depositData.signature, + depositData.depositDataRoot, + depositData.ipfsHashForEncryptedValidatorKey, + createdBids[0], + etherFiNode + )); + + assertEq( + uint8(stakingManager.validatorCreationStatus(validatorHash)), + uint8(IStakingManager.ValidatorCreationStatus.CONFIRMED) + ); + } + + function test_batchCreateBeaconValidators_accountsForEthCorrectly() public { + address spawner = vm.addr(0x1234); + + vm.prank(admin); + nodeOperatorManager.addToWhitelist(spawner); + + vm.prank(operatingTimelock); + liquidityPool.registerValidatorSpawner(spawner); + + vm.deal(spawner, 100 ether); + vm.prank(spawner); + nodeOperatorManager.registerNodeOperator("ipfs_hash", 1000); + + vm.prank(spawner); + uint256[] memory createdBids = auctionManager.createBid{value: 0.1 ether}(1, 0.1 ether); + + // Get the data for the validator key gen + (bytes memory pubkey, bytes memory signature, bytes memory withdrawalCredentials, bytes32 depositDataRoot, IStakingManager.DepositData memory depositData, address etherFiNode) = helper_getDataForValidatorKeyGen(); + + IStakingManager.DepositData[] memory depositDataArray = new IStakingManager.DepositData[](1); + depositDataArray[0] = depositData; + + vm.prank(spawner); + liquidityPool.batchRegister(depositDataArray, createdBids, etherFiNode); + + // Record initial balances + uint128 initialTotalOut = liquidityPool.totalValueOutOfLp(); + uint128 initialTotalIn = liquidityPool.totalValueInLp(); + + vm.deal(address(liquidityPool), 100 ether); + + uint256 expectedEthOut = 1 ether; // 1 ether per validator + + vm.prank(admin); + liquidityPool.batchCreateBeaconValidators(depositDataArray, createdBids, etherFiNode); + + // Verify accounting + assertEq( + liquidityPool.totalValueOutOfLp(), + initialTotalOut + uint128(expectedEthOut) + ); + assertEq( + liquidityPool.totalValueInLp(), + initialTotalIn - uint128(expectedEthOut) + ); + } + + function test_batchCreateBeaconValidators_withMultipleValidators() public { + address spawner = vm.addr(0x1234); + + vm.prank(admin); + nodeOperatorManager.addToWhitelist(spawner); + + vm.prank(operatingTimelock); + liquidityPool.registerValidatorSpawner(spawner); + + vm.deal(spawner, 100 ether); + vm.prank(spawner); + nodeOperatorManager.registerNodeOperator("ipfs_hash", 1000); + + // Create 3 bids + vm.prank(spawner); + uint256[] memory createdBids = auctionManager.createBid{value: 0.3 ether}(3, 0.1 ether); + + vm.prank(operatingTimelock); + address etherFiNode = stakingManager.instantiateEtherFiNode(true); + + address eigenPod = address(IEtherFiNode(etherFiNode).getEigenPod()); + bytes memory withdrawalCreds = etherFiNodesManager.addressToCompoundingWithdrawalCredentials(eigenPod); + + // Create deposit data for 3 validators + IStakingManager.DepositData[] memory depositData = new IStakingManager.DepositData[](3); + for (uint256 i = 0; i < 3; i++) { + bytes memory pubkey = vm.randomBytes(48); + bytes memory signature = vm.randomBytes(96); + bytes32 depositRoot = depositDataRootGenerator.generateDepositDataRoot( + pubkey, + signature, + withdrawalCreds, + stakingManager.initialDepositAmount() + ); + + depositData[i] = IStakingManager.DepositData({ + publicKey: pubkey, + signature: signature, + depositDataRoot: depositRoot, + ipfsHashForEncryptedValidatorKey: "test_ipfs" + }); + } + + // Register all + vm.prank(spawner); + liquidityPool.batchRegister(depositData, createdBids, etherFiNode); + + vm.deal(address(liquidityPool), 100 ether); + + // Create all validators - should require 3 ether + vm.prank(admin); + liquidityPool.batchCreateBeaconValidators(depositData, createdBids, etherFiNode); + + // Verify all are confirmed + for (uint256 i = 0; i < 3; i++) { + bytes32 validatorHash = keccak256(abi.encodePacked( + depositData[i].publicKey, + depositData[i].signature, + depositData[i].depositDataRoot, + depositData[i].ipfsHashForEncryptedValidatorKey, + createdBids[i], + etherFiNode + )); + + assertEq( + uint8(stakingManager.validatorCreationStatus(validatorHash)), + uint8(IStakingManager.ValidatorCreationStatus.CONFIRMED) + ); + } + } + + // ==================== invalidateRegisteredBeaconValidator Tests ==================== + + function test_invalidateRegisteredBeaconValidator_revertsWhenNoRole() public { + address unauthorizedUser = vm.addr(0xDEAD); + + // Setup a registered validator + address spawner = vm.addr(0x1234); + vm.prank(admin); + nodeOperatorManager.addToWhitelist(spawner); + + vm.prank(operatingTimelock); + liquidityPool.registerValidatorSpawner(spawner); + + vm.deal(spawner, 100 ether); + vm.prank(spawner); + nodeOperatorManager.registerNodeOperator("ipfs_hash", 1000); + + vm.prank(spawner); + uint256[] memory createdBids = auctionManager.createBid{value: 0.1 ether}(1, 0.1 ether); + + (bytes memory pubkey, bytes memory signature, bytes memory withdrawalCredentials, bytes32 depositDataRoot, IStakingManager.DepositData memory depositData, address etherFiNode) = helper_getDataForValidatorKeyGen(); + + // Register the validator first + vm.prank(spawner); + liquidityPool.batchRegister(toArray(depositData), createdBids, etherFiNode); + + // Try to invalidate without the role + vm.prank(unauthorizedUser); + vm.expectRevert(IStakingManager.IncorrectRole.selector); + stakingManager.invalidateRegisteredBeaconValidator(depositData, createdBids[0], etherFiNode); + } + + function test_invalidateRegisteredBeaconValidator_revertsWhenNotRegistered() public { + address spawner = vm.addr(0x1234); + + vm.prank(admin); + nodeOperatorManager.addToWhitelist(spawner); + + vm.prank(operatingTimelock); + liquidityPool.registerValidatorSpawner(spawner); + + vm.deal(spawner, 100 ether); + vm.prank(spawner); + nodeOperatorManager.registerNodeOperator("ipfs_hash", 1000); + + vm.prank(spawner); + uint256[] memory createdBids = auctionManager.createBid{value: 0.1 ether}(1, 0.1 ether); + + (,,, , IStakingManager.DepositData memory depositData, address etherFiNode) = helper_getDataForValidatorKeyGen(); + + // Don't register - validator doesn't exist + vm.prank(admin); + vm.expectRevert(IStakingManager.InvalidValidatorCreationStatus.selector); + stakingManager.invalidateRegisteredBeaconValidator(depositData, createdBids[0], etherFiNode); + } + + function test_invalidateRegisteredBeaconValidator_revertsWhenAlreadyConfirmed() public { + address spawner = vm.addr(0x1234); + + vm.prank(admin); + nodeOperatorManager.addToWhitelist(spawner); + + vm.prank(operatingTimelock); + liquidityPool.registerValidatorSpawner(spawner); + + vm.deal(spawner, 100 ether); + vm.prank(spawner); + nodeOperatorManager.registerNodeOperator("ipfs_hash", 1000); + + vm.prank(spawner); + uint256[] memory createdBids = auctionManager.createBid{value: 0.1 ether}(1, 0.1 ether); + + (,,, , IStakingManager.DepositData memory depositData, address etherFiNode) = helper_getDataForValidatorKeyGen(); + + vm.prank(spawner); + liquidityPool.batchRegister(toArray(depositData), createdBids, etherFiNode); + + // Confirm the validator + vm.deal(address(liquidityPool), 100 ether); + vm.prank(admin); + liquidityPool.batchCreateBeaconValidators(toArray(depositData), createdBids, etherFiNode); + + // Try to invalidate a confirmed validator - should revert + vm.prank(admin); + vm.expectRevert(IStakingManager.InvalidValidatorCreationStatus.selector); + stakingManager.invalidateRegisteredBeaconValidator(depositData, createdBids[0], etherFiNode); + } + + function test_invalidateRegisteredBeaconValidator_revertsWhenAlreadyInvalidated() public { + address spawner = vm.addr(0x1234); + + vm.prank(admin); + nodeOperatorManager.addToWhitelist(spawner); + + vm.prank(operatingTimelock); + liquidityPool.registerValidatorSpawner(spawner); + + vm.deal(spawner, 100 ether); + vm.prank(spawner); + nodeOperatorManager.registerNodeOperator("ipfs_hash", 1000); + + vm.prank(spawner); + uint256[] memory createdBids = auctionManager.createBid{value: 0.1 ether}(1, 0.1 ether); + + (,,, , IStakingManager.DepositData memory depositData, address etherFiNode) = helper_getDataForValidatorKeyGen(); + + vm.prank(spawner); + liquidityPool.batchRegister(toArray(depositData), createdBids, etherFiNode); + + // Invalidate the validator first time + vm.prank(admin); + stakingManager.invalidateRegisteredBeaconValidator(depositData, createdBids[0], etherFiNode); + + // Try to invalidate again - should revert + vm.prank(admin); + vm.expectRevert(IStakingManager.InvalidValidatorCreationStatus.selector); + stakingManager.invalidateRegisteredBeaconValidator(depositData, createdBids[0], etherFiNode); + } + + function test_invalidateRegisteredBeaconValidator_succeeds() public { + address spawner = vm.addr(0x1234); + + vm.prank(admin); + nodeOperatorManager.addToWhitelist(spawner); + + vm.prank(operatingTimelock); + liquidityPool.registerValidatorSpawner(spawner); + + vm.deal(spawner, 100 ether); + vm.prank(spawner); + nodeOperatorManager.registerNodeOperator("ipfs_hash", 1000); + + vm.prank(spawner); + uint256[] memory createdBids = auctionManager.createBid{value: 0.1 ether}(1, 0.1 ether); + + (,,, , IStakingManager.DepositData memory depositData, address etherFiNode) = helper_getDataForValidatorKeyGen(); + + vm.prank(spawner); + liquidityPool.batchRegister(toArray(depositData), createdBids, etherFiNode); + + // Verify status is REGISTERED + bytes32 validatorHash = keccak256(abi.encodePacked( + depositData.publicKey, + depositData.signature, + depositData.depositDataRoot, + depositData.ipfsHashForEncryptedValidatorKey, + createdBids[0], + etherFiNode + )); + + assertEq( + uint8(stakingManager.validatorCreationStatus(validatorHash)), + uint8(IStakingManager.ValidatorCreationStatus.REGISTERED) + ); + + // Invalidate the validator + vm.expectEmit(true, true, true, true); + emit IStakingManager.ValidatorCreationStatusUpdated(depositData, createdBids[0], etherFiNode, validatorHash, IStakingManager.ValidatorCreationStatus.INVALIDATED); + + vm.prank(admin); + stakingManager.invalidateRegisteredBeaconValidator(depositData, createdBids[0], etherFiNode); + + // Verify status is INVALIDATED + assertEq( + uint8(stakingManager.validatorCreationStatus(validatorHash)), + uint8(IStakingManager.ValidatorCreationStatus.INVALIDATED) + ); + } +} \ No newline at end of file