diff --git a/src/interfaces/IStakingNodeV2.sol b/src/interfaces/IStakingNodeV2.sol new file mode 100644 index 000000000..ecf8bbec0 --- /dev/null +++ b/src/interfaces/IStakingNodeV2.sol @@ -0,0 +1,15 @@ +import "./IStakingNode.sol"; + +import {BeaconChainProofs as BeaconChainProofsv021 } from "../external/eigenlayer/v0.2.1/BeaconChainProofs.sol"; + + +interface IStakingNodeV2 is IStakingNode { + + function verifyWithdrawalCredentials( + uint256 oracleTimestamp, + BeaconChainProofsv021.StateRootProof memory stateRootProof, + uint40[] memory validatorIndexes, + bytes[] memory validatorFieldsProofs, + bytes32[][] memory validatorFields + ) external; +} diff --git a/test/foundry/ActorAddresses.sol b/test/foundry/ActorAddresses.sol index d1ce5f9c9..acac756ae 100644 --- a/test/foundry/ActorAddresses.sol +++ b/test/foundry/ActorAddresses.sol @@ -53,6 +53,23 @@ contract ActorAddresses { DEPOSIT_BOOTSTRAPER: 0xFABB0ac9d68B0B445fB7357272Ff202C5651694a, VALIDATOR_REMOVER_MANAGER: 0x1CBd3b2770909D4e10f157cABC84C7264073C9Ec }); + + + actors[17000] = Actors({ + PROXY_ADMIN_OWNER: 0x70997970C51812dc3A010C7d01b50e0d17dc79C8, + TRANSFER_ENABLED_EOA: 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC, + ADMIN: 0x90F79bf6EB2c4f870365E785982E1f101E93b906, + STAKING_ADMIN: 0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65, + STAKING_NODES_ADMIN: 0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc, + VALIDATOR_MANAGER: 0x976EA74026E726554dB657fA54763abd0C3a0aa9, + FEE_RECEIVER: 0x14dC79964da2C08b23698B3D3cc7Ca32193d9955, + PAUSE_ADMIN: 0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f, + LSD_RESTAKING_MANAGER: 0xa0Ee7A142d267C1f36714E4a8F75612F20a79720, + STAKING_NODE_CREATOR: 0xBcd4042DE499D14e55001CcbB24a551F3b954096, + ORACLE_MANAGER: 0x71bE63f3384f5fb98995898A86B02Fb2426c5788, + DEPOSIT_BOOTSTRAPER: 0xFABB0ac9d68B0B445fB7357272Ff202C5651694a, + VALIDATOR_REMOVER_MANAGER: 0x1CBd3b2770909D4e10f157cABC84C7264073C9Ec + }); } function getActors(uint256 chainId) external view returns (Actors memory) { diff --git a/test/foundry/integration/IntegrationBaseTest.sol b/test/foundry/integration/IntegrationBaseTest.sol index 27de0daf9..c3f5cb42f 100644 --- a/test/foundry/integration/IntegrationBaseTest.sol +++ b/test/foundry/integration/IntegrationBaseTest.sol @@ -28,6 +28,7 @@ import {RewardsReceiver} from "../../../src/RewardsReceiver.sol"; import {RewardsDistributor} from "../../../src/RewardsDistributor.sol"; import {ContractAddresses} from "../ContractAddresses.sol"; import {StakingNode} from "../../../src/StakingNode.sol"; +import {StakingNodeV2} from "../../../src/StakingNodeV2.sol"; import {Utils} from "../../../scripts/forge/Utils.sol"; import {ActorAddresses} from "../ActorAddresses.sol"; @@ -204,7 +205,12 @@ contract IntegrationBaseTest is Test, Utils { } function setupStakingNodesManager() public { - stakingNodeImplementation = new StakingNode(); + if (block.chainid == 17000) { + // for holesky use the upgraded version + stakingNodeImplementation = new StakingNodeV2(); + } else { + stakingNodeImplementation = new StakingNode(); + } StakingNodesManager.Init memory stakingNodesManagerInit = StakingNodesManager.Init({ admin: actors.ADMIN, stakingAdmin: actors.STAKING_ADMIN, @@ -243,14 +249,14 @@ contract IntegrationBaseTest is Test, Utils { assetsAddresses[0] = chainAddresses.lsd.STETH_ADDRESS; strategies[0] = IStrategy(chainAddresses.lsd.STETH_STRATEGY_ADDRESS); priceFeeds[0] = chainAddresses.lsd.STETH_FEED_ADDRESS; - maxAges[0] = uint256(86400); //one hour + maxAges[0] = (block.chainid == 17000) ? type(uint256).max : uint256(86400); // rETH assets[1] = IERC20(chainAddresses.lsd.RETH_ADDRESS); assetsAddresses[1] = chainAddresses.lsd.RETH_ADDRESS; strategies[1] = IStrategy(chainAddresses.lsd.RETH_STRATEGY_ADDRESS); priceFeeds[1] = chainAddresses.lsd.RETH_FEED_ADDRESS; - maxAges[1] = uint256(86400); + maxAges[0] = (block.chainid == 17000) ? type(uint256).max : uint256(86400); YieldNestOracle.Init memory oracleInit = YieldNestOracle.Init({ assets: assetsAddresses, @@ -281,7 +287,7 @@ contract IntegrationBaseTest is Test, Utils { vm.deal(actors.DEPOSIT_BOOTSTRAPER, 10000 ether); vm.prank(actors.DEPOSIT_BOOTSTRAPER); - (bool success, ) = chainAddresses.lsd.STETH_ADDRESS.call{value: 1000 ether}(""); + (bool success, ) = chainAddresses.lsd.STETH_ADDRESS.call{value: 100 ether}(""); require(success, "ETH transfer failed"); vm.prank(actors.DEPOSIT_BOOTSTRAPER); diff --git a/test/foundry/integration/StakingNodeV2.t.sol b/test/foundry/integration/StakingNodeV2.t.sol new file mode 100644 index 000000000..070a56afe --- /dev/null +++ b/test/foundry/integration/StakingNodeV2.t.sol @@ -0,0 +1,268 @@ +// SPDX-License-Identifier: BSD 3-Clause License +pragma solidity ^0.8.24; + +import {UpgradeableBeacon} from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {IPausable} from "../../../src/external/eigenlayer/v0.2.1/interfaces/IPausable.sol"; +import {IDelegationManager} from "../../../src/external/eigenlayer/v0.2.1/interfaces/IDelegationManager.sol"; +import {IEigenPodManager} from "../../../src/external/eigenlayer/v0.2.1/interfaces/IEigenPodManager.sol"; +import {IntegrationBaseTest} from "./IntegrationBaseTest.sol"; +import {IStakingNodeV2} from "../../../src/interfaces/IStakingNodeV2.sol"; +import {IStakingNodesManager} from "../../../src/interfaces/IStakingNodesManager.sol"; +import {IEigenPod} from "../../../src/external/eigenlayer/v0.2.1/interfaces/IEigenPod.sol"; +import {IDelayedWithdrawalRouter} from "../../../src/external/eigenlayer/v0.2.1/interfaces/IDelayedWithdrawalRouter.sol"; +import {BeaconChainProofs} from "../../../src/external/eigenlayer/v0.2.1/BeaconChainProofs.sol"; +import {TestnetEigenPodMock} from "../mocks/testnet/TestnetEigenPodMock.sol"; +import {StakingNode,IStrategyManager} from "../../../src/StakingNode.sol"; +import {StakingNodeTestBase} from "./StakingNode.t.sol"; +import {stdStorage, StdStorage} from "forge-std/Test.sol"; + + + + +contract StakingNodeV2TestBase is IntegrationBaseTest { + + function setupStakingNode(uint256 depositAmount) + public + returns (IStakingNodeV2, IEigenPod) { + + address addr1 = vm.addr(100); + + require(depositAmount % 32 ether == 0, "depositAmount must be a multiple of 32 ether"); + + uint256 validatorCount = depositAmount / 32 ether; + + vm.deal(addr1, depositAmount); + + vm.prank(addr1); + yneth.depositETH{value: depositAmount}(addr1); + + vm.prank(actors.STAKING_NODE_CREATOR); + IStakingNodeV2 stakingNodeInstance = IStakingNodeV2(address(stakingNodesManager.createStakingNode())); + + uint256 nodeId = 0; + + IStakingNodesManager.ValidatorData[] memory validatorData = new IStakingNodesManager.ValidatorData[](validatorCount); + for (uint256 i = 0; i < validatorCount; i++) { + bytes memory publicKey = abi.encodePacked(uint256(i)); + publicKey = bytes.concat(publicKey, new bytes(ZERO_PUBLIC_KEY.length - publicKey.length)); + validatorData[i] = IStakingNodesManager.ValidatorData({ + publicKey: publicKey, + signature: ZERO_SIGNATURE, + nodeId: nodeId, + depositDataRoot: bytes32(0) + }); + } + + bytes memory withdrawalCredentials = stakingNodesManager.getWithdrawalCredentials(nodeId); + + for (uint256 i = 0; i < validatorData.length; i++) { + uint256 amount = depositAmount / validatorData.length; + bytes32 depositDataRoot = stakingNodesManager.generateDepositRoot(validatorData[i].publicKey, validatorData[i].signature, withdrawalCredentials, amount); + validatorData[i].depositDataRoot = depositDataRoot; + } + + bytes32 depositRoot = depositContractEth2.get_deposit_root(); + vm.prank(actors.VALIDATOR_MANAGER); + stakingNodesManager.registerValidators(depositRoot, validatorData); + + uint256 actualETHBalance = stakingNodeInstance.getETHBalance(); + assertEq(actualETHBalance, depositAmount, "ETH balance does not match expected value"); + + IEigenPod eigenPodInstance = IEigenPod(address(stakingNodeInstance.eigenPod())); + + return (stakingNodeInstance, eigenPodInstance); + } + + + modifier onlyHolesky() { + require(block.chainid == 17000, "This test can only be run on Holesky."); + _; + } +} + +contract StakingNodeVerifyWithdrawalCredentials is StakingNodeV2TestBase { + using stdStorage for StdStorage; + + // function testVerifyWithdrawalCredentialsRevertingWhenPaused() public { + + // (IStakingNode stakingNodeInstance, IEigenPod eigenPodInstance) = setupStakingNode(32 ether); + + // MainnetEigenPodMock mainnetEigenPodMock = new MainnetEigenPodMock(eigenPodManager); + + // address eigenPodBeaconAddress = eigenPodManager.eigenPodBeacon(); + // address beaconOwner = Ownable(eigenPodBeaconAddress).owner(); + + // UpgradeableBeacon beacon = UpgradeableBeacon(eigenPodBeaconAddress); + // address previousImplementation = beacon.implementation(); + + // vm.prank(beaconOwner); + // beacon.upgradeTo(address(mainnetEigenPodMock)); + + + // MainnetEigenPodMock(address(eigenPodInstance)).sethasRestaked(true); + + // uint64[] memory oracleBlockNumbers = new uint64[](1); + // oracleBlockNumbers[0] = 0; // Mock value + + // uint40[] memory validatorIndexes = new uint40[](1); + // validatorIndexes[0] = 1234567; // Validator index + + // BeaconChainProofs.ValidatorFieldsAndBalanceProofs[] memory proofs = new BeaconChainProofs.ValidatorFieldsAndBalanceProofs[](1); + // proofs[0] = BeaconChainProofs.ValidatorFieldsAndBalanceProofs({ + // validatorFieldsProof: new bytes(0), // Mock value + // validatorBalanceProof: new bytes(0), // Mock value + // balanceRoot: bytes32(0) // Mock value + // }); + + // bytes32[][] memory validatorFields = new bytes32[][](1); + // validatorFields[0] = new bytes32[](2); + // validatorFields[0][0] = bytes32(0); // Mock value + // validatorFields[0][1] = bytes32(0); // Mock value + + // // Note: Deposits are currently paused as per the PAUSED_DEPOSITS flag in StrategyManager.sol + // // See: https://github.com/Layr-Labs/eigenlayer-contracts/blob/c7bf3817c5e1430672bf8bc80558d8439a2022af/src/contracts/core/StrategyManager.sol#L168 + // vm.expectRevert("Pausable: index is paused"); + // vm.prank(actors.STAKING_NODES_ADMIN); + // stakingNodeInstance.verifyWithdrawalCredentials(oracleBlockNumbers, validatorIndexes, proofs, validatorFields); + + // // go back to previous implementation + // vm.prank(beaconOwner); + // beacon.upgradeTo(previousImplementation); + + // // Note: reenable this when verifyWithdrawals works + // // // Note: once deposits are unpaused this should work + // // vm.expectRevert("StrategyManager._removeShares: shareAmount too high"); + // // stakingNodeInstance.startWithdrawal(withdrawalAmount); + + + // // // Note: once deposits are unpaused and a withdrawal is queued, it may be completed + // // vm.expectRevert("StrategyManager.completeQueuedWithdrawal: withdrawal is not pending"); + // // WithdrawalCompletionParams memory params = WithdrawalCompletionParams({ + // // middlewareTimesIndex: 0, // Assuming middlewareTimesIndex is not used in this context + // // amount: withdrawalAmount, + // // withdrawalStartBlock: uint32(block.number), // Current block number as the start block + // // delegatedAddress: address(0), // Assuming no delegation address is needed for this withdrawal + // // nonce: 0 // first nonce is 0 + // // }); + // // stakingNodeInstance.completeWithdrawal(params); + // } + + // function testCreateEigenPodReturnsEigenPodAddressAfterCreated() public { + // vm.prank(actors.STAKING_NODE_CREATOR); + // IStakingNode stakingNodeInstance = stakingNodesManager.createStakingNode(); + // IEigenPod eigenPodInstance = stakingNodeInstance.eigenPod(); + // assertEq(address(eigenPodInstance), address(stakingNodeInstance.eigenPod())); + // } + + // function testDelegateFailWhenNotAdmin() public { + // vm.prank(actors.STAKING_NODE_CREATOR); + // IStakingNode stakingNodeInstance = stakingNodesManager.createStakingNode(); + // vm.expectRevert(); + // stakingNodeInstance.delegate(address(this)); + // } + + // function testStakingNodeDelegate() public { + // vm.prank(actors.STAKING_NODE_CREATOR); + // IStakingNode stakingNodeInstance = stakingNodesManager.createStakingNode(); + // IDelegationManager delegationManager = stakingNodesManager.delegationManager(); + // IPausable pauseDelegationManager = IPausable(address(delegationManager)); + // vm.prank(chainAddresses.eigenlayer.DELEGATION_PAUSER_ADDRESS); + // pauseDelegationManager.unpause(0); + + // // register as operator + // delegationManager.registerAsOperator(IDelegationTerms(address(this))); + // vm.prank(actors.STAKING_NODES_ADMIN); + // stakingNodeInstance.delegate(address(this)); + // } + + // function testStakingNodeUndelegate() public { + // vm.prank(actors.STAKING_NODE_CREATOR); + // IStakingNode stakingNodeInstance = stakingNodesManager.createStakingNode(); + // IDelegationManager delegationManager = stakingNodesManager.delegationManager(); + // IPausable pauseDelegationManager = IPausable(address(delegationManager)); + + // // Unpause delegation manager to allow delegation + // vm.prank(chainAddresses.eigenlayer.DELEGATION_PAUSER_ADDRESS); + // pauseDelegationManager.unpause(0); + + // // Register as operator and delegate + // delegationManager.registerAsOperator(IDelegationTerms(address(this))); + // vm.prank(actors.STAKING_NODES_ADMIN); + // stakingNodeInstance.delegate(address(this)); + + // // // Attempt to undelegate + // vm.expectRevert(); + // stakingNodeInstance.undelegate(); + + // IStrategyManager strategyManager = stakingNodesManager.strategyManager(); + // uint256 stakerStrategyListLength = strategyManager.stakerStrategyListLength(address(stakingNodeInstance)); + // assertEq(stakerStrategyListLength, 0, "Staker strategy list length should be 0."); + + // // Now actually undelegate with the correct role + // vm.prank(actors.STAKING_NODES_ADMIN); + // stakingNodeInstance.undelegate(); + + // // Verify undelegation + // address delegatedAddress = delegationManager.delegatedTo(address(stakingNodeInstance)); + // assertEq(delegatedAddress, address(0), "Delegation should be cleared after undelegation."); + // } + + // function testImplementViewFunction() public { + // vm.prank(actors.STAKING_NODE_CREATOR); + // IStakingNode stakingNodeInstance = stakingNodesManager.createStakingNode(); + // assertEq(stakingNodeInstance.implementation(), address(stakingNodeImplementation)); + // } + + function testVerifyWithdrawalCredentialsWithStrategyUnpausedOnHoleksy() onlyHolesky public { + + uint256 depositAmount = 32 ether; + + (IStakingNodeV2 stakingNodeInstance, IEigenPod eigenPodInstance) = setupStakingNode(depositAmount); + + TestnetEigenPodMock testnetEigenPodMock = new TestnetEigenPodMock(IEigenPodManager(address(eigenPodManager))); + + address eigenPodBeaconAddress = eigenPodManager.eigenPodBeacon(); + address beaconOwner = Ownable(eigenPodBeaconAddress).owner(); + + UpgradeableBeacon beacon = UpgradeableBeacon(eigenPodBeaconAddress); + address previousImplementation = beacon.implementation(); + + vm.prank(beaconOwner); + beacon.upgradeTo(address(testnetEigenPodMock)); + + TestnetEigenPodMock(address(eigenPodInstance)).sethasRestaked(true); + + { + + uint256 oracleTimestamp = 98765; + uint40[] memory validatorIndexes = new uint40[](1); + validatorIndexes[0] = 1234567; // Validator index + BeaconChainProofs.StateRootProof memory stateRootProof = BeaconChainProofs.StateRootProof({ + beaconStateRoot: bytes32(0), // Dummy value + proof: new bytes(0) // Dummy value + }); + bytes32[][] memory validatorFields = new bytes32[][](1); + validatorFields[0] = new bytes32[](2); + validatorFields[0][0] = bytes32(0); // Mock value + validatorFields[0][1] = bytes32(0); // Mock value + vm.prank(actors.STAKING_NODES_ADMIN); + + bytes[] memory validatorFieldsProofs = new bytes[](validatorIndexes.length); + for(uint i = 0; i < validatorIndexes.length; i++) { + validatorFieldsProofs[i] = bytes("dummy"); + } + + stakingNodeInstance.verifyWithdrawalCredentials(oracleTimestamp, stateRootProof, validatorIndexes, validatorFieldsProofs, validatorFields); + + // go back to previous implementation + vm.prank(beaconOwner); + beacon.upgradeTo(previousImplementation); + } + + uint256 shares = strategyManager.stakerStrategyShares(address(stakingNodeInstance), stakingNodeInstance.beaconChainETHStrategy()); + assertEq(shares, depositAmount, "Shares do not match deposit amount"); + + } + +} diff --git a/test/foundry/mocks/testnet/TestnetEigenPodMock.sol b/test/foundry/mocks/testnet/TestnetEigenPodMock.sol new file mode 100644 index 000000000..a44cdf687 --- /dev/null +++ b/test/foundry/mocks/testnet/TestnetEigenPodMock.sol @@ -0,0 +1,172 @@ +pragma solidity >=0.8.12; + +import { BeaconChainProofs } from "../../../../src/external/eigenlayer/v0.2.1/BeaconChainProofs.sol"; +import {IEigenPodManager} from "../../../../src/external/eigenlayer/v0.2.1/interfaces/IEigenPodManager.sol"; +import {IEigenPod} from "../../../../src/external/eigenlayer/v0.2.1/interfaces/IEigenPod.sol"; + +import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; + +abstract contract TestnetInitializableMock { + /** + * @dev Indicates that the contract has been initialized. + * @custom:oz-retyped-from bool + */ + uint8 private _initialized; + + /** + * @dev Indicates that the contract is in the process of being initialized. + */ + bool private _initializing; + +} + +abstract contract TestnetReentrancyGuardUpgradeableMock is TestnetInitializableMock { + // Booleans are more expensive than uint256 or any type that takes up a full + // word because each write operation emits an extra SLOAD to first read the + // slot's contents, replace the bits taken up by the boolean, and then write + // back. This is the compiler's defense against contract upgrades and + // pointer aliasing, and it cannot be disabled. + + // The values being non-zero value makes deployment a bit more expensive, + // but in exchange the refund on every call to nonReentrant will be lower in + // amount. Since refunds are capped to a percentage of the total + // transaction's gas, it is best to keep them low in cases like this one, to + // increase the likelihood of the full refund coming into effect. + uint256 private constant _NOT_ENTERED = 1; + uint256 private constant _ENTERED = 2; + + uint256 private _status; + + /** + * @dev This empty reserved space is put in place to allow future versions to add new + * variables without shifting down storage in the inheritance chain. + * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + */ + uint256[49] private __gap; +} + +contract TestnetEigenPodMock is TestnetReentrancyGuardUpgradeableMock { + + // CONSTANTS + IMMUTABLES + // @notice Internal constant used in calculations, since the beacon chain stores balances in Gwei rather than wei + uint256 internal constant GWEI_TO_WEI = 1e9; + + /// @notice Emitted when an ETH validator's withdrawal credentials are successfully verified to be pointed to this eigenPod + event ValidatorRestaked(uint40 validatorIndex); + + /// @notice Emitted when an ETH validator's balance is proven to be updated. Here newValidatorBalanceGwei + // is the validator's balance that is credited on EigenLayer. + event ValidatorBalanceUpdated(uint40 validatorIndex, uint64 balanceTimestamp, uint64 newValidatorBalanceGwei); + + + ///@notice The maximum amount of ETH, in gwei, a validator can have restaked in the eigenlayer + uint64 public immutable MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR; + /// @notice The single EigenPodManager for EigenLayer + IEigenPodManager public immutable eigenPodManager; + + // STORAGE VARIABLES + /// @notice The owner of this EigenPod + address public podOwner; + + /** + * @notice The latest timestamp at which the pod owner withdrew the balance of the pod, via calling `withdrawBeforeRestaking`. + * @dev This variable is only updated when the `withdrawBeforeRestaking` function is called, which can only occur before `hasRestaked` is set to true for this pod. + * Proofs for this pod are only valid against Beacon Chain state roots corresponding to timestamps after the stored `mostRecentWithdrawalTimestamp`. + */ + uint64 public mostRecentWithdrawalTimestamp; + + /// @notice the amount of execution layer ETH in this contract that is staked in EigenLayer (i.e. withdrawn from the Beacon Chain but not from EigenLayer), + uint64 public withdrawableRestakedExecutionLayerGwei; + + /// @notice an indicator of whether or not the podOwner has ever "fully restaked" by successfully calling `verifyCorrectWithdrawalCredentials`. + bool public hasRestaked; + + /// @notice This is a mapping of validatorPubkeyHash to timestamp to whether or not they have proven a withdrawal for that timestamp + mapping(bytes32 => mapping(uint64 => bool)) public provenWithdrawal; + + /// @notice This is a mapping that tracks a validator's information by their pubkey hash + mapping(bytes32 => IEigenPod.ValidatorInfo) internal _validatorPubkeyHashToInfo; + + /// @notice This variable tracks any ETH deposited into this contract via the `receive` fallback function + uint256 public nonBeaconChainETHBalanceWei; + + /// @notice This variable tracks the total amount of partial withdrawals claimed via merkle proofs prior to a switch to ZK proofs for claiming partial withdrawals + uint64 public sumOfPartialWithdrawalsClaimedGwei; + + /// @notice Number of validators with proven withdrawal credentials, who do not have proven full withdrawals + uint256 activeValidatorCount; + + + constructor(IEigenPodManager _eigenPodManager) { + eigenPodManager = _eigenPodManager; + MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR = 32e9; + } + + function verifyWithdrawalCredentials( + uint64 oracleTimestamp, + BeaconChainProofs.StateRootProof calldata stateRootProof, + uint40[] calldata validatorIndices, + bytes[] calldata validatorFieldsProofs, + bytes32[][] calldata validatorFields + ) + external + { + require( + (validatorIndices.length == validatorFieldsProofs.length) && + (validatorFieldsProofs.length == validatorFields.length), + "EigenPod.verifyWithdrawalCredentials: validatorIndices and proofs must be same length" + ); + + uint256 totalAmountToBeRestakedWei; + for (uint256 i = 0; i < validatorIndices.length; i++) { + totalAmountToBeRestakedWei += _verifyWithdrawalCredentials( + oracleTimestamp, + stateRootProof.beaconStateRoot, + validatorIndices[i], + validatorFieldsProofs[i], + validatorFields[i] + ); + } + + // Update the EigenPodManager on this pod's new balance + eigenPodManager.recordBeaconChainETHBalanceUpdate(podOwner, int256(totalAmountToBeRestakedWei)); + } + + + function _verifyWithdrawalCredentials( + uint64 oracleTimestamp, + bytes32 beaconStateRoot, + uint40 validatorIndex, + bytes calldata validatorFieldsProof, + bytes32[] calldata validatorFields + ) internal returns (uint256) { + bytes32 validatorPubkeyHash = BeaconChainProofs.getPubkeyHash(validatorFields); + IEigenPod.ValidatorInfo memory validatorInfo = _validatorPubkeyHashToInfo[validatorPubkeyHash]; + + + // Proofs complete - update this validator's status, record its proven balance, and save in state: + activeValidatorCount++; + validatorInfo.status = IEigenPod.VALIDATOR_STATUS.ACTIVE; + validatorInfo.validatorIndex = validatorIndex; + validatorInfo.mostRecentBalanceUpdateTimestamp = oracleTimestamp; + + validatorInfo.restakedBalanceGwei = MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR; + + _validatorPubkeyHashToInfo[validatorPubkeyHash] = validatorInfo; + + emit ValidatorRestaked(validatorIndex); + emit ValidatorBalanceUpdated(validatorIndex, oracleTimestamp, validatorInfo.restakedBalanceGwei); + + return validatorInfo.restakedBalanceGwei * GWEI_TO_WEI; + } + + + function _podWithdrawalCredentials() internal view returns (bytes memory) { + return abi.encodePacked(bytes1(uint8(1)), bytes11(0), address(this)); + } + + function sethasRestaked(bool v) public { + hasRestaked = v; + } + +}