From 8d60c00661e1331c0d6df28511ae3d7574bb4e7c Mon Sep 17 00:00:00 2001 From: danoctavian Date: Mon, 25 Mar 2024 09:21:45 +0200 Subject: [PATCH 1/5] set boostrap amount unit to 1 ETH fix ynlsd donation attack issue audit-6.1 --- src/ynLSD.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ynLSD.sol b/src/ynLSD.sol index 136f9a927..8b74ba560 100644 --- a/src/ynLSD.sol +++ b/src/ynLSD.sol @@ -46,7 +46,7 @@ contract ynLSD is IynLSD, ynBase, ReentrancyGuardUpgradeable, IynLSDEvents { bytes32 public constant LSD_RESTAKING_MANAGER_ROLE = keccak256("LSD_RESTAKING_MANAGER_ROLE"); bytes32 public constant LSD_STAKING_NODE_CREATOR_ROLE = keccak256("LSD_STAKING_NODE_CREATOR_ROLE"); - uint256 public constant BOOTSTRAP_AMOUNT_UNITS = 10; + uint256 public constant BOOTSTRAP_AMOUNT_UNITS = 1; //-------------------------------------------------------------------------------------- //---------------------------------- VARIABLES --------------------------------------- From 975d2675ae75018f3111bb02b323a69967d808d1 Mon Sep 17 00:00:00 2001 From: danoctavian Date: Sat, 23 Mar 2024 05:53:09 +0200 Subject: [PATCH 2/5] add functions to handle eigenlayer v0.2 --- src/StakingNode.sol | 3 ++ src/StakingNodeV2.sol | 93 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 src/StakingNodeV2.sol diff --git a/src/StakingNode.sol b/src/StakingNode.sol index 9019269f5..5e543cdba 100644 --- a/src/StakingNode.sol +++ b/src/StakingNode.sol @@ -9,6 +9,7 @@ import {IDelegationManager} from "./external/eigenlayer/v0.1.0/interfaces/IDeleg import {IDelayedWithdrawalRouter} from "./external/eigenlayer/v0.1.0/interfaces/IDelayedWithdrawalRouter.sol"; import {IStrategy, IStrategyManager} from "./external/eigenlayer/v0.1.0/interfaces/IStrategyManager.sol"; import {BeaconChainProofs} from "./external/eigenlayer/v0.1.0/BeaconChainProofs.sol"; + import {IStakingNodesManager} from "./interfaces/IStakingNodesManager.sol"; import {IStakingNode} from "./interfaces/IStakingNode.sol"; @@ -180,6 +181,7 @@ contract StakingNode is IStakingNode, StakingNodeEvents, ReentrancyGuardUpgradea /** * @notice Delegates the staking operation to a specified operator. + * @dev Calls the function on the version v0.2 of the Eigenlayer interface. * @param operator The address of the operator to whom the staking operation is being delegated. */ function delegate(address operator) public virtual onlyAdmin { @@ -193,6 +195,7 @@ contract StakingNode is IStakingNode, StakingNodeEvents, ReentrancyGuardUpgradea /** * @notice Undelegates the staking operation from the current operator. * @dev It retrieves the current operator by calling `delegatedTo` on the DelegationManager for event logging. + * Calls the function on the version v0.2 of the Eigenlayer interface. */ function undelegate() public virtual onlyAdmin { diff --git a/src/StakingNodeV2.sol b/src/StakingNodeV2.sol new file mode 100644 index 000000000..3d3e745a9 --- /dev/null +++ b/src/StakingNodeV2.sol @@ -0,0 +1,93 @@ +pragma solidity ^0.8.24; + +import {IDelegationManager as IDelegationManagerV02 } from "../../../src/external/eigenlayer/v0.2.1/interfaces/IDelegationManager.sol"; +import {IEigenPod as IEigenPodV02 } from "../../../src/external/eigenlayer/v0.2.1/interfaces/IEigenPod.sol"; +import {BeaconChainProofs as BeaconChainProofsV02} from "../../../src/external/eigenlayer/v0.2.1/BeaconChainProofs.sol"; +import {ISignatureUtils} from "../../../src/external/eigenlayer/v0.2.1/interfaces/ISignatureUtils.sol"; +import {BeaconChainProofs as BeaconChainProofsV02} from "../../../src/external/eigenlayer/v0.2.1/BeaconChainProofs.sol"; +import {IStrategy as IStrategyV02} from "../../../src/external/eigenlayer/v0.2.1/interfaces/IStrategy.sol"; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {StakingNode} from "../../../src/StakingNode.sol"; + +/** + The purpose of this file, StakingNodeM2.sol, is to extend the functionality of the StakingNode contract + specifically for the M2 deployment on EigenLayer that is currently operational on the Goerli testnet. + This deployment aims to address and integrate with the unique features and requirements of the EigenLayer M2, + including enhanced delegation management, withdrawal credential verification, and validator management, + tailored to the EigenLayer's specifications and protocols. + + Release: + + https://github.com/Layr-Labs/eigenlayer-contracts/releases/tag/v0.2.1-goerli-m2 + + For more detailed information and updates, refer to the GitHub release at: + https://github.com/Layr-Labs/eigenlayer-contracts/releases/ + +*/ +contract StakingNodeV2 is StakingNode { + + //-------------------------------------------------------------------------------------- + //---------------------------------- VERIFICATION AND DELEGATION -------------------- + //-------------------------------------------------------------------------------------- + + /** + * @notice Delegates the staking operation to a specified operator. + * @param operator The address of the operator to whom the staking operation is being delegated. + */ + function delegate( + address operator, + ISignatureUtils.SignatureWithExpiry memory approverSignatureAndExpiry, + bytes32 approverSalt + ) public onlyAdmin { + + IDelegationManagerV02 delegationManager = IDelegationManagerV02(address(stakingNodesManager.delegationManager())); + delegationManager.delegateTo(operator, approverSignatureAndExpiry, approverSalt); + + emit Delegated(operator, approverSalt); + } + + + /** + * @notice Undelegates the staking operation from the current operator. + * @dev It retrieves the current operator by calling `delegatedTo` on the DelegationManager for event logging. + * Calls the function on the version v0.2 of the Eigenlayer interface + */ + function undelegate() public override onlyAdmin { + + IDelegationManagerV02 delegationManager = IDelegationManagerV02(address(stakingNodesManager.delegationManager())); + address operator = delegationManager.delegatedTo(address(this)); + delegationManager.undelegate(address(this)); + + emit Undelegated(operator); + } + + /** + * @notice Validates the withdrawal credentials of validators through the Eigenlayer protocol. + * @dev Upon successful validation, Eigenlayer issues shares to the StakingNode, equivalent to the staked ETH amount. + * @dev Calls the function on the version v0.2 of the Eigenlayer interface. + */ + function verifyWithdrawalCredentials( + uint64 oracleTimestamp, + BeaconChainProofsV02.StateRootProof calldata stateRootProof, + uint40[] calldata validatorIndices, + bytes[] calldata withdrawalCredentialProofs, + bytes32[][] calldata validatorFields + ) external onlyAdmin { + + if (validatorIndices.length != withdrawalCredentialProofs.length) { + revert MismatchedValidatorIndexAndProofsLengths(validatorIndices.length, withdrawalCredentialProofs.length); + } + if (withdrawalCredentialProofs.length != validatorFields.length) { + revert MismatchedProofsAndValidatorFieldsLengths(withdrawalCredentialProofs.length, validatorFields.length); + } + + IEigenPodV02(address(eigenPod)).verifyWithdrawalCredentials( + oracleTimestamp, + stateRootProof, + validatorIndices, + withdrawalCredentialProofs, + validatorFields + ); + } +} From 4aafffc89dff6a0e83a46f278c89ed18ad8432c8 Mon Sep 17 00:00:00 2001 From: danoctavian Date: Sun, 24 Mar 2024 02:52:33 +0200 Subject: [PATCH 3/5] update deployment scripts --- scripts/forge/BaseScript.s.sol | 7 ++- scripts/forge/DeployYieldNest.s.sol | 27 ++++++++-- scripts/forge/DeployYnLSD.s.sol | 76 +++++++++++++++++------------ 3 files changed, 74 insertions(+), 36 deletions(-) diff --git a/scripts/forge/BaseScript.s.sol b/scripts/forge/BaseScript.s.sol index 2f204003e..6eb83cfb2 100644 --- a/scripts/forge/BaseScript.s.sol +++ b/scripts/forge/BaseScript.s.sol @@ -7,6 +7,7 @@ import "../../src/StakingNode.sol"; import "../../src/RewardsReceiver.sol"; import "../../src/ynLSD.sol"; import "../../src/YieldNestOracle.sol"; +import "../../src/ynViewer.sol"; import "../../src/LSDStakingNode.sol"; import "../../src/RewardsDistributor.sol"; import "../../src/external/tokens/WETH.sol"; @@ -27,6 +28,7 @@ abstract contract BaseScript is Script, Utils { RewardsReceiver consensusLayerReceiver; RewardsDistributor rewardsDistributor; StakingNode stakingNodeImplementation; + ynViewer ynViewer; } struct ynLSDDeployment { @@ -49,6 +51,7 @@ abstract contract BaseScript is Script, Utils { vm.serializeAddress(json, "consensusLayerReceiver", address(deployment.consensusLayerReceiver)); vm.serializeAddress(json, "rewardsDistributor", address(deployment.rewardsDistributor)); vm.serializeAddress(json, "stakingNodeImplementation", address(deployment.stakingNodeImplementation)); + vm.serializeAddress(json, "ynViewer", address(deployment.ynViewer)); string memory finalJson = vm.serializeString(json, "object", "dummy"); vm.writeJson(finalJson, getDeploymentFile()); @@ -59,6 +62,7 @@ abstract contract BaseScript is Script, Utils { string memory jsonContent = vm.readFile(deploymentFile); Deployment memory deployment; deployment.ynETH = ynETH(payable(jsonContent.readAddress(".ynETH"))); + deployment.ynViewer = ynViewer(payable(jsonContent.readAddress(".ynViewer"))); deployment.stakingNodesManager = StakingNodesManager(payable(jsonContent.readAddress(".stakingNodesManager"))); deployment.executionLayerReceiver = RewardsReceiver(payable(jsonContent.readAddress(".executionLayerReceiver"))); deployment.consensusLayerReceiver = RewardsReceiver(payable(jsonContent.readAddress(".consensusLayerReceiver"))); @@ -103,7 +107,8 @@ abstract contract BaseScript is Script, Utils { LSD_RESTAKING_MANAGER: vm.envAddress("LSD_RESTAKING_MANAGER_ADDRESS"), STAKING_NODE_CREATOR: vm.envAddress("LSD_STAKING_NODE_CREATOR_ADDRESS"), ORACLE_MANAGER: vm.envAddress("YIELDNEST_ORACLE_MANAGER_ADDRESS"), - DEPOSIT_BOOTSTRAPER: vm.envAddress("DEPOSIT_BOOTSTRAPER_ADDRESS") + DEPOSIT_BOOTSTRAPER: vm.envAddress("DEPOSIT_BOOTSTRAPER_ADDRESS"), + VALIDATOR_REMOVER_MANAGER: vm.envAddress("VALIDATOR_REMOVER_MANAGER_ADDRESS") }); } diff --git a/scripts/forge/DeployYieldNest.s.sol b/scripts/forge/DeployYieldNest.s.sol index 055ad356a..2dc190717 100644 --- a/scripts/forge/DeployYieldNest.s.sol +++ b/scripts/forge/DeployYieldNest.s.sol @@ -9,6 +9,7 @@ import "../../src/StakingNodesManager.sol"; import "../../src/RewardsReceiver.sol"; import "../../src/RewardsDistributor.sol"; import "../../src/ynETH.sol"; +import "../../src/StakingNodeV2.sol"; import "../../src/interfaces/IStakingNode.sol"; import "../../src/external/ethereum/IDepositContract.sol"; import "../../src/interfaces/IRewardsDistributor.sol"; @@ -81,9 +82,22 @@ contract DeployYieldNest is BaseScript { // Deploy implementations yneth = new ynETH(); stakingNodesManager = new StakingNodesManager(); - executionLayerReceiver = new RewardsReceiver(); - consensusLayerReceiver = new RewardsReceiver(); // Instantiating consensusLayerReceiver - stakingNodeImplementation = new StakingNode(); + + { + RewardsReceiver rewardsReceiverImplementation = new RewardsReceiver(); + TransparentUpgradeableProxy executionLayerReceiverProxy = new TransparentUpgradeableProxy(address(rewardsReceiverImplementation), actors.PROXY_ADMIN_OWNER, ""); + executionLayerReceiver = RewardsReceiver(payable(executionLayerReceiverProxy)); + + TransparentUpgradeableProxy consensusLayerReceiverProxy = new TransparentUpgradeableProxy(address(rewardsReceiverImplementation), actors.PROXY_ADMIN_OWNER, ""); + consensusLayerReceiver = RewardsReceiver(payable(consensusLayerReceiverProxy)); + } + + if (block.chainid == 17000) { // holeksy + stakingNodeImplementation = new StakingNode(); + } else { + stakingNodeImplementation = new StakingNodeV2(); + } + yieldNestOracle = new YieldNestOracle(); ynlsd = new ynLSD(); @@ -119,13 +133,13 @@ contract DeployYieldNest is BaseScript { }); yneth.initialize(ynethInit); - // Initialize StakingNodesManager with example parameters StakingNodesManager.Init memory stakingNodesManagerInit = StakingNodesManager.Init({ admin: actors.ADMIN, stakingAdmin: actors.STAKING_ADMIN, stakingNodesAdmin: actors.STAKING_NODES_ADMIN, validatorManager: actors.VALIDATOR_MANAGER, + validatorRemoverManager: actors.VALIDATOR_REMOVER_MANAGER, stakingNodeCreatorRole: actors.STAKING_NODE_CREATOR, maxNodeCount: 10, depositContract: depositContract, @@ -157,6 +171,8 @@ contract DeployYieldNest is BaseScript { executionLayerReceiver.initialize(rewardsReceiverInit); consensusLayerReceiver.initialize(rewardsReceiverInit); // Initializing consensusLayerReceiver + ynViewer ynviewer = new ynViewer(IynETH(address(yneth)), IStakingNodesManager(address(stakingNodesManager))); + vm.stopBroadcast(); Deployment memory deployment = Deployment({ @@ -165,8 +181,11 @@ contract DeployYieldNest is BaseScript { executionLayerReceiver: executionLayerReceiver, consensusLayerReceiver: consensusLayerReceiver, // Adding consensusLayerReceiver to the deployment rewardsDistributor: rewardsDistributor, + ynViewer: ynviewer, stakingNodeImplementation: stakingNodeImplementation }); + + saveDeployment(deployment); } diff --git a/scripts/forge/DeployYnLSD.s.sol b/scripts/forge/DeployYnLSD.s.sol index aca8f9682..1d208d9f9 100644 --- a/scripts/forge/DeployYnLSD.s.sol +++ b/scripts/forge/DeployYnLSD.s.sol @@ -61,17 +61,54 @@ contract DeployYnLSD is BaseScript { } IERC20[] memory assets = new IERC20[](2); - assets[0] = IERC20(chainAddresses.lsd.RETH_ADDRESS); - assets[1] = IERC20(chainAddresses.lsd.STETH_ADDRESS); + assets[0] = IERC20(chainAddresses.lsd.STETH_ADDRESS); + assets[1] = IERC20(chainAddresses.lsd.RETH_ADDRESS); + IStrategy[] memory strategies = new IStrategy[](2); - strategies[0] = IStrategy(chainAddresses.lsd.RETH_STRATEGY_ADDRESS); - strategies[1] = IStrategy(chainAddresses.lsd.STETH_STRATEGY_ADDRESS); + strategies[0] = IStrategy(chainAddresses.lsd.STETH_STRATEGY_ADDRESS); + strategies[1] = IStrategy(chainAddresses.lsd.RETH_STRATEGY_ADDRESS); + + { + uint256[] memory maxAgesArray = new uint256[](assets.length); + address[] memory priceFeedAddresses = new address[](assets.length); + for (uint256 i = 0; i < assets.length; i++) { + maxAgesArray[i] = type(uint256).max; + if (assets[i] == IERC20(chainAddresses.lsd.RETH_ADDRESS)) { + priceFeedAddresses[i] = chainAddresses.lsd.RETH_FEED_ADDRESS; + } else if (assets[i] == IERC20(chainAddresses.lsd.STETH_ADDRESS)) { + priceFeedAddresses[i] = chainAddresses.lsd.STETH_FEED_ADDRESS; + } + } + { + address[] memory assetsAddresses = new address[](assets.length); + for (uint256 i = 0; i < assets.length; i++) { + assetsAddresses[i] = address(assets[i]); + } + YieldNestOracle.Init memory yieldNestOracleInit = YieldNestOracle.Init({ + assets: assetsAddresses, + priceFeedAddresses: priceFeedAddresses, + maxAges: maxAgesArray, + admin: actors.ORACLE_MANAGER, + oracleManager: actors.ORACLE_MANAGER + }); + yieldNestOracle.initialize(yieldNestOracleInit); + } + + + } + // Initialize ynLSD with example parameters { address[] memory lsdPauseWhitelist = new address[](1); lsdPauseWhitelist[0] = _broadcaster; + IERC20 stETH = IERC20(chainAddresses.lsd.STETH_ADDRESS); + uint256 mintAmount = 1.0001 ether; + (bool success, ) = address(stETH).call{value: mintAmount}(""); + require(success, "ETH to stETH mint failed"); + stETH.approve(address(ynlsd), mintAmount); + ynLSD.Init memory ynlsdInit = ynLSD.Init({ assets: assets, strategies: strategies, @@ -84,37 +121,12 @@ contract DeployYnLSD is BaseScript { stakingAdmin: actors.STAKING_ADMIN, lsdRestakingManager: actors.LSD_RESTAKING_MANAGER, // Assuming no restaking manager is set initially lsdStakingNodeCreatorRole: actors.STAKING_NODE_CREATOR, // Assuming no staking node creator role is set initially - pauseWhitelist: lsdPauseWhitelist + pauseWhitelist: lsdPauseWhitelist, + depositBootstrapper: actors.DEPOSIT_BOOTSTRAPER }); ynlsd.initialize(ynlsdInit); } - uint256[] memory maxAgesArray = new uint256[](assets.length); - address[] memory priceFeedAddresses = new address[](assets.length); - for (uint256 i = 0; i < assets.length; i++) { - maxAgesArray[i] = type(uint256).max; - if (assets[i] == IERC20(chainAddresses.lsd.RETH_ADDRESS)) { - priceFeedAddresses[i] = chainAddresses.lsd.RETH_FEED_ADDRESS; - } else if (assets[i] == IERC20(chainAddresses.lsd.STETH_ADDRESS)) { - priceFeedAddresses[i] = chainAddresses.lsd.STETH_FEED_ADDRESS; - } - } - - { - address[] memory assetsAddresses = new address[](assets.length); - for (uint256 i = 0; i < assets.length; i++) { - assetsAddresses[i] = address(assets[i]); - } - YieldNestOracle.Init memory yieldNestOracleInit = YieldNestOracle.Init({ - assets: assetsAddresses, - priceFeedAddresses: priceFeedAddresses, - maxAges: maxAgesArray, - admin: actors.ORACLE_MANAGER, - oracleManager: actors.ORACLE_MANAGER - }); - yieldNestOracle.initialize(yieldNestOracleInit); - } - { LSDStakingNode lsdStakingNodeImplementation = new LSDStakingNode(); ynlsd.registerLSDStakingNodeImplementationContract(address(lsdStakingNodeImplementation)); @@ -127,5 +139,7 @@ contract DeployYnLSD is BaseScript { saveynLSDDeployment(deployment); } + + vm.stopBroadcast(); } } From 30ed01abdf9e5b5be23bcb03a15e49366580c728 Mon Sep 17 00:00:00 2001 From: danoctavian Date: Mon, 25 Mar 2024 10:58:38 +0200 Subject: [PATCH 4/5] fix wei difference in ynLSD test --- test/foundry/integration/ynLSD.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/foundry/integration/ynLSD.t.sol b/test/foundry/integration/ynLSD.t.sol index caa4dd866..d57a15deb 100644 --- a/test/foundry/integration/ynLSD.t.sol +++ b/test/foundry/integration/ynLSD.t.sol @@ -227,7 +227,7 @@ contract ynLSDAssetTest is IntegrationBaseTest { uint256 expectedDepositPreview = amount * stethPrice / 1e18; uint256 previewDeposit = ynlsd.previewDeposit(asset, amount); - assertEq(previewDeposit, expectedDepositPreview, "Preview deposit does not match expected value"); + assertTrue(compareWithThreshold(previewDeposit, expectedDepositPreview, 1), "Preview deposit does not match expected value within threshold"); } function testConvertToETH() public { From e1ad19923a44f0e5fb7746d151557aefcde67104 Mon Sep 17 00:00:00 2001 From: danoctavian Date: Mon, 25 Mar 2024 21:31:37 +0200 Subject: [PATCH 5/5] add test holesky verifyWithdrawalCredentials --- src/interfaces/IStakingNodeV2.sol | 15 + test/foundry/ActorAddresses.sol | 17 ++ .../integration/IntegrationBaseTest.sol | 14 +- test/foundry/integration/StakingNodeV2.t.sol | 268 ++++++++++++++++++ .../mocks/testnet/TestnetEigenPodMock.sol | 172 +++++++++++ 5 files changed, 482 insertions(+), 4 deletions(-) create mode 100644 src/interfaces/IStakingNodeV2.sol create mode 100644 test/foundry/integration/StakingNodeV2.t.sol create mode 100644 test/foundry/mocks/testnet/TestnetEigenPodMock.sol 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; + } + +}