diff --git a/contracts/NodeDelegator.sol b/contracts/NodeDelegator.sol index a6e8c0d..2f92c4f 100644 --- a/contracts/NodeDelegator.sol +++ b/contracts/NodeDelegator.sol @@ -15,7 +15,7 @@ import { PausableUpgradeable } from "@openzeppelin/contracts-upgradeable/securit import { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; import { IEigenPodManager } from "./interfaces/IEigenPodManager.sol"; -import { IEigenPod, BeaconChainProofs } from "./interfaces/IEigenPod.sol"; +import { IEigenPod, BeaconChainProofs, IBeaconDeposit } from "./interfaces/IEigenPod.sol"; /// @title NodeDelegator Contract /// @notice The contract that handles the depositing of assets into strategies @@ -181,11 +181,43 @@ contract NodeDelegator is INodeDelegator, LRTConfigRoleChecker, PausableUpgradea whenNotPaused onlyLRTOperator { - // Call the stake function in the EigenPodManager IEigenPodManager eigenPodManager = IEigenPodManager(lrtConfig.getContract(LRTConstants.EIGEN_POD_MANAGER)); eigenPodManager.stake{ value: 32 ether }(pubkey, signature, depositDataRoot); - // Increment the staked but not verified ETH + // tracks staked but unverified native ETH + stakedButUnverifiedNativeETH += 32 ether; + + emit ETHStaked(pubkey, 32 ether); + } + + /// @notice Stake ETH from NDC into EigenLayer + /// @param pubkey The pubkey of the validator + /// @param signature The signature of the validator + /// @param depositDataRoot The deposit data root of the validator + /// @param expectedDepositRoot The expected deposit data root, which is computed offchain + /// @dev Only LRT Operator should call this function + /// @dev Exactly 32 ether is allowed, hence it is hardcoded + /// @dev offchain checks withdraw credentials authenticity + /// @dev compares expected deposit root with actual deposit root + function stake32EthValidated( + bytes calldata pubkey, + bytes calldata signature, + bytes32 depositDataRoot, + bytes32 expectedDepositRoot + ) + external + whenNotPaused + onlyLRTOperator + { + IBeaconDeposit depositContract = eigenPod.ethPOS(); + bytes32 actualDepositRoot = depositContract.get_deposit_root(); + if (expectedDepositRoot != actualDepositRoot) { + revert InvalidDepositRoot(expectedDepositRoot, actualDepositRoot); + } + IEigenPodManager eigenPodManager = IEigenPodManager(lrtConfig.getContract(LRTConstants.EIGEN_POD_MANAGER)); + eigenPodManager.stake{ value: 32 ether }(pubkey, signature, depositDataRoot); + + // tracks staked but unverified native ETH stakedButUnverifiedNativeETH += 32 ether; emit ETHStaked(pubkey, 32 ether); diff --git a/contracts/interfaces/IEigenPod.sol b/contracts/interfaces/IEigenPod.sol index ca617b8..b486e27 100644 --- a/contracts/interfaces/IEigenPod.sol +++ b/contracts/interfaces/IEigenPod.sol @@ -45,7 +45,16 @@ library BeaconChainProofs { } } +interface IBeaconDeposit { + /// @notice Query the current deposit root hash. + /// @return The deposit root hash. + function get_deposit_root() external view returns (bytes32); +} + interface IEigenPod { + /// @notice This is the beacon chain deposit contract + function ethPOS() external returns (IBeaconDeposit); + /// @return delayedWithdrawalRouter address of eigenlayer delayedWithdrawalRouter, /// which does book keeping of delayed withdrawls function delayedWithdrawalRouter() external returns (address); diff --git a/contracts/interfaces/INodeDelegator.sol b/contracts/interfaces/INodeDelegator.sol index e548c89..5da3a1c 100644 --- a/contracts/interfaces/INodeDelegator.sol +++ b/contracts/interfaces/INodeDelegator.sol @@ -17,8 +17,10 @@ interface INodeDelegator { error StrategyIsNotSetForAsset(); error InvalidETHSender(); error InvalidRewardAmount(); + error InvalidDepositRoot(bytes32 expectedDepositRoot, bytes32 actualDepositRoot); // getter + function stakedButUnverifiedNativeETH() external view returns (uint256); // methods diff --git a/test/integration/LRTNativeEthStakingIntegrationTest.t.sol b/test/integration/LRTNativeEthStakingIntegrationTest.t.sol index 70d11cf..a02b512 100644 --- a/test/integration/LRTNativeEthStakingIntegrationTest.t.sol +++ b/test/integration/LRTNativeEthStakingIntegrationTest.t.sol @@ -12,6 +12,7 @@ import { NodeDelegator } from "contracts/NodeDelegator.sol"; import { LRTDepositPool } from "contracts/LRTDepositPool.sol"; import { UtilLib } from "contracts/utils/UtilLib.sol"; import { getLSTs } from "script/foundry-scripts/DeployLRT.s.sol"; +import { IEigenPod, IBeaconDeposit } from "contracts/interfaces/IEigenPod.sol"; import { ITransparentUpgradeableProxy } from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; import { ProxyAdmin } from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; @@ -198,6 +199,39 @@ contract LRTNativeEthStakingIntegrationTest is Test { ); } + function test_stake32EthValidated() external returns (bytes32) { + // create eigen pod + vm.prank(manager); + nodeDelegator1.createEigenPod(); + + address eigenPod = address(nodeDelegator1.eigenPod()); + // same eigenPod address should be created + assertEq(eigenPod, 0xf7483e448c1B94Ea557A53d99ebe7b4feE0c91df, "Wrong eigenPod address"); + + // stake 32 eth for validator1 + bytes memory pubkey = + hex"8ff0088bf2bc73a41c74d1b1c6c997e4963ceffde55a09fef27596016d919b74b45372e8aa69fda5aac38a0c1a38dfd5"; + bytes memory signature = hex"95e07ee28de0316ecdf9b528c222d81242898ee0095e284582bb453d331b7760" + hex"6d8dca23ab8980459ea8a9b9710e2f740fceb1a1c221a7fd75eb3ef4a6b68809" + hex"f3e76387f01f5d31718e6306375b20b29cb08d1374c7fb125d50c1b2f5a5cc0b"; + + bytes32 depositDataRoot = hex"6f30f44f0d8dada6ba5d8fd617c727020c01c697587d1a04ff6661be656198bc"; + + IBeaconDeposit depositContract = IEigenPod(eigenPod).ethPOS(); + bytes32 expectedDepositRoot = depositContract.get_deposit_root(); + + vm.deal(address(nodeDelegator1), 32 ether); + uint256 balanceBefore = address(nodeDelegator1).balance; + + vm.startPrank(operator); + nodeDelegator1.stake32EthValidated(pubkey, signature, depositDataRoot, expectedDepositRoot); + + uint256 balanceAfter = address(nodeDelegator1).balance; + assertEq(balanceAfter, balanceBefore - 32 ether, "stake32eth unsuccesful"); + + return expectedDepositRoot; + } + function test_withdrawRewards() external { // create eigen pod vm.prank(manager);